约瑟夫环


约瑟夫问题

今天力扣周赛刚好涉及到了这个知识点,于是顺手记录一下~

问题描述:问题描述:编号为 1-n 的 n 个士兵围坐在一起形成一个圆圈,从编号为 1 的士兵开始依次报数(1,2,3…这样依次报),数到 m 的 士兵会被杀死出列,之后
的士兵再从 1 开始报数。直到最后剩下一士兵,求这个士兵的编号。

方法一、暴力模拟

int main(){
    int n,m;
    cin >> n >> m;
    int cnt = 0 ,dead = 0 , i = 0;
    while(dead <= n){
        i++;
        if(i > n) i = 1;  //当计数超过n时,需要回到1
        if(!vis[i]) cnt ++;  //报数是只有没有出列的人才能报数
        if(cnt == m) {    //报数时报到m的人需要出列
            cnt = 0;      //下一轮需要从0开始报数
            dead ++;      //出列的人个数
            vis[i] = 1;   //标记其已经出列
            if(dead == n) //当第n个人出列时,它就是安全的
            cout << i ;
        }
    }     
     return 0;
}

方法二、模拟链表

思路:学过链表的人,估计都会用链表来处理约瑟夫环问题,用链表来处理其实和上面处理的思路差不多,只是用链表来处理的时候,对于被选中的编号,不再是做标记,而是直接移除,因为从链表移除一个元素的时间复杂度很低,为 O(1)。当然,上面数组的方法你也可以采用移除的方式,不过数组移除的时间复杂度为 O(n)。所以采用链表的解决方法如下:

struct ha{
    int num;
    int next;
};
ha a[1000];  //用数组模拟链表,其实直接用指针也可以
int main(){
    int n,m,live,count = 0,cur,pre;
    cin >> n >> m;
    for(int i = 1;i< n; i++){
        a[i].num = i;
        a[i].next = i+1;  //当前节点下一个指向的位置
    }
    a[n].num = n, a[n].next = 1;
    live= n;
    cur = 1;
    pre = n;
    while(live> 1){
        count ++;
        if(count == m){
            a[pre].next = a[cur].next;
            live-- ;
            count = 0;            
        }
        else{
            pre = cur;
        }
        cur = a[cur].next;
    }
    cout << a[cur].num << endl;
    return 0;
}

方法三、数学方法(原理就是递归)

先是n个人玩这个游戏,编号从0->n-1编号,数m退出,当第一个m-1号人退出后,就变成了n-1个人玩这个游戏了,将这n-1个人重新编号,如下图,会发现是由原来的编号 - m得到这重新编的号。那么这时问题变成了n-1个人玩这个游戏了,机智的你会发现问题没变,只是问题规模变了,这是不是有点递归的味道。
那么想想递归式,设f(n)表示n个人数m退出时问题的解,那么f(n-1)是n-1个人数m退出时问题的解,假设f(n-1)已求出,那么:
  f( n ) = f(n - 1) + m;
 因为考虑到如图中红色字样的编号为 n-m、n-m+1、n-m+2…加过m之后会超出n,那么需要将等式更改为:
  f( n ) = (f(n - 1) + m)%n;
  那么,当得知f(n-2)的解时,反推至f(n-1)时,需要编号不超过n-1,因此等式为:
  f(n-1)=(f(n-2) + m) % n-1;
  那么推广到一般:
f( i ) = (f (i - 1) + m )% i;
  那么递归的边界是什么呢,想想当只有一个人玩的时候f(1)= 1;那么程序便可以用递归写了,当然,也可以反之从已知的1推出未知的n的解。

//数学方法
int ha(int n,int m){
    int f = 0;
    for(int i = 2; i <= n; ++i){
        f = (f + m)%i;  //即f( i ) = (f (i - 1) + m )% i;
    }
    return f;
}


//补充一种递归解法
int f(int n, int m){
    if(n == 1)   return 0;
    return (f(n - 1, m) + m) % n ;
}

反约瑟夫问题

所谓反约瑟夫,就是通过n和其他信息来求报的数m。

一、经典反~

问题描述:著名的约瑟夫问题是这样描述的:N个人排成一个圆圈,然后把这N个人按逆时针方向编号为1、2、…、N;随机产生一个正整数M,然后从编号为1的人开始按逆时针计数,当某人计数为M的倍数时,该人出队;如此循环下去,直到队列里只有一个人留下。你现在的任务是:对于输入文件的N和K,其中N为初始时约瑟夫圆圈中的人数,K为约瑟夫问题中最后留下的人的编号;请你确定一个最小能发生这种结果的正整数M

Input
为N和K,0<N≤1000
Output
为最小的正整数M

#include<bits/stdc++.h>
using namespace std;
int n, k;
int num[1005];

bool check(int m)
{
    for(int i = 2; i <= n; i++)
        num[i] = (num[i - 1] + m) % i;//O(n)的递推,这里是不可能爆的

    if(num[n] + 1 == k) return true;//毕竟是从0开始编号,所以要+1
    return false;
}

int main()
{
    cin>>n>>k;
    if(k == n) cout<<1;  //当然可以直接处理!!!
    else
    {
        for(int i = 1; ; i++)//不能设上界,不然就WA了
        {
            if(check(i))
            {
                cout<<i;
                return 0;
            }
        }
    }
}

二、好人与坏人

问题描述:原始的Joseph问题的描述如下:有n个人围坐在一个圆桌周围,把这n个人依次编号为1,…,n。从编号是1的人开始报数,数到第m个人出列,然后从出列的下一个人重新开始报数,数到第m个人又出列,…,如此反复直到所有的人全部出列为止。比如当n=6,m=5的时候,出列的顺序依次是5,4,6,2,3,1。现在的问题是:假设有k个好人和k个坏人。好人的编号的1到k,坏人的编号是k+1到2k。我们希望求出m的最小值,使得最先出列的k个人都是坏人

Input
只有一行就是k,0 < k < 14
Output
只有一行就是m

其实也就是在之前的经典反上加了一个条件——在n>k时,出圈的人的编号全部 > k

#include<bits/stdc++.h>
using namespace std;
int k, res = 0;

bool check(int m)
{
    res = 0;
    for(int i = 1; i <= k; i++)
    {
        res = (res + m - 1) % (k * 2 - i + 1);这里从1开始……我也不知道为什么从0开始就WA了
        if(res < k) return false;
    }
    return true;
}

int main()
{
    scanf("%d", &k);
    for(int i = k; ; i++)//第一轮当然要直接保证>k啊~
    {
        if(check(i))
        {
            printf("%d\n", i);
            return 0;
        }
    }
} 

以上列举的只是部分反约瑟夫问题,当然还有许多变式没有全部介绍到


原文链接请戳这里


文章作者: 再也不会
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 再也不会 !
  目录