C语言程序设计漫谈之从约瑟夫问题谈起.docx

上传人:b****5 文档编号:8005731 上传时间:2023-01-27 格式:DOCX 页数:18 大小:22.54KB
下载 相关 举报
C语言程序设计漫谈之从约瑟夫问题谈起.docx_第1页
第1页 / 共18页
C语言程序设计漫谈之从约瑟夫问题谈起.docx_第2页
第2页 / 共18页
C语言程序设计漫谈之从约瑟夫问题谈起.docx_第3页
第3页 / 共18页
C语言程序设计漫谈之从约瑟夫问题谈起.docx_第4页
第4页 / 共18页
C语言程序设计漫谈之从约瑟夫问题谈起.docx_第5页
第5页 / 共18页
点击查看更多>>
下载资源
资源描述

C语言程序设计漫谈之从约瑟夫问题谈起.docx

《C语言程序设计漫谈之从约瑟夫问题谈起.docx》由会员分享,可在线阅读,更多相关《C语言程序设计漫谈之从约瑟夫问题谈起.docx(18页珍藏版)》请在冰豆网上搜索。

C语言程序设计漫谈之从约瑟夫问题谈起.docx

C语言程序设计漫谈之从约瑟夫问题谈起

从“约瑟夫问题”谈起

约瑟夫问题是一个出现在计算机科学和数学中的问题。

在计算机编程的算法中,类似问题又称为约瑟夫环。

据说著名犹太历史学家Josephus有过以下的故事:

在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。

然而Josephus和他的朋友并不想自杀。

为避免与其他39个决定自杀的犹太人发生冲突,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:

15个教徒和15个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:

30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行,直到仅余15个人为止。

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

【例1】约瑟夫问题。

N个人围成一圈,从某个人开始,按顺时针方向从1开始依次编号。

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

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

由于人的个数是有限的,因此最终会剩下一个人,该人就是优胜者。

输入N和M,输出出圈顺序。

例如,N=6、M=5,出圈的顺序是:

5,4,6,2,3,1。

(1)编程思路。

为输出出圈顺序,采用一个数组来进行模拟。

定义intcircle[N+1],并按circle[i]=i+1的方式赋予各元素初值。

该值代表两个含义:

1)值为0,代表编号i+1的人不再圈中;2)值非0,代表圈中第i个位置的人编号为i+1。

定义变量i代表报数位置的流动,i的初值为0,代表编号为1的人的位置,i的变化方式为:

i=(i+1)%(n),即0-->1-->2……->n-1->0-->1……。

i流动到了位置i后,该位置的人若已出圈(circle[i]==0),显然无法报数,得跳过该位置;若该位置的人在圈中,则报数(定义一个表示报数的变量p,初值为0,每次报数p++)。

当报数到m(即p==m)时,位置i的人出圈,记录出圈人数cnt++,同时p置为0。

当出圈人数等于N时循环结束。

(2)源程序。

#include

intmain()

