本来我是不想学这个东西的,但是谁让他考试考到了呢?
约瑟夫问题:
n
个人(编号为0,1,...,n-1
)围成一个圈子,从0
号开始依次报数,每数到第m
个人,这个人就得自杀,之后从下个人开始继续报数,直到所有人都死亡为止。问最后一个死的人的编号。
方法1:暴力 O(nm)
会打码的都知道。
方法2:白书P65 时间O(n) 空间O(1)
这是一个很容易理解的算法。
如果只关心最后一个人的编号,可以用$f(n)$表示0~n-1的n个数,从0开始每m个数删除一个,最后留下来的数字编号,那么有递推式$f(n)=(f(n-1)+m) mod n$。
怎么得来的呢?我们先用几个例子来看一看。
当n=5,m=3时:
0 1 2 3 4
死掉一个2:
0 1 3 4
相当于:
3 4 0 1
用这样排列方式的原因是我们可以发现
0 1 2 3
与
3 4 0 1
是可以一一对应的。
具体就是说$(0+3) mod 5 = 3$、$(1+3) mod 5 = 4$、$(2+3) mod 5 = 0$、$(3+3) mod 5 = 1$。
那么当m相同时,如果我们知道n=4的答案,就可以直接用此规律算出n=5的答案。
方法3:时间O(log n),空间O(log n)
仍是递推,但是并不是很复杂。
我们现在思考每轮(如果目前有n个人,0到n-1全都报一遍数叫做一轮)前后的情况。
如果n=8,m=3,则:
0 1 2 3 4 5 6 7
死掉2和5:
0 1 . 3 4 . 6 7
重新编号:
2 3 . 4 5 . 0 1
注意,我专门把死掉两个人的位置空出来,是因为这样子更直观。
我们把重新编号的序列分为两部分。一部分是2~5,一部分是0~1。
假如在重新编号后最后死的人的编号是$x$,我们需要得到的答案是$Ans$。
那么有:
$egin{cases}& Ans =x + ( lfloor frac{n}{m} floor imes m ) , x leq n mod m \ & Ans = x - (n mod m) + ( lfloor frac{x-n mod m}{m-1} floor ) , x > n mod m end{cases}$
但是注意,在$n<m$的时候,这样做就没有意义了,所以这时候只能用方法2的递推式了。
方法4:时间O(log n),空间O(1)
这个方法不仅可以求最后一个死的人的编号,而且可以求第k个(从0开始数)死的人的编号,而且写法贼简便,复杂度贼优秀,是当之无愧的好算法。
但是就没有前几个算法那么好懂了。
首先我们知道第$k$个自杀的人就是第$(k+1) imes m-1$次报数的人,根据他之前每次报数的时刻来确定他的编号。
例如n=5,m=3,k=5:
报数的时刻:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
人的编号: 0 1 2 3 4 0 1 3 4 1 3 1 3 3 3
我们知道第2、5、8、11、14个报数的要自杀。
我们设第$x=a imes m+b(b<m)$次自杀的人的编号为$y$。
$x$次报数后,一共死了$a=lfloor frac{x}{m} floor$人。
如果$y$这个人没有在这次报数后自杀,那么他还需要等待剩下$n-a$人报数后才会再报数。
即他下次报数将是 $t=x+n-a=a imes m+b+n-a=a imes (m-1)+b+n$ 时刻。
那么如果我们知道这一次他报数的时刻$t$,反过来求上一次报数的时刻$x$呢?
那就是:$x=t-n+a=t-n+lfloor frac{t-n}{m-1} floor$。
我们知道如果知道第$k$个人最后一次报数的时刻,然后反着推他上一次报数的时刻,一直到时刻数$< n$(因为是从0开始的)的时候,时刻数就是他的编号。