猴子选大王问题

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

猴子选大王问题
集团文件版本号:(M928-T898-M248-WU2669-I2896-DQ586-M1988)
这是17世纪的法国数学家加斯帕在《数目的游戏问题》中讲的一个故事:15个教徒和15个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。

问怎样排法,才能使每次投入大海的都是非教徒。

*问题分析与算法设计
约瑟夫问题并不难,但求解的方法很多;题目的变化形式也很多。

这里给出一种实现方法。

题目中30个人围成一圈,因而启发我们用一个循环的链来表示。

可以使用结构数组来构成一个循环链。

结构中有两个成员,其一为指向下一个人的指针,以构成环形的链;其二为该人是否被扔下海的标记,为1表示还在船上。

从第一个人开始对还未扔下海的人进行计数,每数到9时,将结构中的标记改为0,表示该人已被扔下海了。

这样循环计数直到有15个人被扔下海为止。

[编辑本段]
约瑟夫问题的一般形式:
约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。

例如N=6,M=5,被杀掉的人的序号为5,4,6,2,3。

最后剩下1号。

假定在圈子里前K个为好人,后K个为坏人,你的任务是确定这样的最少M,使得所有的坏人在第一个好人之前被杀掉。

C++代码示例:
#include<iostream>
usingnamespacestd;
voidmain()
{
intn,m,a[101],k,i,j,num;//计数器是从1开始的,所以100个人用101
cout<<"请输入参加游戏的玩家人数(不超过100人):";
cin>>n;
cout<<"----------------------------------------"<<endl;
if(n>100)
{
cout<<"玩家太多,请重新登陆此程序!"<<endl;
return;
}
cout<<"输入游戏中要玩的数字:";
cin>>m;
cout<<"----------------------------------------"<<endl;
for(i=1;i<=n;i++)
{
a【i】=1;//注意百度百科里不让使用ASCII里的方括号,这里是中文字符集里的方括号,
}
j=0;
k=0;
for(i=1;i<=n+1;i++){
if(a【i】==1){
j=j+a【i】;
if(j==m)
{
j=0;
a【i】=0;
k++;
}
if(k==n){
num=i;
break;
}
}
if(i==n+1)
i=0;
}
cout<<"最后获胜的玩家是第"<<num<<"号玩家!"<<endl;
cout<<"----------------------------------------"<<endl; }
写完密码约瑟夫就想到原来看到约瑟夫问题的一个数学解法很巧妙很简单不过只能推出最后一个出列的人
无论是用链表实现还是用数组实现都有一个共同点:要模拟整个游戏过程,不仅程序写起来比较烦,而且时间复杂度高达O(nm),当n,m非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间内出结果的。

我们注意到原问题仅仅是要求出最后的胜利者的序号,而不是要读者模拟整个过程。

因此如果要追求效率,就要打破常规,实施一点数学策略。

为了讨论方便,先把问题稍微改变一下,并不影响原意:
问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。

求胜利者的编号。

我们知道第一个人(编号一定是mmodn-1)出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=mmodn的人开始):
kk+1k+2...n-2,n-1,0,1,2,...k-2
并且从k开始报0。

现在我们把他们的编号做一下转换:
k-->0
k+1-->1
k+2-->2
...
...
k-2-->n-2
k-1-->n-1
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n 个人情况的解吗?!!变回去的公式很简单,相信大家都可以推出来:
x'=(x+k)modn
如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。

(n-2)个人的解呢?当然是先求(n-3)的情况----这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:
令f表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n] 递推公式
f[1]=0;
f=(f+m)modi;(i>1)
有了这个公式,我们要做的就是从1-n顺序算出f的数值,最后结果是f[n]。

因为实际生活中编号总是从1开始,我们输出f[n]+1 由于是逐级递推,不需要保存每个f,程序也是异常简单:
c++
#include<stdio.h>
intmain()
{
intn,m,i,s=0;
printf("NM=");scanf("%d%d",&n,&m);
for(i=2;i<=n;i++)s=(s+m)%i;
printf("Thewinneris%d\n",s+1);
}
pascal
varn,m,i,s:integer;
begin
write('NM=');
read(n,m);
fori:=2tondo
s:=(s+m)modi;
writeln('Thewinneris',s+1);
end.
这个算法的时间复杂度为O(n),相对于模拟算法已经有了很大的提高。

