约瑟夫问题——其中一种算法的理解(神级代码)

约瑟夫问题

      据说著名犹太历史学家 Josephus有过以下的故事:

       在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。所以Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

一般性描述

       为了探索约瑟夫问题的奥秘,我们不妨现将问题简化。

       已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为1的人开始报数,数到3的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌仅剩下一个人。

神级代码(摘抄)

#include<stdio.h>
int main()
{
	int i,r=0,people;
	scanf("%d",&people);
	for(i=2;i<=people;i++)
		r=(r+3)%i;
	printf("%d\n",r+1);
	return 0;
}

个人理解

上面的代码十分简洁地解决了这个问题,它的核心到底是什么,我来谈谈我的看法。研究这个问题,我们可以统一将编号减一(即0,1,2……n-1),数到2的人被淘汰,这样进行取余时可以与编号对应。

那我们先取n=10吧,首先将编号展开:

①【0,1,2,3,4,5,6,7,8,9】

由此可知  编号2将被淘汰。更新一次列表:

②【0,1,3,4,5,6,7,8,9】

此时报数将从编号3开始,我们不妨再将它设为0,即把所有编号减3,为了得到圆桌的循环,我们将前面的两个负数加上上一次的人数10,可以得到:

③【7,8,0,1,2,3,4,5,6】

继续淘汰2号,重复上一步的步骤

④【4,5,6,7,0,1,2,3】

一直到最后我们得到【1,0】,这时候我们没有2了怎么办?

因为这是圆桌循环,模拟出来便是 编号0数0,编号1再数1,回到编号0数1(被淘汰),不难发现淘汰的人就是2%2=0。

再进一步我们可以推出当报数上限m-1(本例中为m-1=2)≥剩下的人数n时,淘汰的编号是(m-1)%n。

          接下来编号1再变为0,得到【0】

          此时的编号0就是胜利者。

而神级代码的核心就是从这个0出发,找到了它原来的编号,具体实现步骤如下:

比如,表④中的0加上3得到它在表③中的编号3,再加上3得到它在表②中的编号⑥。 

  1. 从上面的分析中,我们知道每次报数我们都会减3产生一个新的列表,那么新编号加上3就可以得到它在上一列表中的编号
  2. 我们从胜利者编号0出发,先加上3,但是上一列表显然只有0,1,我们从上面的分析中得到启示,那不妨这样处理:

    (初处理)上一列表 【3】  补充完整后【0,1,2,3】

    (实际步骤)上一列表【0,1,0(淘汰),1】 


    这样我们发现,越界后还是取余,被除数是上一列表的元素个数,这样就能得到正常的编号。

  3. 当没有发生越界情况时,我们仍然可以像步骤2那样取余,结果无影响。因此我们就知道了神级代码中的:

                              r=(r+3)%i 的具体含义    (r 初始化为0说明最后剩下编号0,对编号0进行回溯,i初始化为2是因为上一列表还有两个元素,然后i自增也就是列表中的元素慢慢增加,直到达到原来的人数,完成后记得r+1,毕竟一开始减了1)

进阶

神级代码解决了只剩1人时的情形,但是约瑟夫问题最后剩下2人,那又该如何处理呢?

不难发现,我们只需要对 i 与 r 进行一定的修改,即可达到目的(甚至达到更多的要求)。具体就不多说了,直接贴上代码。

#include<stdio.h>
int main()
{
	int jose(int sum,int numb,int rest,int old);
	int n,old,people,left;
	printf("请输入总人数:\n");
	scanf("%d",&people);
	printf("请输入口令上限:\n");
	scanf("%d",&n);
	printf("请输入留下的人数:\n");
	scanf("%d",&left);
	printf("留下的人是:\n");
	for(old=0;old<left;old++)	
		printf("%d\n",jose(people,n,left,old));
	return 0;
}
int jose(int sum,int numb,int rest,int old)// sum是总人数,numb口令上限
{                                         //rest是要求剩下的人数,old是剩下的编号
	int temp=rest;//temp作为临时变量,因为rest会修改
	for(rest=temp+1;rest<=sum;rest++)
		old=(old+numb)%rest;
	return old+1;
}


 

由此不难发现为啥Josephus将朋友与自己安排在第16个与第31个位置,从而逃过了这场死亡游戏。

灵感来源 

https://blog.youkuaiyun.com/okasy/article/details/79503398

感谢这位博主的启发!

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值