约瑟夫环(JosephProblem)
来源:互联网 发布:粤贵银手机行情软件 编辑:程序博客网 时间:2024/06/10 17:19
约瑟夫环(JosephProblem)
题目:
有41个人围坐成一圈玩游戏,编号分别是0,1,2,…,39,40.
从1开始,每次数到3的人就退出游戏,下个人再次从1开始。
请问最后剩下的人的编号?
约瑟夫环有很多解题思路:
1.用一个标识数组来模拟;
2.用数据结构来实现;
3.用递归实现;
4.用递推实现。
用一个标识数组来模拟:
#include <stdio.h>#include <stdlib.h>/****************************************************** * 函数名称:JosephProblem * 参数列表: * 1.n:从0到n-1共有n个人 * 2.m:每次数到m就退出游戏 * 返回值 :最后的人的编号 * Author :test1280 * History :2017/05/02 * 备注 : * ***************************************************/int JosephProblem(int n, int m){ int i = 0; int *flagArr = malloc(sizeof(int)*n); // 初始化标识数组 for (i = 0; i<n; i++) { flagArr[i] = 1; } // 计数变量 int cnt = 0; // 剩余人数,为1时退出 int last = n; int index = 0; while (last != 1) { if (flagArr[index] == 1) { cnt++; if (cnt == m) { flagArr[index] = 0; last--; cnt = 0; } } index++; index %= n; } int result = -1; for (i = 0; i<n && flagArr[i]==0; i++); result = i; free(flagArr); flagArr = NULL; return result;}int main(){ int n = 41; int m = 3; int result = JosephProblem(n, m); printf("the result is %d.\n", result); return 0;}
基本上就是模拟人工来进行,使用一个数组来标识当前的位置的人是否已经退出游戏。
这个仔细看看应该比较好理解。
用数据结构来实现:
#include <iostream>#include <queue>using namespace std;/***************************************************** * 函数名称:JosephProblem * 参数列表: * 1.n:从0到n-1共有n个人 * 2.m:数到m的人退出游戏 * 返回值 :最后的人的编号 * Author :test1280 * History:2017/05/02 * 备注 : * **************************************************/int JosephProblem(int n, int m){ queue<int> q; int i = 0, j; for (; i<n; i++) { q.push(i); } while (q.size() > 1) { j = m-1; while (j--) { q.push(q.front()); q.pop(); } q.pop(); } return q.front();}int main(){ int result = JosephProblem(41, 3); cout<<"the result is "<<result<<endl; return 0;}
这个算是很简洁的了。
现将所有的编号(从0到n-1)存储到一个queue中,然后也还是模拟。
虽然简单易懂,但是效率低。
用递归实现:
使用递归来实现就很有意思。
首先,m%n得到的值一定是0到n-1范围内的某一值(不论m和n的大小关系如何)。
其次,如果我想要求n个人第一次数到m的那个人的编号,可以使用:
pos = m%nif pos == 0 then pos = n - 1else pos = pos - 1end
其实上面的这个逻辑等价于:
数到m的那个人的编号=(m%n-1+n)%n。
-1+n是防止变负数,再%n可以防止本身就是正值超出n范围。
再其次,我现在有个位置是pos,我想将其右移x个位置,求得右移后的位置?
右移后的位置=(pos + x) % n
例如:
我有序列:0 1 2 3 4 此时n=5
我想要:
将2右移1位:2+1=3
将2右移2位:2+2=4
将2右移3为:2+3=5=0
将2右移4位:2+4=6=1
将2右移5位:2+5=7=2
将2右移6位:2+6=8=3
……
将pos右移x位就是(pos+x)%n啦。
最后想想左移,我现在的位置是pos,想要将其左移x位,求左移后的位置?
还是那个序列:0 1 2 3 4 此时n=5
我想要:
将2左移1位:2-1=1
将2左移2位:2-2=0
将2左移3位:2-3=-1=4
将2左移4位:2-4=-2=3
将2左移5位:2-5=-3=2
将2左移5位:2-6=-4=1
……
将pos左移x位就是(pos-x+n)%n啦。
想明白这几点后,我们可以思考:
我想要求n个人,实际是先求第一个退出的人的位置(记为delPos,意为delete-position),此时知道谁退出游戏了;
从退出游戏的那个人(delPos)的下一个人(记为k)开始,让他(k)成为0编号,相当于将所有的元素右移了n-1-k+1个(n-1是最后的编号,k是当前的编号,n-1-k是将k移动到n-1处的右移量,而0编号是n-1的下一个位置,故移动量为n-1-k+1);
然后将n-1个人(相对一开始的n个人少了一个,那个人已经数到m退出游戏了)按照谁数到m就退出游戏的规则再进行,得到n-1个人数m的最后的编号;
将此编号再左移n-1-k+1个位置回到原位置,即我们求得的n个人数m退出的解。
代码如下:
#include <stdio.h>#include <stdlib.h>/******************************************* * 函数名称:JosephProblem * 参数列表: * 1.n:从0到n-1共有n个人 * 2.m:数到m的人退出游戏 * 返回值 :最后的人的编号 * 备注 : * Autohr :test1280 * History :2017/05/08*******************************************/int JosephProblem(int n, int m){ // 递归终止条件 // 如果只有一个人,那么必然是返回0位置; if (n == 1) return 0; // 本次剔除的位置 // 假设m==x*n:delPos = n-1(x为[0,...]); // 假设m=6, n=3, delPos=2=(6%3-1+3)%3; // 假设m=5, n=3, delPos=1=(5%3-1+3)%3; // 假设m=4, n=3, delPos=0=(4%3-1+3)%3; // 假设m=3, n=3, delPos=2=(3%3-1+3)%3; // 假设m=2, n=3, delPos=1=(2%3-1+3)%3; // 假设m=1, n=3, delPos=0=(1%3-1+3)%3; int delPos = (m % n -1 + n) % n; // k是delPos的下一位 // 假设m=6, n=3, k=0=(2+1)%3=0; // 假设m=5, n=3, k=2=(1+1)%3=2; // 假设m=4, n=3, k=1=(0+1)%3=1; // 假设m=3, n=3, k=0=(2+1)%3=0; // 假设m=2, n=3, k=2=(1+1)%3=2; // 假设m=1, n=3, k=1=(0+1)%3=1; int k = (delPos + 1) % n; // 0 1 .. delPos k .. n-1; // 使上一行全部的数字向右移动,使得k移动到左首第0个; // 移动的位数为:((n-1)+1-k)%n; // 假设有序列:0 1 2 3 4 此时n=5; // 当delPos=1时(k=2),将k右移至0处,共移动3=((5-1)+1-2)%5; // 当delPos=4时(k=0),将k右移至0处,共移动0=((5-1)+1-0)%5; int move = ((n-1)+1-k)%n; // 由subResult回推到原位置 int subResult = JosephProblem(n-1, m); return (subResult - move + n) % n;}int main(){ int result = JosephProblem(10, 3); printf("result is %d\n", result); return 0;}
其实核心步骤就是:先剔除一个,然后将剩余的人右移,求n-1个人的解,再左移回来即可。
就像一开始的拨号电话,先旋转数字键盘,然后放开手回退…
代码本来没有几行,注释比较多。。。
另,递归有个缺点,数字不能很大,否则栈溢出。
(如果有好的尾调用倒是可以避免,但是也有很多限制)
用递推实现:
有了上面递归的铺垫,我们能不能写的更简单一点?
我的递归是从大到小(解决n规模是要解决n-1规模,解决n-1规模是解决n-2规模…)进行的,我能否从小到大进行?
我们的0-(n-1)的序列,删除第一次数到m的人之后的序列为:
0 1 2 … k … n-2 n-1 共计n-2个。
将k右移n-1+1-k个位置后,得到的序列为:
kk+1k+2...n-2n-101...k-2
其中k对应0编号(右移):
k 0k+1 1k+2 2...n-2 ?(p)n-1 ?0 ?1 ?...k-2 n-2
其中有很多?不知道值是多少。
假定第一个?为p,很容易有等式:p-2=(n-2)-(k+2),得出p=n-k-2,于是:
k 0k+1 1k+2 2...n-2 n-k-2n-1 n-k-10 n-k1 n-k+1...k-2 n-2结束......x1<-----x2
我们的目的是从右侧向左侧进行推导:
假定右侧的最后编号是x2,x2对应的左侧编号是x1,即从左侧右移思考改变为:右侧左移。
左侧的右移量是k,那么右侧的左移量也是k,此时还记得我们之前的左移右移吗?
x1=(x2-k+n)%n
而k的值是多少?看看上面的递归:
k = (delPos + 1) % n = ((m % n -1 + n) % n + 1)%n
x5=(x4-k4+4)%4;
x4=(x3-k3+3)%3;
x3=(x2-k2+2)%2;
x2=(x1-k1+1)%1;
x1仔细一想,当然就是0啦。
f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果是f[n]。
f[1]=0f[i]=(f[i-1] -k* + i) % i
代码如下:
#include <stdio.h>#include <stdlib.h>/******************************************* * 函数名称:JosephProblem * 参数列表: * 1.n:从0到n-1共有n个人 * 2.m:数到m的人退出游戏 * 返回值 :最后的人的编号 * 备注 : * Autohr :test1280 * History :2017/05/08*******************************************/int JosephProblem(int n, int m){ int s = 0; for (int i=2; i<=n; i++) { int k = ((m % i - 1 + i) % i + 1) % i; s = (s -k + i) % i; } return s;}int main(){ int result = JosephProblem(41, 3); printf("the result is %d\n", result); return 0;}
那么一长串其实可以化简:
int JosephProblem(int n, int m){ int s = 0; for (int i=2; i<=n; i++) { s = (s + m) % i; } return s;}
至于是怎么化简的,大家可以多找找规律。
其实我更提倡用数学方法化简。。回头我再看看再补上来,嘿嘿。
额,至此,几种解题方法都实现过啦,恩,大家可以多看看,多想想,其实还有很多可以优化来处理。
参考资料:
1.http://blog.163.com/soonhuisky@126/blog/static/157591739201321341221179/
2.http://blog.csdn.net/kangroger/article/details/39254619
3.https://www.zhihu.com/question/20065611
……
如果有错误请大伙指出来,哈!
- 约瑟夫环(JosephProblem)
- 约瑟夫问题(约瑟夫环) java
- 约瑟夫环(joseph)
- 约瑟夫环(链表)
- 队列(约瑟夫环)
- poj1012Joseph(约瑟夫环)
- 约瑟夫环(Java)
- 约瑟夫环(非递推版)
- 约瑟夫问题、约瑟夫环
- 九度OJ 1188:约瑟夫环 (约瑟夫环)
- 九度OJ 1189:还是约瑟夫环 (约瑟夫环)
- 约瑟夫环(约瑟夫问题的变形,LA 3882)
- 约瑟夫斯问题(约瑟夫环)?待解决
- 约瑟夫环(约瑟夫问题) 采用循环单链表实现
- Josephus问题(约瑟夫环)
- 约瑟夫环(报数游戏)
- 约瑟夫环(纯模拟)
- zoj1088(模拟/约瑟夫环)
- RESTful
- sbt安装及测试
- SpringMvc入门
- 我为什么不继续做java,用4个月转做了Hadoop开发?
- 大牛谈看文献的方法,共勉之
- 约瑟夫环(JosephProblem)
- FILE与文件描述符的对比
- Xamarin XAML语言教程Xamarin.Forms中活动指示器的显示隐藏
- android基础复习
- mysql 远程连接 mySql数据库
- linux常用命令
- xutils3.0
- 前端框架天下三分:Angular React 和 Vue的比较
- JS学习(六) ----闭包