大家好,我是老梁。
今天是周一,我们照惯例来聊聊昨天的LeetCode周赛。
这一场是LeetCode周赛第336场,由LeetCode官方自己赞助自己举办,提供电脑包以及扑克等奖品。
截取了几条比较欢乐的评论:
统计范围内的元音字符串数
给你一个下标从 0 开始的字符串数组 words 和两个整数:left 和 right 。
如果字符串以元音字母开头并以元音字母结尾,那么该字符串就是一个 元音字符串 ,其中元音字母是 'a'、'e'、'i'、'o'、'u' 。
返回 words[i] 是元音字符串的数目,其中 i 在闭区间 [left, right] 内。
题解
签到题,不解释
class Solution {
public:
int vowelStrings(vector<string>& words, int left, int right) {
set<char> st {'a', 'e', 'i', 'o', 'u'};
int cnt = 0;
for (int i = left; i <= right; i++) {
if (st.count(words[i][0]) && st.count(words[i][words[i].length() - 1])) {
cnt ++;
}
}
return cnt;
}
};
重排数组以得到最大前缀分数
给你一个下标从 0 开始的整数数组 nums 。你可以将 nums 中的元素按 任意顺序 重排(包括给定顺序)。
令 prefix 为一个数组,它包含了 nums 重新排列后的前缀和。换句话说,prefix[i] 是 nums 重新排列后下标从 0 到 i 的元素之和。nums 的 分数 是 prefix 数组中正整数的个数。
返回可以得到的最大分数。
题解
贪心,要使得前缀和数组当中大于0的数量尽量多,很容易想到尽量把大数放在靠前的位置,那么我们对元素进行倒排,之后统计答案即可。
class Solution {
public:
int maxScore(vector<int>& nums) {
sort(nums.begin(), nums.end(), greater<>());
int n = nums.size();
long long tot = 0;
int ret = 0;
for (int i = 0; i < n; i++) {
tot += nums[i];
if (tot > 0) ret++;
}
return ret;
}
};
统计美丽子数组数目
给你一个下标从 0 开始的整数数组nums 。每次操作中,你可以:
- 选择两个满足
0 <= i, j < nums.length的不同下标i和j。 - 选择一个非负整数
k,满足nums[i]和nums[j]在二进制下的第k位(下标编号从 0 开始)是1。 - 将
nums[i]和nums[j]都减去2k。
如果一个子数组内执行上述操作若干次后,该子数组可以变成一个全为 0 的数组,那么我们称它是一个 美丽 的子数组。
请你返回数组 nums 中 美丽子数组 的数目。
子数组是一个数组中一段连续 非空 的元素序列。
题解
这道题乍一看上去可能比较棘手,这是因为它使用了一个障眼法。
题目中说要求美丽子数组的数量,美丽子数组的定义是经过若干次题意中的操作之后能够全部变为0的数组。我们再观察一下操作的细节,每次选取两个数,然后减去同一个2的幂。如果若干次这样的操作之后能够得到全0的结果,说明了什么?说明了这个子数组中所有元素转换成二进制之后,每一个二进制位上1的总和是偶数。
如果大家对于位运算敏感的话,看到这个条件往往能够联想到异或运算。异或运算中,对于某个二进制位,相同为0,不同为1。对于这个子数组来说,由于其中每个数字转化成二进制之后的1的总和是偶数,那么这些数进行异或运算之后,得到的结果必然为0。
所以到这里,我们可以对题目进行一个变形:求所有元素异或值为0的子数组的数量。
到这里,整个问题依然没有简单太多。因为题目的数据范围很大,要统计所有子数组的数量还是比较棘手的。我们可以借助动态规划的思路来进行思考,假设以nums[i-1]为结尾的子数组中元素的异或值的集合为s,那么对于s中的所有元素,都可以通过异或nums[i](看成是策略)到达一个新的状态,这个新状态的集合记作ns。
我们当然可以使用容器来存储s和ns,以及它们中每个状态出现的次数。但实际上没有必要,因为异或具有可逆性。假设nums[0] ^ nums[1] ^ ... ^ nums[i] = x,对于结尾nums[i]来说,如果我们能找到一个位置k,使得nums[k] ^ nums[k+1] ... ^ nums[i] = 0,那么可以推导出nums[0] ^ nums[1] ... ^ nums[k-1] = x。这样一来,我们只需要知道之前异或值x出现的次数,就能知道以nums[i]为结尾的完美子数组的数量。
这样一来,我们使用一个map来记录nums[0] ^ nums[1] ^ ... ^ nums[i]的值,一边累加答案一边更新即可。
这道题还是很巧妙的,既用到了异或的性质,还用到了动态规划的思想,综合在一起推导才能得到正解。但思路都整理清楚之后,代码很简单。
class Solution {
public:
long long beautifulSubarrays(vector<int>& nums) {
long long ret = 0;
map<int, long long> mp;
int cur = 0;
for (auto x: nums) {
cur ^= x;
if (mp.count(cur)) {
ret += mp[cur];
}
mp[cur] ++;
}
return ret + mp[0];
}
};
完成所有任务的最少时间
你有一台电脑,它可以 同时 运行无数个任务。给你一个二维整数数组 tasks ,其中 tasks[i] = [starti, endi, durationi] 表示第 i 个任务需要在 闭区间 时间段 [starti, endi] 内运行 durationi 个整数时间点(但不需要连续)。
当电脑需要运行任务时,你可以打开电脑,如果空闲时,你可以将电脑关闭。
请你返回完成所有任务的情况下,电脑最少需要运行多少秒。
题解
在之前讲解贪心算法的文章当中,举过一道会议安排的例题,和本题非常相似。
很容易想到,我们可以对每个位置能够覆盖的任务数进行统计,每次选择可以覆盖最多的位置进行执行。但这样有一个问题,当多个位置覆盖的任务数相同时,我们应该怎么选呢。这个问题不解决使用贪心算法一定有反例。
那怎么解决这个问题呢?还是要回到题目的分析上。我们要执行任务i,我们假设排在i之前的任务都已经完成,此时很容易想到,我们执行的时间越晚越好。因为之前的任务已经完成了,之后的任务还没有,我们执行得越晚,和之后任务有交集的可能性也就越大。顺着这个思路,我们可以对所有的任务按照结束时间排序,优先处理结束时间早的任务,每次都将任务的执行时间尽量放在最后,这样可以保证与之后任务产生覆盖的可能性最大。
在这样的思路下,是可以保证没有反例的。虽然最后的思路以及代码都比较简单,但是在做题的时候,想要找到思路并且分析准确还是比较有难度的。从这点来说,本次的题目质量不错,很值得一做。
class Solution {
public:
int findMinimumTime(vector<vector<int>>& tasks) {
auto cmp = [](vector<int> &a, vector<int>& b) {
return a[1] < b[1];
};
// 自定义排序,按照第二个键值排序
sort(tasks.begin(), tasks.end(), cmp);
int n = tasks.size();
bool used[2010];
memset(used, 0, sizeof used);
int ret = 0;
for (int i = 0; i < n; i++) {
auto &vt = tasks[i];
int s = vt[0], e = vt[1], left = vt[2];
// 计算对于任务i还剩下多少需要完成
for (int j = s; j <= e; j++) {
if (used[j]) left--;
}
if (left > 0) {
// 从后往前,越晚执行越好
for (int j = e; j >= s; j--) {
if (used[j]) continue;
ret++;
used[j] = true;
left--;
if (left == 0) break;
}
}
}
return ret;
}
};