约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3。
分析:
(1)由于对于每个人只有死和活两种状态,因此可以用布尔型数组标记每个人的状态,可用true表示死,false表示活。
(2)开始时每个人都是活的,所以数组初值全部赋为false。
(3)模拟杀人过程,直到所有人都被杀死为止。
暴力模拟的具体代码如下:
//约瑟夫环
//暴力实现 复杂度O(mn)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int n, m;//n个人,报m的退出游戏
//从1开始报数
scanf("%d %d", &n, &m);
int *p = (int*)malloc(sizeof(int)*(n));
//编号1~n,对应数组下标0~n-1
memset(p, 0, sizeof(int)*n);
//初始化0
int i=0,j=0;//i用于遍历,j表示当前有多少人退出
int cnt=0;//当前轮次已经报数人数
while (j != n - 1)//最后剩下一个人
{
if (!p[i])//0表示没退出,进行报数
cnt++;
if (cnt == m)//找到报m的人了
{
p[i] = 1;//1表示退出
j++;
cnt = 0;//重置报数
}
if (i == n - 1)//遍历到了最后一个人
i = -1;
i++;//遍历后移
}
//查找谁还存活
for (i = 0; i < n;i++)
if (!p[i])
{
printf("%d\n", i + 1);
break;
}
return 0;
}
暴力模拟的时间复杂度不难看出是O(mn),当m和n都很大时该程序运算的时间将会较长。
下面给出一个时间复杂度优化至O(n)的方案。
为了讨论方便,先把问题稍微改变一下,并不影响原意:
问题描述:
n个人(编号0~n-1),从1开始报数,报到m-1的退出,剩下的人继续从0开始报数。求胜利者的编号b。
递推公式:
f(N,M)=(f(N−1,M)+M)%N
f(N,M)表示,N个人报数,每报到M时那个人出局,最终胜利者的编号
f(N−1,M)表示,N-1个人报数,每报到M时那个人出局,最终胜利者的编号
下面说明一下这个递推公式:
具体的给出下面例子
若已知f(50,3)欲求f(49,3)该用何种方法呢?
现在先将50个人中剔除一个,按照题目的剔除方,我们要将3号剔除,而将1号和2号最终放在50号之后。即将数组循环左移3个单位。那么我们的f(49,3)=(f(50,3)-3)%49,取余的原因是f(50,3)-3可能出现负数,我们要化负数为正数。
∴得f(n-1,m)=(f(n,m)-m)%(n-1)
经过变换后可得 f(n,m)=(f(n-1,m)+m)%n
这里的%n也可以理解不让数组越界。
代码实现如下:
//递推法 f(n)=(f(n-1)+m)%m
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int n, m,i;
scanf("%d %d", &n, &m);
int* f = (int*)malloc(sizeof(int)*(n+5));
memset(f, 0, sizeof(int)*(n+5));
f[1] = 0;//一个人最后的赢家就是自己下标为0,编号为1
for (i = 2; i <= n; i++)
f[i] = (f[i-1] + m) % i;
printf("%d\n", f[n]+1);//编号1~n,下标0~n-1
//f[n]的值是下标,要得到编号需要+1
return 0;
}
另外该递推法同斐波那契数列的递推法一样也可以做空间复杂度的优化:
#include<stdio.h>
int main()
{
int n, m,i;
scanf("%d %d", &n, &m);
int ans=0;//一个人最后的赢家就是自己下标为0,编号为1
//最开始ans代表一个人。。。
for (i = 2; i <= n; i++)
ans = (ans + m) % i;
printf("%d\n", ans+1);//编号1~n,下标0~n-1
//ans的值是下标,要得到编号需要+1
return 0;
}