【中学】大家一起做游戏 - 约瑟夫问题

【中学】大家一起做游戏

幼儿园的小朋友们刚学习了如何数数,阿姨在下课时组织大家一起玩游戏。规则如下:所有的小朋友绕成一圈,顺序排号,从第一个小朋友开始报数,凡是报到固定数字(例如5)的,都退出该游戏,直到只剩下一位小朋友游戏才中止。
每个小朋友都希望自己能有更多的练习数数的机会,所以都希望成为最终被留下的那位。
现在,请大家帮小朋友们计算一下,在第一次排号的时候排到第几位才能成为最终被留下的小朋友。

输入:

小朋友的个数(<=50) 要被练习的数字

输出:

最终被留下的小朋友的序号

样例:

序号测试输入期待的输出额外进程
14 3↵The left child is NO 1.↵0

思路

我们最直接想到的就是用一个数组模拟小朋友,则数组下标为小朋友序号,对应的值表示小朋友的死活,然后通过模拟得出最后剩下的小朋友

代码1

这段代码来自我的一位同学(找不到我自己最开始写的了,已授权)

#include<stdio.h>

int main(){
	int a[99],b[99],i=0,j=0,m=0,n=0,x=0,y=0;
	scanf("%d %d",&m,&n);
	while(i<m){
		a[i]=i+1,i++;
	}
	while(j<n){
		b[j]=j+1,j++;
	}
	i=0,j=0;
	while(x!=m-1)
	{
		while(i<m)
		{
			if(a[i]!=0)
			{
				a[i]=b[j];
				if(a[i]==n)
				{
					a[i]=0,j=0,i++,x++;
				}
				else{
					y=i,i++,j++;
				}
			}
			else{
				i=i+1;
			}
		}
		i=0;	
	}
	printf("The left child is NO %d.\n",y+1);
	return 0;
} 

同学们可以试着阅读一下
这段代码里使用数组a模拟小朋友,数组b模拟要数的数字,整个逻辑是没问题的,但最后却失败了,因为超时
那我们就来看看是哪些部分影响了复杂度

首先,就是开头的两个循环

while(i<m){
		a[i]=i+1,i++;
	}
	while(j<n){
		b[j]=j+1,j++;
	}

本意是想要初始化数组,但从整体考虑未免有些多余:

  • 一是纵览整段代码,b的存在完全可以用一个计数器代替
  • 二是数组a完全可以使用下标作为小朋友的序号,而不用依次赋值,并且所赋的值也没有在后续的代码中表现出应有的作用
  • 三是,既然数组初始化值全为0,那不妨就将0作为小朋友的“活”,不一定要按照经验1“活”0“死”

其次,我们看到这段代码用了两层循环来模拟圆环,但实际上我们可以通过计数器加条件判断将这个循环减少到一层

将上述问题修改后,我们得到

代码2

#include <stdio.h> 
main()
{
	int a[51] = { 0 };
	int boy, num, death = 0, i, num_ = 0;
	scanf("%d%d", &boy, &num);
	for (i = 1; death != boy; i++)
	{
		if (i > boy) i = 1;
		if (a[i] == 0) num_++;
		if (num_ == num)
		{
			num_ = 0;
			a[i] = 1;
			death++;
		}
	}
	printf("The left child is NO %d.\n", i - 1);
}

这次看起来就简洁很多了,测试也可以完美通过

课外思考

我们之前说过,对于这类问题可以先尝试优化数学模型,那道题是否也可以呢?
答案是肯定的,这道题其实就是“约瑟夫问题”,可以使用递归算法:

m 个小朋友,数字为 n ,先报一次数,报到 n 的小朋友(序号一定是 n%m )拉出去毙了 退出游戏,此时还剩下 m-1 个小朋友
这不就相当于 m-1 个小朋友,数字为 n,求最后剩下的序号吗,只不过我们要对序号作一些变换,原来序号是 n%m+1 的小朋友,现在的新序号是1,原来序号是 n%m-1 的小朋友现在序号是 m-1 ,也就是:

最初离开后新序号
11m-n%m+1
22m-n%m+2
33m-n%m+3
··················
n%m-1n%m-1m-1
n%m--
n%m+1n%m+11
··················
m-2m-2m-n%m-2
m-1m-1m-n%m-1
mmm-n%m

观察规律可以得到 x = ( x’ + n - 1 ) % m + 1
我们假设F(m)为m个小朋友时的答案,则 F(m) = ( F(m-1) + n - 1 ) % m + 1
详细过程可以参考 这或许是你能找到的最详细约瑟夫环数学推导! - 负雪明烛的文章 - 知乎
这篇文章也提供了C语言的解法,同学们可以看看和我的有什么不同

代码

#include <stdio.h>
int FF(int x, int y)
{
    if (x == 1) return x;
    else return (FF(x - 1, y) + y - 1) % x + 1;
}
int main()
{
    int boy, num;
    scanf("%d%d", &boy, &num);
    printf("The left child is NO %d.\n", FF(boy, num));
}

事实上,递归的次数是固定的,所以我们还可以写成

#include <stdio.h>

int main()
{
    int boy, num, f = 1;
    scanf("%d%d", &boy, &num);
    for (int i = 2; i <= boy; i++)
    {
        f = (f + num - 1) % i + 1;
    }
    printf("The left child is NO %d.\n", f);
}
  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值