LeetCode 314 周赛

25 阅读4分钟

6200. 处理用时最长的那个任务的员工

共有 n 位员工,每位员工都有一个从 0n - 1 的唯一 id 。

给你一个二维整数数组 logs ,其中 logs[i] = [idi, leaveTimei]

  • idi 是处理第 i 个任务的员工的 id ,且
  • leaveTimei 是员工完成第 i 个任务的时刻。所有 leaveTimei 的值都是 唯一 的。

注意,第 i 个任务在第 (i - 1) 个任务结束后立即开始,且第 0 个任务从时刻 0 开始。

返回处理用时最长的那个任务的员工的 id 。如果存在两个或多个员工同时满足,则返回几人中 最小 的 id 。

示例

输入:n = 10, logs = [[0,3],[2,5],[0,9],[1,15]]
输出:1
解释:
任务 0 于时刻 0 开始,且在时刻 3 结束,共计 3 个单位时间。
任务 1 于时刻 3 开始,且在时刻 5 结束,共计 2 个单位时间。
任务 2 于时刻 5 开始,且在时刻 9 结束,共计 4 个单位时间。
任务 3 于时刻 9 开始,且在时刻 15 结束,共计 6 个单位时间。
时间最长的任务是任务 3 ,而 id 为 1 的员工是处理此任务的员工,所以返回 1

思路

简单模拟即可。遍历一次并维护最大的间隔即可。

C++:

class Solution {
public:
    int hardestWorker(int n, vector<vector<int>>& logs) {
        int ans = logs[0][0], t = logs[0][1];
        for (int i = 1; i < logs.size(); i++) {
            int gap = logs[i][1] - logs[i - 1][1];
            if (gap > t) {
                ans = logs[i][0];
                t = gap;
            } else if (gap == t) {
                ans = min(ans, logs[i][0]);
            }
        }
        return ans;
    }
};

Java:

class Solution {
    public int hardestWorker(int n, int[][] logs) {
        int ans = logs[0][0], t = logs[0][1];
        for (int i = 1; i < logs.length; i++) {
            int g = logs[i][1] - logs[i - 1][1];
            if (g > t) ans = logs[i][0];
            else if (g == t) ans = Math.min(ans, logs[i][0]);
            t = Math.max(t, g);
        }
        return ans;
    }
}

6201. 找出前缀异或的原始数组

给你一个长度为 n整数 数组 pref 。找出并返回满足下述条件且长度为 n 的数组 arr

  • pref[i] = arr[0] ^ arr[1] ^ ... ^ arr[i].

注意 ^ 表示 按位异或(bitwise-xor)运算。

可以证明答案是 唯一 的。

示例

输入:pref = [5,2,0,3,1]
输出:[5,7,2,3,2]
解释:从数组 [5,7,2,3,2] 可以得到如下结果:
- pref[0] = 5
- pref[1] = 5 ^ 7 = 2
- pref[2] = 5 ^ 7 ^ 2 = 0
- pref[3] = 5 ^ 7 ^ 2 ^ 3 = 3
- pref[4] = 5 ^ 7 ^ 2 ^ 3 ^ 2 = 1

思路

前缀和差分。这道题是给出前缀和数组,求解差分数组。

只不过将前缀和的和运算,变成了异或运算

由于异或运算满足交换律和结合律。并且相同的数做异或,结果为0。根据这些特性,容易得知arr[i] = pref[i] ^ pref[i - 1],因为

pref[i] = arr[0] ^ arr[1] ^ ... ^ arr[i]

pref[i - 1] = arr[0] ^ arr[1] ^ ... ^ arr[i - 1]

pref[i]pref[i - 1]做异或,相同的部分可以抵消掉,最后只剩下一个arr[i]

C++:

class Solution {
public:
    vector<int> findArray(vector<int>& pref) {
        int n = pref.size();
        vector<int> ans(n);
        int t = 0; // 0和任何数异或等于其本身
        for (int i = 0; i < n; i++) {
            ans[i] = t ^ pref[i];
            t = t ^ ans[i];
        }
        return ans;
    }
};

class Solution {
public:
    vector<int> findArray(vector<int>& pref) {
        int n = pref.size();
        vector<int> ans(n);
        ans[0] = pref[0];
        for (int i = 1; i < n; i++) ans[i] = pref[i] ^ pref[i - 1];
        return ans;
    }
};

