算法(一):Josephus问题

来源:互联网 发布:找出两列数据的重复项 编辑:程序博客网 时间:2024/05/31 05:28

Josephus是一个著名的犹太历史学家,他有过这样的故事(来自互动百科,http://www.baike.com/wiki/Josephus):在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3个人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。Josephus想了一个办法,帮助自己和朋友逃过了这场死亡游戏。

由这个故事衍生出了一个著名的问题,叫Josephus问题:假定有n个人,编号为1到n,由第1个人开始报数,每报数到m的人就出列,直到剩下最后一个人,求最后一个人的编号,即:
     输入:编号为1到n的n长度数组,参数m
     输出:幸存者编号f(n)
在数学上,通常要得到一个复杂问题的解,可以从一些小的特例出发,再看这些特例的结论能否得到推广,因此,我们可以从m为2的情况开始。

我们先看一个m为2的情况下的特例,我们假定n = 2^k,由此得到:
     第一次遍历后,剩下n/2个人;
     第二次遍历后,剩下n/4个人;
     。。。。。。
     第k次遍历后,剩下1个人,这个人的编号为1(因为每次遍历淘汰的都是偶数位的人,编号为1的人始终处于奇数位)。
这样,当n为2的k次方的情况下,我们可以得到:f(n) = 1。
那么,接下来看看能否将这个特例推广开来。当n不为2的k次方的情况下,我们可以将n表示为:n = 2^k + u,并且u >= 0且u < 2^k,这样,我们很容易得到:n - u = 2^k,由此,我们可以推断:当n减少了u后,问题可以规约到n = 2^k的情况。
由于遍历中每2个人就会淘汰一个人,因此要减少u个人,需要遍历2u个人,那么我们从2u + 1开始,剩下的数将是2^k,并且:
     由于:u < 2^k
     所以:2u < 2^k + u,即2u < n
     因此,2u + 1 <= n
由此,可以得出结论:当m为2时,f(n) = 2u + 1 (u = n - 2^k,并且u >= 0且u < 2^k)。

接下来,我们来看m为2的结论能够得到推广,进一步来看m为3的情况。我们假定n = 3^k,则:
     第一次遍历后,剩下(2/3)*n个人;
     第二次遍历后,剩下(2/3)^2*n个人;
     。。。。。。
     第k次遍历后,剩下(2/3)^k*n个人,带入n = 3^k,得到剩下2^k个人。
这并没有达到我们希望的效果,也就是当m为2时得到的结论并不能推广到m为3时的情况,我们需要考虑其它办法。

我们在每个人通过(未淘汰)之后,就为它分配一个新编号,该新编号为当前最大值加1,这样,我们就能得到:1变为n+1,2变为n+2,3被淘汰,4变为n+3,5变为n+4,6被淘汰,。。。。。。,3k+1变为n+2k+1,3k+2变为n+2k+2,3k+3被淘汰,。。。。。。,3n被淘汰(或者幸存)。为什么最后一个编号是3n?因为每数3个人淘汰一个人,n个人数3n个人则全部淘汰。
看下面的一个例子:

这样,如果我们通过最后一个编号3n,找到3n在上一轮中的值,那么我们就有可能一直向上,找到3n最初的值,即f(n)。
在开始之前,先给出几个定义:
 1)floor(x):表示x的下限,如 floor(3.15) = 3;
 2)ceil(x):表示x的上限,如 ceil(3.15) = 4,
 3)prev(n):表示n上一轮中的值。
我们假定当前的值为N,如果N>n,则N必然存在prev(N),且:N = n + 2k + 1 或者 N = n + 2k +2
由此,我们得到:k = floor((N - n - 1) / 2)。
而由先前的编号分配方式我们得到:prev(N)=3k + 1 或者 prev(N)=3k + 2。
如果我们定义:N = n + 2k + a(a为1或者2),则:
 prev(N) = 3k + a
               = 3k + (N - n - 2k)
               = k + N - n
               = floor((N - n - 1) / 2) + N - n。
我们就得到了prev(N)的一个表达式,由此我们从3n出发,就可以:
     N = 3n
     while N > n do N = floor((N - n - 1) / 2) + N - n
     f(n) = N
这个表达式已经可以保证在n很大的情况下快速计算出f(n),但它还不够简单,我们可以通过一些技巧来简化它。

令:D = 3n + 1 - N
我们考虑用D来替换N,由于N是从3n出发,直到小于n,那么D就应该是从1出发,直到2n(因为当N=3n时,D=1,而当N=n+1时,D=2n),我们假定当前N对应的值为D(D = 3n + 1 - N),则:
 prev(D) = 3n + 1 - prev(N)
               = 3n + 1 - (floor((N - n - 1) / 2) + N - n)
               = 3n + 1 - (floor(((3n + 1 - D) - n - 1) / 2) + (3n + 1 - D) - n)
               = n + D - floor((2n - D) / 2)
               = D - floor(-D / 2)
               = D + ceil(D / 2) 
               = ceil(3/2 * D)。
(注:上面用到了一个技巧 floor(-x) = -ceil(x))
这样,我们得到:
     D = 1
     while D <= 2n do D = ceil(3/2 * D)
     f(n) = 3n + 1 - D
啊哈,一个更简单的表达式出现了。

我们是否能将m为3的结论推广到m为任意值的场景呢?
当m为任意值时,根据上面同样的定义方式,我们能够得到:1变为n+1,2变为n+2,。。。,m-1变为n+m-1,m淘汰,。。。。。。,mk+1变为n+(m-1)k+1,mk+2变为n+(m-1)k+2,。。。,mk+m-1变为n+(m-1)k+m-1,mk+m被淘汰,。。。。。。,mn被淘汰(或者幸存)。
根据同样的推导过程我们能够得到(有兴趣的朋友可以自己推导一下):
     D = 1
     while D <= (m - 1)n do D = ceil(m/(m-1) * D)
     f(n) = mn + 1 - D
到这里整个问题结束。

参考资料:
     《concrete mathematics》second edition

 

0 0
原创粉丝点击