题目
几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 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
解释:你必须拿起所有卡牌,可以获得的点数为所有卡牌的点数之和。示例 4:
输入:cardPoints = [1,1000,1], k = 1
输出:1
解释:你无法拿到中间那张卡牌,所以可以获得的最大点数为 1 。示例 5:
输入:cardPoints = [1,79,80,1,1,1,200,1], k = 3
输出:202
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/maximum-points-you-can-obtain-from-cards
前缀和+后缀和
这道题目我们也可以使用前缀和+后缀和的思路去解,我们可以从题目中看到规律,
- k k k的个数可以拆分成是从数组前往后取连续的 0 − m 0-m 0−m个数加上从数组最后往前取连续的 0 − n 0-n 0−n个数,并且 m + n = k m+n=k m+n=k,从示例[1,2,3,4,5,6,1], k = 3来看,即我们可以前面先取0个后面取3个然后前面取1个后面取2个以此类推来计算
- 每次 k k k个数的和为从数组前往后取连续的 0 − m 0-m 0−m个数加上从数组后往前取连续的 0 − n 0-n 0−n个数所有数的总和
- 取最大的和即为最终结果
综合上面的规律,我们很容易可以写出来这种思路的代码
public int maxScore(int[] cardPoints, int k) {
int[] prefixSums = new int[cardPoints.length];
int[] suffixSums = new int[cardPoints.length];
//前缀和
int preSum = 0;
for(int i = 0; i < cardPoints.length; i++){
preSum += cardPoints[i];
prefixSums[i] = preSum;
}
//后缀和
int sufSum = 0;
for(int j = cardPoints.length - 1; j >= 0; j--){
sufSum += cardPoints[j];
suffixSums[j] = sufSum;
}
int maxSum = 0;
for(int m = 0; m <= k; m++){
int prefixSum = m == 0 ? 0 : prefixSums[m - 1];
int n = k - m;
int suffixSum = n == 0 ? 0 : suffixSums[cardPoints.length - n];
int sum = prefixSum + suffixSum;
maxSum = Math.max(maxSum, sum);
}
return maxSum;
}
不断优化
上面代码要注意的是,由于我们计算前缀和的时候,第0个位置的前缀和其实是拿1个数的时候的和,所以我们需要在计算最终和的时候需要在前缀和数组中取当前m值减1下标位置的值,所以需要做一下取0个数的判断,即取0个数的和即为0,不然会数组下标越界啦
另外,在计算前缀和的时候可以放入最终的计算和的遍历一起,这样也得到了一定优化,可以优化成如下代码
public int maxScore(int[] cardPoints, int k) {
int[] suffixSums = new int[cardPoints.length];
//后缀和
int sufSum = 0;
for(int j = cardPoints.length - 1; j >= 0; j--){
sufSum += cardPoints[j];
suffixSums[j] = sufSum;
}
//前缀和与最终计算一起进行
int maxSum = 0;
int prefixSum = 0;
for(int m = 0; m <= k; m++){
prefixSum += m == 0 ? 0 : cardPoints[m - 1];
int n = k - m;
int suffixSum = n == 0 ? 0 : suffixSums[cardPoints.length - n];
int sum = prefixSum + suffixSum;
maxSum = Math.max(maxSum, sum);
}
return maxSum;
}
经过上面的优化后,我们又发现,其实我们的后缀和在计算的时候计算了很多不必要的数据,因为如果k的个数远远小于数组的个数,我们根本就不需要计算出所有数据的后缀和,只需要计算出k个就行了,所以我们有了另一种思路,我们只要将在k范围内的 0 − m 0-m 0−m的所有情况的每个和全部计算出来后记录下来,再把在k范围内的 0 − n 0-n 0−n的所有情况的和也计算并记录,将m和n组合来获取最大和的组合即可,代码如下
public int maxScore(int[] cardPoints, int k) {
int[] nSums = new int[k + 1];//多了0个的情况,所以要+1个
int tempSum = 0;
for(int n = 0; n <= k; n++){
tempSum += n == 0 ? 0 : cardPoints[cardPoints.length - n];
nSums[n] = tempSum;
}
int ans = 0;
tempSum = 0;
for(int m = 0; m <= k; m++){
tempSum += m == 0 ? 0 : cardPoints[m - 1];
int sum = tempSum + nSums[k - m];
ans = Math.max(ans, sum);
}
return ans;
}
滑动窗口
感觉挺好了哈,但是这道题目其实还不是最优解,最优解其实可以用滑动窗口来做,其实我们上面的解法已经有了点这个意思了,我们上面的思路都是顺着题目来的,分别取两头的数据和来做的,其实我们逆向思维一下,很快也能想出来,题目要求的其实是数组两端的一个和,那么我们这次计算中间的连续段的和,然后用数组的总和减去中间连续段的和既可以得出来两端连续数的总和了,我们取最小的一段总和,那么最终答案就是用总和减去最小的这段总和,这样我们只要维护一个中间的连续段的窗口,每次向右移动窗口,即可,是不是思路很清奇,我们直接写代码
public int maxScore(int[] cardPoints, int k) {
int windowSize = cardPoints.length - k;
int totalPoint = 0;
for(int cardPoint : cardPoints) totalPoint += cardPoint;
int windowPoint = 0;
for(int i = 0; i < windowSize; i++){
windowPoint += cardPoints[i];
}
int ans = windowPoint;
for(int j = windowSize; j < cardPoints.length; j++){
windowPoint += cardPoints[j] - cardPoints[j - windowSize];
ans = Math.min(ans, windowPoint);
}
return totalPoint - ans;
}
经过这些步骤,我们从最明显的做法,一步一步优化,一步一步的分析,最终以最优化的方式做出来了这道题目,所以我们做题目都要一步一步来,不能一口吃个胖子,这道题目是很好的前后缀和以及滑动窗口的练习题目,所以这样的算法,你理解了吗?