LeetCode周赛336,评论区:两题选手,woshishabi……

356 阅读3分钟

大家好,我是老梁。

今天是周一,我们照惯例来聊聊昨天的LeetCode周赛。

这一场是LeetCode周赛第336场,由LeetCode官方自己赞助自己举办,提供电脑包以及扑克等奖品。

截取了几条比较欢乐的评论:

统计范围内的元音字符串数

给你一个下标从 0 开始的字符串数组 words 和两个整数:leftright

如果字符串以元音字母开头并以元音字母结尾,那么该字符串就是一个 元音字符串 ,其中元音字母是 '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 重新排列后下标从 0i 的元素之和。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 的不同下标 ij
  • 选择一个非负整数 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

我们当然可以使用容器来存储sns,以及它们中每个状态出现的次数。但实际上没有必要,因为异或具有可逆性。假设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;
    }
};