zoukankan      html  css  js  c++  java
  • 约瑟夫环--数学高效率解法

    问题描述

            有n个囚犯站成一个圆圈,准备处决。首先从一个人开始报数,报到m的人被处死,剩下n-1个人继续这个过程,直到最终只剩下一个人留下。问题是:给定了n和m,一开始要站在什么位置才能避免被处决?

      

            对于这个题目最基本的解法是使用循环链表模拟全过程。
    #include<iostream>
     
    using namespace std;
     
    /************约瑟夫问题****************/
     
    typedef struct CLinkList
    {
        int data;
        struct CLinkList *next;
    }node;
     
     
    int main()
    {
    ///建立循环链表
        node *L,*r,*s;
        L = new node;
        r =L;
        int n,k;
    cin>>n>>k;
    for(i = 1;i<=n;i++) //尾插法建立链表 { s = new node; s->data = i; r->next = s; r= s; } r->next =L->next; //让最后一个结点指向第一个有数据结点 node *p; p = L->next; delete L; //删除第一个空的结点 ///模拟解决约瑟夫问题 while(p->next != p) //判断条件:因为最后肯定剩下一个人, 循环链表的最后一个数据的next还是他本身 { for(i = 1;i<k-1;i++) { p = p->next; //每k个数死一个人 } cout<<p->next->data<<"->"; p->next = p->next->next; //将该节点从链表上删除。 p = p->next; } cout<<p->data<<endl; return 0; }
            当n,m数据量很小的时候,我们可以用循环链表模拟约瑟夫环的过程。当模拟到人数等于1的时候,输出剩下的人的序号即可。这种方法往往实现起来比较简单,而且也很容易理解。但是时间复杂度却是很糟糕的,达到了O(n*m),因为即使2个人,也要模拟K遍,存在着大量的浪费。在n,m比较大的时候(n*m达到10^8或者更大),那么要得出结果往往需要耗费很长的时间,但是我们可以运用一点数学上的技巧,将最后结果推导出来。
     

            为了讨论方便,先把问题稍微改变一下,并不影响原意:
            问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。

            我们知道第一个出列的人编号一定是 m%n -1。那么剩下的n-1个人便可以看做组成了一个新的约瑟夫环(以编号为k=m%n的人开始):

            k  k+1  k+2  ... n-2, n-1, 0, 1, 2, ... k-2并且从k开始报0。

           现在我们把n-1个人的时候的编号做一下转换(全部减去k):
      k     --> 0
      k+1   --> 1
      k+2   --> 2
      ...
      ...
      k-2   --> n-2

        变换后就完完全全成为了(n-1)个人报数的子问题!(这不是让我们倒推吗?!)

      假如我们知道这个子问题的解:例如A[n-1]是在n-1个人中的最终胜利者,那么根据上面对A[n-1]进行转换  便刚好就是n个人情况的解。变回去的公式很简单,如下:

      A[n]=(A[n-1] +k)%n。(k是在n个人中第一个出列的人的编号,此处这个公式涉及了上面叙述到的编号的转换,需要搞懂了才能理解)

      由于k=m%n,那么公式也可以写为A[n]=(A[n-1]   +m)%n。为什么呢?很容易理解,7%3=1 , 1%3=1。


      如此也是得出了递推公式:

      A[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是A[n]

      A[1]=0;

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

      有了这个公式,我们要做的就是从1-n顺序算出A[i]的数值,最后结果是A[n]。因为实际生活中编号总是从1开始,我们输出A[n]+1
      由于是逐级递推,不需要保存每个A[i],于是便得到了最终的程序:

    #include<iostream>
    using namespace std;
    int main()
    {
        int n,m;
    cin>>n>>m;
    int winner=0;//总人数n,数到m的倍数离开,最后的人winner for(int i=2;i<=n;i++) winner=(winner+m)%i; cout<<"Winner:"<<winner+1<<endl; }
  • 相关阅读:
    BZOJ2219数论之神——BSGS+中国剩余定理+原根与指标+欧拉定理+exgcd
    Luogu 3690 Link Cut Tree
    CF1009F Dominant Indices
    CF600E Lomsat gelral
    bzoj 4303 数列
    CF1114F Please, another Queries on Array?
    CF1114B Yet Another Array Partitioning Task
    bzoj 1858 序列操作
    bzoj 4852 炸弹攻击
    bzoj 3564 信号增幅仪
  • 原文地址:https://www.cnblogs.com/cjtds/p/13433740.html
Copyright © 2011-2022 走看看