Medium
题目描述
有几张卡牌排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。
每次行动,你可以从行的开头或者末尾拿一张卡牌,必须拿恰好 k 张卡牌。
你的点数就是你拿的卡牌点数的总和。
给你一个整数数组 cardPoints 和整数 k,请你返回可以获得的最大点数。
示例 1:
输入:cardPoints = [1,2,3,4,5,6,1], k = 3
输出:12
解释:第一次行动,不管拿哪张牌,你的点数总是 1 。但是,先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌,最终得分为 1 + 6 + 5 = 12 。
示例 2:
输入:cardPoints = [2,2,2], k = 2
输出:4
解释:无论你拿起哪两张卡牌,可获得的点数总是 4 。
示例 3:
输入:cardPoints = [9,7,7,9,7,7,9], k = 7
输出:55
解释:你必须拿所有卡牌,可以获得的点数为所有卡牌的点数之和。
提示:
1 <= cardPoints.length <= 10^51 <= cardPoints[i] <= 10^41 <= k <= cardPoints.length
解题思路
这道题有两种主要思路:
方法一:前缀和枚举(推荐)
我们可以枚举从左边取 i 张牌,从右边取 k-i 张牌的所有情况。使用前缀和预处理,快速计算任意区间的和。时间复杂度 O(k),空间复杂度 O(k)。
方法二:滑动窗口
换个角度思考:要取 k 张牌获得最大点数,等价于剩余 n-k 张连续的牌点数和最小。我们可以用滑动窗口找到长度为 n-k 的子数组的最小和,然后用总和减去这个最小和即为答案。
两种方法的时间复杂度都是 O(n),但方法一的常数因子更小,实现也更直观。当 k 较小时,方法一的实际复杂度接近 O(k),更加高效。
核心思想是:由于只能从两端取牌,最优解必然是"从左边取 i 张,从右边取 k-i 张"的某种组合。我们枚举所有可能的 i 值,计算对应的点数和,取最大值即可。
代码实现
class Solution {
public:
int maxScore(vector<int>& cardPoints, int k) {
int n = cardPoints.size();
// 计算从左边取 i 张牌的前缀和
vector<int> leftSum(k + 1, 0);
for (int i = 0; i < k; i++) {
leftSum[i + 1] = leftSum[i] + cardPoints[i];
}
// 计算从右边取 j 张牌的前缀和
vector<int> rightSum(k + 1, 0);
for (int i = 0; i < k; i++) {
rightSum[i + 1] = rightSum[i] + cardPoints[n - 1 - i];
}
int maxPoints = 0;
// 枚举从左边取 i 张,从右边取 k-i 张
for (int i = 0; i <= k; i++) {
maxPoints = max(maxPoints, leftSum[i] + rightSum[k - i]);
}
return maxPoints;
}
};
class Solution:
def maxScore(self, cardPoints: List[int], k: int) -> int:
n = len(cardPoints)
# 计算从左边取 i 张牌的前缀和
left_sum = [0] * (k + 1)
for i in range(k):
left_sum[i + 1] = left_sum[i] + cardPoints[i]
# 计算从右边取 j 张牌的前缀和
right_sum = [0] * (k + 1)
for i in range(k):
right_sum[i + 1] = right_sum[i] + cardPoints[n - 1 - i]
max_points = 0
# 枚举从左边取 i 张,从右边取 k-i 张
for i in range(k + 1):
max_points = max(max_points, left_sum[i] + right_sum[k - i])
return max_points
public class Solution {
public int MaxScore(int[] cardPoints, int k) {
int n = cardPoints.Length;
// 计算从左边取 i 张牌的前缀和
int[] leftSum = new int[k + 1];
for (int i = 0; i < k; i++) {
leftSum[i + 1] = leftSum[i] + cardPoints[i];
}
// 计算从右边取 j 张牌的前缀和
int[] rightSum = new int[k + 1];
for (int i = 0; i < k; i++) {
rightSum[i + 1] = rightSum[i] + cardPoints[n - 1 - i];
}
int maxPoints = 0;
// 枚举从左边取 i 张,从右边取 k-i 张
for (int i = 0; i <= k; i++) {
maxPoints = Math.Max(maxPoints, leftSum[i] + rightSum[k - i]);
}
return maxPoints;
}
}
/**
* @param {number[]} cardPoints
* @param {number} k
* @return {number}
*/
var maxScore = function(cardPoints, k) {
const n = cardPoints.length;
// 计算从左边取 i 张牌的前缀和
const leftSum = new Array(k + 1).fill(0);
for (let i = 0; i < k; i++) {
leftSum[i + 1] = leftSum[i] + cardPoints[i];
}
// 计算从右边取 j 张牌的前缀和
const rightSum = new Array(k + 1).fill(0);
for (let i = 0; i < k; i++) {
rightSum[i + 1] = rightSum[i] + cardPoints[n - 1 - i];
}
let maxPoints = 0;
// 枚举从左边取 i 张,从右边取 k-i 张
for (let i = 0; i <= k; i++) {
maxPoints = Math.max(maxPoints, leftSum[i] + rightSum[k - i]);
}
return maxPoints;
};
复杂度分析
| 复杂度类型 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(k) | 需要计算两个长度为k的前缀和,然后枚举k+1种情况 |
| 空间复杂度 | O(k) | 需要两个长度为k+1的数组存储前缀和 |