您的位置:首页 > 其它

约瑟夫问题及其各种优化

2012-06-19 20:11 841 查看

约瑟夫问题及其各种优化

【约瑟夫问题】

【问题描述一】:

据说著名犹太历史学家 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
+1

这个应该很好理解吧?因为:我们对每个人的假想编号是比那个人得实际编号小一的,这也就是为什么结果是:f
+1,而不是f
的原因了。

下面是我对这个问题的算法实现:

【标程】:

#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
+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;

}

好了,就这么多了。

呼!终于完了,累死我了!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: