一、题目描述
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
示例1:
输入: n = 5, m = 3
输出: 3
示例2:
输入: n = 10, m = 17
输出: 2
限制:
1 <= n <= 105
1 <= m <= 106
二、思路分析
注:思路分析中的一些内容和图片参考自力扣各位前辈的题解,感谢他们的无私奉献
约瑟夫环问题
约瑟夫是犹太军队的一个将军,在反抗罗马的起义中,他所率领的军队被击溃,只剩下残余的部队40余人。他们都是宁死不屈的人,所以不愿投降做叛徒。一群人表决说要死,所以用一种策略来先后杀死所有人。于是约瑟夫建议:每次杀死一个人,而被kill的人的先后顺序是由抽签决定的,约瑟夫有预谋地抽到了最后一签,在杀死了除了他和剩余那个人之外的最后一人,他劝服了另外一个没死的人投降了罗马。
对应的数学题目
一间房间总共有 n n n 个人(下标 0 ~ n − 1 0~n-1 0~n−1),最后只能有一个人活下来,按照如下规则去杀死人:所有人围成一圈顺时针报数,报到 m m m 的人将被杀死。被杀死的人将从房间内被移走,然后从被杀死的下一个人重新开始报数。继续将报到 m m m 的人杀死,移走,重复上面过程,直到只剩一人。你要做的是:当你在这一群人之间时,你必须选择一个位置以使得你变成那剩余的最后一人,也就是活下来。
核心思路
我们知道最后剩下一个人时,胜利者的下标是0。也就是我们已知胜利者最后的位置,要一直逆推到最初的时候胜利者所在的位置。我们用 f ( n , m ) f(n, m) f(n,m) 表示 n n n 个人报数,每报到 m m m 时杀掉那个人,返回最终胜利者的编号。
每次杀掉一个人,队伍的移动情况
现在我们让 m = 3 m=3 m=3,假设最后存活的人是 G G G,用绿色表示,每次杀死的人用红色表示。可以看到每杀死一个人, G G G 的位置都向前移动 m = 3 m=3 m=3 位。 于是我们可以暂时得出: f ( n , m ) = f ( n − 1 , m ) + m f(n,m)=f(n-1,m)+m f(n,m)=f(n−1,m)+m。但是这不完全正确,因为每次向前移动穿过队首时,需要去队尾继续往前移动。也就是说,每次 + m +m +m 操作后,若超过当前的总人数 n n n 时,需要回到队伍头计数。 于是我们可以得出: f ( n , m ) = ( f ( n − 1 , m ) + m ) % n f(n,m)=(f(n-1,m)+m) \% n f(n,m)=(f(n−1,m)+m)%n。从这个公式可以看出,其实这也就是一个状态转移方程,即 f ( n ) f(n) f(n) 可由 f ( n − 1 ) f(n - 1) f(n−1) 得到, f ( n − 1 ) f(n - 1) f(n−1) 可由 f ( n − 2 ) f(n - 2) f(n−2) 得到,……, f ( 2 ) f(2) f(2) 可由 f ( 1 ) f(1) f(1) 得到。我们这里 f ( 1 ) = 0 f(1)=0 f(1)=0。
案例分析
为 n = 5 , m = 3 n = 5, m = 3 n=5,m=3 时对应的模拟删除过程
复杂度分析:
时间复杂度 O ( N ) \rm{O(N)} O(N):状态转移循环N-1
次使用O(N)
时间,状态转移方程计算使用O(1)
时间
空间复杂度 O ( 1 ) \rm{O(1)} O(1):使用常数大小的额外空间
三、整体代码
整体代码如下
int lastRemaining(int n, int m){
int x = 0;
for(int i = 2; i <= n; i++){
x = (x+m)%i;
}
return x;
}
运行,测试通过