滑动窗口的巧妙应用:反转思路求解卡牌最大点数问题

0 阅读4分钟

几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k 张卡牌。你的点数就是你拿到手中的所有卡牌的点数之和。给你一个整数数组 cardPoints 和整数 k,请你返回可以获得的最大点数。


🧠 算法核心思想:滑动窗口 + 补集思想

直接考虑“从两端取 k 张卡”比较复杂(因为组合很多),但可以换个角度思考:

  • 总共有 n 张卡。
  • 你要拿走 k 张 → 那么剩下 n - k 张 没被拿走
  • 而且这 n - k 张一定是连续的一段(因为只能从两端拿,中间剩下的必然是连续子数组)。

👉 所以问题转化为:

在数组中找一个长度为 m = n - k 的连续子数组,使其和最小。 那么总和减去这个最小和,就是你能拿到的最大点数!


🔍 代码逐行解释

js
编辑
var maxScore = function(cardPoints, k) {
    const n = cardPoints.length;
    const m = n - k; // 剩下的连续卡牌数量(不能拿的部分)
  • n 是总卡牌数。
  • m = n - k 是必须留下的连续卡牌数量
    let s = 0;
    for (let i = 0; i < m; i++){
        s += cardPoints[i];
    }
  • 初始化一个长度为 m 的窗口(最左边的 m 个元素),计算其和 s
  • 这是第一个可能的“留下区间”。
    let minS = s; // 当前最小的留下区间和
    let total = s; // 先把前 m 项加进 total
  • minS 记录所有长度为 m 的子数组中的最小和
  • total 后面会变成整个数组的总和。
    for (let i = m; i < n; i++) {
        total += cardPoints[i];           // 把后面的元素加到 total,最终 total = sum(cardPoints)
        s += cardPoints[i] - cardPoints[i - m]; // 滑动窗口:右移一位,加入新元素,去掉最左元素
        minS = Math.min(minS, s);         // 更新最小窗口和
    }
  • 这个循环完成两件事:

    1. 计算整个数组的总和 total(通过逐步累加)。

    2. 用滑动窗口遍历所有长度为 m 的连续子数组,更新最小和 minS

      • s += cardPoints[i] - cardPoints[i - m] 是典型的滑动窗口更新方式。
    return total - minS;
  • 最终答案 = 总点数 - 最小保留区间的点数 = 最大可获得点数 ✅

📌 举个例子

假设:

cardPoints = [1, 2, 3, 4, 5, 6, 1], k = 3
  • n = 7, m = 4

  • 必须留下 4 张连续的卡,使得它们的和最小。

  • 所有可能的留下区间:

    • [1,2,3,4] → sum=10
    • [2,3,4,5] → 14
    • [3,4,5,6] → 18
    • [4,5,6,1] → 16
  • 最小是 10 → total = 22 → 答案 = 22 - 10 = 12

  • 对应拿走的是最后 3 张:[5,6,1]?不对,其实是拿走开头 3 张 [1,2,3] 和结尾 0 张?等等——其实最优是拿 [1 (开头), 6, 1 (结尾)]?不对。

等等,正确拿法应该是:拿最后 3 张 [5,6,1] → sum=12,或者 拿 [1,6,5] ?不,只能从两端连续拿!

实际上,合法拿法是:

  • 拿前 3:[1,2,3] → 6
  • 拿前 2 + 后 1:[1,2,1] → 4
  • 拿前 1 + 后 2:[1,6,1] → 8
  • 拿后 3:[5,6,1] → 12 ← 最大!

而留下的就是 [1,2,3,4] → sum=10,确实是最小的。所以算法正确。


✅ 时间 & 空间复杂度

  • 时间复杂度:O(n) —— 只遍历两次数组(一次初始化窗口,一次滑动)。
  • 空间复杂度:O(1) —— 只用几个变量。

全部代码

/**
 * 
 * @param {*} cardPoints 
 * @param {*} k 
 * @returns 
 * 几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。

    每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k 张卡牌。

    你的点数就是你拿到手中的所有卡牌的点数之和。

    给你一个整数数组 cardPoints 和整数 k,请你返回可以获得的最大点数。
 */
var maxScore = function(cardPoints, k){
    const n = cardPoints.length;
    const m = n-k;
    let sum = 0;
    for(let i = 0; i < m ; i++){
        sum=cardPoints[i]+sum;
    } 
    let total = sum;
    let minS = sum;
    for(let i = m;i < n ; i++){
        sum +=cardPoints[i]-cardPoints[i-m];
        total += cardPoints[i];
        minS = Math.min(sum,minS);
    }
    return total - minS;
};
cardPoints = [1,79,80,1,1,1,200,1];
let k = 3;
console.log(maxScore(cardPoints,k));

image.png

总结

这个算法巧妙地将“从两端取 k 个”的问题,转化为“找中间长度为 n−k 的最小和子数组”的问题,利用滑动窗口高效求解,是非常经典的逆向思维 + 滑动窗口应用。