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)。

四、总结

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

  • 理解碰撞只会发生在相邻且方向相反的物体之间。
  • 使用栈维护可能发生碰撞的候选物体(通常是向右的)。
  • 按位置顺序处理,保证碰撞顺序正确。
  • 根据具体规则(大小比较、生命值变化等)实现碰撞逻辑。
相关推荐
米粒129 分钟前
力扣算法刷题 Day 31 (贪心总结)
算法·leetcode·职场和发展
少许极端33 分钟前
算法奇妙屋(四十)-贪心算法学习之路7
java·学习·算法·贪心算法
AlenTech1 小时前
647. 回文子串 - 力扣(LeetCode)
算法·leetcode·职场和发展
py有趣1 小时前
力扣热门100题之合并两个有序链表
算法·leetcode·链表
8Qi81 小时前
LeetCode热题100--45.跳跃游戏 II
java·算法·leetcode·贪心算法·编程
foundbug9992 小时前
基于STM32的步进电机加减速程序设计(梯形加减速算法)
stm32·单片机·算法
北顾笙9802 小时前
day12-数据结构力扣
数据结构·算法·leetcode
凌波粒2 小时前
LeetCode--454.四数相加 II(哈希表)
算法·leetcode·散列表
漫随流水2 小时前
c++编程:D进制的A+B(1022-PAT乙级)
数据结构·c++·算法
tankeven2 小时前
HJ159 没挡住洪水
c++·算法