约瑟夫环问题的几种解法
2015-12-27 14:55
375 查看
一、问题的来历
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲在一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。问题是,给定了总人数n和报数值m,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
二、问题的基本描述
n个人围成圈,依次编号为1、2、3、...、n,从1号开始依次报数,当报到m时,报m的人退出,下一个人重新从1报起,当报到m时,报m的人退出,如此循环下去,问最后剩下的那个人的编号是多少?
三、解题方法
(一)模拟法
这个问题的解决,最容易想到的方法就是模拟法,利用一个循环链表,节点的数值部分存储整数1至n,每次遍历m步,把第m个节点数值打印输出后删除该节点,如此循环下去,直至只剩一个节点,(只剩一个节点的判断方法是:节点的指向节点自己,也就是p->next=p),节点的数值部分就是最后那个人的编号。下面是c语言程序:
(二)递归法
要想用到递归法就必须找到f(n)和f(n-1)之间的关系,那么约瑟夫环有没有这样一个规律关系在呢,答案是有的。
我们假设n个人,报数到m的退出,最后剩下人的编号为x。那么第一次报数后,编号为m的人退出,那么剩下的人从编号为M+1继续报数,如果我们把m+1看成1,m+2看成2,....,n看成n-m,1看成是1-m+n,2看成是2-m+n,...,m-1看成是(m-1)-m+n也就是n-1,那么这就变成了一个n-1个人报数为m的约瑟夫环的问题,而且这里最后剩下人就是原来编号为x的那个人,按前面的对应关系f(n)=(f(n-1)+m)%n,这里有个例外,就是如果x=n的话,就会出现f(n)=n,而(f(n-1)+m)%n=((n-m)+m)%n=n%n=0,所以我们把编号加点小技巧,如果n个人编号从0编到n-1,那么f(n)=(f(n-1)+m)%n成立,如果换算成1到n编号,f(n)-1=((f(n-1)-1)+m)%n,也就是f(n)=((f(n-1)-1)+m)%n+1。
有了f(n)=(f(n-1)-1+m)%n+1这个公式,另外我们知道,当n=2时,m为奇数时最后留下的是2,m为偶数时最后留下的是1,我们就可以写出递归程序了,下面是递归法的C语言程序:
(三)迭代法
有递归法,我们看看有没有相应的迭代法,用的还是刚才的那个公式,方法就是从总人数为2开始一步一步推导到总人数为n时最后应该留下谁。下面是迭代法的C语言程序:
四、总结
模拟法的好处是,可以模拟过程,可以将出列人员按次序输出出来,不足之处是算法复杂度为O(mn),当m和n数值较大时,计算量太大。递归法和迭代法的好处是算法复杂度为O(n),计算量小,运算速度快,但无法模拟过程,无法按次序输出先后的出列人员。
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲在一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。问题是,给定了总人数n和报数值m,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
二、问题的基本描述
n个人围成圈,依次编号为1、2、3、...、n,从1号开始依次报数,当报到m时,报m的人退出,下一个人重新从1报起,当报到m时,报m的人退出,如此循环下去,问最后剩下的那个人的编号是多少?
三、解题方法
(一)模拟法
这个问题的解决,最容易想到的方法就是模拟法,利用一个循环链表,节点的数值部分存储整数1至n,每次遍历m步,把第m个节点数值打印输出后删除该节点,如此循环下去,直至只剩一个节点,(只剩一个节点的判断方法是:节点的指向节点自己,也就是p->next=p),节点的数值部分就是最后那个人的编号。下面是c语言程序:
#include <stdio.h> #include <stdlib.h> typedef struct node /*声明一个链表节点*/ { int number; struct node *next; }Node; Node* CreatNode(int x) /*创建链表节点的函数*/ { Node *p; p=(Node*)malloc(sizeof(Node)); p->number=x; p->next=NULL; return p; } Node* CreatJoseph(int n) /*创建环形链表,存放整数1到n*/ { Node *head,*p,*q; int i; for(i=1;i<=n;i++) { p=CreatNode(i); if(i==1) head=p; else q->next=p; q=p; } q->next=head; return head; } void RunJoseph(int n,int m) /*模拟运行约瑟夫环,每数到一个数,将它从环形链表中摘除,并打印出来*/ { Node *p,*q; p=CreatJoseph(n); int i; while(p->next!=p) { for(i=1;i<m-1;i++) { p=p->next; } q=p->next; p->next=q->next; p=p->next; printf("%d--",q->number); free(q); } printf("\n最后剩下的数为:%d\n",p->number); } int main() { int n,m; scanf("%d %d",&n,&m); RunJoseph(n,m); return 0; }
(二)递归法
要想用到递归法就必须找到f(n)和f(n-1)之间的关系,那么约瑟夫环有没有这样一个规律关系在呢,答案是有的。
我们假设n个人,报数到m的退出,最后剩下人的编号为x。那么第一次报数后,编号为m的人退出,那么剩下的人从编号为M+1继续报数,如果我们把m+1看成1,m+2看成2,....,n看成n-m,1看成是1-m+n,2看成是2-m+n,...,m-1看成是(m-1)-m+n也就是n-1,那么这就变成了一个n-1个人报数为m的约瑟夫环的问题,而且这里最后剩下人就是原来编号为x的那个人,按前面的对应关系f(n)=(f(n-1)+m)%n,这里有个例外,就是如果x=n的话,就会出现f(n)=n,而(f(n-1)+m)%n=((n-m)+m)%n=n%n=0,所以我们把编号加点小技巧,如果n个人编号从0编到n-1,那么f(n)=(f(n-1)+m)%n成立,如果换算成1到n编号,f(n)-1=((f(n-1)-1)+m)%n,也就是f(n)=((f(n-1)-1)+m)%n+1。
有了f(n)=(f(n-1)-1+m)%n+1这个公式,另外我们知道,当n=2时,m为奇数时最后留下的是2,m为偶数时最后留下的是1,我们就可以写出递归程序了,下面是递归法的C语言程序:
#include <stdio.h> int Joseph(int n,int m)/*计算约瑟夫环的递归函数*/ { if(n<=1||m<=1) return -1; if(n==2) { if(m%2==0) return 1; else return 2; } else { return (Joseph(n-1,m)+m-1)%n+1; } } int main() { int n,m,x; scanf("%d %d",&n,&m); x=Joseph(n,m); printf("最后一个数为:%d\n",x); return 0; }
(三)迭代法
有递归法,我们看看有没有相应的迭代法,用的还是刚才的那个公式,方法就是从总人数为2开始一步一步推导到总人数为n时最后应该留下谁。下面是迭代法的C语言程序:
#include <stdio.h> int Josephus(int n,int m)/*计算约瑟夫环问题的迭代法函数*/ { int i; int x,y; if(n<=1||m<=1) return -1; if(m%2==0) y=1; else y=2; for(i=3;i<=n;i++) { x=(y-1+m)%i+1; y=x; } return y; } int main() { int n,m,x; scanf("%d %d",&n,&m); x=Josephus(n,m); printf("最后一个的编号是: %d\n",x); return 0; }
四、总结
模拟法的好处是,可以模拟过程,可以将出列人员按次序输出出来,不足之处是算法复杂度为O(mn),当m和n数值较大时,计算量太大。递归法和迭代法的好处是算法复杂度为O(n),计算量小,运算速度快,但无法模拟过程,无法按次序输出先后的出列人员。
相关文章推荐
- 如何组织构建多文件 C 语言程序(二)
- 如何写好 C main 函数
- 书评:《算法之美( Algorithms to Live By )》
- 动易2006序列号破解算法公布
- C#数据结构之顺序表(SeqList)实例详解
- Ruby实现的矩阵连乘算法
- C#插入法排序算法实例分析
- Lua和C语言的交互详解
- Lua教程(七):数据结构详解
- 解析从源码分析常见的基于Array的数据结构动态扩容机制的详解
- 超大数据量存储常用数据库分表分库算法总结
- C#数据结构与算法揭秘二
- C#冒泡法排序算法实例分析
- C#数据结构之队列(Quene)实例详解
- C#数据结构揭秘一
- C#数据结构之单链表(LinkList)实例详解
- 算法练习之从String.indexOf的模拟实现开始
- C#算法之关于大牛生小牛的问题
- C#实现的算24点游戏算法实例分析
- 关于C语言中参数的传值问题