{

intn,m,i,p,cnt;

intcircle[50];

while(scanf("%d%d",&n,&m)&&n!

=0)

{

for(i=0;i

circle[i]=i+1;

i=0;//报数指示

p=0;//报数计数器

cnt=0;//出队人数

while(cnt

{

if(circle[i]!

=0)p++;

if(p==m)

{

printf("%d",circle[i]);

cnt++;

circle[i]=0;

p=0;

}

i=(i+1)%(n);

}

printf("\n");

}

return0;

}

下面我们从例1的基础上进行扩展讨论。

例如,运行例1的程序时,输入413,则输出为:

3691215182124273033363915101419232832374171320263440817293811252224351631

为这个输出结果进行的模拟是需要耗时的。

实际上,在大多数问题中,我们不关心中间的结果,只关心某个最终结果。

例如,在Josephus的故事中,Josephus和他的朋友不想自杀,Josephus需要关心的是最后一个和倒数第2个出圈的编号是多少,至于中间过程(39个犹太人谁先自杀,谁后自杀)对Josephus来说无意义。

因此,Josephus需要的是快速确定最后一个和倒数第2个出圈的编号,然后站到对应位置即可。

而无需耗时模拟整个过程。

【例2】猴子选大王。

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

已知猴子数m和报数间隔n(设1<=n<=m<=50),问编号为多少的猴子当大王?

(1)编程思路1。

将例1的源程序略作修改,增加一个变量last记录最后获胜者编号,不输出中间过程。

显然,if(cnt==n)last=circle[i];

(2)源程序1。

#include

intmain()

{

intn,m,i,p,cnt,last;

intcircle[50];

while(scanf("%d%d",&n,&m)&&n!

=0)

{

for(i=0;i

circle[i]=i+1;

i=0;//报数指示

p=0;//报数计数器

cnt=0;//出队人数

while(cnt

{

if(circle[i]!

=0)p++;

if(p==m)

{

cnt++;

if(cnt==n)last=circle[i];

circle[i]=0;

p=0;

}

i=(i+1)%(n);

}

printf("%d\n",last);

}

return0;

}

(3)编程思路2。

源程序1中采用数组模拟,由于猴子在圈中还是出圈是通过数组元素circle[i]的值非0还是0来判断,位置并未真正删除,因此当n和m很大时,程序的执行效率很低。

例如,仅求最后一个出圈的元素,循环就得执行m*n次(p从1报到m,每次报数流动i得走完整一圈,其中n-1个已出圈,圈中仅一个元素)。

为提高运行效率,可以考虑采用循环链表来进行模拟,这样每次出圈就将链表中的相应元素删除。

循环链表只剩最后一个元素时,输出胜者编号。

(4)源程序2。

#include

structJose

{

intcode;//编号

Jose*next;

};

intmain()

{

Jose*head,*p1,*p2;

intn,m,i,cnt,tmp;

scanf("%d%d",&n,&m);

while(n!

=0&&m!

=0)

{

head=newJose;

head->code=1;

p2=head;

for(i=2;i<=n;i++)//创建循环链表

{

p1=newJose;

p1->code=i;

p2->next=p1;

p2=p1;

}

p2->next=head;

p1=head;

cnt=n;

while(cnt>1)

{

tmp=m%cnt;//提高效率之举,当m大于圈中人数时不用循环多圈

if(tmp==0)tmp=cnt;

i=1;

while(i

{

i++;

p2=p1;

p1=p1->next;

}

p2->next=p1->next;//报m的结点出圈

deletep1;//释放出圈结点的空间

cnt--;

p1=p2->next;

}

printf("%d\n",p1->code);

deletep1;

scanf("%d%d",&n,&m);

}

return0;

}

(5)编程思路3。

本例中的源程序2相比源程序1可以提高运行效率,但毕竟也是采用过程模拟,因此对于n和m较大的情况,效率仍然不高。

有没有可以根据n和m的值直接推出最后出圈人编号的办法呢?

为了讨论方便,先把问题稍微改变一下,并不影响原意。

问题描述:

n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。

求胜利者的编号。

  我们知道第1个人(编号一定是(m-1)%n)出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m%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-3-->n-3  k-2-->n-2

  变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:

例如x是最终的胜利者,那么根据转换把这个x变回去不刚好就是n个人情况的解吗?

下面我们来推导变回去的公式。

序列1:

1,2,3,4,…k-1,k,k+1,…,n-2,n-1,n

  序列2:

1,2,3,4,…k-1,k+1,…,n-2,n-1,n

  序列3:

k+1,k+2,k+3,…,n-2,n-1,n,1,2,3,…,k-2,k-1

  序列4:

1,2,3,4,…,5,6,7,8,…,n-2,n-1

  ∵k=m%n;

 ∴x'=x+k=x+m%n;而x+m%n可能大于n

  ∴x'=(x+m%n)%n=(x+m)%n。

  如何知道(n-1)个人报数的问题的解f(n-1)呢?

显然只要知道(n-2)个人的解f(n-2)就行了。

(n-2)个人的解呢?

当然是先求f(n-3)----这显然就是一个倒推问题!

  令f[i]表示i个人玩报m退出的约瑟夫环游戏的最后胜利者的编号,则有递推公式:

  f[1]=0;

  f[i]=(f[i-1]+m)%i;(i>1)

  有了这个递推公式,我们就很容易求得n个人报m退出的约瑟夫问题的最后胜利者编号f[n]。

因为实际生活中编号总是从1开始,我们输出f[n]+1即可。

编写程序时,我们可以采用数组递推以便保存中间结果,也可以不保存中间任何结果采用迭代直接得到最后胜利者编号。

(6)采用迭代方式实现的源程序3。

#include

intmain()

{

intn,m,i,s;

scanf("%d%d",&n,&m);

while(n!

=0&&m!

=0)

{

s=0;

for(i=2;i<=n;i++)

s=(s+m)%i;

printf("%d\n",s+1);

scanf("%d%d",&n,&m);

}

return0;

}

(7)采用递推方式实现的源程序4。

//采用打表的方式,先将所有的值求出来保存在二维数组f[51][51]中。

//f[n][m]的值代表n个人报m游戏的最后胜利者编号。

//则有f[i][m]=0,(i=1)

//f[i][m]=(f[i-1][m]+m)%i(i>1)

#include

intmain()

{

intn,m,i,j,f[51][51];

for(i=1;i<51;i++)

f[1][i]=0;

for(i=1;i<51;i++)

{

for(j=2;j<51;j++)

f[j][i]=(f[j-1][i]+i)%j;

}

scanf("%d%d",&n,&m);

while(n!

=0&&m!

=0)

{

printf("%d\n",f[n][m]+1);

scanf("%d%d",&n,&m);

}

return0;

}

【例3】城市断电。

有n(3<=n<150)个城市围成圈,先将第1个城市(编号为1)断电,然后每隔m个城市使一个城市断电,直到剩下最后一个城市不断电。

问使2号城市不断电的最小的m是多少?

(1)编程思路。

采用例2的求最后胜利者的方式,对n个城市,从m=1开始搜索,若当前m可使2号城市作为胜利者,则m就是所求,否则m=m+1后,继续搜索。

程序采用打表的方式,先将n=3~149的对应m值求出来并保存到数组ans[150]中。

另外,需要注意的是第1个城市先断电了,2号城市相当第1个城市,也可以把问题看成编号从1~n-1的约瑟夫问题。

(2)源程序。

#include

intmain()

{

intans[150],i,j,m,tmp;

for(i=3;i<150;i++)

{

m=1;

while

(1)

{

tmp=1;//注意第1个城市已经断电,相当从1~n-1个城市

for(j=2;j

{

tmp=(tmp+m)%j;

if(tmp==0)

{

tmp=j;

}

}

if(tmp==1)//最后胜利者是2号城市

{

ans[i]=m;

break;

}

m++;

}

}

intn;

scanf("%d",&n);

while(n!

=0)

{

printf("%d\n",ans[n]);

scanf("%d",&n);

}

return0;

}

将此源程序提交给POJ2244“EenyMeenyMoo”,可以Accepted。

例2、例3采用约瑟夫递推公式,直接得到的是最后胜利者的编号,中间的出圈顺序就没得到。

下面我们进一步讨论一下,能否不用模拟的方式,采用递推公式计算的方法,得到例1所示的出圈顺序呢?

设有n个人(0,...,n-1),报数m出圈,则第i轮出圈的人为

f(i)=(f(i-1)+m-1)%(n-i+1)(i>=1),f(0)=0;f(i)表示当前子序列中要出圈的那个人(当前序列编号为0~(n-i));

例如,设n=6,m=5

f(0)=0;

f

(1)=[f(0)+5-1]%6=4;子序列(0,1,2,3,4,5)中的4(也就是实际序列(1,2,3,4,5,6)中的5)

f

(2)=[f

(1)+5-1]%5=3;子序列(0,1,2,3,5)中的3(也就是实际序列(1,2,3,4,6)中的4)

f(3)=[f

(2)+5-1]%4=3;子序列(0,1,2,5)中的5(也就是实际序列(1,2,3,6)中的6)

f(4)=[f(3)+5-1]%3=1;子序列(0,1,2)中的1(也就是实际序列(1,2,3)中的2)

f(5)=[f(4)+5-1]%2=1;子序列(0,2)中的1(也就是实际序列(1,3)中的3)

f(6)=[f(5)+5-1]%1=0;子序列(0)中的0(也就是实际序列

(1)中的1)

故得到的出圈顺序为:

5,4,6,2,3,1。

结果正确。

按照这样的思路,可以修改例1的源程序为:

#include

intmain()

{

intn,m,i,j,cnt,circle[51],f[51];

scanf("%d%d",&n,&m);

while(n!

=0&&m!

=0)

{

for(i=0;i

circle[i]=i+1;

f[0]=0;

for(i=1;i<=n;i++)

{

f[i]=(f[i-1]+m-1)%(n-i+1);

}

cnt=n;

for(i=1;i<=n;i++)

{

printf("%d",circle[f[i]]);

for(j=f[i];j

circle[j]=circle[j+1];

cnt--;

}

printf("\n");

scanf("%d%d",&n,&m);

}

return0;

}

在弄清楚上面例子的情况下,建议大家刷下面几道POJ的题目,加深对约瑟夫问题及其变形的解决方法的理解与运用。

特别是约瑟夫递推公式的灵活运用。

1012Joseph题意为:

k个好人和k个坏人围成一圈(将他们顺序编号,好人编号为1~K,坏人编号k+1~2k)。

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

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

问M最小为多少时,前k个出圈的人全部是坏人?

1781InDanger题意为:

n个人排成一圈,从第2个人开始隔一个杀一个,直到剩下最后一人。

最后一个人的编号为多少?

2359Questions题意为:

输入一个字符串,从第一个字符开始循环数,数到第1999就删除这个字符,继续数,直到只剩下一个字符。

如果剩下的那个字符等于空格输出“no”,等于"?

"输出"yes",其他输出"Nocomments"。

3517AndThenThereWasOne题意为:

n个人围一圈,第m个人先出圈,然后从第m+1个人开始报k出圈,求最后胜利者编号。

3750小孩报数问题。

最后,给出上面5道题的源程序供参考,这些程序均可以Accepted。

参考源程序

POJ1012Joseph

#include

intmain()

{

intm,k,i,j,s;

intjoseph[15]={0};

while(scanf("%d",&k)&&k!

=0)

{

if(joseph[k]!

=0)

{

printf("%d\n",joseph[k]);

continue;

}

m=k+1;

while

(1)

{

for(i=0;i

{

s=(m-1)%(2*k-i);

for(j=2*k-i;j<2*k;j++)

{

s=(s+m)%(j+1);

}

if(s

}

if(i==k)

{

break;

}

m++;

}

printf("%d\n",m);

joseph[k]=m;

}

return0;

}

POJ1781InDanger

#include

intmain()

{

charp[5];

inta,b,c;

while(scanf("%s",p)!

=EOF)

{

if(p[0]=='0'&&p[1]=='0'&&p[3]=='0')

break;

a=(p[0]-'0')*10+p[1]-'0';

for(b=1;b<=p[3]-'0';b++)

{

a=a*10;

}

c=1;

while

(1)

{

if(c>=a)break;

a=a-c;

c=2*c;

}

printf("%d\n",2*a-1);

}

return0;

}

POJ2359Questions

#include

#include

#defineN1999

intmain()

{

charc,s[30001];

intlen,i,cnt;

len=0;

while((c=getchar())!

=EOF)

{

if(c=='\n')continue;

s[len++]=c;

}

s[len]='\0';

cnt=0;

for(i=2;i<=len;i++)

cnt=(cnt+N)%i;

if(s[cnt]=='?

')

printf("Yes\n");

elseif(s[cnt]=='')

printf("No\n");

else

printf("Nocomments\n");

return0;

}

POJ3517AndThenThereWasOne

#include

intmain()

{

intn,k,m,i,s;

scanf("%d%d%d",&n,&k,&m);

while(n!

=0&&k!

=0&&m!

=0)

{

s=0;

for(i=2;i<=n-1;i++)

s=(s+k)%i;

printf("%d\n",(s+m)%n+1);

scanf("%d%d%d",&n,&k,&m);

}

return0;

}

POJ3750小孩报数问题

//参考源程序1

#include

intmain()

{

intn,w,s,i,p,cnt;

charname[65][16];

intoutId[65];

scanf("%d",&n);

for(i=1;i<=n;i++)

scanf("%s",name[i]);

for(i=0;i

outId[i]=i+1;

scanf("%d,%d",&w,&s);

i=w-1;//报数指示

p=0;//报数计数器

cnt=0;//出队人数

while(cnt

{

if(outId[i]!

=0)p++;

if(p==s)

{

cnt++;

printf("%s\n",name[outId[i]]);

outId[i]=0;

p=0;

}

i=(i+1)%(n);

}

return0;

}

//参考源程序2

#include

structJose

{charname[16];

Jose*next;

};

intmain()

{

Jose*head,*p1,*p2;

intn,w,s,i;

scanf("%d",&n);

head=newJose;

scanf("%s",head->name);

p2=head;

for(i=2;i<=n;i++)//创建循环链表

{

p1=newJose;

scanf("%s",p1->name);

p2->next=p1;

p2=p1;

}

p2->next=head;

scanf("%d,%d",&w,&s);

p1=head;

i=1;

while(i

{

i++;

p2=p1;

p1=p1->next;

}

while(p1->next!

=p1)

{

i=1;

while(i

{

i++;

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 自然科学 > 数学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1