约瑟夫环问题(通过观察得出递推式从而建立递归求解)
简介
问题大意:m个人围坐成一圈,编号为0~m-1,从0号的人开始报数,他先报0,报到k-1的那个人出局,然后下一个人继续从0开始报,下一个报到k-1的人又出局……一直重复直到最后环内剩下一个人,求这个人的编号。
首先我们自然是可以用链表把这个题模拟出来的,但是这里还有一种稍加推导得到递推关系,然后递归的方法。
基础
假设现在有0~9,十个人,k=4,所以每次从0开始,报到3的人就会出局。现在用一张图将这个情况完整展现出来。
从上图可以看到,第一轮报数的时候每个人将要报的数是和自己编号一样的,每一次移除一个人之后,整个环中的人报的数会发生变化,可以看到4变成了0,5变成了1……以此类推。也就是说环中的人每一轮报的数都会发生变化,我们要求的是最后出局的人(也就是最后一轮报“0”,并且整个环只剩下他一个的那个人)在最开始的时候报的数是啥,也就是他的初始编号是啥。
这个时候我们发现,以10人环和9人环这两列为例,下面一轮的每一个人要推出这个人在上一轮报的是啥,只需要(下面的数+k)%上一轮的人数 即可,那我们又可以看到当剩下一人的时候,他报的数肯定是0,而既然每一轮的人能够推出它在上一轮所报的数,而且每一轮的总人数都会减少1,满足递归的性质,那么我们就可以用递归的方法,从最底端推上来。
int joseph(int m,int k){
if(m == 1) return 0; //到了最底层,也就是上图中的1人环的时候它在这一轮报的数肯定是0啦。
else {
int temp = (joseph(m-1,k)+k)%m; //如果不是最底层,那么就要由这个人在下一层报的数来推出他在上一层报的数
return temp;
}
}
进阶
增加难度:按出局的顺序输出 出局的人的编号。
因为我们上面只是因为知道最后一个人肯定是报0,而知道了他在这一轮报的数,可以求出这个人在上一轮报的数这样层层地推上去来求解这个人在最后的编号的。
那么这个时候要求在他前面就淘汰出局的人的编号。因为最后一个人我们相当于是递归进到了最深层然后再层层返回的,最后一层只有一个人我们肯定也知道就剩那一个人编号为0,那么如果我们不递归进入到最深层,在中间这些层,每次需要被淘汰的肯定还是在那一层报k-1的那个人,那么既然知道了在某一层被淘汰的人报的数,又知道怎么递推得到他最初报的数,那么每一层被淘汰的人的初始编号也就不难求得了。
加一个变量i来控制递归深入的层数,i 其实也就表示我这个函数求的是在第 i 轮被淘汰的那个人的最初编号。
//总共有m个人,每次报到k-1的时候,那个人就出局
int joseph(int m,int k,int i) {
if(i==1) return (k-1)%m; //如果是到了那一层的话,我们也知道在这一层这个被淘汰的人报的数肯定是k-1,那么就返回k-1,让上面的递归层去递推从而得到它的最初编号,只不过因为k有可能比m大所以还要对m取模。
else return ((Joseph(m-1,k,i-1)+k)%m); //还在递归中间层的话,就是将下一层递归返回的数据去进行递推得到这个人在这一层报的数然后继续返回上一层交由上一层去递推。
}
for(int i=1;i<=m;i++){ //从1到m,依次输出在第i轮被淘汰的人的编号。
cout<<joseph(m,k,i)<<endl;
}
有了上面的思路之后,如果题目再稍微变一下,比如从1-n编号(因为取模的问题,直接将其用0~n-1去做然后最后全部再加一会好一些),也不难解决。
参考资料
https://blog.csdn.net/yanweibujian/article/details/50876631
(文中图片来自于此博客)