约瑟夫问题及其各种优化

约瑟夫问题及其各种优化

【约瑟夫问题】
【问题描述一】:
    据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式:41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
【问题描述二】:
    17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。
【猴子选大王】:典型的约瑟夫问题
【问题描述】:
    有m个猴子围成一圈,按顺时针编号,分别为1到m。现打算从中选出一个大王。经过协商,决定选大王的规则如下:从第一个开始顺时针报数1,报到n的猴子出圈,紧接着从下一个又从1顺时针循环报数,数到n的出去...,如此下去,最后剩下来的就是大王。
【输入】:第一行是一个正整数T表示测试数据的组数。下面共有T行,每行两个整数m和n,用一个空格隔开,分别表示猴子的个数和报数n(1<=m<=100,1<=n<=200)。
【输出】:每组数据对应有一个输出,表示大王的编号。
【分析】:由于n和m的数据规模很小,本题可用一个简单的模拟来对算法进行实现,具体的做法是采用一个循环链表,一个一个的模拟,下面是我对它算法实现:
【标程】:
#include<iostream>
#include<cstdio>
using namespace std;
int n,m,t,next[201],pre[201];
int main()
{
    scanf("%d",&t);
    for(int ca=1;ca<=t;ca++)
    {
        scanf("%d%d",&m,&n);
        for(int i=1;i<m;i++) next[i]=i+1;
        next[m]=1;
        for(int i=2;i<=m;i++) pre[i]=i-1;
        pre[1]=m;
        int i=1;
        for(int c=1;c<m;c++)
        {
            for(int tot=1;tot<n;tot++) i=next[i];
            int k=i;
            i=next[i];
            next[pre[k]]=next[k];
            pre[next[k]]=pre[k];
        }
        printf("%d\n",i);
    }
    return 0;
}
但是,当n和m都很大时,上面的做法就会显得很慢很慢。因为其时间复杂度为:o(m*n)
有没更高效的算法呢?当然是有的!
为了讨论方便,我们换一种问题的描述方式,但并不影响题意。
【问题描述】:
    有m个猴子围成一圈,按顺时针编号,分别为0到m-1。现打算从中选出一个大王。经过协商,决定选大王的规则如下:从第一个开始顺时针报数0,报到n-1的猴子出圈,紧接着从下一个又从0顺时针循环报数,数到n-1的出去...,如此下去,最后剩下来的就是大王。
【分析】:
    我们知道:第一个出去的人肯定是:m mod n-1 那么下一个开始报数的人编号是:m mod n 下面,我们把他们的编号做一下改变:令:k=m mod n
     左——>右
    k+0——>0
    k+1——>1
    k+2——>2
     .      .
     .      .
     .      .
    n-2——>n-2-k
    n-1——>n-1-k
     0 ——>0+n-k    注意:因为是一个环,这里显然0-k<0,那么,如果
     1 ——>1+n-k          把它加上个n就能保证这个元素在这个环上
     2 ——>2+n-k
     .      .
     .      .
     .      .
    k-3——>k-3+n-k=n-1
    k-2——>k-2+n-k=n-2
    这样,我们就把n个人报数的问题完全转化成了n-1个人报数的问题了,如果n-1个人报数的子问题的答案是:x,那么我们就可以根据左右两边的对应关系找到n个人报数的问题的答案了。
    那么,左右两边到底有着怎样的对应关系呢?
    我们可以试着把右边的数加上个k,然后通过观察可以发现:咦,当左边的数为k+0到n-1时,左边的数刚好和右边的数加k相等,但是,当左边的数为0到k-2时,右边的数加k后刚好比左边的数多了个n,这样,左右两边的关系就浮出水面了:设左边的数为:xx,其对应的右边的数为:x,那么:xx=(x+k) mod n
    这样,如果我们知道了n-1的子问题的解,我们不是就知道了n的解了吗?
    而要的到n-1的子问题的解,我们又可以通过n-2的子问题的解来找,这样,这个递推关系式就出来了那就是:
    f[i]=(f[i-1]+k) mod i
    其中:k=m mod i;
    其中:f[i]表示:有i个人参加报数k的游戏的赢家是谁
    这里就有一个问题了,k的值是否会变化?
    答案是否定的!
    原因很简单:k的意义是i个人报数第一次后,下一次报数的第一人。这里可能读者会有点纠结。我们这样想:m足够小时,k的值始终都是: m mod i 其中:i代表有几个人玩游戏。其实根据求模运算的运算法则:

        (f[i-1]+k) mod i
    ==(f[i-1]+m mod i) mod i
    ==f[i-1] mod i+m mod i mod i
    ==f[i-1] mod i+m mod i
    ==(f[i-1]+m) mod i
    这样,我们就得到了一个和k毫无关系的递推式了:
    f[i]=(f[i-1]+m) mod i
    那么,我们不难得出,当i=1时,f[1]=0;  (因为当i为一时,只有一个人,他的编号是:0)
    现在我们又回到最开始的问题:
