今天周日,但是要补休,五一的。所以参赛人员少,而且题目也比较简单。
第一题:枚举。
第二题:滑动窗口 或者 前缀后缀和。
第三题:模拟。
第四题:DP 或者 贪心。
详细题解如下。
1.分割字符串的最大得分(Maximum Score after Splitting A String)
AC代码(C++)
2. 可获得的最大点数(Maximum Points You Can Obtain from Cards)
AC代码(方法一 滑动窗口 C++)
AC代码(方法二 前缀和与后缀和 C++)
3.对角线遍历 II(Diagonal Traverse II)
AC代码(C++)
4.带限制的子序列和(Build Array Where You Can Find the Maximum Exactly K Comparisons)
AC代码(方法一 DP C++)
AC代码(方法二 贪心 C++)
LeetCode第186场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-186/
https://leetcode-cn.com/problems/maximum-score-after-splitting-a-string/
给你一个由若干 0 和 1 组成的字符串 s ,请你计算并返回将该字符串分割成两个 非空 子字符串(即 左 子字符串和 右 子字符串)所能获得的最大得分。
「分割字符串的得分」为 左 子字符串中 0 的数量加上 右 子字符串中 1 的数量。
示例 1:
输入:s = "011101" 输出:5 解释: 将字符串 s 划分为两个非空子字符串的可行方案有: 左子字符串 = "0" 且 右子字符串 = "11101",得分 = 1 + 4 = 5 左子字符串 = "01" 且 右子字符串 = "1101",得分 = 1 + 3 = 4 左子字符串 = "011" 且 右子字符串 = "101",得分 = 1 + 2 = 3 左子字符串 = "0111" 且 右子字符串 = "01",得分 = 1 + 1 = 2 左子字符串 = "01110" 且 右子字符串 = "1",得分 = 2 + 1 = 3
示例 2:
输入:s = "00111" 输出:5 解释:当 左子字符串 = "00" 且 右子字符串 = "111" 时,我们得到最大得分 = 2 + 3 = 5
示例 4:
输入:s = "1111" 输出:3
提示:
2 <= s.length <= 500
- 字符串
s
仅由字符'0'
和'1'
组成。
根据题意,我们可以枚举前一个字符串可能的长度,也就是从 0 到 n - 2。
那么我们在每一个可能的长度中,怎么在 O(1) 中快速计算出,前一个字符串中的 0 和 后一个字符串中 1 的个数呢?
我们可以用一个变量,一直记录前一个 字符串中 0 的个数。
那么后面字符串 1 的个数 = 后面字符串长度 - (全部 0 - 前面的 0)。也就是,后面字符串的长度去掉其出现 0 的个数,即为后面字符串中 1的个数。
所以总的时间复杂度是 O(n)
class Solution {
public:
int maxScore(string s) {
int n = s.size();
int total = 0; // 所有 0 的个数
for(int i = 0;i < n; ++i)
{
if(s[i] == '0') ++total;
}
int ans = 0;
int cnt = 0;
for(int i = 0;i < n - 1; ++i) // 枚举前一个字符串的长度
{
if(s[i] == '0') ++cnt; // 前面字符串中 0 的个数
int last = n - i - 1 - (total - cnt); // 此时后面字符串中 1 的个数
ans = max(ans, cnt + last); // 算出所有枚举情况中,最大的那一个答案。
}
return ans;
}
};
https://leetcode-cn.com/problems/maximum-points-you-can-obtain-from-cards/
几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。
每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k 张卡牌。
你的点数就是你拿到手中的所有卡牌的点数之和。
给你一个整数数组 cardPoints 和整数 k,请你返回可以获得的最大点数。
示例 1:
输入:cardPoints = [1,2,3,4,5,6,1], k = 3 输出:12 解释:第一次行动,不管拿哪张牌,你的点数总是 1 。但是,先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌,最终点数为 1 + 6 + 5 = 12 。
示例 3:
输入:cardPoints = [9,7,7,9,7,7,9], k = 7 输出:55 解释:你必须拿起所有卡牌,可以获得的点数为所有卡牌的点数之和。
示例 5:
输入:cardPoints = [1,79,80,1,1,1,200,1], k = 3 输出:202
提示:
1 <= cardPoints.length <= 10^5
1 <= cardPoints[i] <= 10^4
1 <= k <= cardPoints.length
方法一、滑动窗口(问题的转换)
根据题目的意思,一开始想的是 DP,但是后来发现 我们要选出来的部分是前面 或者 后面,那么剩下的部分,就是中间连起来的。
那么,我们利用一个窗口,大小是 n - k,我们取这个连续窗口中的最小值(那么就转换为了,求出了我们取出前面或者后面部分中的最大值)
所以我们的目的就是,通过计算出,滑动窗口的值,得到我们要取出的值(总值 - 滑动窗口部分的值),然后使得这部分的值最大,那么根据滑动窗口的计算,是 O(n) 就可以计算出所有可能的值了。
方法二、前缀和、后缀和
上一个方法是将问题转换成了中间部分的值。
那么如果就是利用前缀和后缀怎么计算呢?
我们可以枚举,可能的前缀长度(那么根据总长度是 k,就可以得到 后缀长度),那么怎么在 O(1) 时间内,算出 前缀和,与 后缀部分和呢(因为如果不是 O(1),那么暴力计算,需要 O(k),那么总时间复杂度就会超时)。
计算前缀和,我们可以利用一个数组,来记录,这样子,当我们得到前缀长度的时候,就可以马上计算出 前缀和了。
类似的,后缀和也是一样。
class Solution {
public:
int maxScore(vector& cardPoints, int k) {
int sum = 0;
for(auto c : cardPoints) sum += c; // 先算出总和
int ans = 0;
int n = cardPoints.size();
int cur = 0; // 当前窗口的值
k = n - k;
for(int i = 0;i < k; ++i) cur += cardPoints[i];
ans = sum - cur;
for(int i = k;i < n; ++i)
{
cur = cur + cardPoints[i] - cardPoints[i - k]; // 每一次移动窗口后,新进来后面的,出去了最前面的
ans = max(ans, sum - cur);
}
return ans;
}
};
const int MAXN = 1e5 + 50;
class Solution {
public:
int pre[MAXN], last[MAXN]; // 前缀和 与 后缀和
int maxScore(vector& cardPoints, int k) {
int n = cardPoints.size();
// 计算前缀和
pre[0] = 0;
for(int i = 1;i <= n; ++i) pre[i] = pre[i - 1] + cardPoints[i - 1];
// 计算后缀和
last[0] = 0;
for(int i = 1;i <= n; ++i) last[i] = last[i - 1] + cardPoints[n - 1 - i + 1];
int ans = 0;
for(int i = 0;i <= k; ++i) // 枚举所有可能的前缀长度,然后计算得到对应取出的前缀和与后缀和
{
int cur = pre[i] + last[k - i];
ans = max(ans, cur);
}
return ans;
}
};
https://leetcode-cn.com/problems/diagonal-traverse-ii/
给你一个列表
nums
,里面每一个元素都是一个整数列表。请你依照下面各图的规则,按顺序返回nums
中对角线上的整数。示例 1:
【示例有图,具体看链接】 输入:nums = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,4,2,7,5,3,8,6,9]
示例 2:
【示例有图,具体看链接】 输入:nums = [[1,2,3,4,5],[6,7],[8],[9,10,11],[12,13,14,15,16]] 输出:[1,6,2,8,7,3,9,4,12,10,5,13,11,14,15,16]
示例 3:
输入:croakOfFrogs = "croakcrook" 输出:-1 解释:给出的字符串不是 "croak" 的有效组合。
提示:
1 <= nums.length <= 10^5
1 <= nums[i].length <= 10^5
1 <= nums[i][j] <= 10^9
nums 中最多有 10^5 个数字。
一开始,以为是直接从左边和下边(即对角线元素的开始)开始,然后枚举所有数,但是这样子的时间复杂度是,O(n * m),那么根据题目就会超时。
根据示例 2 发现,并不是一个完整的 n * m 数组,而是一个动态长度的。(同时数据中保证了,最多只有 10 ^ 5 个数),因此,只要是枚举数据,而不是枚举 n,m。就不会超时。
那么我们分析出,从一个 n * m (其中 m 是每一行数组的最大长度),总共是可能有 n + m - 1 个对角线(首先是左边 的 n 个,然后下边的 m - 1 个)。
然后我们发现了,同一对角线上的 i + j 是相同值。
因此,我们用一个二维动态数组,行数是 n + m - 1,每一个后面该表示对角线上的元素。
这里要注意,我们存进去的时候,是一个倒过来的,也就是,对于 i 行这里面存的,比如示例 2 中第二个对角线,存进去是 2 4。也就是,我们处理的是,是一行一行的数据处理,所以是对角线上的元素,是反过来存储的
所以我们将 n + m - 1 数据存进 一维数组的时候,每一行的数据是反过来 的。
【其中也是可以用 unordered_map ,或者 vector
class Solution {
public:
vector findDiagonalOrder(vector>& nums) {
int n = nums.size();
int m = 0;
for(auto c : nums) m = max(m, int(c.size()));
vector > tep(n + m - 1, vector (1, 0)); // 总共 n + m - 1 行,先把第一列都存好
for(int i = 0;i < nums[0].size(); ++i) tep[i][0] = nums[0][i];
for(int i = 1;i < n; ++i) // 开始从原数组的第二行开始
{
for(int j = 0;j < nums[i].size(); ++j)
{
tep[i + j].push_back(nums[i][j]);
}
}
vector ans;
for(int i = 0;i < n + m - 1; ++i)
{
for(int j = tep[i].size() - 1;j >= 0; --j) // 对于每一行,我们是反过来的,所以是从后往前放入 ans
{
if(tep[i][j] == 0) break;
ans.push_back(tep[i][j]);
}
}
return ans;
}
};
https://leetcode-cn.com/problems/constrained-subset-sum/
给你一个整数数组 nums 和一个整数 k ,请你返回 非空 子序列元素和的最大值,子序列需要满足:子序列中每两个 相邻 的整数 nums[i] 和 nums[j] ,它们在原数组中的下标 i 和 j 满足 i < j 且 j - i <= k 。
数组的子序列定义为:将数组中的若干个数字删除(可以删除 0 个数字),剩下的数字按照原本的顺序排布。
示例 1:
输入:nums = [10,2,-10,5,20], k = 2 输出:37 解释:子序列为 [10, 2, 5, 20] 。
示例 4:
输入:nums = [10,-2,-10,-5,20], k = 2 输出:23 解释:子序列为 [10, -2, -5, 20] 。
提示:
1 <= k <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
方法一、动态规划DP
看了题目,其实很像是一个动态规划的问题,只是加了额外的限制条件。
1)设 dp[ i ] 表示 对于 第 i 个数字,取该数的情况下,最大和 结果
2)状态转移方程:
dp[ i ] = nums[ i - 1] +max(dp[ j ] )
其中 j 是 j < i,同时,我们是从 j -> i 的,那么按照要求 i - j <= k,所以 i - k <= j < i。
如果直接算的话,总共复杂度是 O(n * k),那么就会超时了。
所以要进行优化,如何快速的求 max(dp[ j ] )
我们可以用一个 优先队列 来存 i 前面的哪些 dp[ j ] 和 对应下标 j。
那么我们就用优先队列,就可以取到最大值
但是有一个地方要注意,我们 取出的最大 dp[ j ],有要求 i - k <= j。(所以我们可以先利用判断,找到一个 满足 j 这个条件的最大值)
3)初始值
根据 示例 2,我们知道至少要取一个数,所以初始值就是 dp[ all i ] = nums[i - 1],也就是对于每个 dp ,初始值就是单单取了自身。
4)答案结果
由于 dp[ i ] 表示 对于 第 i 个数字,取该数的情况下,最大和 结果,但是我们具体不知道 i 这个数取了没,因此,结果应该是 所有 dp[ i ] 中的最大值。
方法二、贪心
其实我们可以发现,如果我们从后往前取,那么当 i 取的时候,说明了 i - j <= k 中的所有 j ,下一次都可以取。
那么对于下一次而言,我们取出最大的来加
然后每一次加,都重复(加过的出来),这个过程(直到没有 数可以再取,也就是队列为空)。然后每一次都得其最大值来记录。最后就是答案。
那么由于每一次也都是找最大得来加,因此,我们用 优先队列。
因此,问题就主要要,从后往前,第一个取得是谁
那我们知道,最后一个要取得值,一定 >= 0,那么从后往前,第一个 >= 0得数
这里有一个问题,假如没有呢?比如 示例 2,所以对于每一个数,我们都认为其是最大值,那么如果都没有 >= 0得,我们也就取出了其中最大的作为最后答案。
这道题主要就是,每一个点,都进 and 出 队列一次。
const int MAXN = 1e5 + 50;
#define pii pair
#define mk(x, y) make_pair(x, y)
class Solution {
public:
int dp[MAXN]; // dp[i] = nums[i] + max{dp[j]} 其中 i - j <= k -> i - k <= j < i
priority_queue, less > pq;
int constrainedSubsetSum(vector& nums, int k) {
int n = nums.size();
for(int i = 0;i < n; ++i) dp[i] = nums[i];
while(!pq.empty()) pq.pop();
pq.push(mk(dp[0], 0));
for(int i = 1;i < n; ++i)
{
while(!pq.empty() && i - pq.top().second > k) pq.pop();
if(!pq.empty())
{
dp[i] = max(dp[i], nums[i] + pq.top().first);
}
pq.push(mk(dp[i], i));
// cout << dp[i] << endl;
}
int ans = -MAXN; // 最小值,应该是 -1e4,但是我们设的 MAXN 大,所以直接利用即可。
for(int i = 0;i < n; ++i) ans = max(ans, dp[i]);
return ans;
}
};
const int MAXN = 1e5 + 50;
#define pii pair
#define mk(x, y) make_pair(x, y)
class Solution {
public:
int vis[MAXN];
priority_queue, less> pq;
int constrainedSubsetSum(vector& nums, int k) {
memset(vis, 0, sizeof(vis));
int ans = -MAXN;
int cur = 0;
int n = nums.size();
int i;
for(i = n - 1; i >= 0; --i)
{
ans = max(ans, nums[i]);
vis[i] = 1;
if(nums[i] >= 0)
{
cur = nums[i];
break;
}
}
if(i - 1 < 0) return ans;
int e = i - 1;
while(!pq.empty()) pq.pop();
for(i = e; i >= max(0, e + 1 - k);--i)
{
vis[i] = 1;
pq.push(mk(nums[i], i));
}
while(!pq.empty())
{
// cout << cur << " " << pq.top().first << " " << pq.top().second << endl;
cur += pq.top().first;
ans = max(ans, cur);
int ee = pq.top().second - 1;
int ss = max(0, ee + 1 - k);
pq.pop();
if(ee >= 0)
{
// 对于 i 取了,那么这个 i 满足得 i - j <= k 中得所有 j 都应该加入。但是已经加入就不用加入了,所以用 vis 来标记
// 同时优化一下,就是对于 vis,我们 j 都是从最开始满足得往后加,那么只要需要了 vis[j] = 1,所以后面都已经加过了,就可以直接跳出了。
for(int j = ss;j <= ee; ++j)
{
if(vis[j]) break;
vis[j] = 1;
pq.push(mk(nums[j], j));
}
}
}
return ans;
}
};