• 第二十四周项目4-猴子选大王(约瑟夫问题)


    一群猴子,编号是1,2,3 ...m,这群猴子(m个)按照1-m的顺序围坐一圈。从第1只开始数,每数到第n个,该猴子就要离开此圈,这样依次下来,直到圈中只剩下最后一只猴子,则该猴子为大王。输入m和n,输出为大王的猴子是几号。

    提示1:(1)链表解法:可以用一个循环的单链表来表示这一群猴子。表示结点的结构体中有两个成员:一个保存猴子的编号,一个为指向下一个人的指针,编号为m的结点再指向编号为1的结点,以此构成环形的链。当数到第n个时,该结点被删除,继续数,直到只有一个结点。(2)使用结构数组来表示循环链:结构体中设一个成员表示对应的猴子是否已经被淘汰。从第一个人未被淘汰的数起,每数到n时,将结构中的标记改为0,表示这只猴子已被淘汰。当数到数组中第m个元素后,重新从第一个数起,这样循环计数直到有m-1被淘汰。

    这是一个约瑟夫问题。

    约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
    分析:
    (1)由于对于每个人只有死和活两种状态,因此可以用布朗型数组标记每个人的状态,可用true表示死,false表示活。
    (2)开始时每个人都是活的,所以数组初值全部赋为false。
    (3)模拟杀人过程,直到所有人都被杀死为止。
    无论是用链表实现还是用数组实现都有一个共同点:要模拟整个游戏过程,不仅程序写起来比较烦,而且时间复杂度高达O(nm),当n,m非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间内出结果的。我们注意到原问题仅仅是要求出最后的胜利者的序号,而不是要读者模拟整个过程。因此如果要追求效率,就要打破常规,实施一点数学策略。
    为了讨论方便,先把问题稍微改变一下,并不影响原意:
    问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。
    我们知道第一个人(编号一定是(m-1) mod n) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m mod 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-2 --> n-2
    变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗?!!变回去的公式很简单,相信大家都可以推出来:x'=(x+k) mod n
    如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:
    令f表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]
    递推公式
    f[1]=0;
    f=(f+m) mod i; (i>1)
    有了这个公式,我们要做的就是从1-n顺序算出f的数值,最后结果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1
    由于是逐级递推,不需要保存每个f。

    #include <iostream>
    using namespace std;
    const int m = 3;
    int main()
    {
        int n, f = 0;
        cin >> n;
        for (int i = 1; i <= n; i++) f = (f + m) % i;
        cout << f + 1 << endl;
    }

    这个算法的时间复杂度为O(n),相对于模拟算法已经有了很大的提高。算n,m等于一百万,一千万的情况不是问题了。可见,适当地运用数学策略,不仅可以让编程变得简单,而且往往会成倍地提高算法执行效率。

    笔算解决约瑟夫问题
    在M比较小的时候 ,可以用笔算的方法求解,
    M=2
    即N个人围成一圈,1,2,1,2的报数,报到2就去死,直到只剩下一个人为止。
    当N=2^k的时候,第一个报数的人就是最后一个死的,
    对于任意的自然数N 都可以表示为N=2^k+t,其中t<n/2
    于是当有t个人去死的时候,就只剩下2^k个人 ,这2^k个人中第一个报数的就是最后去死的。这2^k个人中第一个报数的人就是2t+1
    于是就求出了当M=2时约瑟夫问题的解:
    求出不大于N的最大的2的整数次幂,记为2^k,最后一个去死的人是2(N-2^k)+1
    M=3
    即N个人围成一圈,1,2,3,1,2,3的报数,报到3就去死,直到只剩下一个人为止。
    此时要比M=2时要复杂的多
    我们以N=2009为例计算
    N=2009,M=3时最后被杀死的人记为F(2009,3),或者可以简单的记为F(2009)
    假设这种情况下还剩下n个人,则下一轮将杀死[n/3]个人,[]表示取整,还剩下n-[n/3]个人
    设这n个人为a1,a2,...,a(n-1),an
    从a1开始报数,一圈之后,剩下的人为a1,a2,a4,a5,...a(n-n mod 3-1),a(n-n mod 3+1),..,an
    于是可得:
    1、这一轮中最后一个死的是a(n-n mod 3),下一轮第一个报数的是a(n-n mod 3+1)
    2、若3|n,则最后死的人为新一轮的第F(n-[n/3])个人
    若n mod 3≠0 且f(n-[n/3])<=n mod 3则最后死的人为新一轮的第n-[n/3]+F(n-[n/3])-(n mod 3)人
    若n mod 3≠0 且f(n-[n/3])>n mod 3则最后死的人为新一轮的第F(n-[n/3])-(n mod 3)人
    3、新一轮第k个人对应原来的第 3*[(k-1)/2]+(k-1)mod 2+1个人
    综合1,2,3可得:
    F(1)=1,F(2)=2,F(3)=2,F(4)=1,F(5)=4,F(6)=1,
    当f(n-[n/3])<=n mod 3时 k=n-[n/3]+F(n-[n/3])-(n mod 3),F(n)=3*[(k-1)/2]+(k-1)mod 2+1
    当f(n-[n/3])>n mod 3时 k=F(n-[n/3])-(n mod 3) ,F(n)=3*[(k-1)/2]+(k-1)mod 2+1
    这种算法需要计算 [log(3/2)2009]次 这个数不大于22,可以用笔算了
    于是:
    第一圈,将杀死669个人,这一圈最后一个被杀死的人是2007,还剩下1340个人,
    第二圈,杀死446人,还剩下894人
    第三圈,杀死298人,还剩下596人
    第四圈,杀死198人,还剩下398人
    第五圈,杀死132人,还剩下266人
    第六圈,杀死88人,还剩下178人
    第七圈,杀死59人,还剩下119人
    第八圈,杀死39人,还剩下80人
    第九圈,杀死26人,还剩下54人
    第十圈,杀死18人,还剩36人
    十一圈,杀死12人,还剩24人
    十二圈,杀死8人,还剩16人
    十三圈,杀死5人,还剩11人
    十四圈,杀死3人,还剩8人
    十五圈,杀死2人,还剩6人
    F(1)=1,F(2)=2,F(3)=2,F(4)=1,F(5)=4,F(6)=1,
    然后逆推回去
    F(8)=7 F(11)=7 F(16)=8 f(24)=11 f(36)=16 f(54)=23 f(80)=31 f(119)=43 f(178)=62 f(266)=89 f(398)=130
    F(596)=191 F(894)=286 F(1340)=425 F(2009)=634

    #include <iostream>
    using namespace std;
    struct Monkey
    {
        int num;  //猴子的编号
        struct Monkey *next; //下一只猴子
    };
    
    int main()
    {
        int m,n,i,j,king;
        Monkey *head, *p1,*p2;
        cin>>m>>n;
        if(n==1)
        {
            king=m;
        }
        else
        {
            //建立猴子围成的圆圈
            p1=p2=new Monkey;
            head = p1;
            head->num=1;
            for(i=1,p1->num=1; i<m; i++)  //其余m-1只猴子
            {
                p1=new Monkey;  //p1是新增加的
                p1->num=i+1;
                p2->next=p1;
                p2=p1;          //p2总是上一只
            }
            p2->next=head;      //最后一只再指向第一只,成了一个圆圈
    
            //下面要开始数了
            p1=head;
            for(i=1; i<m; i++)  //循环m-1次,淘汰m-1只猴子
            {
                //从p1开始,数n-1只就找到第n只了
                for(j=1; j<n-1; j++)  //实际先找到第n-1只,下一只将是被淘汰的
                    p1=p1->next;    //围成圈的,可能再开始从第一只数,如果还未被淘汰的话
    
                //找到了,
                p2=p1->next;  //p2将被删除
                //cout<<"第"<<i<<"轮淘汰"<<p2->num<<endl;   //可以这样观察中间结果
                p1->next=p2->next;  //p2就这样被“架空了”
                p1=p2->next;  //下一轮数数的新起点
                delete p2;  //将不在链表中的结点放弃掉
            }
            king=p1->num;
            delete p1;
        }
        cout<<king<<endl;
        return 0;
    }
    

    运行结果:



    @ Mayuko

  • 相关阅读:
    swift5.x for-in, switch语句
    swift5.x 数组(Array)的基本操作
    OC NSDictionary的属性一般为什么要设置为copy
    iOS APP 从编译到运行
    重装win10系统之后,如何使用之前的虚拟机
    [Delphi]接口认识
    [QPlugins]学习大纲
    [QPlugins]概述
    [转发]Oauth 1.0 1.0a 和 2.0 的之间的区别有哪些?
    [Delphi] Webbroker ISAPI 示例说明
  • 原文地址:https://www.cnblogs.com/mayuko/p/4567543.html
Copyright © 2020-2023  润新知