[College] 《C++程序设计-实践教程》约瑟夫问题

约瑟夫问题

题目描述

n个人围成一圈,从编号为1的开始从1~m报数,报到m的人离开,接着再从下一个人重新报数。这样反反复复,直到只剩下一个人,求编号为多少的人留下?

输入格式

两个整数n、m。n表示总人数,m表示报数的截止上限。

输出格式

一个整数,表示剩下的人的编号。

AC代码

链表版(模拟) O(nm)

#include<iostream>
#include<cstdio>
#include<cstdlib>

using namespace std;

struct  node
{
    int next;//记录当前参与者的后一位的位置
    int post;//记录当前参与者的前一位的位置
};
node circle[1000];

int main()
{
    int num,exit_num,present=1;//定义:剩余人数num,报数上限exit_num,当前位置present
    cout<<"Please input the number of people: "<<endl;
    cin>>num;
    cout<<"Please input the exit number: "<<endl;
    cin>>exit_num;

    //初始化链表为一个圈
    for(int i=1;i<=num;i++)
    {
        circle[i].next=i+1;
        circle[i].post=i-1;
    }
    circle[num].next=1;//最后一位的下一个为第一位
    circle[1].post=num;//第一位的上一位为最后一位

    while(num>1)
    {
        for(int i=1;i<exit_num;i++)//模拟报数
            present=circle[present].next;
        circle[circle[present].post].next=circle[present].next;//对于退出者进行删除操作:上一位的下一位为当前的下一位;下一位的上一位为当前的上一位
        circle[circle[present].next].post=circle[present].post;
        present=circle[present].next;//从当前的下一位再一次开始报数
        num--;//更新剩余人数
    }
    cout<<present<<endl;
    system("pause");
    return 0;
}

采用链表的优势就是:可以非常出色地维护一个环形的结构,并且避免了处理跳过不参与报数的位置,而且在效率上——由于直接跳过了退出的位置——相较于一般的数组更为优秀~ 

不过这个是下学期才学的知识......

数组版(模拟) O(nm)

#include<iostream>
#include<cstdio>
#include<cstdlib>

using namespace std;

bool circle[1000];

int main()
{
    int num,exit_num,present=1,cnt;//定义num为初始参与人数,exit_num为报数的上限,present为当前位置,cnt为剩余人数
    cout<<"Please input the number of participants: "<<endl;
    cin>>num;
    cout<<"Please input the exit number"<<endl;
    cin>>exit_num;

    cnt=num;//初始化cnt为总人数
    while(cnt>1)
    {
        for(int i=1;i<=exit_num;i++)//模拟报数
        {
            if(circle[present]==true)//若当前位置的参与者已经退出,则不计入报数,还原i值
                i--;
            else if(i==exit_num)//若当前参与者未退出,且报数已经达到上限,则令其退出
                circle[present]=true;
            present=(present==num)?(1):(present+1);//更新当前位置为下一位
        }
        cnt--;//更新剩余人数
    }
    for(int i=1;i<=num;i++)//遍历整个数组,寻找剩下的唯一参与者
        if(circle[i]==false)
            cout<<i<<endl;
    system("pause");
    return 0;
}

数组版的代码要相对复杂一点,原因在于我们不能对数组结构中的元素进行“删除”操作,这使得我们在遍历时(也就是模拟报数过程时)要考虑已经不参与报数的位置,在更新当前位置present时也要注意模拟环状的结构。另外,由于我们无法通过present的值直接确定答案,我们在最后需要单独从1~num遍历一次数组,找到尚未被标记的位置并输出。

递推版(数学技巧) O(n)

#include<iostream>
#include<cstdlib>
#include<cstdio>

using namespace std;

int main()
{
    int num,exit_num,f=0;//定义总人数为num,报数的截止上限为exit_num
    cout<<"Please input the number of people: "<<endl;
    cin>>num;
    cout<<"Please input the exit number: "<<endl;
    cin>>exit_num;

    for(int i=1;i<=num;i++)
        f=(f+exit_num)%i;

    cout<<f+1<<endl;

    system("pause");
    return 0;
}

啧啧啧,是不是感觉清爽了不少?这个代码是如此的简洁,让我们来着重了解一下~

为方便叙述,我们先将num个人从0~(num-1)编号。

接着,我们不妨假设有num个人,我们很容易得出,第一个退出的人的编号肯定是k=(exit_num−1)%num。当编号为k的人退出后,接下来我们可以将问题看作:从原编号为k+1的人开始重新编号,然后就是一个人数为num−1个人的子问题了。这里其实包含了递归的思想,本题可用递归法求解~ (无论是递归还是递推,其实两者思路基本一致)

当然,这num−1个人的答案并不是最终答案,因为它是重新编号后的答案,所以我们要将其恢复原编号。

怎么恢复呢???我们发现,新编号为0的人前面有exit_num−1个人,也就是说,新的编号相对于原来的编号左移了exit_num,所以最终的答案应该要加上exit_num。

那么我们就可以得到递推公式:设f[n]为总人数为n时最后剩下的人的编号,则有f[n]=(f[n−1]+exit_num)%n(模n是为了防止计算所得值溢出),并且易得f(1)=0,可借此进行循环递推。

当n=num时,f[num]就是最后剩余人的编号。由于我们是按0~(num-1)编号,而题目要求1~num编号,因此答案应为f[num]+1。

递归实现 O(n)

#include<iostream>
#include<cstdlib>
#include<cstdio>

using namespace std;

int work(int n,int m)
{
    if(n==1)
        return 0;
    return (work(n-1,m)+m)%n;
}

int main()
{
    int num,exit_num;//定义总人数为num,报数的截止上限为exit_num
    cout<<"Please input the number of people: "<<endl;
    cin>>num;
    cout<<"Please input the exit number: "<<endl;
    cin>>exit_num;

    int ans=work(num,exit_num)+1;
    cout<<ans<<endl;

    system("pause");
    return 0;
}

 

posted @ 2019-10-24 19:32  SinGuLaRiTy2001  阅读(307)  评论(0编辑  收藏  举报