算n,m等于一百万,一千万的情况不是问题了。

可见,适当地运用数学策略,不仅可以让编程变得简单,而且往往会成倍地提高算法执行效率。

约瑟夫问题10e100版(fromvijios)
描述Description
n个人排成一圈。

从某个人开始,按顺时针方向依次编号。

从编号为1的人开始顺时针“一二一”报数,报到2的人退出圈子。

这样不断循环下去,圈子里的人将不断减少。

由于人的个数是有限的,因此最终会剩下一个人。

试问最后剩下的人最开始的编号。

输入格式InputFormat
一个正整数n,表示人的个数。

输入数据保证数字n不超过100位。

输出格式OutputFormat
一个正整数。

它表示经过“一二一”报数后最后剩下的人的编号。

样例输入SampleInput
9
样例输出SampleOutput
3
时间限制TimeLimitation
各个测试点1s
注释Hint
样例说明
当n=9时,退出圈子的人的编号依次为:
24681597
最后剩下的人编号为3
初见这道题,可能会想到模拟。

可是数据实在太大啦!!
我们先拿手来算,可知n分别为1,2,3,4,5,6,7,8...时的结果是1,1,3,1,3,5,7,1...
有如下规律:从1到下一个1为一组,每一组中都是从1开始递增的奇数,且每组元素的个数分别为1,2,4...
这样就好弄了!!
大体思路如下:
①read(a)
②b:=1,c:=1{b为某一组的元素个数,c为现在所加到的数}
③whilec<ado(b:=b*2,c:=b+c){超过目标时停止加数}
⑥c:=c-b{退到前一组}
⑦x:=a-c{算出目标为所在组的第几个元素}
⑧ans:=x*2-1{求出该元素}
⑨write(ans)
有了思路,再加上高精度就可以了。

我写的代码比较猥琐,因为是先把上面的思路敲进去,再写过程,又把一些简单的过程合到主程序中了,所以有点乱,也有点猥琐。

起提供思路的作用还是完全可以的吧~~~
vara,b,c:array[1..105]ofinteger;
la,lb,lc,i:integer;
s:string; procedureincc;
vari:integer;
begin
fori:=1to105doc:=c+b; fori:=1to104doifc>9then begin
c:=c+cdiv10;
c:=cmod10;
end;
end;
functioncxiaoa:boolean; vari:integer;
begin
cxiaoa:=false;
fori:=105downto1do
ifc<athenbegincxiaoa:=true;break;end elseifc>athenbreak;
end;
proceduredoubleb;
vari:integer;
begin
fori:=1to105dob:=b*2;
fori:=1to104doifb>9then
begin
b:=b+bdiv10;
b:=bmod10;
end;
end;
proceduredecc;
vari,j:integer; begin
fori:=1to104do
ifc>=bthenc:=c-belse begin
j:=i+1;
whilec[j]=0doinc(j); whilej>ido
begin
c[j]:=c[j]-1;
c[j-1]:=c[j-1]+10; dec(j);
end;
c:=c-b;
end;
end; procedurefua;
vari:integer;
begin
fori:=1to104do
ifa>cthena:=a-celse begin
a:=a-1;
a:=a+10;
a:=a-c;
end;
end; procedureoutit; vari,j:integer; begin
fori:=1to105doa:=a*2;
fori:=1to104doifa>9then begin
a:=a+adiv10;
a:=amod10;
end;
ifa[1]>0thena[1]:=a[1]-1else begin
j:=2;
whilea[j]=0doinc(j);
whilej>1do
begin
a[j]:=a[j]-1;
a[j-1]:=a[j-1]+10;
dec(j);
a[1]:=a[1]-1;
end;
fori:=105downto1doifa>0thenbeginj:=i;break;end; fori:=jdownto1dowrite(a);
end;
begin
readln(s);
la:=length(s);
fori:=ladownto1doa:=ord(s[la+1-i])-ord('0');
b[1]:=1;
c[1]:=1;
whilecxiaoado
begin
doubleb;
end;
decc;
fua;
outit;
end.
[编辑本段]
约瑟夫问题的另外一个有名的例子:
[编辑本段]
猴子选大王
一.问题描述:
一堆猴子都有编号,编号是1,2,3...m,这群猴子(m个)按照1-m的顺序围坐一圈,从第1开始数,每数到第N个,该猴子就要离开此圈,这样依次下来,直到圈中只剩下最后一只猴子,则该猴子为大王。

