约瑟夫环
【题目】n 个孩子编号从1到n,顺时针站一圈。从1号孩子开始1,2,3顺时针报数。报到 3 的孩子退出游戏。他的顺时针方向下一个孩子重新开始1,2,3报数。。游戏直到剩下一个孩子,求剩下的这个孩子的编号。
这个问题如果用命令式语言的思考习惯,最先想到的就是模拟。作一个数列,装入1~n 的数,然后,模拟游戏的规则,不断地删去某个元素,直到只剩下一个元素。
haskell 也可以这么做,但这不是它的 style,因为它没有办法修改状态,所以,只能传入一个状态参数,再传出一个变化了的状态。如果希望简洁描述,就要用到 State Monad 了。概念抽象。这此以后再表吧。
先上一个 java 的模拟式想法的:
import java.util.*;
public class T21
{
static int ysf(int n){
List lst = new ArrayList();
for(int i=0; i<n; i++) lst.add(i+1);
while(lst.size()>1){
lst.add(lst.remove(0));
lst.add(lst.remove(0));
lst.remove(0);
}
return (Integer)lst.get(0);
}
public static void main(String[] args){
for(int n=1; n<20; n++){
System.out.println( n + " -> " + ysf(n));
}
}
}
为发挥 Haskell 函数式优势,另选一思路:
假设,我们已经知道了如下事实:
当有 n 个孩子游戏时,编号为 x 的孩子最后剩下。这意味着什么呢?
如果不从1 号开始报数,而是从 5 号开始报数,你能立即推算出最后剩谁吗?
很明显,剩下的编号具体是多少不重要,剩下的号与第一个报数人的编号的距离
才是个重要的信息,它与从哪里开始报数是无关的。
任意两个号码,a 到 b 的距离是 (n + b - a) % n
有了上面的知识,要开动脑筋了:
如果 N-1个孩子游戏,最后剩下号码为 x
现在把第N个孩子加入到队伍中。我们怎么做,才能让他第一次就出列呢?
显然,从N-2 开始玩这个游戏就可以了。到最后剩下号码必为 x
开始报数的号 | 结果剩下的号 |
---|---|
N - 2 | x |
1 | ?? |
现在问题清楚起来,已知从N-2号开始报数,剩下为 x号,
那么,从1号开始报数,剩下哪个号呢?
用递归写出来如下:
ysf :: Int -> Int
ysf 1 = 1
ysf n = let delta = (n + ysf (n-1) - (n-2)) `mod` n
in 1+delta
ok = [show i ++ " -> " ++ show (ysf i) |i<-[1..20]]
main = putStr $ unlines ok
对比一下上面的暴力结果,避免低级失误。
如果报数不是 1,2,3,而是逢 k 出列呢? 稍加改造就可以。。。