LeetCode 735 & 2751.栈模拟碰撞问题详解

LeetCode 735 & 2751.栈模拟碰撞问题详解

在算法题中,有一类经典问题:直线上的碰撞。物体沿直线移动,方向相反时相遇并发生碰撞,根据一定规则决定生死。这类问题通常可以用栈来高效模拟,因为碰撞只会发生在相邻且方向相对的物体之间,符合栈的后进先出特性。

本文将通过两道 LeetCode 题目 ------ 735. 小行星碰撞 和 2751. 机器人碰撞 ------ 来详细讲解这种题型,并总结其核心思想和解题模板。


一、LeetCode 735:小行星碰撞

1. 题目描述

给定一个整数数组 asteroids,表示一行小行星。每个小行星的绝对值 表示其大小,正负号 表示移动方向(正数向右,负数向左)。所有小行星以相同速度移动。

碰撞规则:

  • 如果两个小行星相遇(即方向相反),较小的会爆炸。
  • 如果大小相等,则两者都爆炸。
  • 方向相同的小行星永远不会相遇。

返回碰撞后剩余的小行星,顺序与原数组相同。

示例

输入:asteroids = [5,10,-5]

输出:[5,10]

解释:10 和 -5 碰撞,-5 爆炸,10 存活;5 和 10 方向相同,不碰撞。

2. 算法思路

  • 使用栈(vector 模拟)保存尚未爆炸的小行星。
  • 遍历数组:
    • 如果当前小行星向右(正数),直接入栈(因为它不会与栈内元素碰撞,栈顶若为正则同向,若为负则它已经在左边)。
    • 如果当前小行星向左(负数),则与栈顶元素碰撞:
      • 只要栈非空且栈顶为正(向右)且栈顶大小小于当前小行星的绝对值,栈顶爆炸(出栈)。
      • 碰撞结束后:
        • 如果栈顶为正且大小相等,则两者同时爆炸(弹出栈顶,当前也不入栈)。
        • 否则,如果栈为空或栈顶为负(向左),则当前小行星入栈(因为它不会与栈内任何行星碰撞)。

3. 代码实现

cpp 复制代码
class Solution {
public:
    vector<int> asteroidCollision(vector<int>& asteroids) {
        vector<int> st;          // 用 vector 模拟栈
        for (int x : asteroids) {
            if (x > 0) {
                st.push_back(x); // 向右,直接入栈
            } else {
                // 向左,与栈顶碰撞
                while (!st.empty() && st.back() > 0 && st.back() < -x) {
                    st.pop_back();   // 栈顶爆炸
                }
                if (!st.empty() && st.back() == -x) {
                    st.pop_back();   // 两者同归于尽
                } else if (st.empty() || st.back() < 0) {
                    st.push_back(x); // 当前小行星存活
                }
                // 其他情况(栈顶为正且更大),当前小行星爆炸,什么都不做
            }
        }
        return st;
    }
};

4. 关键点

  • 栈中元素可能为正也可能为负,但正数一定在负数之前(因为向左的负数如果与正数碰撞,结果要么正数消失,要么负数消失,不会出现正数在负数之后的情况)。
  • 碰撞只发生在"右→左"的相邻元素上,因此用栈可以完美模拟最近邻的碰撞过程。

二、LeetCode 2751:机器人碰撞

1. 题目描述

有 n 个机器人,每个机器人有:

  • 位置 positions[i](整数,互不相同)
  • 健康值 healths[i](正整数)
  • 移动方向 directions[i]('L' 或 'R')

所有机器人同时以相同速度移动。当两个机器人相遇(位置相同)时发生碰撞:

  • 健康值较大的机器人存活,健康值减 1。
  • 健康值相等的两个机器人同归于尽。
  • 一个机器人不会与多个机器人同时碰撞。

碰撞后,存活的机器人继续沿原方向移动。返回最终所有存活机器人的健康值(按原输入顺序)。

示例

输入:positions = [3,5,2,6], healths = [10,10,15,12], directions = "RLRL"

输出:[14]

解释:按位置排序后,处理过程略。

2. 算法思路

  • 首先,机器人位置可能无序,我们需要按位置排序后才能保证碰撞顺序正确。
  • 使用栈保存方向为 'R' 的机器人索引(因为只有向右的机器人可能与后面的向左机器人碰撞)。
  • 遍历排序后的机器人:
    • 如果方向为 'R',直接入栈。
    • 如果方向为 'L',则与栈顶的 'R' 机器人碰撞:
      • 比较健康值:
        • 若栈顶健康值更大:栈顶健康减 1,当前机器人死亡。
        • 若相等:两者死亡,栈顶弹出。
        • 若栈顶健康值更小:栈顶死亡(弹出),当前机器人健康减 1,继续与新的栈顶碰撞。
  • 碰撞结束后,如果当前机器人存活,它将继续向左移动,但左边已经没有机器人(因为所有左边机器人均已处理且不会与它相遇),因此无需入栈。
  • 最后,遍历原 healths 数组,收集健康值 > 0 的机器人,保持原顺序。

3. 代码实现