二.基本要求:
(1)输入数据:输入m,nm,n为整数,n<m
(2)中文提示按照m个猴子,数n个数的方法,输出为大王的猴子是几号,建立一个函数来实现此功能
C程序:
#include<stdio.h>
#include<malloc.h>
#defineLENsizeof(structmonkey)//定义structmonkey这个类型的长度
structmonkey
{
intnum;
structmonkey*next;
};
structmonkey*create(intm)
{
structmonkey*head,*p1,*p2;
inti;
p1=p2=(structmonkey*)malloc(LEN);
head=p1;
head->num=1;
for(i=1,p1->num=1;i<m;i++)
{
p1=(structmonkey*)malloc(LEN);
p1->num=i+1;
p2->next=p1;
p2=p1;
}
p2->next=head;
returnhead;
}
structmonkey*findout(structmonkey*start,intn) {
inti;
structmonkey*p;
i=n;
p=start;
for(i=1;i<n-1;i++)
p=p->next;
returnp;
}
structmonkey*letout(structmonkey*last) {
structmonkey*out,*next;
out=last->next;
last->next=out->next;
next=out->next;
free(out);
returnnext;
}
intmain()
{
intm,n,i,king;
structmonkey*p1,*p2;
printf("请输入猴子的个数m:\n"); scanf("%d",&m);
printf("每次数猴子的个数n:\n"); scanf("%d",&n);
if(n==1)
{
king=m;
}
else
{
p1=p2=create(m);
for(i=1;i<m;i++)
{
p2=findout(p1,n);
p1=p2;
p2=letout(p1);
p1=p2;
}
king=p2->num;
free(p2);
}
printf("猴王的编号是:%d\n",king); return0;
}
pascal程序:
var
a:array[1..10000]ofinteger; n,s,i,j:integer;
begin
read(m,n);
fori:=1tomdoa[i]:=1;
j:=0;
fori:=1tomdo
begin
s:=0;
whiles<ndo
begin
ifj<mtheninc(j)
elsej:=1;
s:=s+a[j];
end;
write(j);
a[j]:=0;
end;
end.
#include<stdio.h>
intmain()
{
intn,m,i,s=0;
printf("NM=");scanf("%d%d",&n,&m); for(i=2;i<=n;i++)s=(s+m)%i;
printf("Thewinneris%d\n",s+1); return0;
}
约瑟夫数学算法
#include<stdio.h>
#include<conio.h>
intmain(void)
{
intn,i=0,m,p;
scanf("%d%d",&n,&m);//n总人数,m步长while(++i<=n)
{
p=i*m;
while(p>n)
p=p-n+(p-n-1)/(m-1);
printf("%d\n",p);
}
getch();
return0;
}
[编辑本段]
笔算解决约瑟夫问题
在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的报数,报到2就去死,直到只剩下一个人为止。

此时要比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-nmod3-1),a(n-nmod3+1),..,an
于是可得:
1、这一轮中最后一个死的是a(n-nmod3),下一轮第一个报数的是a(n-
nmod3+1)
2、若3|n,则最后死的人为新一轮的第F(n-[n/3])个人
若nmod3≠0且f(n-[n/3])<=nmod3则最后死的人为新一轮的第n-[n/3]+F(n-[n/3])-(nmod3)人
若nmod3≠0且f(n-[n/3])>nmod3则最后死的人为新一轮的第F(n-[n/3])-(nmod3)人
3、新一轮第k个人对应原来的第3*[(k-1)/2]+(k-1)mod2+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])<=nmod3时k=n-[n/3]+F(n-[n/3])-(nmod3),F(n)=3*[(k-1)/2]+(k-1)mod2+1
当f(n-[n/3])>nmod3时k=F(n-[n/3])-(nmod3),F(n)=3*[(k-1)/2]+(k-1)mod2+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)=7F(11)=7F(16)=8f(24)=11f(36)=16f(54)=23f(80)=31f(119)=43f(178)=62f (266)=89f(398)=130
F(596)=191F(894)=286F(1340)=425F(2009)=634。

相关文档
最新文档