LeetCode周赛318,高质量思维场

大家好,我是梁唐。

今天是周一,照惯例给大家带来昨天上午的LeetCode周赛题解。

昨天进行的是LeetCode第318场,由空中云汇Airwallex赞助。排名前300的同学可以获得简历内推的机会。

评论区对于本场比赛的评价不错,我个人做下来也觉得题目的质量高了不少,少了很多没意义的题,感受到了题目中对思维能力的考验。

闲言少叙,下面让我们来看题吧。

对数组执行操作

给你一个下标从 0 开始的数组 nums ,数组大小为 n ,且由 非负 整数组成。

你需要对数组执行 n - 1 步操作,其中第 i 步操作(从 0 开始计数)要求对 nums 中第 i 个元素执行下述指令:

  • 如果 nums[i] == nums[i + 1] ,则 nums[i] 的值变成原来的 2 倍,nums[i + 1] 的值变成 0 。否则,跳过这步操作。

在执行完 全部 操作后,将所有 0 移动 到数组的 末尾

  • 例如,数组 [1,0,2,0,0,1] 将所有 0 移动到末尾后变为 [1,2,1,0,0,0] 。

返回结果数组。

注意 操作应当 依次有序 执行,而不是一次性全部执行。

题解

水题,模拟即可。

class Solution {
public:
    vector<int> applyOperations(vector<int>& nums) {
        vector<int> ret;
        int n = nums.size();
        for (int i = 0; i < n; i++) {
            if (i < n-1 && nums[i] == nums[i+1]) {
                nums[i] *= 2;
                nums[i+1] = 0;
            }
            if (nums[i] != 0) ret.push_back(nums[i]);
        }
        
        while (ret.size() < n) ret.push_back(0);
        return ret;
    }
};
复制代码

长度为 K 子数组中的最大和

给你一个整数数组 nums 和一个整数 k 。请你从 nums 中满足下述条件的全部子数组中找出最大子数组和:

  • 子数组的长度是 k,且
  • 子数组中的所有元素 各不相同 。

返回满足题面要求的最大子数组和。如果不存在子数组满足这些条件,返回 0 。

子数组 是数组中一段连续非空的元素序列。

题解

two pointers算法的变题。

容易想到我们可以使用set维护一个区间内的不同元素,再使用tmp维护set中所有元素的和。当set中不同元素达到k个时,使用tmp更新答案。

当当前元素已经在set中出现或者元素个数已经达到k时弹出set中最左侧的元素。

class Solution {
public:
    long long maximumSubarraySum(vector<int>& nums, int k) {
        set<int> st;
        // tmp维护set中的元素和
        long long ret = 0, tmp = 0;
        int l = 0;
        for (auto x: nums) {
            // 当x在set中,或者set中元素已经达到k时,弹出nums[l]
            while (st.count(x) || st.size() == k) {
                st.erase(nums[l]);
                tmp -= nums[l++];
            }
            // 插入x
            st.insert(x);
            tmp += x;
            if (st.size() == k) {
                ret = max(ret, tmp);
            }
        }
        return ret;
    }
};
复制代码

雇佣 K 位工人的总代价

给你一个下标从 0 开始的整数数组 costs ,其中 costs[i] 是雇佣第 i 位工人的代价。

同时给你两个整数 k 和 candidates 。我们想根据以下规则恰好雇佣 k 位工人:

  • 总共进行 k 轮雇佣,且每一轮恰好雇佣一位工人。

  • 在每一轮雇佣中,从最前面 candidates 和最后面 candidates 人中选出代价最小的一位工人,如果有多位代价相同且最小的工人,选择下标更小的一位工人。

    • 比方说,costs = [3,2,7,7,1,2] 且 candidates = 2 ,第一轮雇佣中,我们选择第 4 位工人,因为他的代价最小 [3,2,7,7,1*,2*] 。
    • 第二轮雇佣,我们选择第 1 位工人,因为他们的代价与第 4 位工人一样都是最小代价,而且下标更小,[*3,****2***,7,7,2] 。注意每一轮雇佣后,剩余工人的下标可能会发生变化。
  • 如果剩余员工数目不足 candidates 人,那么下一轮雇佣他们中代价最小的一人,如果有多位代价相同且最小的工人,选择下标更小的一位工人。

  • 一位工人只能被选择一次。

返回雇佣恰好 k 位工人的总代价。

题解

显然,数据范围很大,暴力行不通。

分析题意之后可以发现,本质上就是在有增删的前提下,动态维护前candidate以及后candidate个元素的最小值。动态维护最值很容易想到优先队列。

所以本题当中我们创建两个优先队列,一个维护前candidate个元素一个维护后candidate个元素。但题目不保证前后不会有重叠,所以我们要手动控制,避免重叠。另外,当出现多个最值相等时需要根据下标判断,所以我们需要把下标也作为优先队列排序的key。

C++中的优先队列默认将大值靠前,所以我们可以维护(-costs[i], i)的pair对。这样可以保证队列顶端的元素最小且下标最小。

维护好了之后,我们查询K次,每次查询之后需要弹出队列顶端元素,并且插入一个新值,方便下次查询。

本题的难度在于对优先队列的理解和掌握。

