LeetCode 390 消除游戏 - Swift 题解


文章目录

摘要

这道题其实挺有意思的,它要求我们模拟一个交替从左到右、从右到左的消除过程,最后找出剩下唯一一个数字。听起来像是约瑟夫问题的变种,但实际上可以通过数学规律来高效解决。

由于 n 最大可以到 10^9,如果直接模拟整个消除过程,时间和空间都会爆炸。我们需要找出其中的规律:每一轮消除后,剩余数字形成一个等差数列,我们只需要维护"头元素"和"步长",就能推算出下一轮的状态,而不需要真正维护整个数组。今天我们就用 Swift 来搞定这道题,顺便聊聊这种数学建模的思路在实际开发中的应用。

描述

题目要求是这样的:列表 arr 由在范围 [1, n] 中的所有整数组成,并按严格递增排序。请你对 arr 应用下述算法:

  1. 从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾
  2. 重复上面的步骤,但这次是从右到左,删除最右侧的数字,然后剩下的数字每隔一个删除一个
  3. 不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字

给你整数 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

提示:

  • 1 <= n <= 10^9

这道题的核心思路是:每一轮消除后,剩余数字构成一个等差数列。我们只需维护当前等差数列的"头元素"和"步长",以及剩余数量,就能用 O(log n) 的时间推算出最终答案,而不需要真正模拟整个数组。

题解答案

下面是完整的 Swift 解决方案:

swift 复制代码
class Solution {
    func lastRemaining(_ n: Int) -> Int {
        if n == 1 {
            return 1
        }
        
        // head: 当前剩余序列的第一个元素
        var head = 1
        // step: 相邻剩余元素之间的差值(等差数列的公差)
        var step = 1
        // count: 剩余元素的数量
        var count = n
        // leftToRight: 本轮是否从左到右消除
        var leftToRight = true
        
        while count > 1 {
            if leftToRight {
                // 从左到右:总是会删除第一个元素,所以 head 要后移
                head += step
            } else {
                // 从右到左:只有当剩余数量为奇数时,才会删到第一个元素
                if count % 2 == 1 {
                    head += step
                }
            }
            step *= 2
            count /= 2
            leftToRight.toggle()
        }
        
        return head
    }
}

题解代码分析

让我们一步步分析这个解决方案。

核心观察:等差数列

每一轮消除后,剩余数字都形成一个等差数列。例如 n=9 时:

  • 初始:[1, 2, 3, 4, 5, 6, 7, 8, 9],头=1,步长=1,数量=9
  • 从左到右后:[2, 4, 6, 8],头=2,步长=2,数量=4
  • 从右到左后:[2, 6],头=2,步长=4,数量=2
  • 从左到右后:[6],头=6,步长=8,数量=1

我们只需要维护 head(头元素)、step(步长)、count(剩余数量),就能完整描述当前状态,无需保存整个数组。

从左到右消除时 head 的更新

从左到右消除时,我们总是先删掉第一个元素,然后每隔一个删一个。因此,无论剩余数量是奇数还是偶数,原来的第一个元素都会被删掉,新的第一个元素就是原来的第二个元素。

由于剩余序列是等差数列,相邻元素相差 step,所以新的 head 为 head + step

swift 复制代码
if leftToRight {
    head += step
}

从右到左消除时 head 的更新

从右到左消除时,我们先删最右边,再每隔一个删。这时头元素是否被删,取决于剩余数量的奇偶性。

  • count 为偶数:例如 [2, 4, 6, 8],从右删 8、4,剩下 [2, 6],头 2 保留,head 不变
  • count 为奇数:例如 [2, 4, 6],从右删 6、2,剩下 [4],头 2 被删,新的头是 4,head 需要加上 step

所以:

swift 复制代码
if !leftToRight {
    if count % 2 == 1 {
        head += step
    }
}

step 和 count 的更新

每轮消除后,相邻剩余元素之间的间隔会翻倍,因此 step *= 2。剩余数量大约减半,所以 count /= 2

swift 复制代码
step *= 2
count /= 2
leftToRight.toggle()

边界情况

n == 1 时,直接返回 1,无需进入循环。

