约瑟夫环问题详解
很久以前,有个叫Josephus的老头脑袋被门挤了,提出了这样一个奇葩的问题:
已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列
这就是著名的约瑟夫环问题,这个问题曾经风靡一时,今天我们就来探讨一下这个问题。
这个算法并不难,都是纯模拟就能实现的。
思路一:
用两个数组,mark[10000]和num[10000],mark这个数组用来标记是否出队,num这个用来存值,然后每次到循环节点的时候就判断mark是否为0(未出队),为0则输出num[i],并标记mark[i]=1,直到所有的num都出队。
附上C++代码:
#include<iostream> #include<cstring> #include<cstdio> #include<cstdlib> #include<cctype> #include<cmath> #include<algorithm> using namespace std; char num[1000]; bool mark[1000]; int main() { while(true) { memset(mark,0,sizeof(mark)); int n; int k,m; int i,j; int del=k-1; cin>>n>>k>>m; for(i=0;i<n;++i) { cin>>num[i]; } int cnt=n; for(i=cnt;i>1;--i) { for(int j=1;j<=m;) { del=(del+1)%n; if(mark[del]==0) j++; } cout<<num[del]<<" "; mark[del]=1; } for(i=0;i<n;++i) { if(mark[i]==0) break; } cout<<endl<<"The final winner is:"<<num[i]<<endl; } return 0; }
思路二:
用一个数组就可,每次到了循环节点了就将num[i]输出,然后将后面的值往前移动一位,直到所有的节点出列。
虽然说这种方法比第一种方法所用的内存有所优化,但时间上却增加了好多,这也体现了程序设计中用空间来换时间的思想。
附上代码:
#include<iostream> #include<cstring> #include<cstdio> #include<cstdlib> #include<cctype> #include<queue> #include<cmath> #include<algorithm> using namespace std; char num[1000]; int main() { while(true) { int n; int k,m; int i,j; cin>>n>>k>>m; int del=k; for(i=0;i<n;++i) { cin>>num[i]; } int cnt=n; for(i=cnt;i!=1;--i) { del=(del+m-1)%i; cout<<num[del]<<" "; for(j=del;j<=i-2;++j) { num[j]=num[j+1]; } } cout<<endl<<"The final winner is:"<<num[0]<<endl; } return 0; }
思路三:
也就是书上说的用循环链表,因为整个过程都是一个完整的环,所以用循环链表是一个不错的选择,但是就我个人来说我很不喜欢用链表,因为现在的程序设计题目中一般都是卡时间,基本上卡内存的很少,所以链表的优势根本体现不出来,当然方法还是要掌握的.
思路:构建一个循环链表,每个结点的编号为0, 1, ...... n-1。每次从当前位置向前移动m-1步,然后删除这个结点。最后剩下的结点就是胜利者。给出两种方法实现,一种是自定义链表操作,另一种用是STL库的单链表。不难发现,用STL库可以提高编写速度。
创建的循环链表的结构图:
解决约瑟夫环问题的过程:
附上我的代码:(好久没写指针,感觉有点生疏了,挣扎了半天才弄出来)
#include<iostream> #include<cstring> #include<cstdio> #include<cstdlib> #include<cctype> #include<queue> #include<cmath> #include<algorithm> using namespace std; struct Node { char num; Node *next; }; Node *head,*p1,*p2; void Make_Linklist(int n)//构造循环链表 { int i,j; head=p1=(Node *)malloc(sizeof(Node));//开辟一段大小为sizeof(Node)的空间,并将地址给p1和head cin>>(*p1).num; (*head).num=(*p1).num; for(i=2;i<=n;i++) { //p2用来开辟结点,p1指向最后一个结点,对所指的内容赋值前必须先对指针分配内存 p2=(Node*)malloc(sizeof(Node)); //Node 新的内存空间,并把地址给p2 cin>>(*p2).num; p1->next=p2; p1=p1->next; } p1->next=head;//将带链表首尾连起来生成循环链表 } void Josephus(int n,int k,int m) { int cnt=0; int i,j; if(n==1) { cout<<(*p1).num; } else { // for(i=0;i<k;i++)//移动到第k个位置,从第k个位置开始 // { // head->next=head->next->next; // } while(cnt<n-1) { for(i=1;i<=m-2;i++)//找到要出队的上一个head { head = head->next; } Node *temp; temp=head->next; printf("%c ",(*temp).num); head->next = head->next->next;//通过将指针指向移至下一个,来删除该节点 head = head->next; cnt++; } cout<<endl<<"The winner is:"<<(*head).num<<endl; } } int main() { while(true) { int n,k,m; cin>>n>>k>>m; Make_Linklist(n); Josephus(n,k,m); } return 0; }
方法二:数学推导(这个有一定的难度,看了好久的博客才看懂)
推导: O (0+5)%2 = 0 o O (0+5)%2 = 1 O o o (1+5)%3 = 0 o O o o (0+5)%4 = 1 o O o o o (1+5)%5 = 1 O o o o o o (1+5)%6 = 0 o o o o o O o (0+5)%7 = 5 o o O o o o o o (5+5)%8 = 2 o o o o o o o O o (2+5)%9 = 7
前面的字母代表当前有多少个节点,后面的算式代表当前删除的节点在重映射后的位置。
考察约瑟夫环的变化过程,当第k个节点存在的时候,序列理应如此:1,2,3,4,5,6,7,8,9
将元素左移3次,得到3,4,5,6,7,8,9,1,2
接着删除第一个,得到4,5,6,7,8,9,1,2
继续左移3次,得到6,7,8,9,1,2,4,5
删除第一个:7,8,9,1,2,4,5
左移:9,1,2,4,5,7,8
删除:1,2,4,5,7,8
左移:4,5,7,8,1,2
删除:5,7,8,1,2
左移:8,1,2,5,7
删除:1,2,5,7
左移:5,7,1,2
删除:7,1,2
左移:2,7,1
删除:7,1
左移:7,1
删除:1
按照这个手法,不断移动并删除,元素最终会减少到1。检查某个点何时被删除的测试也可以如此进行,不断左移,排在第一个的元素就是被删除的。
因此,思考逆过程,从1个元素开始,每个元素就应该是右移k位,不断右移,最终就能找回原始的约瑟夫环的位置:
1元素:1<------------------>1, (1+3)%1 eq 1
2元素:1,2<---------------->7,1, (1+3)%2 eq 2
3元素:1,2,3<-------------->7,1,2, (2+3)%3 eq 2
4元素:1,2,3,4<------------>1,2,5,7, (2+3)%4 eq 1
5元素:1,2,3,4,5<---------->5,7,8,1,2, (1+3)%5 eq 4
6元素:1,2,3,4,5,6<-------->1,2,4,5,7,8, (4+3)%6 eq 1
7元素:1,2,3,4,5,6,7<------>7,8,9,1,2,4,5, (1+3)%7 eq 4
8元素:1,2,3,4,5,6,7,8<---->4,5,6,7,8,9,1,2, (4+3)%8 eq 7
9元素:1,2,3,4,5,6,7,8,9<-->1,2,3,4,5,6,7,8,9,(7+3)%9 eq 1
而做这个事情的,就是(num+k)%line_length。
另外还有双向约瑟夫环问题,比这个要复杂一些,在这里就不展开了。
整理了一上午,终于弄好了,不过还是很值得的.
作者:北岛知寒
出处:https://www.cnblogs.com/crazyacking/p/3668364.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?