问题描述:设有编号 1,2,···,n 的 n(n>0)个人围成一个圈,从某个人开始报数,报到 m 时停止报数,报 m 的人出圈,再从他的下一个人起重新报数,报到 m 时停止报数,报 m 的出圈,……,如此下去,直到所有人全部出圈为止。当任意给定 n 和 m 后,设计算法求 n 个人出圈的次序。
在解决这个问题之前,我们先来了解一个小故事。
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。
然而 Josephus 和他的朋友并不想遵从,Josephus 要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
这就很有意思,这也就说明了 16 和 31 这两个数是最后出局的。
其实在我们初学C语言的时候,也碰到过类似的问题。例如:“猴子选大王”问题 :N个猴子坐成一圈 1,2,3,···,N 报数,报道 M 的出列,下一个猴子从 1 开始再报数,如此往复,打印出列的次序及最后一个出列的猴子序号,最后一个出列的猴子为大王!
当时使用数组来解决这个问题。
解题代码:
/* 猴子选大王 */
#include<iostream>
#define N 5 // 猴子数
#define M 3 // 出局数字
using namespace std;
int main()
{
int a[N+1];
int i;
int count=0; // 报数
int out_count=0; // 出局猴子数量
for(i=1;i<=N;i++)
a[i]=i; // 给每个猴子编号 1~N (即将数组 a[1]~a[N] 分别赋值为 1~N)
i=1;
while(out_count!=N)
{
if(a[i]!=0) // 编号为 0 直接跳过
{
count++;
if(count==M)
{
cout<<a[i]<<' ';
if(out_count==N-1)
cout<<'\n'<<"最后一个出局的为:"<<a[i]<<" 称为大王!"<<endl;
a[i]=0; // 出局记为 0
out_count++; // 出局猴子数量 +1
count=0; // 出局一个重新计数
}
}
i++;
if(i==N+1) // 若 i 超过数组最大下标,则 i 从第一个元素重新开始
i=1;
}
}
那么现在再看文章中一开始提到的这个问题,此题要求从第k个人开始报数,而上述“猴子选大王”问题当中是从第1个开始报数,其实换汤不换药,只需对上述代码稍作修改即可。
下面附上完整 code 1:
/* 约瑟夫环问题 */
#include<iostream>
#define n 6 // 总人数
#define m 4 // 出局数字
#define k 3 // 第 k个人开始报数
using namespace std;
int main()
{
int a[n+1];
int i;
int count=0; // 报数
int out_count=0; // 出圈人数数量
for(i=1;i<=n;i++)
a[i]=i; // 给每个人编号 1~n (即将数组 a[1]~a[n] 分别赋值为 1~n)
i=k; // 第i个开始报数
cout<<"最后出队序列编号为:";
while(out_count!=n)
{
if(a[i]!=0) // 编号为 0 直接跳过
{
count++;
if(count==m)
{
cout<<a[i]<<' ';
a[i]=0; // 出圈记为 0
out_count++; // 出圈人数数量 +1
count=0; // 每出圈一个人重新计数
}
}
i++;
if(i==n+1) // 若 i 超过数组最大下标,则 i 从第一个下标重新开始
i=1;
}
cout<<endl;
}
现在我们采用循环单链表去解决这个问题。
基本算法思路:
我们将编号为1,2,···,n 的 n (n>0)个人围成一个圈表示成一个不带头结点的循环单链表 L ,其中 L 指向第一个结点,每个编号对应一个结点。
假设从第 k 个人开始报数,即从第 k 个结点开始报数,报 m 的人出圈,也就是删除报 m 的结点,再从它的下一结点重新报数,报到 m 时删除结点,如此下去,直到所有结点删除完为止。
在解决这个问题之前,我们要首先了解下什么是循环单链表,如何创建呢?
无论是单链表,还是带头结点单链表,从表中的某个结点开始,只能访问到这个结点及其后面的结点,不能访问到它前面的结点,除非再从首指针指示的结点开始访问。如果希望从表中的任意一个结点开始,都能访问到表中的所有其他结点,可以设置表中最后一个结点的指针域指向表中的第一个结点,这种链表称为循环单链表。
下图为带头结点的循环单链表的一般形式:
此题我们创建一个不带头结点的循环单链表,代码如下:
void Create_CircularSingleLinkList(LinkList &L,int n)
{
int i;
LNode *s,*r;
L=NULL;
r=L;
for(i=1;i<=n;i++)
{
s=(LinkList)malloc(sizeof(LNode));
s->data=i;
if(L==NULL)
{
L=s;
r=s;
}
else
{
r->next=s;
r=r->next;
}
}
r->next=L;
}
因为此题要求从第 k 个人开始报数,所以还需要有“按序号查找”函数,代码如下:
LNode *GetLinkList(LinkList L,int i)
{
LNode *p;
int j;
p=L;
j=0;
while(p!=NULL && j<i)
{
p=p->next;
j++;
}
return p;
}
Josephus算法核心代码:
void Josephus(LinkList L,int k,int n,int m)
{
LNode *s;
LNode *t;
s=GetLinkList(L,k-1);
cout<<"所有人出队序列如下:";
while(s->next!=s) // 当只剩下一个结点时退出循环
{
for(int i=1;i<m;i++)
{
t=s;
s=s->next;
}
t->next=s->next;
cout<<s->data<<' ';
free(s);
s=t->next;
}
cout<<s->data<<endl; // 输出最后一个剩下的结点
free(s);
}
下面附上完整 code 2:
#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int DataType;
typedef struct Node
{
DataType data;
struct Node *next;
}LNode,*LinkList;
void Create_CircularSingleLinkList(LinkList &L,int n)
{
int i;
LNode *s,*r;
L=NULL;
r=L;
for(i=1;i<=n;i++)
{
s=(LinkList)malloc(sizeof(LNode));
s->data=i;
if(L==NULL)
{
L=s;
r=s;
}
else
{
r->next=s;
r=r->next;
}
}
r->next=L;
}
LNode *GetLinkList(LinkList L,int i)
{
LNode *p;
int j;
p=L;
j=0;
while(p!=NULL && j<i)
{
p=p->next;
j++;
}
return p;
}
void Josephus(LinkList L,int k,int n,int m)
{
LNode *s;
LNode *t;
s=GetLinkList(L,k-1);
cout<<"所有人出队序列如下:";
while(s->next!=s) // 当只剩下一个结点时退出循环
{
for(int i=1;i<m;i++)
{
t=s;
s=s->next;
}
t->next=s->next;
cout<<s->data<<' ';
free(s);
s=t->next;
}
cout<<s->data<<endl; // 输出最后一个剩下的结点
free(s);
}
int main()
{
LinkList L;
Create_CircularSingleLinkList(L,41);
Josephus(L,1,41,3);
return 0;
}
运行结果
果然,16 和 31 是最后两个出局的。