cpp 复制代码
class Solution {
public:
    vector<int> survivedRobotsHealths(vector<int>& positions, vector<int>& healths, string directions) {
        int n = positions.size();
        vector<int> idx(n);
        ranges::iota(idx, 0);                     // 生成索引 0..n-1
        ranges::sort(idx, {}, [&](int i) { return positions[i]; }); // 按位置排序索引

        stack<int> st;                             // 栈中存放索引(仅方向为 'R' 的机器人)
        for (int i : idx) {
            if (directions[i] == 'R') {
                st.push(i);
                continue;
            }
            // 当前机器人方向为 'L'
            while (!st.empty()) {
                int j = st.top();
                if (healths[j] > healths[i]) {
                    // 栈顶健康值更大,当前机器人死亡,栈顶健康减1
                    healths[i] = 0;
                    healths[j]--;
                    break;
                }
                if (healths[j] == healths[i]) {
                    // 健康值相等,两者死亡,弹出栈顶
                    healths[i] = 0;
                    healths[j] = 0;
                    st.pop();
                    break;
                }
                // 栈顶健康值更小,栈顶死亡,当前机器人健康减1,继续与新的栈顶比较
                healths[i]--;
                healths[j] = 0;
                st.pop();
            }
            // 如果当前机器人存活,它将继续向左移动,但不会再与任何机器人碰撞,因此不入栈
        }

        vector<int> ans;
        for (int h : healths) {
            if (h > 0) ans.push_back(h);          // 按原始顺序收集存活健康值
        }
        return ans;
    }
};

4. 关键点

  • 排序:必须按位置顺序处理,否则无法确定碰撞顺序。
  • 栈中只存 'R' 机器人 :因为只有 'R' 可能与后面的 'L' 碰撞,'L' 不会与后面的 'R' 碰撞(方向相反,但 'L' 在左时才会相向,而这里 'L' 已经在右边了)。
  • 健康值更新:碰撞后存活机器人的健康值减 1,需要持续更新,这与 735 中直接消失不同。
  • 结果顺序:最终按原输入顺序返回,不能打乱。

三、两题对比与总结

对比项 LeetCode 735 小行星碰撞 LeetCode 2751 机器人碰撞
方向表示 正数(右)/ 负数(左) 'R' / 'L'
碰撞条件 右→左相向 右→左相向
处理顺序 直接遍历数组(隐含位置顺序) 需要按位置排序后再处理
碰撞规则 比较绝对值大小,大的存活,相等同归于尽 比较健康值,大的存活且健康减1,相等同归于尽
栈中元素 未碰撞的行星(正负均可) 仅存方向为 'R' 的机器人
状态变化 死亡即消失 存活后健康值减少,可能继续碰撞

相通之处

  1. 核心模型:都是"相向而行"的碰撞问题,用栈模拟最近邻碰撞。
  2. 栈的使用:利用栈的后进先出特性,保证碰撞发生在相邻且方向相对的物体之间。
  3. 遍历顺序:必须按照空间位置顺序处理(735 中数组已隐含位置顺序;2751 需要显式排序)。
  4. 碰撞处理:都是比较大小(绝对值或健康值),并根据结果决定谁消失、谁保留(或继续)。

解题模板

对于这类问题,可以总结出以下通用步骤:

  1. 排序(如果需要):保证按位置顺序处理。
  2. 初始化栈:用于存放待碰撞的物体(通常是向右的)。
  3. 遍历 :
    • 若当前物体向右(或正数),直接入栈。
    • 若当前物体向左(或负数),则与栈顶碰撞:
      • 根据规则循环比较,直到当前物体死亡或栈空。
      • 如果当前物体存活,根据规则决定是否入栈(一般向左的物体不会入栈,因为不会再与右边物体碰撞)。
  4. 收集结果:根据要求输出最终存活的物体(可能要保持原顺序)。

复杂度分析

  • LeetCode 735:时间复杂度 O(n),每个元素入栈一次、出栈一次。空间复杂度 O(n)。
  • LeetCode 2751:时间复杂度 O(n log n)(主要是排序),碰撞过程 O(n)。空间复杂度 O(n)。

四、总结

栈模拟碰撞问题是算法题中的经典题型,掌握后可以轻松应对许多变种。关键在于:

  • 理解碰撞只会发生在相邻且方向相反的物体之间。
  • 使用栈维护可能发生碰撞的候选物体(通常是向右的)。
  • 按位置顺序处理,保证碰撞顺序正确。
  • 根据具体规则(大小比较、生命值变化等)实现碰撞逻辑。
相关推荐
德彪稳坐倒骑驴23 分钟前
Leetcode力扣刷题记录与题解
算法·leetcode·职场和发展
lzhdim6 小时前
AI的分类
人工智能·算法·机器学习·分类·数据挖掘
凤凰院凶涛QAQ9 小时前
《Java版数据结构 & 集合类剖析》Map & Set & 反射:“从哈希碰撞到红黑树转换,反射帮你扒开 HashMap 的桶数组看个究竟”
java·数据结构·算法
w_zero_one11 小时前
算法--模拟
java·数据结构·算法
秋天的怀念_David12 小时前
算法一千零一夜 · 第十三夜 幻方,武侠世界里的数学迷宫
java·程序人生·算法
秋天的怀念_David12 小时前
算法一千零一夜 · 第十四夜 幻方与奇妙的二进制
java·程序人生·算法
薛定e的猫咪12 小时前
(ICLR 2020)MAML 靠的不是快速学习,而是特征复用
人工智能·深度学习·学习·算法·机器学习
薛定e的猫咪13 小时前
(ICLR 2026)DF-META:面向 FJSP 的无导数元学习实例自适应调度
人工智能·深度学习·学习·算法·机器学习
java_nnnn14 小时前
LeetCode刷题滑动窗口(水果成篮+找到字符串中所有字母的异位词)Java
java·开发语言·leetcode
小羊没烦恼!14 小时前
异步编程:线程概述及使用
java·开发语言·mysql·算法·c#