约瑟夫环

来源:互联网 发布:定金和尾款淘宝客 编辑:程序博客网 时间:2024/05/20 17:08

约瑟夫环问题:一圈共有N(1~N)个人,开始报数(1,2,3,...,M),报到M的人自杀,然后重新开始报数,问最后自杀的人是谁?


如图:内环表示人排列的环,外环表示自杀顺序;上面N=41,M=3。

1. 最普通办法就是模拟整个过程:原文地址:http://blog.csdn.net/kangroger/article/details/39254619

建一个bool数组,true表示此人还活着,false表示已经自杀。可以模拟整个过程

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. #include<iostream>  
  2. using namespace std;  
  3. int main()  
  4. {  
  5.     int N;//人的总个数  
  6.     int M;//间隔多少个人  
  7.   
  8.     cin>>N;  
  9.     cin>>M;  
  10.   
  11.     bool *p=new bool[N+1];//[1……N]为true表示此人还活着  
  12.     for (int i=1; i <= N; i++)  
  13.         *(p+i)=true;  
  14.       
  15.     int count=0;//统计自杀的人数  
  16.   
  17.     for (int i=1, j=0; ;i++)//i用来表示循环,j用来计算是不是第N个人  
  18.     {  
  19.         if (*(p+i))//此人还活着  
  20.         {  
  21.             j++;  
  22.             if (j == M)  
  23.             {  
  24.                 *(p+i)=false;  
  25.                 j=0;  
  26.                 count++;//统计自杀的人  
  27.             }  
  28.             if (count == N)  
  29.             {  
  30.                 cout<<"最后自杀的人是:"<<i<<endl;  
  31.                 break;  
  32.             }  
  33.         }  
  34.   
  35.         if(i == N)  
  36.             i=0;  
  37.     }  
  38.   
  39.     delete []p;  
  40.       
  41.     return 0;  
  42. }  

模拟整个过程,复杂度为O(NM)。


2. 可以用数学方法来求解:http://hi.baidu.com/anywei/item/294351b5f432f144ba0e12f2

把问题换个方式描述一下:N个人(编号0~(N-1)),从0开始报数,报到(M-1)的自杀,剩下的人继续从0开始报数。求最后自杀者的编号。

首先,为什么要从0开始编号?答:因为递推式里有%(取余)运算,这个运算必然是从0开始;至于结束,如果是对n取余(%n),当然是n-1了。

其次,为什么要从0开始报数?答:要不然呢?还是从1开始报数,也可以,但是不是很混乱么?所以可以认为是为了和编号想对应。


先总结一下约瑟夫环的递推公式:

  1. f[1]=0; f[i]=(f[i-1]+m)%i; (i>1)

  2. f[1]=1; f[i]=(f[i-1]+m)%i  (i>1);   if(f[i]==0) f[i]=i;

那么这2个公式有什么不同?

首先可以肯定的是这2个公式都正确。公式1,得到的是以0~n-1标注的最终序号;公式2得到的就是正常的1~n的序号。

公式1的推导:——————————

给出一个序列,从0~n-1编号。其中,k代表出列的序号的下一个,即k-1出列。

a 0, 1, …, k-1, k, k+1, …, n-1

那么,出列的序号是(m-1)%n,且k=m%n(这个真的是显而易见!)。出列k-1后,序列变为

b 0, 1, …, k-2, k, k+1, …, n-1

然后,我们继续从n-1后延长这个序列,可以得到

c 0, 1, …, k-2, k, k+1, …, n-1, n+0, n+1, …, n+k-2

我们取从k开始直到n+k-2这段序列。其实这段序列可以看作将序列b的0~k-2段移到了b序列的后面。这样,得到一个新的序列

c k, k+1, …, n-1, n, n+1, …, n+k-2

好了,整个序列c都减除一个k,得到

0, 1, …, n-2

c序列中的n-1, n, n+1都减除个k是什么?这个不需要关心,反正c序列是连续的,我们知道了头和尾,就能知道d序列是什么样的。

从序列a到序列d,就是一个n序列到n-1序列的变化,约瑟夫环可以通过递推来获得最终结果。

剩下的,就是根据n-1序列递推到n序列。假设在n-1序列中,也就是序列d中,我们知道了最终剩下的一个序号是x,那么如果知道了x转换到序列a中的编号x`(同样都是最后一个自杀的人,n-1个人的x相当于n个人x`——这才是逆推真正的含义),不就是知道了最终的结果了么?

下面我们就开始推导出序列a中x的序号是什么。

d->c,这个变换很容易,就是x+k;

c->b,这个变换是网上大家都一带而过的,也是令我郁闷的一个关键点。从b->c,其实就是0~k-2这段序列转换为n~n+k-2这段序列,那么再翻转回去,简单的就是%n,即(x+k)%n。%n以后,k~n-1这段序列值不会发生变化,而n~n+k-2这段序列则变成了0~k-2;这两段序列合起来,就是序列b。

于是乎,我们就知道了,x`=(x+k)%n。并且,k=m%n,所以x`=(x+m%n)%n=(x+m)%n。公式1就出来了:f[i]=(f[i-1]+m)%i。当然,i=1就是特殊情况了,f[1]=0。这里还有一个小问题。也许你会迷惑为什么x`=(x+m%n)%n=(x+m)%n中的%n变成公式中f[i]=(f[i-1]+m)%i中的%i?其实这个稍微想想就能明了。我们%n就是为了从序列c转换到序列b——这是在n-1序列转换成n序列时%n;那么从n-2转换到n-1呢?不是要%(n-1)了吗?所以这个值是变量,不是常量。

有了递推公式就可以在O(N)时间求出结果:

#include<iostream>using namespace std;int main(){int N;//人的总个数int M;//间隔多少个人cin>>N;cin>>M;int result=0;//N=1情况for (int i=2; i<=N; i++){result=(result+M)%i;}cout<<"最后自杀的人是:"<<result+1<<endl;    //result别忘了要加1return 0;}

感性认识:为什么能用a,b,c,d四个序列逆推?答:a,b,c,d是第1个人出列后变成n-1人序列的演变过程,那么从n-1变成n人序列,当然就要加上这第1个被删除的人(就当什么都没发生过)。

0 0