题目链接地址:
九度OJ-题目1356:孩子们的游戏(圆圈中最后剩下的数)
题目描述:
每年六一儿童节,JOBDU都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为JOBDU的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为1的小朋友开始报数。每次喊到m的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续1...m报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到JOBDU名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?
输入:
输入有多组数据。
每组数据一行,包含2个整数n(0<=n<=1,000,000),m(1<=m<=1,000,000),n,m分别表示小朋友的人数(编号1....n-1,n)和HF指定的那个数m(如上文所述)。如果n=0,则结束输入。
输出:
对应每组数据,输出最后拿到大奖的小朋友编号。
样例输入:
1 10
8 5
6 6
0
样例输出:
1
3
4
解题思路:
从题目的描述可以看出这个就是传说中的约瑟夫环问题,以前在学C++的时候做过这道题,用的是循环链表。再看看这道题的时间限制居然有10S!于是没有多想,写了个循环链表,一提交居然超时了。。。 只好请教范神,大神说求约瑟夫环可以直接套用数学公式的。后来上网查了一下,还真有这个公式,具体可以参考 约瑟夫问题。
下面介绍一下算法流程:
假设初始约瑟夫环为0,1,2,...n-2,n–1,每次报到 m-1的元素被踢出环;
则第一次报数完毕后,编号为m–1的元素必定被踢出环,此时圈子里还剩下:m,m+1,m+2 ... n-2,n-1,0,1,2,... m-2
而第二次从圆圈的第m号重新开始报数,于是对这剩下的n - 1个数重新进行编号(这是解决本题最关键的一步),重新编号的映射关系如下:
m --> 0
m+1 --> 1
m+2 --> 2
......
n-1 --> n-m-1
0 --> n-m
......
m-2 --> n-2
此时问题的本质不变,但是问题的规模却从n减少到了n–1,所以我们可以利用递归的思想来解题:
假设具有n个元素的约瑟夫环,将每次报m-1的元素踢出后,最后所剩下的元素编号是X(n) (n为约瑟夫环的元素个数)。
则可以得知在原始0,1,2,...n-3,n-2,n-1约瑟夫环中最后剩下的数是X(n),
在删除m - 1后重新编号的0,1,2,...n-3,n-2约瑟夫环中,最后剩下的数是X(n-1),
由上面重新编号的映射关系可以得出:(X(n) - m) % n =X(n-1),
利用数学公式 (X(n) - m) % n = X(n-1) 可以推算出X(n) = (X(n-1) + m) % n;
同理想要得出X(n-1),可以由(X(n-1) - m) % (n - 1) = X(n-2)推出X(n-1) = (X(n-2) + m) % (n - 1);
……
以此类推,当n = 2时,X(2) = (X(1) + m) % 2;
当n = 1时,约瑟夫环中只有一个编号为0的元素,因此X(1) = 0。
由X(1) = 0,X(n) = (X(n-1) + m) % n 这两个条件可以推算出X(n) 。
需要注意的是因为题目中孩子编号从1开始,而上面算法中的元素编号从0开始,所以最后的结果X(n) 还要加上1。
AC代码如下:
#include
int main()
{
int n,m;
int result; // 约瑟夫环中最后剩下的数字
int i;
while(EOF != scanf("%d",&n))
{
if(0 == n)
break;
scanf("%d",&m);
result = 0; // 当n == 1时,result = 0;
for(i = 2;i <= n;i++)
{
result = (result + m) % i; // 利用递推公式求出当问题规模为i时的解,最后得出问题规模为n的解
}
printf("%d\n",result + 1); // 因为题目中的孩子编号从1开始,所以最后的结果要加1
}
return 0;
}
/**************************************************************
Problem: 1356
User: blueshell
Language: C
Result: Accepted
Time:300 ms
Memory:912 kb
****************************************************************/