Josephus问题(最后一个退出的人)

[问题:]如果有 m 个人围成一圈而坐,每个人的位置都带编号,编号从 1 m (没有重复的),从第一个位置开始数数,当数到 n 时,那个人退出圈子,再从退出的那个人的下一个位置开始数(假定是顺时针数的),问最后一个退出的人编号是几?

 

[问题起源:]据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特後,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。

  然而Josephus 和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

[单循环链表解法代码:]

typedef struct Guy
{
    unsigned int index;
    Guy *next;

} GUY;

void Initial(GUY **head, unsigned int length)
{
    assert( NULL == *head);
    GUY  *guy = NULL;
   
    for(unsigned int i = 0; i< length; i++)
    {
        GUY *point = new GUY();
        point->index = i+1;
        point->next = *head;
        if(NULL != guy)
        {
            guy->next = point;
            guy = guy->next;
        }
        else
        {
            guy = point;
            *head = guy;
        }
    }

    for(unsigned int i = 0; i< length; i++)
    {
        guy = guy->next;
        cout<<guy->index<<endl;
    }
}

void deletePoint(GUY *head, unsigned int n)
{
    assert(NULL != head);
    GUY *point = head;
    while(point != point->next)
    {
        for(int i=0; i< n-2; i++)
        {
            point = point->next;
        }
        GUY *p = point->next;
        point->next = p->next;
        delete p;
        point = point->next;
    }
    cout<<point->index<<endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
    unsigned int number;
    GUY *guy = NULL;
    Initial(&guy, cog_m);
    deletePoint(guy, cog_n);

    return 0;
}

 

[循环链表解法比数组解法的好处:] 1.在保存数据的方面上可以不需要限制个数;
2.可扩展性强,记得论坛上用某个朋友提出每个人有一个密码,这样只需要小小的改动下链表的结构就可以了!

 

[数学简化:]无论是用链表实现还是用数组实现都有一个共同点:要模拟整个

  游戏过程,不仅程序写起来比较烦,而且时间复杂度高达O(nm),当n

  ,m非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间

  内出结果的。我们注意到原问题仅仅是要求出最后的胜利者的序号,

  而不是要读者模拟整个过程。因此如果要追求效率,就要打破常规,

  实施一点数学策略。

  为了讨论方便,先把问题稍微改变一下,并不影响原意:

  问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出

  ,剩下的人继续从0开始报数。求胜利者的编号。

  我们知道第一个人(编号一定是(m-1)%n) 出列之后,剩下的n-1个人组

  成了一个新的约瑟夫环(以编号为k=m%n的人开始):

  k k+1 k+2 ... n-2, n-1, 0, 1, 2, ... k-2

  并且从k开始报0。

  现在我们把他们的编号做一下转换:

  k --> 0

  k+1 --> 1

  k+2 --> 2

  ...

  ...

  k-3 --> n-3

  k-2 --> n-2

  变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这

  个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x

  变回去不刚好就是n个人情况的解吗?!!变回去的公式很简单,相

  信大家都可以推出来:x‘=(x+k)%n

  如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就

  行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是

  一个倒推问题!好了,思路出来了,下面写递推公式:

  令f表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然

  是f[n]

  递推公式

  f[1]=0;

  f=(f[i-1]+m)%i; (i>1)

  有了这个公式,我们要做的就是从1-n顺序算出f的数值,最后结

  果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1

  由于是逐级递推,不需要保存每个f,程序也是异常简单:

  #include <stdio.h>

  int main(void )

  {

  int n, m, i, s=0;

  printf ("N M = "); scanf("%d%d", &n, &m);

  for (i=2; i<=n; i++) s=(s+m)%i;

  printf ("The winner is %d/n", s+1);

  }

  这个算法的时间复杂度为O(n),相对于模拟算法已经有了很大的提高

  。算n,m等于一百万,一千万的情况不是问题了。可见,适当地运用

  数学策略,不仅可以让编程变得简单,而且往往会成倍地提高算法执

  行效率。

[数学公式推导:]

第一类Josephus数
假设n 个竞赛者排成一个环形,依次顺序编号1,2,…,n。从某个指定的第1 号开始,沿环计数,每数到第2个人就让其出列,且从下一个人开始重新计数,继续进行下去。这个过程一直进行到所有的人都出列为止。最后出列者为优胜者。优胜者的号码定义为第一类Josephus数,J(n)。显然,1<=J(n)<=n。
对于第一类Josephus数,在《Concrete Mathematics》(参考文献[1])第一章1.3有很详尽的讨论。下面的递推公式引用其中:
J(1)     = 1;
J(2*n)   = 2*J(n)-1,  n>=1;
J(2*n+1) = 2*J(n)+1,  n>=1;
简单推导可得:J(n) = 1 + 2*n - pow(2, (1 + floor(ln(n)/ln(2))))
1 floor()函数表示向下取整
2 pow(x, y)函数表示x的y次幂
表达式C语言描述:J(n) = 1 + (n << 1) - (2 << (int)(ln(n)/ln(2)))

第二类Josephus数
竞赛规则不变,只是每次让第m个人出列。这种情况下,优胜者的号码定义为第二类Josephus数,J(n,m)。可见第一类Josephus数J(n)就是m=2的特例J(n,2)。
在参考文献[4]中给出了J(n,3)的非递归算法,在此不做过多叙述。这里讨论其他一般情况。

为讨论简单化,先规定1<=m<=n。
(文中用mod表示取模操作。)
显然有公式,
J(n,m) = (J(n-1,m) + m) mod n

第三类Josephus数
竞赛规则同第二类Josephus数,只是刚开始计数的初始位置不是第1号,而是第i号(1<=i<=n)。这种情况下,优胜者的号码定义为第三类Josephus数,J(n,m,i)。显然第二类Josephus数J(n,m)就是i=1的特例J(n,m,1)。
很显然,J(n,m,i)=(J(n,m,1)+i-1) mod n
第三类Josephus数的引入主要是为了让第二类Josephus数的递归变得更加简单化。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值