前言
本文为leetcode上的题目简单分析总结,仅作记录,欢迎提出建议,共同学习交流。
390.消除游戏
列表 arr
由在范围 [1, n]
中的所有整数组成,并按严格递增排序。请你对 arr
应用下述算法:
- 从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾。
- 重复上面的步骤,但这次是从右到左。也就是,删除最右侧的数字,然后剩下的数字每隔一个删除一个。
- 不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。
给你整数 n
,返回 arr
最后剩下的数字。
示例 1:
输入:n = 9 输出:6 解释: arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] arr = [2, 4, 6, 8] arr = [2, 6] arr = [6]示例 2:
输入:n = 1 输出:1
分析
方法一
先创建一个动态数组List,对其中部分元素进行循环删除,num为奇数时表示正向从左到右删除,当到达最右端时反转数组,达到再次从左向右删除的效果。当list中只剩下一个元素时,利用get方法进行返回。
此方法大量的运用remove方法,删除元素。Arraylist会有大量的数据移动,所以时间复杂度较高。
class Solution {
public int lastRemaining(int n) {
ArrayList<Integer> list = new ArrayList<>();
for(int i=0; i<n; i++){
list.add(i+1);
}
// 表示删除第几轮
int num=0;
while(true){
num++;
// 从左往右
if(num%2==1){
// 删除时数在往左挪,下标在减一所以,i只需+1
for(int i=0;i<list.size();i++){
if(list.size()==1){return list.get(0);}
list.remove(i);
}
}
// 反转数组
else{
Collections.reverse(list);
}
}
}
}
方法二
每个回合更新和记录head
变量,当数组元素个数变为1时,head
就是最后的一个数
什么时候更新这个head变量呢?
- 当我们从左边开始移除的时
- 当我们从右边开始移除并且剩余的数的总数 number % 2 == 1时
比如 2 4 6 8 10,我们从10开始移除,我们将会移除10,6,2,head被移除并且变为4
比如 2 4 6 8 10 12,我们从12开始移除,我们将会移除12,8,4,head仍然是2
class Solution {
public int lastRemaining(int n) {
int head = 1;
// step用来表示head移动步长
int step = 1;
// left用来判断是从左边开始移除还是从右边开始移除(true表示从左边开始)
boolean left = true;
while (n > 1) {
//从左边开始移除 or(从右边开始移除,数列总数为奇数)
if (left || n % 2 != 0) {
head += step;
}
step *= 2; //步长 * 2
left = !left; //取反移除方向
n /= 2; //总数 / 2
}
return head;
}
}
方法三
约瑟夫环
与求解约瑟夫环类似,本题也可以通常找规律,分析出公式之后进行递推求解。
对于本题,定义 f[i] 为在 连续序列 [1,i] 中进行「起始先从左到右,再从右往左」的轮流换向间隔删除,最终左边剩余的编号;定义g[i]为在 连续序列 [1,i] 中进行「起始先从右到左,再从左往右」的轮流换向间隔删除,最终右边剩余的编号。
由于「从左往右」和「从右往左」分别为「从左端点发起,间隔删除」和「从右端点发起,间隔删除」,因此整个删除过程在连续序列中 [1,i] 中具有对称性,两者最终剩余的编号在连续序列中也具有对称性。
即可得出第一个公式:
由于我们对 f[i] 和 g[i] 的定义都是「连续序列」,因此如果我们希望使用 f[i] 和 g[i] 得出最终答案,我们需要在每次消除后对序列进行「重新编号」,确保能够使用 f[i] 和 g[i] 作为合法状态值,在计算出「重新编号」后的,需要将答案(编号)映射回去重新编号前的值。
起始时,我们对连续序列 [1,2,3,...,i] 执行了一次「从左往右」的消除之后,得到的序列为 [2,4,6,...,x](其中 x 根据 i 的奇偶性不同,可能为 i 或 i−1)。新序列的长度为 i/2,考虑对得到的序列进行重新编号,使其继续保有「连续序列」的定义,即变为 [1,2,3,...,⌊ ,然后执行「从右往左」的间隔删除,最终得到 之后考虑将答案编号映射回「重新编号」前的值。
此时可得到第二个公式:
通过上述两个公式,我们可以将 f′[i] 进行消除,得到最终的 f[i] 关系式:
我们知道需要实现的函数 lastRemaining 其实就是 f[i],因此该递推过程我们可以使用递归进行实现(注意的出口条件 f[1]=1)。
class Solution {
public int lastRemaining(int n) {
if(n == 1) return 1;
else return 2 * (n / 2 + 1 - lastRemaining(n / 2));
}
}