背景
相传公元一世纪著名犹太历史学家约瑟夫在罗马人占领乔塔帕特後,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
约瑟夫问题描述
设有n个犯人坐成一个圈(编号1~n),从第k(1<=k<=n)个人开始报数,数到m的人将被处决掉,接着从下一个人开始从新报数,数到m的人再被处决,如此循环,直至剩下一名犯人. 求处决犯人的顺序和最后的幸存者编号.
我们可以用代码模拟整个过程从而得出正确的答案:
模拟法
1 /**//// <summary>
2 /// 解约瑟夫问题
3 /// </summary>
4 /// <param name="total">环节点总数</param>
5 /// <param name="eliminate">每次排除数到eliminate的人</param>
6 /// <param name="start">起始节点号</param>
7 /// <returns>最后剩下的节点号</returns>
8 private static int SolveJosephus(int total,int eliminate,int start)
9 {
10 int[] killed = new int[total];
11 int OrderNum = 1;//序号
12 int Suffix = (start-1)%total;//Suffix标记每次计数的开始点,做为killed的下标
13 int CountNum = 0;//killed数
14 int Survivor = 0;
15
16 Console.WriteLine("The kill order is:");
17
18 while(CountNum<total-1)
19 {
20 if(((OrderNum % eliminate)|(killed[Suffix]))==0)
21 {
22 killed[Suffix] = 1;//标记为已kill
23 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 (int i = 0; i < total; i++)
36 if (killed[i] == 0)
37 {
38 Survivor = i;
39 Console.WriteLine("\nSurvivor's number: {0}", i + 1);
40 }
41 return Survivor;
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)个位置上的人也得退出圈子。根据前面的递推公式,显然我们可以通过逆推得到最终解,原先的约瑟夫函数可以写成如下形式:
代数法
private static int SolveJosephus2(int total, int eliminate, int start)
{
int j, k = 0;
int[] count = new int[total + 1];
int[] s = new int[total + 1];
for (int i = 0; i < total+1; i++)
{
s[i] = i;
}
for (int i = 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("The kill order is:");
for (int i = 0; i < total-1; i++)
{
Console.Write("{0,-5:d}",count[i]);
}
Console.WriteLine("\nSurvivor's number: {0}", count[total-1]);
return count[total-1];
}
推论
从上面的分析中已经知道,每次添加一个人到游戏中,原来剩余的那r个人的位置就会“后移”m个单位,因为新加的人使得ta之前的第m个人退出了圈子。所以我们可以进一步地作出这样的推论:
假设n个人玩游戏,最后一个幸存者(即r = 1时)占据了编号为p的位置,而在n + x个人玩的时候,最后一个幸存者占据第y个位置。那么y = (p + mx )MOD (n + x)。
这样我们就可以根据不同的初值n迅速计算出最后一个人的位置了。
参考资料
1.W.W.Rouse Ball and H.S.M.Coxeter, Mathematical Recreations and Essays, Dover, 1987
2.Wikipedia,Josephus Problem,http://en.wikipedia.org/wiki/Josephus_problem
黄季冬
2009年8月4日