题目描述
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
如果没有小朋友,请返回-1
解题思路
1. 暴力模拟:这个思路就不说了,比较简单。
2. 经典的约瑟夫环问题(参考该博客)
假设f(N, M)表示要求问题的解。
举个栗子:总人数N为10人,从0开始,每报到3就把一人淘汰(M=3)。
初始情况为: 0 1 2 3 4 5 6 7 8 9
淘汰掉一个之后: 0 1 3 4 5 6 7 8 9
此时,这些编号已经不能组成一个环,但是可以看出3至1之间还是连着的(4 5 6 7 8 9 0 1 2),且下一次报数将从3开始。但是,之后的报数将总要考虑原编号2处的空位问题。
如何才能避免已经产生的空位对报数所造成的影响呢?
可以将剩下的9个连续的数组成一个新的环(将1、3连接),这样报数的时候就不用在意3的空位了。但是新产生的环的数字并非连续的,报数时不像之前那样好处理了(之前没淘汰的人的编号可以递推,即(当前编号+1)%N),无法不借助存储结构得知下一个应该报数的现存人员编号。
如何使新环上的编号能够递推来简化我们之后的处理呢?
可以建立一种有确定规则的映射,要求映射之后的数字可以递推,且可以将在新环中继续按原规则报数得到的结果逆推出在旧环中的对应数字。
方法:将它与 N-1 个人组成的(0 ~ N-1)环一 一映射。
比如之前的栗子,将剩余的 9 人与 9 人环(0~8)一 一映射
既然2被淘汰之后,报数要从3开始 (3其实在数值上等于最大报数值),那么就将3映射到0~8的新环中0的位置,也就是说在新环中从0开始报数即可,且新环中没有与2对应的数字,因此不必担心有空位的问题。从旧环的3开始报数等效于从新环中的 0 开始报数。
原始 0 1 2 3 4 5 6 7 8 9
旧环 0 1 3 4 5 6 7 8 9
新环 7 8 0 1 2 3 4 5 6
于是不难有以下理解:
每淘汰掉一个人,下一个人成为头,相当于把数组向前移动M位。若已知N-1个人时,胜利者的下标位置(f(N−1,M),则N个人的时候,就是往后移动M位(因为有可能数组越界,超过的部分会被接到头上,所以还要模N),即(f(N−1,M) = ((f(N−1,M) + M)%N。
理解这个递推式的核心在于关注胜利者的下标位置是怎么变的。每淘汰一个人,其实就是把这个数组向前移动了M位。然后逆过来,就可以得到这个递推式。
AC代码
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if(n<=0)
return -1;
vector<int>out(n, false); //标记小朋友是否出局
int start = 0;
int count = 0;
while(n-count>1)
{
int all = 0; //计数
while(all<m)
{
if(!out[start])
{
++all;
if(all == m)
{
out[start] = true;
count++;
}
}
start = (start+1)%n;
}
}
int res = 0;
for(int i = 0;i<out.size();++i)
if(out[i]==false)
{
res = i;
break;
}
return res;
}
};
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if( n == 0)
return -1;
if(n == 1)
return 0; //如果只有一位小朋友,则不用淘汰,直接返回去下标
return (LastRemaining_Solution(n-1, m) + m)%n; //递推公式
}
};