LeetCode 3413. 收集连续 K 个袋子可以获得的最多硬币数量

81 阅读4分钟

LeetCode 3413. 收集连续 K 个袋子可以获得的最多硬币数量

题目描述

在一条数轴上有无限多个袋子,每个坐标对应一个袋子,其中一些袋子里有硬币。

给你一个二维数组 coins,其中 coins[i] = [li, ri, ci] 表示从坐标 liri 的每个袋子中都有 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

核心思路

  1. 滑动窗口 + 双向扫描

    • 左向右扫描:假设最优区间右端点与某个袋子的右端点对齐
    • 右向左扫描:假设最优区间左端点与某个袋子的左端点对齐
  2. 部分覆盖处理

    • 当窗口长度不等于整袋长度时,需要减去 不在窗口内的金币(使用 extra
  3. 排序

    • 先按袋子左端点排序,保证滑动窗口处理顺序正确
  4. 最大值更新

    • 每次扫描窗口后,更新当前最大金币总数

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. 学习要点

  1. 滑动窗口双向扫描能覆盖所有最优区间情况:

    • 右端点对齐
    • 左端点对齐
  2. 部分覆盖处理 (extra) 是算法正确性的关键。

  3. 排序保证窗口移动顺序正确。

  4. BigInt 不必要,JavaScript 的 Number 在题目范围内安全。