首先理解题意
设有n个人围坐一圈并按顺时针方向从1到n编号。
从第s个人开始进行1到m的报数, 报数到第m个人, 此人出圈, 再从他的下一个人重新开始1到m的报数,如此进行下去直到所有的人都出圈为止。
现要求按出圈次序,给出这n个人的顺序表p。
木易版答案:
void Josegh(void)
{ int i,j,s1,w;
s1=s;
for(i=1; i<=n; i++)
p[i-1]=i;
for(i=n; i>=2; i--)
{ s1=(s1+m-1)%i;
if(s1==0) s1=i;
w=p[s1-1];
for(j=s1; j<i; j++)
p[j-1]=p[j];
p[i-1]=w;
}
}
这里提出几个广泛流传的错误:
本人在学习此题过程中发现,不管是网上还是书店的一些参考书中,对此题的讲解都有一些错误!本人在这里提出几个广泛流传的错误,希望大家指正!
① 南开上机题或无忧上机题的题干错误!
原题干:
若第i个人报数后出圈,则将p[i]置于数组的倒数第i个位置上,而原来第i+1个至倒数第i个元素依次向前移动一个位置。
举个例子:
套用原题干:
若第10个人报数后出圈,则将p[10]置于数组的倒数第10个位置上,而原来第11个至倒数第10个元素依次向前移动一个位置。
上面这句话显然不对!
而实际上是:
原列队第10个人报数后出圈(即编号为10的人报数出圈),则将p[9]置于数组的倒数第1个位置上,而原来第11个至倒数第1个元素依次向前移动一个位置。
或这么表述:
第1个报数后出圈的人(因为编号为10的人是第1个报数出圈的),将p[9]置于数组的倒数第1个位置上,而原来第11个至倒数第1个元素依次向前移动一个位置。
错误分析:
编题的人没有搞清其实“出圈次序”“出圈人编号”“出圈人所对应的数组下标”是三个不同的概念!
如:第一个出圈的人,出圈次序是1,下标是p[9],编号是10。
而编题人简单地都用i表示了!显然不对!
这里我认为本句话应该改为:
第i个人报数出圈的人,将“此人的编号”置于数组的倒数第i个位置上。
②市面上卖的谭浩强主编的《三级C语言上机指导》一书中(清华大学出版社2005年7月第1版封皮是红色的价格是22.00元)对此题的解释也有错误:
讲解中说:s1=(s1+m-1)%i的作用是找出报数后出圈人的下标。
本人认为编者也犯了类似的错误:
应该改为:
s1=(s1+m-1)%i的作用是找出报数后出圈人的“编号”。
因为变量s1表示的不是出圈人的所对应的数组下标,而是其“编号”。
清楚变量:
变量i:表示没有出圈的人数。
变量s1:表示出圈人所对应的编号。编号s1=p[s1-1]。
变量w:暂存变量,用于暂时存放刚刚出圈人的编号。
数组p[]:存放出圈人编号的数组。程序结束后,该数组按出圈次序倒序存放出圈人的编号。
思路步骤:
①建立数组,应注意:p[下标]=编号 ;下标=编号-1;编号s1=p[s1-1]。
对应代码是:for(i=1; i<=n; i++) p[i-1]=i;
②倒序循环for(i=n; i>=2; i--),找出出圈人的“编号”s1。
对应的算法是通过求余得到的!s1=(s1+m-1)%i;
③将刚出圈人的编号先寄存在暂存变量中。
④将刚出圈的人之后至原队列最后一个未出圈的人依次在数组中前移一位。对应的代码是:for(j=s1; j<i; j++) p[j-1]=p[j];
⑤将刚出圈人的编号放置在最后一个未出圈的人的后面,即p[i-1]的位置上。
注意事项:
①此题有两种变形,差别在于出圈人编号的存放顺序。考生应注意考题中是哪种变形!
形式一:
若第i个人报数后出圈,则将此人编号置于数组的倒数第i个位置上,而原来第i+1个至倒数第i个元素依次向前移动一个位置;即第1个出圈的编号存放在p[n-1]中,第2个出圈的编号存放在p[n-2]中,直至第n个出圈的编号存放在p[0]中。
对应的void WriteDat(void) 函数中的循环语句是for(i=N-1; i>=0; i--)
形式二:
第1个出圈的编号存放在p[0]中,第2个出圈的编号存放在p[1]中,直至第n个出圈的编号存放在p[n-1]中。
对应的void WriteDat(void) 函数中的循环语句是for(i = 0 ; i <= N - 1 ; i++)
上面答案是形式一的答案。
若遇到形式二,考生须在原有答案的基础上再增加两个循环以使循序颠倒过来!
for(i=0,j=n-1;i<n,j>=0;i++,j--) /*也可改为for(i=0,j=n-1;i<n;i++,j--) */
q[j]=p[i];
for(i=0;i<n;i++)
p[i]=q[i];
②由于求余的作用,当报数人正好到最后一个人时s1为0,故要有if(s1==0) s1=i;
③根据题意,开始时莫忘设置s1=s;
//
这是17世纪的法国数学家加斯帕在《数目的游戏问题》中讲的一个故事:15个教徒和15个非教徒在海上遇险,必须将一半的人投入海中,其余的人才能幸免于
难,于是想了一个办法:30个人围成一个圆圈,从第一个人开始依次报数,每数到第9个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,
才能使每次投入大海的都是非教徒。
约瑟夫问题本身并不难,但求解的方法很多;题目的变化形式也很多,这里介绍一个用链表来实现的方法。
题目中30个人围成一圈,因而启发我们用一个循环链来表示。可以使用结构数组构成一个环形链。结构中有两个成员,其一为指向下一个人的指针,以构成环形的 链;其二为该人是否被扔下海的标记,为1表示该人已被扔下海了。这样循环计数直到有15个人被扔下海为止。
算法实现如下:
#include <stdio.h>
struct node{
int nextp; //指向下一个人(数组元素下标)
int no_out; //是否被扔下海的标记:没有被扔下海,0:已被扔下海
}link[31]; //30个人,0号元素没有使用
void main(){
int i,j,k;
printf("The original circle is(+:pagandom,@christian):/n");
for(i=1;i<=30;i++){ //初始化结构数组
link[i].nextp=i+1; //指针指向下一个人(数组元素下标)
link[i].no_out=1; //标志置为1,表示人都在船上
}
link[30].nextp=1; //第30个人的指针指向第1个人以构成环
j=30; //j:指向已经处理完毕的数组元素,从link[j]指向的人开始计数
for(i=0;i<15;i++){ //i:已扔下海的人数计数器
for(k=0;;) //k:决定哪个人被扔下海的计数器
if(k<9){ //计数不到9则继续计数
j=link[j].nextp; //修改指针,取下一个人
k+=link[j].no_out; //进行计数。因已扔下海的人标记为0,故这样计数不会影响正常结果
}
else break; //计数到9则停止计数
link[j].no_out=0; //将标记置0,表示该人被扔下海
}
for(i=1;i<=30;i++)
printf("%c",link[i].no_out?'@':'+'); //+:被扔下海,@:在船上
printf("/n");
}
计算机等级考试三级的C语言上机题里有一道比较有难度的题:这就是“出圈”题,完整题目如下:
设有n个人围坐一圈并按顺时针方向从1到n编号,从第s个人开始进行1到m的报数,
报数到第m个人,此人出圈,再从他的下一个人重新开始1到m的报数,如此进行下去直到所有的人都出
圈为止。现要求按出圈次序,每10人一组,给出这n个人的顺序表。
请考生编制函数Josegh()实现此功能并调用函数WriteDat()把结果p输出
到文件JOSE.OUT中。
这里给出另一种算法,分析:
设 n = 100, s = 1,m = 10。
(1) 将1到n个人的序号存入一维数组p中;
(2) 若第i个人报数后出圈,则将p[i]置于数组的倒数第i个位置上,而原来第i+1个至倒数第i个
元素依次向前移动一个位置;
(3) 重复第(2)步直至圈中只剩下p[1]为止。
注意:部分源程序存放在文件prog1.c中。
请勿改动主函数main()和输出数据函数WriteDat()的内容。
#include <stdio.h>
#define N 100
#define S 1
#define M 10
int p[100], n, s, m ;
void WriteDat(void) ;
void Josegh(void){
int i,j,s1,w;
s1=s;
for(i=1; i<=n; i++)
p[i-1]=i; /*数组p依次存放n个人的原始位置,在每次报数离圈后,移动数组p中元素,如图,最终p的逆序 为小孩出圈的次序*/
for(i=n; i>=2; i--){ //从圈子有n个人到只剩下2个人
s1=(s1+m-1)%i; //出圈人的编号是(s1+m-1)%i
if(s1==0) s1=i; //要开始报数的是最后一个人
w=p[s1-1]; //将要出圈的人的原始编号先赋给w
for(j=s1; j<=i-1; j++)
p[j-1]=p[j]; //离圈人后的数组各项依次向前移一位,覆盖离圈人位置
p[i-1]=w; //将要出圈的人放在后面腾出的位置,(考虑到WriteDat函数输出时对存放离圈顺序编号的p[]按从末至首读取)
}
}
void main()
{
m = M ;
n = N ;
s = S ;
Josegh() ;
WriteDat() ;
}
void WriteDat(void)
{
int i ;
FILE *fp ;
fp = fopen("jose.out", "w") ;
for(i = N - 1 ; i >= 0 ; i--) { //
printf("%4d ", p[i]) ;
fprintf(fp, "%4d", p[i]) ;
if(i % 10 == 0) {
printf("/n") ;
fprintf(fp, "/n") ;
}
}
fclose(fp) ;
}
本算法示意图:联系本人
用具体数字提出该出圈问题,并用C++语法描述:
10个小孩围成一圈玩游戏,每个小孩的编号依次为1,2,3,3,4,... ,10。从第1个小孩开始,顺时针数3个小孩,该小孩离开圈子,接着再数3个小孩,该小孩又离开圈子,最后剩下的一个小孩为胜利者。编程输出离开圈子小孩 的顺序和胜利者的编号。
分析:利用一维数组编写程序,每个元素表示一个小孩,元素的值为小孩的编号。离开圈子的小孩对应的元素为0.这里有一个关键问题需要解决:如何用一维数组 表示小孩围成一圈?或者说,当数到数组尾时如何自动回到数组首?解决此问题可以采用“加1求余”法,即将元素的下标改为下标加1除以人数的余数:i= (i+1)%10,便可解决围成一圈的问题。
程序如下:
#include<iostream.h>
void main()
{
int a[10];
for(int i=0;i<10;i++) //给每个小孩一个编号
a[i]=i+1;
int k=1; //标识处理第k个离开的小孩
i=-1; //下标初值为-1,下一个值0就是第1个小孩的下标
cout<<"离开圈子的小孩依次是:";
while(1)
{
for(int j=0;j<3;) //在圈中数3个小孩
{
i=(i+1)%10;
if(a[i]!=0) //如果该小孩在圈中,则数数有效
j++;
}
if(k==10)break;
cout<<a[i]<<" "; //输出离开圈子的小孩编号
a[i]=0; //标识该小孩已离开
k++;
}
cout<<endl<<"胜利者是:"<<a[i]<<endl; //输出胜利者编号
}
运行情况:
离开圈子的小孩依次是:3 6 9 2 7 1 8 5 10
胜利者是:4
当然,你可以该程序,使其具有通用性。例如,总人数可以改变,每次数到第几个人离开圈子也可以改变。
http://topic.csdn.net/t/20061027/17/5114598.html