约瑟夫问题是个很经典的问题,没事玩杀人游戏,N个人站成一圈,从1开始报数,报到M时必须死掉,不想死的那个人站在哪个位置,可以幸免于难(描述不详细,凑合看哈)。
方法一:这个问题初看可以看到,这不就是一个游戏么,游戏怎么说就怎么做,暴力模拟吧。模拟过程见程序:
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;
}
方法二:在上面程序中,那些已经退出的人每次都被点到,但因为vis状态是不存活的,所以不报数,那么后面多出了好多无用的循环次数,那么我们可以考虑将已经出列的人让他真正的出列,不再出现在游戏的环中。我们可以考虑用链的形式将所有人串起来,当报到M时,将其出列。从链起的环中去掉,因此对于每个人,增加一个属性值next,代表他的下一个人的编号。去掉一个人的操作如图:
这样便可以让模拟的过程去掉大量不需要的重复,提高效率。
struct monkey{
int num;
int next;
};
monkey a[306];
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 main(){
int n,m;
cin >> n >> m;
int f = 0;
for(int i = 2;i<= n;i++){
f = (f+ m)%i;
}
cout <<f+1 << endl;
return 0;
}