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' 的机器人 |
| 状态变化 | 死亡即消失 | 存活后健康值减少,可能继续碰撞 |
相通之处
- 核心模型:都是"相向而行"的碰撞问题,用栈模拟最近邻碰撞。
- 栈的使用:利用栈的后进先出特性,保证碰撞发生在相邻且方向相对的物体之间。
- 遍历顺序:必须按照空间位置顺序处理(735 中数组已隐含位置顺序;2751 需要显式排序)。
- 碰撞处理:都是比较大小(绝对值或健康值),并根据结果决定谁消失、谁保留(或继续)。
解题模板
对于这类问题,可以总结出以下通用步骤:
- 排序(如果需要):保证按位置顺序处理。
- 初始化栈:用于存放待碰撞的物体(通常是向右的)。
- 遍历 :
- 若当前物体向右(或正数),直接入栈。
- 若当前物体向左(或负数),则与栈顶碰撞:
- 根据规则循环比较,直到当前物体死亡或栈空。
- 如果当前物体存活,根据规则决定是否入栈(一般向左的物体不会入栈,因为不会再与右边物体碰撞)。
- 收集结果:根据要求输出最终存活的物体(可能要保持原顺序)。
复杂度分析
- LeetCode 735:时间复杂度 O(n),每个元素入栈一次、出栈一次。空间复杂度 O(n)。
- LeetCode 2751:时间复杂度 O(n log n)(主要是排序),碰撞过程 O(n)。空间复杂度 O(n)。
四、总结
栈模拟碰撞问题是算法题中的经典题型,掌握后可以轻松应对许多变种。关键在于:
- 理解碰撞只会发生在相邻且方向相反的物体之间。
- 使用栈维护可能发生碰撞的候选物体(通常是向右的)。
- 按位置顺序处理,保证碰撞顺序正确。
- 根据具体规则(大小比较、生命值变化等)实现碰撞逻辑。