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; }
  • 相关阅读:
    tar (child): gzip: Cannot exec: No such file or directory
    通过应用数字格式来修复文本格式的数字
    Logback
    Transport
    nginx note
    docker note
    k8s note
    spring boot properties
    Windows 和 Linux 的文件名
    安装配置ubuntu的web项目(新)
  • 原文地址:https://www.cnblogs.com/cjtds/p/13433740.html
Copyright © 2011-2022 走看看