Bootstrap

约瑟夫问题概述

我在很久之前就已经接触了约瑟夫问题了,但是最近终于找到了时间效率比较优秀的解决办法,写一篇博客以纪念一下。

什么是“约瑟夫问题”?

以下内容来自百度百科:(可以忽略以下三段内容)

约瑟夫问题(有时也称为约瑟夫斯置换,是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。又称“丢手绢问题”.)

关于它的故事:

据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39
个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus
和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

另一个故事:

17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15
个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。

下文中所介绍的问题与上面的两个类似,我们称它为“约瑟夫序列模型”。它的内容如下:

若干个人排成一圈,按照顺时针编号为1~n。从一号开始对环进行如下处理:1号人跳过,2号出环,3号跳过,4号出环,5号跳过(即每隔一个人,出圈一个人)…又因为所有的人站成一个环,最终所有的人一定都会出环(最后剩下的那个人被看成是最后一个出环的人)。用这种方法,我们把每个人按照出圈次序排序,就得到了一个“出圈序列”,下文中称其为“约瑟夫序列”。

约瑟夫问题概述

如上图, n=7 时的约瑟夫序列为: {An}={2,4,6,1,5,3,7}

问题1:快速计算约瑟夫序列末尾元素

——每个人都想成为笑到最后的人,所以他总是想找到最后一个出圈的位置。

本题的一种暴力做法是用链表去模拟,但其时间效率比较差(如果只是求末尾元素的话。)

首先,无论n是奇数还是偶数,一个编号为偶数的位置一定不可能成为最后一个出圈的位置,因为在第一趟走的时候这些位置就会出圈,剩下的是所有的奇数。

此时我们不妨将所有的奇数重新编号为 1..n2 。你可以找到重新编号后的最后一个出圈的编号,然后再把它还原回它原来的编号。

我们不妨令 J(n) 表示长度为n的约瑟夫序列的末尾元素。

首先分析当n为偶数时的情况(令 n=2n ):

第一步:所有的偶数位置被弹出。

第二步:将奇数序列 {1,3,5,..,2n1} 通过函数 f(x)=x+12 映射到新的编号 {1,2,3,...,n} 。然后递归的求解 J(n) ,再通过 f(x) 的反函数 f1(x)=2x1 得到原序列中这个位置中的编号:

即: J(2n)=2J(n)1|nN

然后再分析当n为奇数时的情况(令 n=2n+1 ):

第一步:将所有偶数位置弹出。

第二步:因为序列的总长度为奇数,所以下一个出圈的一定是编号为1的那个人。

第三步:把现在剩下的人的序列 {3,5,7,..,2n+1} 按照函数 f(x)=x12 重新编号为 {1,2,3,..,n} ,然后递归求解 J(n) ,再通过 f(x) 的反函数 f1(x)=2x+1 得到原序列中这个位置的编号:

即: J(2n+1)=2J(n)+1|nN

边界条件: J(1)=1 (只有一个人的话,他当然就是被剩下的人。)

程序写出来近乎脑残:

int J(int n){
    if(n==1)return 1;
    if(n%2==0)
        return 2*J(n/2)-1;
    return 2*J(n/2)+1;
}

它的时间复杂度为 O(lg(n)) ,因为n每次至少缩减为原范围的一半。

关于问题1的进一步探索

打一个表,看看函数 J(n) 还有什么神奇的性质:

#include<cstdio>

int J(int n){
    if(n==1)return 1;
    if(n%2==0)
        return 2*J(n/2)-1;
    return 2*J(n/2)+1;
}

int main(){
    printf("  i :");
    for(int i=1;i<=15;i++)
        printf("%5d",i);
    printf("\n");
    printf("J(i):");
    for(int i=1;i<=15;i++)
        printf("%5d",J(i));
    printf("\n");
    return 0;
}

可以打出来这样的一个表:

  i :    1    2    3    4    5    6    7    8    9   10   11   12   13   14   15
J(i):    1    1    3    1    3    5    7    1    3    5    7    9   11   13   15 

不难发现一些神奇的规律

序列J可以看成是若干个奇数等差数列的拼接 {{1},{1,3},{1,3,5,7},{1,3,5,7,9,11,13,15}...} 而这些奇数等差数列的长度恰好为2的若干次幂: {1,2,4,8...}

这个性质可用 J(2m+l)=2l+1mN,0l<2m 表示。

这个性质可以用数学归纳法证明:

定义命题 P(x)J(2x+l)=2l+1mN,0l<2m

1. x=0 该命题显然成立。

2.当该性质已经关于 1..x1 成立,那么:

(1)令 l 为偶数:

P(x):J(2x+l)=2J(2x1+l2)1=2(2×l2+1)1=2l+1

(2)令 l 为奇数:

P(x):J(2x+l)=2J(2x1+l12)+1=2(2×l12+1)+1=2l+1

p.s. 因为 0l<2m 所以有 0l2<2m1 l 为偶数)和0l12<2m1 l 为奇数)

因此得证。

问题2:快速计算某一个人的出圈时间

不妨令f(n,k)表示一个长度为n的约瑟夫序列中,元素k在其中的位置。

k 是偶数的时候,(想都不用想)f(n,k)=k2

k 是奇数的时候呢?

1.n是偶数,令n=2n

同之前求最后一个出队人的方法一样,先去掉所有编号为偶数的人,然后给剩下的人重新编号为 1..n 。编号函数为 g(x)=x+12 。求出这个人转换后的编号在n个人中的出圈时刻再+n即为答案。

即: f(2n,k)=n+f(n,k+12)

2.n是奇数,令 n=2n+1

方法同上,先去掉所有的偶数以及1,将 {3,5,7,..,2n+1} 重新编号为 {1,2,3,..,n} 。编号函数为 g(x)=x12 。计算结果+(n+1)即为答案。

即: f(2n+1,k)=n+1+f(n,x12) 以及 f(2n+1,1)=n+1

这样看可能有点复杂,我们不妨令 f(n,0)=0 ,这样的话, f(2n+1,1)=n+1 的情况就会被并入 f(2n+1,k)=n+1+f(n,x12) 中。

程序还是那么的水:

int f(int n,int k){
    if(k%2==0)return k/2;//k=0 included
    if(n%2==0)return n/2+f(n/2,(k+1)/2);
    return (n+1)/2+f((n-1)/2,(k-1)/2);
}

很显然的是时间复杂度仍然是 O(lg(n))

如有谬误,敬请指正!

;