6200. 处理用时最长的那个任务的员工
共有 n
位员工,每位员工都有一个从 0
到 n - 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
。执行以下操作之一,直到 s
和 t
都变成空字符串:
- 删除字符串
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
的路径数量。则只需要对每个位置计算一下状态即可,每个位置的状态只能从上方或者左方转移过来,所以状态转移的时间复杂度为 ,而总共的状态数目为 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
个位置来往下走,实际就是个组合数。即,从起点走到终点,实际的路线数量为
总结
T1直接模拟;T2是前缀和与差分;T3是栈+贪心;T4是动态规划。