题目描述
在0,1,…,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。
求出这个圆圈里剩下的最后一个数字。
样例
输入:n=5 , m=3
输出:3
思路一 用环形链表模拟圆圈
本题就是有名的约瑟夫环问题。我们可以环形列表来模拟,每次从这个列表中删除第mmm个元素,一直到列表最后剩下一个元素为止。
考虑用STL中std::list来模拟这个环形列表,由于list并不是一个环形的结构,因此每次跌代器扫描到列表末尾的时候,要把迭代器移到列表的头部。这样就是按照一个圆圈的顺序来遍历这个列表了。
时间复杂度是O(mn)O(mn)O(mn)。
class Solution {
public:
int lastRemaining(int n, int m){
if(n < 1 || m < 1)
return -1;
list<int> nums;
for(int i = 0; i < n; ++i)
nums.push_back(i);
list<int>::iterator cur = nums.begin();
while(nums.size() > 1){
for(int i = 1; i < m; ++i){
++cur;
if(cur == nums.end())
cur = nums.begin();
}
list<int>::iterator next = ++cur;
if(next == nums.end())
next = nums.begin();
--cur;
nums.erase(cur);
cur = next;
}
return *(cur);
}
};
思路二 公式推导
首先定义最初的nnn个数字(0,1,…,n−1)(0,1,…,n-1)(0,1,…,n−1)中最后剩下的数字是关于nnn和mmm的方程为f(n,m)f(n,m)f(n,m)。
在这nnn个数字中,第一个被删除的数字是(m−1)(m-1)%n(m−1),为简单起见记为kkk,那么删除kkk之后的剩下n−1n-1n−1的数字为0,1,…,k−1,k+1,…,n−10,1,…,k-1,k+1,…,n-10,1,…,k−1,k+1,…,n−1,并且下一个开始计数的数字是k+1k+1k+1。相当于在剩下的序列中,k+1k+1k+1排到最前面,从而形成序列k+1,…,n−1,0,…k−1k+1,…,n-1,0,…k-1k+1,…,n−1,0,…k−1。该序列最后剩下的数字也应该是关于nnn和mmm的函数。由于这个序列的规律和前面最初的序列不一样(最初的序列是从0开始的连续序列),因此该函数不同于前面函数,记为f′(n−1,m)f'(n-1,m)f′(n−1,m)。最初序列最后剩下的数字f(n,m)f(n,m)f(n,m)一定是剩下序列的最后剩下数字f′(n−1,m)f'(n-1,m)f′(n−1,m),所以f(n,m)=f′(n−1,m)f(n,m)=f'(n-1,m)f(n,m)=f′(n−1,m)。
接下来我们把剩下的的这n−1n-1n−1个数字的序列k+1,…,n−1,0,…k−1k+1,…,n-1,0,…k-1k+1,…,n−1,0,…k−1作一个映射,映射的结果是形成一个从000到n−2n-2n−2的序列:
k+1 -> 0
k+2 -> 1
…
n-1 -> n-k-2
0 -> n-k-1
…
k-1 -> n-2
把映射定义为ppp,则p(x)=(x−k−1)%np(x)= (x-k-1)\%np(x)=(x−k−1)%n,即如果映射前的数字是xxx,则映射后的数字是(x−k−1)%n(x-k-1)\%n(x−k−1)%n。对应的逆映射是p−1(x)=(x+k+1)%np-1(x)=(x+k+1)\%np−1(x)=(x+k+1)%n。
由于映射之后的序列和最初的序列有同样的形式,都是从000开始的连续序列,因此仍然可以用函数fff来表示,记为f(n−1,m)f(n-1,m)f(n−1,m)。根据我们的映射规则,映射之前的序列最后剩下的数字f′(n−1,m)=p−1[f(n−1,m)]=[f(n−1,m)+k+1]%nf'(n-1,m)= p-1 [f(n-1,m)]=[f(n-1,m)+k+1]\%nf′(n−1,m)=p−1[f(n−1,m)]=[f(n−1,m)+k+1]%n。把k=(m−1)%nk=(m-1)\%nk=(m−1)%n代入得到f(n,m)=f′(n−1,m)=[f(n−1,m)+m]%nf(n,m)=f'(n-1,m)=[f(n-1,m)+m]\%nf(n,m)=f′(n−1,m)=[f(n−1,m)+m]%n。
经过上面复杂的分析,我们终于找到一个递归的公式。要得到nnn个数字的序列的最后剩下的数字,只需要得到n−1n-1n−1个数字的序列的最后剩下的数字,并可以依此类推。当n=1n=1n=1时,也就是序列中开始只有一个数字000,那么很显然最后剩下的数字就是000。因此有递推公式:
f(n,m)={0n=1[f(n−1,m)+m]%nn>1f(n,m)=
\begin{cases}
0& \text{n=1}\\
[f(n-1, m) +m] \% n& \text{n>1}
\end{cases}f(n,m)={0[f(n−1,m)+m]%nn=1n>1
这个公式使用递归和迭代都很容易实现,时间复杂度为O(n),下面是基于递归实现的代码
class Solution {
public:
int lastRemaining(int n, int m){
if(n < 1 || m < 1)
return -1;
if(n == 1)
return 0;
return (lastRemaining(n-1, m) + m) % n;
}
};