关于约瑟夫环问题解决
约瑟夫环问题介绍:
已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。
例如:n = 9, k = 1, m = 5
【解答】
出局人的顺序为5, 1, 7, 4, 3, 6, 9, 2, 8。
再例如:有10人,淘汰顺序如图所示:
方法一:普通代码利用循环和标记解决
思考:
- (1)如何实现出局?
- (2)如何实现循环报数?
- 对于(1)问题可以采用标记法,可以利用数组里面的元素代表人数,下标代表人物的序号,让没出局的元素等于0,如果出局该元素标记为1。
那如何将元素为1的人排除?
可以利用if语句,if(arr[i]==0)
则进入报数,再用报数判断是否出局,如下代码:
if (arr[i] == 0)
{
baoshu++;
if (baoshu % M == 0)
{
arr[i] = 1;
printf("%d出局\n", i + 1);//i+1代表人物序号(1,2,3,4...),而i指的是数组下标(从0开始0,1,2,3,4...)
}
}
当arr[i]=1时,该i代表的人物不再进入循环,实现了i代表人物的出局,arr[i]=1时,该i代表的人物可以继续进入报数。
- 对于(2)问题可以利用while循环参考以下代码:
int i = -1;
while (i < N)
{
i++;
if (i == N)
{
i = 0;
}
}
此代码可以实现重复循环报数(N为总人数),那如何退出循环?
可以考虑统计出局人数,若出局人数 = 总人数 - 1,时可以停止循环并打印最后的人的序号。
完善报数代码(加入变量y,并判断y,考虑是否跳出循环):
if (arr[i] == 0)
{
baoshu++;
if (baoshu % M == 0)
{
arr[i] = 1;
printf("%d出局\n", i + 1);
y--;
if (y == 1)
{
break;
}
}
}
最后可以参考笔者的代码:
#include <stdio.h>
#define N 10
#define M 3
int main()
{
int arr[N] = { 0 };
int i = -1;
int baoshu = 0;
int y = N;
while (i < N)
{
i++;
if (i == N)
{
i = 0;
}
if (arr[i] == 0)
{
baoshu++;
if (baoshu % M == 0)
{
arr[i] = 1;
printf("%d出局\n", i + 1);//打印被赋值为1的人,也就是出局的人
y--;
if (y == 1)//剩下最后一个人(元素依旧为0的那个)
{
break;
}
}
}
}
for (i = 0; i < N; i++)//遍历,找出最后没有出局的人
{
if (arr[i] == 0)//最后一个人没有被赋值1
{
printf("最后剩下%d", i + 1);
}
}
return 0;
}
方法二:利用递归函数找出出局人的序号
首先可以列表找规律
举例:有10个人报数,报到的数为N倍数的人则出局
- 若N为3,则第3个人出局,这个不难理解,但如果N为11,那出局的人的序号为11吗?显然应该为1。那如何将这两种情况用一个公式表达呢?我们考虑用%。N %(这时候的总人数)= 出局人的序号
3 % 10 = 3 11 % 10 = 1 ,%的作用其实就是起到连接作用。 - 当N=3时,如图:可以从出局人后的一个人重新标号,当标号为3时,下一次出局的人就为该标号所指向的人。
每一列里第一行为原先序号,第二行为重新标号后的序号,我们不难发现,重新标的3号指向原先的号的人出局,且有关系:同一列里,原先序号 = 重新标号序号 + N(这里为3)前面提到N %(这时候的总人数)= 出局人的序号所以,这把出局的人的序号 = (重新标号后出局人的序号 + N(这里为3))%(这把的人数,也就是排序前人数); - 这次出局的人数可以由上一局出局人数推导而来,以此类推到第一局,上面说到过第一局可以由数学公式推导,可以找到递归出口:第一局,和递归方式
参考以下代码:
#include <stdio.h>
int ysfh(int n, int N, int i)
{
if (i == 1)
return N%n;
else
return (ysfh(n-1,N,i-1)+N)%n;
}
int main()
{
int n = 0, N = 0, i = 1;
scanf("%d%d", &n, &N);
for (i = 1; i <= n; i++)
{
printf("第%d次出局 %d\n", i, ysfh(n, N, i));
}
return 0;
}
运行结果:
- 所以,==此方法有问题!!!==这里我并不是故意写出有问题的文章,而是作为初学者发现的问题,网上太多对约瑟夫环的介绍中多数人物序号为0,1,2,3,4,5,6…我当时认为如果序号从1开始是不是更直观,于是将序号变为1,2,3,4,5,6…于是问题就出现了,导致我卡了好久,终于找到了问题所在:
- 如果人数为10,数到3出局,按原来思路3%10=3;第三个人出局,没问题;如果数到11者出局11%10=1,1号出局,没问题,但是我们忽略了如果数到10,10%10=0,根本没有0号人,于是我们需要从0开始标号才是正确的,从0开始标号才能利用%将报数连续起来。那既然思路没问题,那怎么改进原来代码?
- 先看图:、
- 递归过程没问题(原先序号 = 重新标号序号 + N(这里为3))
但是递归出口变了,第一次出局的人的序号应为(N-1)%(总人数10)= 2;
所以只要修改递归出口即可
如所示正确代码:
#include <stdio.h>
int ysfh(int n, int N, int i)
{
if (i == 1)
return (N-1)%n;//修改的地方
else
return (ysfh(n-1,N,i-1)+N)%n;//+N是由于原先序号 = 重新标号序号 + N
}
int main()
{
int n = 0, N = 0, i = 1;
scanf("%d%d", &n, &N);
for (i = 1; i <= n; i++)
{
printf("第%d次出局 %d\n", i, ysfh(n, N, i));
}
return 0;
}
如果还没有理解可以尝试倒过来分析(任何第几次出局都可以由其他人数的第一局递推出)
如果弄懂此类问题,对递归函数的学习帮助是十分大的,有利于弄懂递归出口和递归条件,将复杂问题,简单化。当然约瑟夫问题还可以用数据结构解决,但笔者的知识有限,无法继续探究。