文章目录

题目描述
示例 1:
输入: robots = [4], distance = [3], walls = [1,10]
输出: 1
解释:
robots[0] = 4 向 左 发射,distance[0] = 3,覆盖范围 [1, 4],摧毁了 walls[0] = 1。
因此,答案是 1。
示例 2:输入: robots = [10,2], distance = [5,1], walls = [5,2,7]
输出: 3
解释:
robots[0] = 10 向 左 发射,distance[0] = 5,覆盖范围 [5, 10],摧毁了 walls[0] = 5 和 walls[2] = 7。
robots[1] = 2 向 左 发射,distance[1] = 1,覆盖范围 [1, 2],摧毁了 walls[1] = 2。
因此,答案是 3。
示例 3:输入: robots = [1,2], distance = [100,1], walls = [10]
输出: 0
解释:
在这个例子中,只有 robots[0] 能够到达墙壁,但它向 右 的射击被 robots[1] 挡住了,因此答案是 0。
提示:1 <= robots.length == distance.length <= 105
1 <= walls.length <= 105
1 <= robots[i], walls[j] <= 109
1 <= distance[i] <= 105
robots 中的所有值都是 互不相同 的
walls 中的所有值都是 互不相同 的
思路简述
卡了很久这道题,难点在于:
- 机器人之间的互相遮挡 :子弹的有效射程不仅取决于
distance,还取决于相邻机器人的位置。 - 方向选择的最优性:每个机器人选择向左还是向右,会影响整体结果,需要动态规划来决策。
- 去重:两个相邻机器人之间的墙壁,只能被其中一个摧毁(如果都能打到的话),不能重复计数。
排序
由于机器人和墙壁在直线上的位置是无序的,我们首先需要对 robots 和 walls 数组进行排序。排序后,我们可以利用单调性使用双指针高效处理。
预处理
对于排序后的第 i 个机器人,我们定义:
- 左边界:如果向左发射,子弹能到达的最左位置。注意:如果不是第一个机器人,左边界不能超过左边机器人的位置(否则子弹会被挡住)。
- 右边界:如果向右发射,子弹能到达的最右位置。同理,不能超过右边机器人的位置。
我们需要计算三个关键数组:
left[i]:第i个机器人向左发射,能摧毁的墙壁数量。right[i]:第i个机器人向右发射,能摧毁的墙壁数量。num[i]:第i-1个和第i个机器人之间的墙壁总数(这部分是可能重复计算的区域)。
动态规划
这是最关键的一步。我们不能简单地把每个机器人的最大值加起来,因为如果第 i-1 个机器人向右打,同时第 i 个机器人向左打,它们中间的墙会被算两次。
我们维护两个状态变量:
subLeft:处理完前i-1个机器人,且第i-1个机器人向左发射时,最大摧毁数。subRight:处理完前i-1个机器人,且第i-1个机器人向右发射时,最大摧毁数。
对于第 i 个机器人,我们有两种选择:
A. 第 i 个机器人向左发射
我们需要看前一个机器人的状态:
- 如果前一个向左 (
subLeft):两者不冲突,直接加left[i]。 - 如果前一个向右 (
subRight):中间的墙num[i]被算了两次,我们需要取min(left[i] + right[i-1], num[i])来修正(即这部分墙只能算一次)。
B. 第 i 个机器人向右发射
这个比较简单,因为右边暂时没有机器人,直接累加最大值即可。
为了方便理解每一步在做什么,这道题我是通过力扣的题解+AI辅助完成的,我在最开始看题解的时候经常被变量绕晕尤其双指针初始化三个前置数组的时候,所以我让AI用中文变量名和逻辑拆解,我们一起把代码和上面的思路一一对应上。
cpp
// 定义解题类
class Solution {
public:
// 主函数:输入 机器人位置数组、机器人射程数组、墙壁位置数组,返回最大摧毁墙数
int maxWalls(vector<int>& robots, vector<int>& distance, vector<int>& walls) {
// ==============================================
// 第一步:初始化所有需要用到的变量
// ==============================================
// 机器人的总数量
int 机器人总数 = robots.size();
// 定义三个数组:
// 左射数组:每个机器人向左能摧毁的墙数
// 右射数组:每个机器人向右能摧毁的墙数
// 中间墙数组:相邻两个机器人之间的总墙数
vector<int> 左射数组(机器人总数, 0), 右射数组(机器人总数, 0), 中间墙数组(机器人总数, 0);
// 哈希表:存储 机器人位置 → 对应射程
unordered_map<int, int> 机器人位置对应射程;
// 遍历所有机器人,填充哈希表
for (int i = 0; i < 机器人总数; i++) {
机器人位置对应射程[robots[i]] = distance[i];
}
// ==============================================
// 第二步:排序(直线问题必须按位置从小到大排序)
// ==============================================
// 机器人按位置从小到大排序
sort(robots.begin(), robots.end());
// 墙壁按位置从小到大排序
sort(walls.begin(), walls.end());
// 墙壁的总数量
int 墙壁总数 = walls.size();
// 初始化四个双指针(只在墙壁数组上向右移动,不回头)
int 右指针 = 0, 左指针 = 0, 当前指针 = 0, 上一机器人指针 = 0;
// ==============================================
// 第三步:统计核心循环
// 遍历每一个机器人,计算并填充 左射/右射/中间墙 三个数组
// ==============================================
for (int 当前机器人索引 = 0; 当前机器人索引 < 机器人总数; 当前机器人索引++) {
// --------------------------
// 子步骤1:用右指针找 pos1
// 找到第一个 > 当前机器人位置 的墙壁索引
// --------------------------
while (右指针 < 墙壁总数 && walls[右指针] <= robots[当前机器人索引]) {
右指针++;
}
int 位置1 = 右指针;
// --------------------------
// 子步骤2:用当前指针找 pos2
// 找到第一个 >= 当前机器人位置 的墙壁索引
// --------------------------
while (当前指针 < 墙壁总数 && walls[当前指针] < robots[当前机器人索引]) {
当前指针++;
}
int 位置2 = 当前指针;
// --------------------------
// 子步骤3:计算左射能摧毁的墙数 left[i]
// --------------------------
// 计算当前机器人向左射击的【合法最远左边界】
int 左边界 = (当前机器人索引 >= 1)
? max( 当前机器人位置 - 对应射程 , 左边机器人位置 + 1 )
: 当前机器人位置 - 对应射程;
// 找到第一个 >= 左边界 的墙壁索引
while (左指针 < 墙壁总数 && walls[左指针] < 左边界) {
左指针++;
}
int 左起点 = 左指针;
// 左射墙数 = 机器人右侧第一个墙索引 - 左边界第一个墙索引
左射数组[当前机器人索引] = 位置1 - 左起点;
// --------------------------
// 子步骤4:计算右射能摧毁的墙数 right[i]
// --------------------------
// 计算当前机器人向右射击的【合法最远右边界】
int 右边界 = (当前机器人索引 < 机器人总数 - 1)
? min( 当前机器人位置 + 对应射程 , 右边机器人位置 - 1 )
: 当前机器人位置 + 对应射程;
// 找到第一个 > 右边界 的墙壁索引
while (右指针 < 墙壁总数 && walls[右指针] <= 右边界) {
右指针++;
}
int 右终点 = 右指针;
// 右射墙数 = 右边界第一个墙索引 - 机器人位置第一个墙索引
右射数组[当前机器人索引] = 右终点 - 位置2;
// --------------------------
// 子步骤5:计算相邻机器人中间总墙数 num[i]
// --------------------------
// 第一个机器人没有上一个,跳过
if (当前机器人索引 == 0) {
continue;
}
// 找到第一个 >= 上一个机器人位置 的墙壁索引
while (上一机器人指针 < 墙壁总数 && walls[上一机器人指针] < robots[当前机器人索引 - 1]) {
上一机器人指针++;
}
int 位置3 = 上一机器人指针;
// 中间总墙数 = 当前机器人右侧墙索引 - 上一个机器人位置墙索引
中间墙数组[当前机器人索引] = 位置1 - 位置3;
}
// ==============================================
// 第四步:DP动态规划核心循环
// 决策每个机器人左/右射,计算最大摧毁墙数
// ==============================================
// DP初始化:第一个机器人选左射/右射的总墙数
int 当前最优左射 = 左射数组[0], 当前最优右射 = 右射数组[0];
// 从第二个机器人开始遍历
for (int i = 1; i < 机器人总数; i++) {
// 计算:当前机器人选左射的最大总墙数
int 新左射 = max(
当前最优左射 + 左射数组[i],
当前最优右射 - 右射数组[i-1] + min(左射数组[i]+右射数组[i-1], 中间墙数组[i])
);
// 计算:当前机器人选右射的最大总墙数
int 新右射 = max(
当前最优左射 + 右射数组[i],
当前最优右射 + 右射数组[i]
);
// 更新DP状态
当前最优左射 = 新左射;
当前最优右射 = 新右射;
}
// ==============================================
// 第五步:返回最终答案
// 最后一个机器人选左射/右射的最大值
// ==============================================
return max(当前最优左射, 当前最优右射);
}
};
代码实现
cpp
class Solution {
public:
int maxWalls(vector<int>& robots, vector<int>& distance, vector<int>& walls) {
// ==============================================
// 第一步:初始化所有需要用到的变量
// ==============================================
int n = robots.size(); // 机器人的总数量
// left[i]: 第i个机器人向左能摧毁的墙数
// right[i]: 第i个机器人向右能摧毁的墙数
// num[i]: 第i-1个和第i个机器人之间的总墙数
vector<int> left(n, 0), right(n, 0), num(n, 0);
unordered_map<int, int> posToDist; // 哈希表:位置 -> 射程
// 填充哈希表(因为排序后会打乱原索引对应关系)
for (int i = 0; i < n; i++) {
posToDist[robots[i]] = distance[i];
}
// ==============================================
// 第二步:排序(直线问题必须按位置排序)
// ==============================================
sort(robots.begin(), robots.end());
sort(walls.begin(), walls.end());
int m = walls.size();
// 四个指针,只向右移动,不回头(保证线性时间)
int rightPtr = 0, leftPtr = 0, curPtr = 0, robotPtr = 0;
// ==============================================
// 第三步:遍历机器人,填充 left/right/num 数组
// ==============================================
for (int i = 0; i < n; i++) {
int currRobotPos = robots[i]; // 当前机器人的位置
// --------------------------
// 1. 找 pos1: 第一个 > currRobotPos 的墙
// --------------------------
while (rightPtr < m && walls[rightPtr] <= currRobotPos) {
rightPtr++;
}
int pos1 = rightPtr;
// --------------------------
// 2. 找 pos2: 第一个 >= currRobotPos 的墙
// --------------------------
while (curPtr < m && walls[curPtr] < currRobotPos) {
curPtr++;
}
int pos2 = curPtr;
// --------------------------
// 3. 计算 left[i] (向左射)
// --------------------------
// 确定左边界:如果有左边机器人,最多只能射到左边机器人右边一格
int leftBound = (i >= 1)
? max(currRobotPos - posToDist[currRobotPos], robots[i-1] + 1)
: currRobotPos - posToDist[currRobotPos];
// 找第一个 >= 左边界的墙
while (leftPtr < m && walls[leftPtr] < leftBound) {
leftPtr++;
}
left[i] = pos1 - leftPtr; // 中间的数量就是差值
// --------------------------
// 4. 计算 right[i] (向右射)
// --------------------------
// 确定右边界:如果有右边机器人,最多只能射到右边机器人左边一格
int rightBound = (i < n - 1)
? min(currRobotPos + posToDist[currRobotPos], robots[i+1] - 1)
: currRobotPos + posToDist[currRobotPos];
// 找第一个 > 右边界的墙
while (rightPtr < m && walls[rightPtr] <= rightBound) {
rightPtr++;
}
right[i] = rightPtr - pos2;
// --------------------------
// 5. 计算 num[i] (两机器人中间的墙数)
// --------------------------
if (i == 0) continue; // 第一个机器人没有左边邻居
// 找第一个 >= 上一个机器人位置的墙
while (robotPtr < m && walls[robotPtr] < robots[i-1]) {
robotPtr++;
}
num[i] = pos1 - robotPtr;
}
// ==============================================
// 第四步:动态规划决策
// ==============================================
int subLeft = left[0]; // 前i个机器人,最后一个向左射的最大值
int subRight = right[0];// 前i个机器人,最后一个向右射的最大值
for (int i = 1; i < n; i++) {
// 如果当前机器人向左射
// 要考虑前一个如果向右射,中间的墙不能重复算
int newLeft = max(
subLeft + left[i],
subRight - right[i-1] + min(left[i] + right[i-1], num[i])
);
// 如果当前机器人向右射
// 直接加就行,因为右边没冲突
int newRight = max(subLeft + right[i], subRight + right[i]);
// 更新状态
subLeft = newLeft;
subRight = newRight;
}
// 取最后是向左还是向右的最大值
return max(subLeft, subRight);
}
};
复杂度分析
-
时间复杂度 :O(n log n + m log m)
- 排序
robots耗时 O(n log n)。 - 排序
walls耗时 O(m log m)。 - 后续的
for循环和while循环是线性的。虽然有多个while,但每个指针(rightPtr,leftPtr等)都只从 0 移动到 m,不会回退,所以总遍历耗时是 O(n + m)。 - 当数据量很大时(1e5级别),主要耗时在排序上。
- 排序
-
空间复杂度 :O(n)
- 我们使用了
left,right,num三个大小为 n 的数组。 - 忽略排序的栈空间开销,额外空间为线性。
- 我们使用了
踩坑记录
-
务必严格明确每个机器人的有效射击边界,机器人自身位置上的墙只能由它自己摧毁,其他机器人无法打到,这一点极易忽略导致重复计数或漏算。
-
在统计并填充
left/right/num数组的过程中,会涉及大量边界、指针与位置变量,一定要分清各自含义,切忌混淆、弄懵,否则极易出现越界、多算、少算等错误。
这道题是"困难"级别,不仅考察数据结构的基本功(排序、双指针),最主要还考察对复杂场景的建模能力也就是我们的代码能力(动态规划状态定义与转移)。虽说我也不能保证完全吸收这道题但是希望这篇博客能帮到正在刷题的你和未来复习的我自己!

