LeetCode 3413. 收集连续 K 个袋子可以获得的最多硬币数量
题目描述
在一条数轴上有无限多个袋子,每个坐标对应一个袋子,其中一些袋子里有硬币。
给你一个二维数组 coins,其中 coins[i] = [li, ri, ci] 表示从坐标 li 到 ri 的每个袋子中都有 ci 枚硬币。
另给你一个整数 k。
目标:返回通过收集连续 k 个袋子可以获得的 最多硬币数量。
示例
输入: coins = [[8,10,1],[1,3,2],[5,6,4]], k = 4
输出: 10
解释: 选择坐标 [3,4,5,6] 的袋子可以获得最多硬币:2 + 0 + 4 + 4 = 10
核心思路
-
滑动窗口 + 双向扫描
- 左向右扫描:假设最优区间右端点与某个袋子的右端点对齐
- 右向左扫描:假设最优区间左端点与某个袋子的左端点对齐
-
部分覆盖处理
- 当窗口长度不等于整袋长度时,需要减去 不在窗口内的金币(使用
extra)
- 当窗口长度不等于整袋长度时,需要减去 不在窗口内的金币(使用
-
排序
- 先按袋子左端点排序,保证滑动窗口处理顺序正确
-
最大值更新
- 每次扫描窗口后,更新当前最大金币总数
JavaScript 实现
var maximumCoins = function(coins, k) {
// 按左端点排序,保证滑动窗口正确
coins.sort((a, b) => a[0] - b[0]);
let ans = 0; // 最大金币
let sum = 0; // 当前窗口总金币
let left = 0; // 滑动窗口左指针
let extra = 0; // 部分不在窗口内的金币
const n = coins.length;
// 左向右扫描(右端点对齐)
for (let i = 0; i < n; i++) {
const coin = coins[i];
const pocketLeft = coin[1] - k + 1; // 窗口左端
sum += coin[2] * (coin[1] - coin[0] + 1); // 当前袋子完整加入总和
// 移除完全不在窗口内的左边袋子
while (coins[left][1] < pocketLeft) {
sum -= coins[left][2] * (coins[left][1] - coins[left][0] + 1);
left++;
}
// 部分不在窗口内的左边袋子
extra = Math.max(pocketLeft - coins[left][0], 0) * coins[left][2];
// 更新最大值
ans = Math.max(ans, sum - extra);
}
// 右向左扫描(左端点对齐)
sum = 0;
let r = n - 1;
for (let i = n - 1; i >= 0; i--) {
const coin = coins[i];
const pocketRight = coin[0] + k - 1; // 窗口右端
sum += coin[2] * (coin[1] - coin[0] + 1);
// 移除完全不在窗口内的右边袋子
while (coins[r][0] > pocketRight) {
sum -= coins[r][2] * (coins[r][1] - coins[r][0] + 1);
r--;
}
// 部分不在窗口内的右边袋子
extra = Math.max(coins[r][1] - pocketRight, 0) * coins[r][2];
// 更新最大值
ans = Math.max(ans, sum - extra);
}
return ans;
};
每步讲解
1. 排序
coins.sort((a, b) => a[0] - b[0]);
- 按袋子左端点排序,使得滑动窗口可以顺序处理。
- 保证扫描时窗口左边界和右边界的移动逻辑正确。
2. 左向右扫描(右端点对齐)
for (let i = 0; i < n; i++) {
const coin = coins[i];
const pocketLeft = coin[1] - k + 1;
sum += coin[2] * (coin[1] - coin[0] + 1);
- 假设最优解右端点落在当前袋子右端。
- 将当前袋子全部加入
sum。
3. 调整左边界
while (coins[left][1] < pocketLeft) {
sum -= coins[left][2] * (coins[left][1] - coins[left][0] + 1);
left++;
}
- 移除完全在窗口外的袋子,保证窗口长度 ≤ k。
4. 处理部分覆盖
extra = Math.max(pocketLeft - coins[left][0], 0) * coins[left][2];
ans = Math.max(ans, sum - extra);
- 计算 左边部分不在窗口内的金币,减掉它。
- 更新最大值。
5. 右向左扫描(左端点对齐)
-
原理与左向右扫描类似,只是:
- 假设最优左端点对齐当前袋子左端
- 移动右指针,减去窗口右边不在范围的袋子
- 计算右边部分不在窗口内的金币
6. 时间复杂度分析
- 排序:
O(n log n) - 双向扫描:
O(n) - 总体复杂度:
O(n log n) - 空间复杂度:
O(1)(只使用常量额外空间)
7. 测试示例
console.log(maximumCoins([[8,10,1],[1,3,2],[5,6,4]], 4)); // 10
console.log(maximumCoins([[31,33,18],[45,49,18],[34,40,8],[17,20,8],[41,42,7],[6,9,10],[23,30,12],[10,14,7]], 16)); // 190
console.log(maximumCoins([[30,49,12]], 28)); // 240
console.log(maximumCoins([[8,12,13],[29,32,2],[13,15,2],[40,41,18],[42,48,18],[33,36,11],[37,38,6]], 28)); // 226
console.log(maximumCoins([[1,1000000000,1000]], 1000000000)); // 1000000000000
8. 学习要点
-
滑动窗口双向扫描能覆盖所有最优区间情况:
- 右端点对齐
- 左端点对齐
-
部分覆盖处理 (
extra) 是算法正确性的关键。 -
排序保证窗口移动顺序正确。
-
BigInt 不必要,JavaScript 的 Number 在题目范围内安全。