虽然这是一个最最基础不过的问题,但是基础也恰恰应该是我们能不费力气,信手拈来的东西。所以好好记一下这道题的做法还是很有必要的。
问题描述是说给你一个数n,让你从1开始到n报数,报到m的那个数字删掉,然后从后面再从1开始数,遇到数组结尾就从头开始继续往后,依次类推,问最后剩下的那个数字的原始编号是多少。
这个题最基本的解法就是模拟整个操作过程,然后输出最后剩下的那个数字即可。
代码如下:
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(mn)。当n和m很大时,很难在短时间内得出结果。不过好处就是可以给出n个人出圈的次序。只要在删除前保存一下即可。
下面利用数学推导,如果能得出一个通式,就可以利用递归、循环等手段解决。下面给出推导的过程:
(1)第一个被删除的数为 (m - 1) % n。
(2)假设第二轮的开始数字为k,那么这n - 1个数构成的约瑟夫环为k, k + 1, k + 2, k +3, .....,k - 3, k - 2。做一个简单的映射。
k -----> 0
k+1 ------> 1
k+2 ------> 2
...
...
k-2 ------> n-2
这是一个n -1个人的问题,如果能从n - 1个人问题的解推出 n 个人问题的解,从而得到一个递推公式,那么问题就解决了。假如我们已经知道了n -1个人时,最后胜利者的编号为x,利用映射关系逆推,就可以得出n个人时,胜利者的编号为 (x + k) % n。其中k等于m % n。代入(x + k) % n <=> (x + (m % n))%n <=> (x%n + (m%n)%n)%n <=> (x%n+m%n)%n <=> (x+m)%n
(3)第二个被删除的数为(m - 1) % (n - 1)。
(4)假设第三轮的开始数字为o,那么这n - 2个数构成的约瑟夫环为o, o + 1, o + 2,......o - 3, o - 2.。继续做映射。
o -----> 0
o+1 ------> 1
o+2 ------> 2
...
...
o-2 ------> n-3
这是一个n - 2个人的问题。假设最后的胜利者为y,那么n -1个人时,胜利者为 (y + o) % (n -1 ),其中o等于m % (n -1 )。代入可得 (y+m) % (n-1)
要得到n - 1个人问题的解,只需得到n - 2个人问题的解,倒推下去。只有一个人时,胜利者就是编号0。下面给出递推式:
f [1] = 0;
f [ i ] = ( f [i -1] + m) % i; (i>1)
有了递推公式,实现就非常简单了。
代码如下:
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 int result=0;//N=1情况 11 for (int i=2; i<=N; i++) 12 { 13 result=(result+M)%i; 14 } 15 cout<<"最后自杀的人是:"<<result+1<<endl;//result要加1 16 return 0; 17 }