Java:

class Solution {
    public int[] findArray(int[] pref) {
        int n = pref.length;
        int[] ans = new int[n];
        ans[0] = pref[0];
        for (int i = 1; i < n; i++) ans[i] = pref[i] ^ pref[i - 1];
        return ans;
    }
}

6202. 使用机器人打印字典序最小的字符串

给你一个字符串 s 和一个机器人,机器人当前有一个空字符串 t 。执行以下操作之一,直到 st 都变成空字符串:

  • 删除字符串 s第一个 字符,并将该字符给机器人。机器人把这个字符添加到 t 的尾部。
  • 删除字符串 t最后一个 字符,并将该字符给机器人。机器人将该字符写到纸上。

请你返回纸上能写出的字典序最小的字符串。

示例

输入:s = "zza"
输出:"azz"
解释:用 p 表示写出来的字符串。
一开始,p="" ,s="zza" ,t="" 。
执行第一个操作三次,得到 p="" ,s="" ,t="zza" 。
执行第二个操作三次,得到 p="azz" ,s="" ,t=""

思路

实际就是给定一个字符串,再给定一个栈。从左往右遍历这个字符串,每次可以将当前字符压栈,或者可以将栈中的字符弹出。问借助这个栈,可以变换得到的最小字典序的字符串是什么。

这是一道贪心的题目。我们借助几个用例数据来观察一下规律。

我们从左往右遍历字符串时,若当前遍历到第i个位置,假设s[i] = 'z',而位置i后面有比字符z更小的字符,那么毫无疑问,s[i]应该直接压栈。我们要字典序最小,那么要尽可能把小的字符先弹出。那么得到第一条规律。

  • 若当前位置i的右侧,还存在比当前位置的字符,更小的字符,则当前位置的字符应该直接压栈(一直压栈到遇到这个最小的字符,再将这个最小的字符弹出,即能保证这个最小的字符尽可能的放在了前面)
  • 否则,说明当前位置右侧的全部字符都比当前字符大,则当前字符应该排在右侧全部字符的前面。此时要看一下栈顶字符,若栈顶字符更小,则需要先弹出栈顶字符。

那么,我们做一下预处理,对每个位置,计算出这个位置右侧最小的字符的下标。然后我们再用上述的贪心策略从左到右遍历字符串做一次模拟。

写出第一份代码如下

class Solution {
public:
    string robotWithString(string s) {
        int n = s.size();
        vector<int> r(n); // 右侧有没有比它更小的字符, 存储右侧最小的字符的下标
        int minimal = n - 1;
        for (int i = n - 1; i >= 0; i--) {
            if (s[i] < s[minimal]) minimal = i;
            r[i] = minimal;
        }
        stack<char> stk;
        string ans = "";
        for (int i = 0; i < s.size(); i++) {
            if (r[i] != i) {
                // 右侧有更小的, 直接将当前位置压栈
                stk.push(s[i]);
            } else {
                // 右侧没有更小的了, 看栈顶, 若栈顶更小, 则弹出栈顶
                while (!stk.empty() && stk.top() < s[i]) ans += stk.top(), stk.pop();
                stk.push(s[i]);
            }
        }
        while (!stk.empty()) ans += stk.top(), stk.pop();
        return ans;
    }
};

提交后WA,错误样例是:bydizfve,预期结果是bdevfziy,实际输出是evfzidyb

来看一下这个样例有什么问题。

我们先把预处理得到的r数组输出一下,得到0 2 2 7 7 7 7 7 。验证一下没有问题,第一个字符b右侧没有比其更小的字符,故r[0] = 0;第二个字符y,右侧最小的字符是d,故r[1] = 2;第三个字符d右侧没有更小的字符,r[2] = 2,后面所有字符右侧最小的字符都是最后一个字符e

模拟一下处理流程。

  • i = 0,有r[i] = i,右侧没有更小的字符了。直接压栈。栈的状态是:b (左侧为栈底,右侧为栈顶)

  • i = 1,有r[i] != i,右侧有更小的字符,直接压栈。栈的状态是:by

  • ....

