背景
相传公元一世纪著名犹太历史学家约瑟夫在罗马人占领乔塔帕特後,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
约瑟夫问题描述
设有n个犯人坐成一个圈(编号1~n),从第k(1<=k<=n)个人开始报数,数到m的人将被处决掉,接着从下一个人开始从新报数,数到m的人再被处决,如此循环,直至剩下一名犯人. 求处决犯人的顺序和最后的幸存者编号.
我们可以用代码模拟整个过程从而得出正确的答案:
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->1

/**////<summary>2
///解约瑟夫问题3
///</summary>4
///<paramname="total">环节点总数</param>5
///<paramname="eliminate">每次排除数到eliminate的人</param>6
///<paramname="start">起始节点号</param>7
///<returns>最后剩下的节点号</returns>8
privatestaticintSolveJosephus(inttotal,inteliminate,intstart)9


{10
int[]killed=newint[total];11
intOrderNum=1;//序号12
intSuffix=(start-1)%total;//Suffix标记每次计数的开始点,做为killed的下标13
intCountNum=0;//killed数14
intSurvivor=0;15

16
Console.WriteLine("Thekillorderis:");17

18
while(CountNum<total-1)19


{20
if(((OrderNum%eliminate)|(killed[Suffix]))==0)21


{22
killed[Suffix]=1;//标记为已kill23
Console.Write("{0,-5:d}",Suffix+1);24
CountNum++;25
OrderNum++;26
}27
if(killed[Suffix]!=1)28


{29
OrderNum++;30
}31
Suffix++;32
Suffix%=total;33
}34

35
for(inti=0;i<total;i++)36
if(killed[i]==0)37


{38
Survivor=i;39
Console.WriteLine("\nSurvivor'snumber:{0}",i+1);40
}41
returnSurvivor;42
}
当然, 这并不是最好最高效的解法.这里我们回到广义的约瑟夫问题来分析一下:
如果有n个人围成一圈而坐,每个人的位置都带编号,编号从1到n(没有重复的),从第k个位置开始数数,当数到m时,那个人退出圈子,再从退出的那个人的下一个位置开始数(假定是顺时针数的),一直到剩下r个人。
进一步分析
n个人玩这个游戏的时候,假设最后剩下的r个人中,其中一个人占据了第p个位置,那当我们以n + 1个人开始玩游戏的时候,显然,这第n + 1个人希望被安排到第p + m个位置,因为如果第p个人是安全的,那么至少这第(p + m)MOD (n + 1)个位置上的人也是安全的,他的理由是“既然现在是n + 1个人玩,仍然只能留r个人,那么只要在n个人剩余的r个人里面有一个人在我之前退出就可以了,所以我要加在这第r个人中某个人的后面m处”。对应的,如果n个人玩的时候,第q个位置上的人退出了圈子,那n + 1个人玩的时候第(q + m)MOD (n + 1)个位置上的人也得退出圈子。根据前面的递推公式,显然我们可以通过逆推得到最终解,原先的约瑟夫函数可以写成如下形式:
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->
privatestaticintSolveJosephus2(inttotal,inteliminate,intstart)

{
intj,k=0;
int[]count=newint[total+1];
int[]s=newint[total+1];
for(inti=0;i<total+1;i++)

{
s[i]=i;
}
for(inti=total;i>1;i--)

{
start=(start+eliminate-1)%i;
if(start==0)
start=i;
count[k]=s[start];
k++;
for(j=start+1;j<=i;j++)
s[j-1]=s[j];
}
count[k]=s[1];
Console.WriteLine("Thekillorderis:");
for(inti=0;i<total-1;i++)

{
Console.Write("{0,-5:d}",count[i]);
}
Console.WriteLine("\nSurvivor'snumber:{0}",count[total-1]);
returncount[total-1];
}
推论
从上面的分析中已经知道,每次添加一个人到游戏中,原来剩余的那r个人的位置就会“后移”m个单位,因为新加的人使得ta之前的第m个人退出了圈子。所以我们可以进一步地作出这样的推论:
假设n个人玩游戏,最后一个幸存者(即r = 1时)占据了编号为p的位置,而在n + x个人玩的时候,最后一个幸存者占据第y个位置。那么y = (p + mx )MOD (n + x)。
这样我们就可以根据不同的初值n迅速计算出最后一个人的位置了。
参考资料
2.Wikipedia,Josephus Problem,http://en.wikipedia.org/wiki/Josephus_problem
黄季冬
2009年8月4日
本文介绍了约瑟夫问题的历史背景及其解决方法,包括通过模拟法和代数法两种途径求解,并探讨了如何通过递推公式高效求解广义约瑟夫问题。

521

被折叠的 条评论
为什么被折叠?



