约瑟夫环问题(Josephus)

本文介绍了约瑟夫环问题,即人们围成一圈按顺序报数,报到特定数字的人出圈,直至所有人出圈。通过C语言实现的‘猴子选大王’问题作为引入,展示了从数组到循环单链表的解题思路转换。通过循环单链表的创建、按序号查找和Josephus算法,详细解释了如何解决约瑟夫环问题,并给出了完整代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题描述:设有编号 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 是最后两个出局的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值