在第二步 i = 1 时就错了。此时不应该直接将y压栈,而需要先看一下y右侧的最小的字符,看它是不是比当前栈顶更小。若它比当前栈顶更小,那么压栈就没有问题;若它比当前栈顶更大,则先要弹出栈顶。而y右侧的最小字符是d,比当前栈顶的b更大,所以理应先弹出栈顶,再进行压栈。

所以,我们修改一下逻辑,在r[i] != i时,还需要判断当前栈顶的字符,和当前位置右侧最小的字符,之间的大小关系。

写出第二版代码如下

class Solution {
public:
    string robotWithString(string s) {
        int n = s.size();
        vector<int> r(n); // 右侧有没有比它更小的
        int minimal = n - 1;
        for (int i = n - 1; i >= 0; i--) {
            if (s[i] < s[minimal]) minimal = i;
            r[i] = minimal;
        }
        stack<char> stk;
        string ans = "";
        for (int i = 0; i < s.size(); i++) {
            // 当 r[i] != i 时, 先判断下栈顶的字符是否更小, 注意判断时要用 <=, 而不能用 <
            // 在 = 时, 先弹出, 一定能保证最小
            // 因为当前栈顶已经是右侧中最小的字符了, 右侧的字符只可能比它大, 那么比它大的字符可能会先于它弹出, 造成字典序更大
            // 如果在 = 时 不弹出栈顶, 举个样例 abcdfa, 那么2个a就不会是相邻的了
            while (r[i] != i && !stk.empty() && stk.top() <= s[r[i]]) {
                ans += stk.top();
                stk.pop();
            }
            
            // 此时对于 r[i] != i的, 可以安全的进行压栈
            if (r[i] != i) {
                // 右侧有更小的, 直接push
                stk.push(s[i]);
            } else {
                // 右侧没有更小的了
                while (!stk.empty() && stk.top() < s[i]) ans += stk.top(), stk.pop();
                stk.push(s[i]);
            }
        }
        while (!stk.empty()) ans += stk.top(), stk.pop();
        return ans;
    }
};

提交后再次WA,错误的样例是mmuqezwmomeplrtskz,预期结果是eekstrlpmomwzqummz,实际输出是eekstrlpmomwzzqumm

最后的5位有问题,预期是qummz,而我的输出是zqumm

经过debug后,发现还是细节问题。

r[i] == i 时,我们用了一个while循环,在栈顶字符小于当前字符时,对栈顶进行了弹出。此时也应当用<=,而不是<,原因跟上面一样,不再赘述。

class Solution {
public:
    string robotWithString(string s) {
        int n = s.size();
        vector<int> r(n); // 右侧有没有比它更小的
        int minimal = n - 1;
        for (int i = n - 1; i >= 0; i--) {
            if (s[i] < s[minimal]) minimal = i;
            r[i] = minimal;
        }
        stack<char> stk;
        string ans = "";
        for (int i = 0; i < s.size(); i++) {
            // 当 r[i] != i 时, 先判断下栈顶的字符是否更小, 注意判断时要用 <=, 而不能用 <
            // 在 = 时, 先弹出, 一定能保证最小
            // 因为当前栈顶已经是右侧中最小的字符了, 右侧的字符只可能比它大, 那么比它大的字符可能会先于它弹出, 造成字典序更大
            // 如果在 = 时 不弹出栈顶, 举个样例 abcdfa, 那么2个a就不会是相邻的了
            while (r[i] != i && !stk.empty() && stk.top() <= s[r[i]]) {
                ans += stk.top();
                stk.pop();
            }
            
            // 此时对于 r[i] != i的, 可以安全的进行压栈
            if (r[i] != i) {
                // 右侧有更小的, 直接push
                stk.push(s[i]);
            } else {
                // 右侧没有更小的了
                // 这里也要用 <=
                while (!stk.empty() && stk.top() <= s[i]) ans += stk.top(), stk.pop();
                stk.push(s[i]);
            }
        }
        while (!stk.empty()) ans += stk.top(), stk.pop();
        return ans;
    }
};

两发WA后提交终于通过了。

Java:

