几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组
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); // 更新最小窗口和
}
-
这个循环完成两件事:
-
计算整个数组的总和
total(通过逐步累加)。 -
用滑动窗口遍历所有长度为
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));
总结
这个算法巧妙地将“从两端取 k 个”的问题,转化为“找中间长度为 n−k 的最小和子数组”的问题,利用滑动窗口高效求解,是非常经典的逆向思维 + 滑动窗口应用。