class Solution {
public:
    long long totalCost(vector<int>& costs, int k, int cd) {
        typedef pair<int, int> pii;
        priority_queue<pii> lef, rig;
        // 维护nums[: cd]
        for (int i = 0; i < cd; i++) {
            lef.push(make_pair(-costs[i], -i));
        }
        // 维护nums[-cd:]
        int l = cd-1, r = max((int) costs.size() - cd, cd);
        for (int i = r; i < costs.size(); i++) {
            rig.push(make_pair(-costs[i], -i));
        }
        long long ret = 0;
        const long long inf = 0x3f3f3f3f3f3f;
        for (int i = 0; i < k; i++) {
            // 防止队列为空
            auto la = make_pair(-inf, -inf), ra = make_pair(-inf, -inf);
            if (!lef.empty()) la = lef.top();
            if (!rig.empty()) ra = rig.top();
            if (la >= ra) {
                ret -= la.first;
                // 注意,先弹出再插入
                lef.pop();
                if (l+1 < r) {
                    lef.push(make_pair(-costs[++l], -l));
                }
            }else {
                ret -= ra.first;
                rig.pop();
                if (l+1 < r) {
                    rig.push(make_pair(-costs[--r], -r));
                }
            }
        }
        return ret;
    }
};
复制代码

最小移动总距离

X 轴上有一些机器人和工厂。给你一个整数数组 robot ,其中 robot[i] 是第 i 个机器人的位置。再给你一个二维整数数组 factory ,其中 factory[j] = [positionj, limitj] ,表示第 j 个工厂的位置在 positionj ,且第 j 个工厂最多可以修理 limitj 个机器人。

每个机器人所在的位置 互不相同 。每个工厂所在的位置也 互不相同 。注意一个机器人可能一开始跟一个工厂在 相同的位置

所有机器人一开始都是坏的,他们会沿着设定的方向一直移动。设定的方向要么是 X 轴的正方向,要么是 X 轴的负方向。当一个机器人经过一个没达到上限的工厂时,这个工厂会维修这个机器人,且机器人停止移动。

任何时刻,你都可以设置 部分 机器人的移动方向。你的目标是最小化所有机器人总的移动距离。

请你返回所有机器人移动的最小总距离。测试数据保证所有机器人都可以被维修。

注意:

  • 所有机器人移动速度相同。
  • 如果两个机器人移动方向相同,它们永远不会碰撞。
  • 如果两个机器人迎面相遇,它们也不会碰撞,它们彼此之间会擦肩而过。
  • 如果一个机器人经过了一个已经达到上限的工厂,机器人会当作工厂不存在,继续移动。
  • 机器人从位置 x 到位置 y 的移动距离为 |y - x| 。

题解

本题刚拿到手毫无头绪,这种时候千万不要慌,一般这种情况往往是有一些隐藏的信息需要我们通过分析和推断获得,只要找到这些信息,就能找到解题的线索。

本题看起来机器人的活动很灵活,运动的中途还能改变方向,实际上分析一下就会知道,机器人改变方向只会增加运动路程,想要移动总距离最小,一定不能中途改变方向。其次是机器人起始位置和终点之间存在直接影响,我们很容易判断,假设有两个机器人A和B,两个工厂X和Y。

A在B的左侧,X在Y的左侧,那么一定不会存在比A去X,B去Y更优的情况。把这个结论推广可以得到什么呢?

得到本题满足动态规划无后效性的要求,即下标较小的机器人去了下标较小的工厂修理之后不会对之后的机器人选择产生影响。

我们用dp[i][j]记录下标robot[:i]的机器人占据了factory[:j]的工厂之后的修理最优结果。显然对于所有的jdp[0][j]=0

对于dp[i][j]状态而言,它有两种转移策略,第一从dp[i][j-1]转移而来,即第j座工厂闲置。第二,最后k个机器人进入工厂j修理,即从dp[i-k][j-1]转移而来。对于这种情况我们需要枚举所有可能的k。根据题意可以得知,k最小是1,最大是工厂的容量,即factory[j][1]

那么整理一下以上思路,动态规划的代码就出炉了。

class Solution {
public:
    long long minimumTotalDistance(vector<int>& robot, vector<vector<int>>& factory) {
        int n = robot.size();
        int m = factory.size();
        // 对机器人和工厂按照下标排序
        sort(robot.begin(), robot.end());
        sort(factory.begin(), factory.end());
        
        vector<vector<long long>> dp(n+1, vector<long long>(m+1, 0x3f3f3f3f3f3f3f3f));

        // dp[0][j] = 0,不需要修理时,消耗为0
        for (int i = 0; i <= m; i++) dp[0][i] = 0;
        
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                long long cost = 0;
                // 工厂j闲置的情况
                dp[i][j] = min(dp[i][j], dp[i][j-1]);
                // 枚举k
                for (int k = 1; k <= min(i, factory[j-1][1]); k++) {
                    // 更新消耗
                    cost += abs(factory[j-1][0] - robot[i-k]);
                    dp[i][j] = min(dp[i][j], dp[i-k][j-1] + cost);
                }
            }
        }
        return dp[n][m];
    }
};
复制代码

日拱一卒,大家一起加油吧。