完整执行流程示例(n=9)

  1. head=1, step=1, count=9, leftToRight=true

    从左到右:head=2, step=2, count=4, leftToRight=false

  2. head=2, step=2, count=4, leftToRight=false

    count 为偶数,head 不变:head=2, step=4, count=2, leftToRight=true

  3. head=2, step=4, count=2, leftToRight=true

    从左到右:head=6, step=8, count=1, leftToRight=false

  4. count=1,退出循环,返回 6

示例测试及结果

示例 1:n = 9

执行过程:

  1. 初始:head=1, step=1, count=9
  2. 从左到右:head=2, step=2, count=4
  3. 从右到左(count 偶数):head=2, step=4, count=2
  4. 从左到右:head=6, step=8, count=1
  5. 返回 6

结果: 6

示例 2:n = 1

执行过程:

  • 直接返回 1,不进入循环

结果: 1

示例 3:n = 6

模拟过程:

  • 初始:[1,2,3,4,5,6]
  • 从左到右:[2,4,6]
  • 从右到左:[4]
  • 返回 4

算法过程:

  1. head=1, step=1, count=6,从左到右:head=2, step=2, count=3
  2. count 为奇数,从右到左会删到头:head=4, step=4, count=1
  3. 返回 4

示例 4:n = 2

模拟过程:

  • 初始:[1,2]
  • 从左到右:删除 1,剩下 [2]
  • 返回 2

算法过程:

  1. head=1, step=1, count=2,从左到右:head=2, step=2, count=1
  2. 返回 2

时间复杂度

时间复杂度:O(log n)

每一轮 count 约减半,因此循环次数约为 log₂(n)。对于 n = 10^9,大约 30 次循环即可结束。

空间复杂度

空间复杂度:O(1)

只使用了 head、step、count、leftToRight 等少量变量,与 n 无关。

实际应用场景

这种"用数学规律替代模拟"的思路在实际开发中很有用:

场景一:约瑟夫问题

经典的约瑟夫问题也是每隔 k 个人淘汰一人,最后剩下谁。同样可以不用模拟,而用递推或公式计算。

场景二:分批处理

当数据量巨大且处理规则有规律时(如每隔一批保留一批),可以像本题一样抽象成 head、step、count,避免真正构建和遍历整个序列。

场景三:游戏逻辑

某些回合制游戏每轮按固定规则淘汰角色,若规则呈现周期性或可归纳的数学规律,可以用类似方式在 O(log n) 时间内算出结果。

总结

本题的关键在于发现:每轮消除后剩余数字构成等差数列,只需维护头元素、步长和数量,就能在 O(log n) 时间和 O(1) 空间内得到最终结果。

要点总结:

  1. 从左到右消除:head 一定增加 step
  2. 从右到左消除:仅在 count 为奇数时 head 增加 step
  3. 每轮 step 翻倍,count 减半

算法优势:

  • 时间复杂度 O(log n),适合 n 极大的情况
  • 空间复杂度 O(1)
  • 逻辑简洁,易于实现
相关推荐
踩坑记录2 小时前
leetcode hot100 131. 分割回文串 medium 递归回溯
leetcode
踢足球09292 小时前
寒假打卡:2026-2-24
数据结构·算法·leetcode
样例过了就是过了2 小时前
LeetCode热题100 环形链表
算法·leetcode·链表
努力学算法的蒟蒻2 小时前
day95(2.24)——leetcode面试经典150
算法·leetcode·面试
技术策划Boring2 小时前
2025年工作复盘:开放世界3A项目配置管线与性能监控的探索
游戏·ue5·虚幻·p4·perforce
风痕天际2 小时前
Godot扫雷游戏制作记录4——计算周围地雷数并显示
游戏·游戏引擎·godot
菜鸡儿齐3 小时前
leetcode-搜索二维矩阵
算法·leetcode·矩阵
安全不再安全3 小时前
某驱动任意读漏洞分析 - 可用于游戏内存数据读取
c语言·测试工具·安全·游戏·网络安全
Charlie_lll3 小时前
力扣解题-无重复字符的最长子串
后端·算法·leetcode