约瑟夫环问题描述:
n个人围成一圈(编号分别为1-n),从某人开始顺序报号1,2,3…m凡报到m者的人出列,再接着从下一个人开始数,输出最终出列的人的编号。
(约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。)
Input
输入总人数n,接着输入一个整数k和m,前者表示从第k人开始数,后者表示每次数到m的人出列,(n,k,m之间均用空格隔开, 1 <= k <= n <= 10000)
Sample Input
5 2 3
7 1 3
Sample Output
5
4
第一种方法:模拟出环过程
代码:#include<iostream>
using namespace std;
int main()
{
int N;//人的总个数
int M;//间隔多少个人
cin>>N;
cin>>M;
bool *p=new bool[N+1];//[1……N]为true表示此人还未出环
for (int i=1; i <= N; i++)
*(p+i)=true;
int count=0;//统计出环的人数
for (int i=1, j=0; ;i++)//i用来表示循环,j用来计算是不是第N个人
{
if (*(p+i))//此人还未出环
{
j++;
if (j == M)
{
*(p+i)=false;
j=0;
count++;//统计出环的人
}
if (count == N)
{
cout<<"最后出环的人是:"<<i<<endl;
break;
}
}
if(i == N)
i=0;
}
delete []p;
return 0;
}
上述方法的效率很低,其时间复杂度为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)
第二种方法:根据以上推出的结论
AC代码:
#include<bits/stdc++.h>
using namespace std;
int solution(int n, int m)
{
if(n < 1 || m < 1)
return -1;
vector<int> f(n+1,0);
f[1]=0;
for(unsigned i = 2; i <= n; i++)
f[i] = (f[i-1] + m) % i;
return f[n];
}
int main(void)
{
int n,m,t;//n表示总人数,m表示间隔人数,t表示起始人的序号
cin>>n>>m>>t;
cout<<(solution(n,m)+t-1)%n+1<<endl;//直接输出
return 0;
}