【问题描述】:
    有m个猴子围成一圈,按顺时针编号,分别为1到m。现打算从中选出一个大王。经过协商,决定选大王的规则如下:从第一个开始顺时针报数1,报到n的猴子出圈,紧接着从下一个又从1顺时针循环报数,数到n的出去...,如此下去,最后剩下来的就是大王。
【输入】:第一行是一个正整数T表示测试数据的组数。下面共有T行,每行两个整数m和n,用一个空格隔开,分别表示猴子的个数和报数n(1<=m<=100,1<=n<=200)。
【输出】:每组数据对应有一个输出,表示大王的编号。
【分析】:我们可以设初值:f[1]=0,而我们要求的结果就是:f[n]+1
          这个应该很好理解吧?因为:我们对每个人的假想编号是比那个人得实际编号小一的,这也就是为什么结果是:f[n]+1,而不是f[n]的原因了。
下面是我对这个问题的算法实现:
【标程】:
#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
    int t;
    scanf("%d",&t);
    for(int ca=1;ca<=t;ca++)
    {
        int n,m,f[500000];
        scanf("%d%d",&n,&m);
        f[1]=0;
        for(int i=2;i<=n;i++)
        {
            f[i]=(f[i-1]+m)%i;
        }
        printf("%d\n",f[n]+1);
    }
    return 0;
}
当然,我们完全不需要保存中间的状态,所以还有以下的写法:
【标程】:
#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
    int t;
    scanf("%d",&t);
    for(int ca=1;ca<=t;ca++)
    {
        int n,m,f=0;
        scanf("%d%d",&n,&m);
        for(int i=2;i<=n;i++)
        {
            f=(f+m)%i;
        }
        printf("%d\n",f+1);
    }
    return 0;
}
    那么,这种方法和上面的比有什么优势呢?
    优势是显然的,其一:它不用开数组,大大的节省了空间;其二:它的时间复杂度是:o(n)的,大大的缩短了运行的时间,提高了计算机运行的效率。而且,当n为10^6--10^7大得数时,也能在一秒内给出答案。而前者,差远了。
    但是,难道不能再做优化吗?
    答案当然又是可以的!
    我们在运行上一个程序时,会发现:f有时会处于一种等差递增的状态,这里浪费了很多的时间!我们来看这个表达式:
    f=(f+m)%i;
    当:f+m 比较小而 i 比较大时,f就会处于一种等差递增的状态,那么怎么结束这个状态或者说跳过这个状态呢?假设从i递推到i+x的过程中,f是在递增的,i+x+1后就不是了,那么,其实,这个x是可以求的!怎么求呢?我们可以列出如下等式:
    f+m*x==i+x;
    相信你已经看懂了吧!然后解出x即可。令i+=x; f+=m*x;就可跳过这个费时的过程了!显然,这里还有个问题,要是i+x>n怎么办?
    其实,这样的结果已经是相当于在告诉我们,这个递推的过程可以结束了!
    我们只需再进行这个操作就行了:
    f+=m*(n-i);     其实就是把多加了的减回去!也可以这样写:
    f-=m*(i-n);     嘿嘿,其实他们俩是一样的!
    这里,我们其实还可以有个小小的技巧:当m==1时我们可以单独讨论:答案就是:n
    下面是我的算法实现(还有点小问题,改天再改了):
【标程】:
#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
    int t;
    scanf("%d",&t);
    for(int ca=1;ca<=t;ca++)
    {
        int n,m,f=0;
        scanf("%d%d",&n,&m);
        if(m==1) f=n-1;
        else
        {
            for(int i=2;i<=n;i++)
            {
                if((f+m)<i)
                {
                    int x=(i-f)/(m-1);
                    f+=m*x;
                    i+=x;
                    if(i>n) f-=(i-n)*m;
                }
                else f=(f+m)%i;
            }
        }
        printf("%d\n",f+1);
    }
    return 0;
}
好了,就这么多了。
呼!终于完了,累死我了!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值