class Solution {
    public String robotWithString(String s) {
        int n = s.length();
        int[] r = new int[n];
        int min = n - 1;
        for (int i = n - 1; i >= 0; i--) {
            if (s.charAt(i) < s.charAt(min)) min = i;
            r[i] = min;
        }

        StringBuilder sb = new StringBuilder();
        Deque<Character> stk = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            if (r[i] != i) {
                // 当栈非空, 且栈顶比s[r[i]] 更小, 则弹出
                while (!stk.isEmpty() && stk.peek() <= s.charAt(r[i])) {
                    sb.append(stk.pop());
                }
                stk.push(s.charAt(i));
            } else {
                while (!stk.isEmpty() && stk.peek() <= s.charAt(i)) {
                    sb.append(stk.pop());
                }
                stk.push(s.charAt(i));
            }
        }
        while (!stk.isEmpty()) sb.append(stk.pop());
        return sb.toString();
    }
}

6203. 矩阵中和能被 K 整除的路径

给你一个下标从 0 开始的 m x n 整数矩阵 grid 和一个整数 k 。你从起点 (0, 0) 出发,每一步只能往 或者往 ,你想要到达终点 (m - 1, n - 1)

请你返回路径和能被 k 整除的路径数目,由于答案可能很大,返回答案对 10^9 + 7 取余 的结果。

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 5 * 10^4
  • 1 <= m * n <= 5 * 10^4
  • 0 <= grid[i][j] <= 100
  • 1 <= k <= 50

示例

输入:grid = [[5,2,4],[3,0,5],[0,7,2]], k = 3
输出:2
解释:有两条路径满足路径上元素的和能被 k 整除。
第一条路径为上图中用红色标注的路径,和为 5 + 2 + 4 + 5 + 2 = 18 ,能被 3 整除。
第二条路径为上图中用蓝色标注的路径,和为 5 + 3 + 0 + 5 + 2 = 15 ,能被 3 整除。

思路

这道题有点类似动态规划的经典题目,数字三角形

我们可以用f(i, j, x)表示从起点到达(i,j)这个点的所有路径中,路径和除以k的余数为x的路径数量。则只需要对每个位置计算一下状态即可,每个位置的状态只能从上方或者左方转移过来,所以状态转移的时间复杂度为 O(1)O(1),而总共的状态数目为 n * m * k,级别在 50 * 5 * 10^4 = 2.5 * 10^6,在 10^6 级别,不会超时。所以我们可以直接用动态规划来做。

C++:

class Solution {
public:
    int numberOfPaths(vector<vector<int>>& g, int k) {
        int m = g.size(), n = g[0].size();
        int MOD = 1e9 + 7;
        // 定义一个三维数组 f[i][j][x]
        vector<vector<vector<int>>> f(m + 1, vector<vector<int>>(n + 1, vector<int>(k)));
        // 起点的状态, 方案数为1
        f[1][1][g[0][0] % k] = 1;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (i == 1 && j == 1) continue; // 起点跳过
                for (int t = 0; t < k; t++) {
                    int r = (t + g[i - 1][j - 1]) % k;
                    f[i][j][r] = (f[i - 1][j][t] + f[i][j - 1][t]) % MOD;
                }
            }
        }
        return f[m][n][0];
    }
};

Java:

class Solution {
    public int numberOfPaths(int[][] g, int k) {
        int m = g.length, n = g[0].length, MOD = 1_000_000_000 + 7;
        int[][][] f = new int[m + 1][n + 1][k];
        f[1][1][g[0][0] % k] = 1;

        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (i == 1 && j == 1) continue;
                for (int t = 0; t < k; t++) {
                    int r = (t + g[i - 1][j - 1]) % k;
                    f[i][j][r] = (f[i - 1][j][t] + f[i][j - 1][t]) % MOD;
                }
            }
        }

        return f[m][n][0];
    }
}

另:这道题,用动态规划做,思维上是比较简单,由于数据范围比较小,也不会超时。

我们观察到,从起点走到终点。一共需要走m + n - 2步。总共需要往右走n - 1步,往下走m - 1步。那么实际上我们就是要决定,在这m + n - 2步中,什么时候往右走,什么时候往下走。其实就相当于有m + n - 2个位置,我们从中挑选出m - 1个位置来往下走,实际就是个组合数。即,从起点走到终点,实际的路线数量为 Cm+n2n1C_{m + n - 2}^{n-1}

总结

T1直接模拟;T2是前缀和与差分;T3是栈+贪心;T4是动态规划。