子问题与原问题........
题意:
有k个坏人k个好人坐成一圈,前k个为好人(编号1~k),后k个为坏人(编号k+1~2k)
现在有一个报数m,从编号为1的人开始报数,报到m的人就要自动死去。问当m为什么值时,可以使得在出现好人死亡之前,k个坏人先全部死掉?PS:当前一轮第m个人死去后,下一轮的编号为1的人 为 前一轮编号为m+1的人。 前一轮恰好是最后一个人死掉,则下一轮循环回到开头那个人报“1”
这道题起初看以为是道模拟题,因为数据结构课上写过。但是必然超时····到11的时候就卡死了。于是乎找到题解打表过了~~~~
搜资料后发现,运用重新标号,子问题求解。
最初的约瑟夫:将问题变为有n个人 编号为0~n-1,从0开始报数,到m-1时,此人出列。再从下一个开始。求胜利者的编号。
/**
问题描述:已知n个人(编号1,2...n)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;
他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,求最后出列的人的编号。
(以下代码是默认k = 1)
(1) 循环链表的方法时间复杂度为O(n*m).
(2) 一个时间复杂度为O(n)的数学方法,思路如下:
假设当前剩下i个人(i<=n),显然这一轮m要挂(因为总是从1开始数).经过这一轮,
剩下的人是:1 2 3...m-1 m+1...i, 我们将从m+1开始的数映射成1,则m+2对应2, n对应i-m,
1对应成i-m+1, m-1对应i-1,那么现在的问题变成了已知 i-1个人进行循环报数m,求出去的人的序号。
假设已经求出了i-1个人循环报数下最后一个出去的人的序号x0,那么它在n个人中的序号 x1 = (x0+m-1)%n + 1,
最初的 x0=1 ,反复迭代 x0和x1可以求出.
**/
#include <stdio.h>
int main()
{
int n, m, s, i;
scanf("%d %d", &n, &m);
s = 1; //初值为1
for (i=2; i<=n; i++)
{
s = (s+m)%i;
if (s == 0) s = i;
//这两个语句也可以用s = (s+m-1)%i + 1;来代替
}
printf("answer = %d/n", s);
}
POJ 1012
ans[i] 对应第i次出队人的编号 ans[0]=0;
ans[i]=(ans[i-1]+m-1)%(n-i+1); (i>1 , 总人数n=2k 则n-i为第i轮剩余的人数)
优化:
看到很多人对m并不是递增+1,而是m+=k+1 或m+=k+2。 为什么?考虑当最后剩下K+1个人。此时必然K+1为终止点,但是起始位置要看上一轮K+2。K+2可能在K+1的左侧,那么m必然是t*(k+1+1),也可能在右侧,m=t*(k+1),
接下来说说m的取值范围:我们考察一下只剩下k+1个人时候情况,即坏人还有一个未被处决,那么在这一轮中结束位置必定在最后一个坏人,那么开始位置在哪呢?这就需要找K+2个人的结束位置,然而K+2个人的结束位置必定是第K+2个人或者第K+1个人,这样就出现两种顺序情况:GGGG.....GGGXB 或 GGGG......GGGBX (X表示有K+2个人的那一轮退出的人)所以有K+1个人的那一轮的开始位置有两种可能即第一个位置或K+1的那个位置,限定m有两种可能:t(k+1) 或 t(k+1)+1; t>=1; 若遍历每一个m必定超时,避免超时则需要打表和限制m的范围。
#include<stdio.h>
int a[14];
int f(int k,int m)
{
int n,i,s;
n=2*k;s=0;
for(i=0;i<k;i++)
{
s=(s+m-1)%(n-i);
if(s<k) return 0;//遇到前k轮中有小于k的直接返回0
}
return 1;
}
int main()
{
int i,k,n;
for(k=1;k<=14;k++)
{
i=k+1;
while(1)
{
if(f(k,i))//t(k+1)的情况
{
a[k]=i;
break;
}
else if(f(k,i+1))//t(k+1)+1的情况
{
a[k]=i+1;
break;
}
i+=k+1;
}
}
while(scanf("%d",&n) && n)
{
printf("%d\n",a[n]);
}
return 0;
}