约瑟夫环(数组和递归)

一、问题描述:一个圈共有n个人(n为不确定的数字),第一个人的编号为0或者1(两个都可以,看你的程序如何编写),假设将第一个人的编号设置为1号,那么第二个人的编号就为2号,第三个人的编号就为3号,第n个人的编号就为n号,现在提供一个数字m作为报数,第一个人开始从1报数,第二个人报的数就是2,依次类推,报到m这个数字的人出局,紧接着从出局的这个人的下一个人重新开始从1报数,和上面过程类似,报到M的人出局,直到N个人全部出局,请问,这个出局的顺序是什么?

我们用一个简单的例子来说明这个问题所描述的意思。

假如我们现在有4个人,编号分别为1、2、3、4,现在第一个人从1开始报数,直到报到2时出局,则根据简单地推算,我们可以得到这样的结果:2,4,3,1。

在这里插入图片描述

二、数组方法

通过上面的分析,我们可以看出这个报数的过程是一个环形的过程。实际上我们可以用环来理解整个过程。

cxl

思路:首先我们要解决出局和未出局的标记问题,我们可以初始化一个数组,规定为0时是未出局的状态,-1为出局的状态(因为初始化为0更方便)。

然后我们用数组的下标来表示每个人的序号,当一个人出局的时候我们就输出下标的值。

接着需要有个记录报数值的变量,并当报到报数m时进行处理。还要有个记录输出了多少的数的计数器来使所有的人都出局。

程序:

#include <stdio.h>
//n总人数;m报数
void Joseph(int a[], int n, int m)
{
	int cnt=0, i=0, k=0;
	//cnt计数器;i编号;k记录报数
	while(cnt<n)//当未输出所有的人时
	{
		i++;
		if(i>n) i=1;//实现在n个人之间的循环,如果超过了n回到开始
		//如n=10,i=11,这时候,让i重新会到1开始在1-10循环报数
		if(a[i]==0)//如果i还没有出局
		{
			k++;//开始报数
			if(k==m)//如果报到了m
			{
				printf("%4d", i);//输出编号,规定格式
				a[i]=-1;//
				k=0;//开始重新报数
				cnt++;//记录出局人数
				if(cnt%10==0) printf("\n");
			}
		}
	}
	if(cnt%10!=0) printf("\n");
}

int main()
{
	int n, m;
	int a[1000]={0};//要给足够大的空间给数组,因为我们不知道n的大小
	printf("请输入人数:");
	scanf("%d", &n);
	printf("请输入报数:");
	scanf("%d", &m);
	Joseph(a, n, m);
	return 0;
} 

上面的程序实际上是用改变k和a[i]的状态去实现环的流程。k不断地重置,a[i]的状态的不断改变来实现这个动态的过程。

三、递归(数学方法)

递归的方法较为难理解(我也不是理解的很清楚),这里大概借鉴一下网上其他人的思路进行一个梳理。

可以发现要模拟整个过程,其实是比较复杂的一个过程,所以我们能不能从数学上去分析,让整个问题解决的效率得到提高?

在这里插入图片描述

从递归的思想出发,我们需要将这个大问题分解为一个个小问题。假定我们的函数关系为f(n,m,i),其中n和m是总人数和报数,i为第i个出局的。我们需要找到递归出口,递归关系。

可以发现当i=1时,出局的人编号(我们从1开始编号)是(m+n)%n。这是一个数学上的规律,与n、m都没有关系,所以i=1是我们的递归出口。

接下来我们需要找到f(n,m,i)和f(n-1,m,i-1)之间的映射关系。

找下规律,以n=2和n=3,且i=n(即最后一个人)时来找规律,改变m的值来观察一下。

在这里插入图片描述

我们找到了一个规律——[f(n,m,i)=f(n-1,m,i-1)+m]%n。

至此,我们已经可以用递归去解决约瑟夫环问题了。

为什么这个规律是这样的?

我们还是以n=9,m=4的第一轮和第二轮所进行的过程举例。

将第一轮结束后剩下的作为旧环,然后再对旧环重新编号,得到新环,可以发现如旧环的5和新环的1,旧环的6和新环的2,旧环1和新环的6之间都是满足这样的关系:旧编号=[新编号+m]%(旧环的人数n)(如上图中即为:旧编号=[新编号+4]%10)。

解释一下上述关系的产生:首先可以看出旧环与新环的编号差之间的关系分为了两种,其中从旧环5到旧环9,与新环的差为4(即m);而从旧环1到旧环3,与新环的差为5(即m+1)。原因在于在第一轮中去除4后,相当于我们将原来的未被去除的8个数从4的位置开始整体向后移4格( 即m的值 ),即新环的1 ~ 8,如果还可以继续对应的话,应该是新环的6,7,8与旧环的10,11,12对应,但是旧环只有1~9,所以后面的10,11,12,我们与9( 即旧环n的值 )计算取余得到1,2,3,对应旧环的1,2,3。

在这里插入图片描述

即上面的关系式:[f(n,m,i)=f(n-1,m,i-1)+m]%n。

![[N(QFAM)N`25]}3YN8BXF}{H.png]]

程序:

#include <stdio.h>
 
int Joseph(int n,int m,int i)
{
    if(i==1)
    {
    	return (m-1+n)%n;
	}
    return (Joseph(n-1,m,i-1)+m)%n;
}
 
 
int main()
{
	int n, m;
	printf("请输入人数(1-100):");
	scanf("%d", &n);
	printf("请输入报数:");
	scanf("%d", &m);
	int i;
	 
	for(i=1;i<=n;i++)
	{
		printf("%4d", Joseph(n,m,i)+1);
		if(i%10==0) putchar('\n');
	}
	if(i%10!=0) putchar('\n');
	return 0;
} 

递归绝对是解决约瑟夫环的最佳方法,但是对于数学上的要求更高,它的时间复杂度和空间复杂度都是最低的,甚至于我们如果只是想求最后一个出局的人只需要一行代码就可以解决。

int f(int n, int m)
{
	return n == 1 ? n : (f(n - 1, m) + m - 1) % n + 1;
}
参考:约瑟夫环问题_小C哈哈哈的博客-CSDN博客
约瑟夫环递归算法(C++)(初学者也能看懂逻辑分析) - Sherry_Yue - 博客园 (cnblogs.com)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值