大家好,我是梁唐。
今天是周一,照惯例,我们来一起看一下昨天的LeetCode周赛。昨天是LeetCode第308场,由地平线赞助。
这次的赛题比上周水了很多,评论区里说自己第一次ak的就有好几个。所以对于新手来说,这是一个很好的练习的机会。
闲言少叙,我们来看题吧。
和有限的最长子序列
给你一个长度为 n
的整数数组 nums
,和一个长度为 m
的整数数组 queries
。
返回一个长度为 m
的数组 answer
,其中 answer[i]
是 nums
中 元素之和小于等于 queries[i]
的 子序列 的 最大 长度 。
子序列 是由一个数组删除某些元素(也可以不删除)但不改变剩余元素顺序得到的一个数组。
题解
这题作为第一题估计很多人看到会蒙,一下子没有思路。
实际上这题用到的一个技巧在之前的题目当中出现过,就是关于子序列和的问题。虽然子序列中的元素是固定的,但是子序列的元素选择是不受限的。当我们确定了子序列中元素个数之后,和最小的子序列就是确定的,即最小的元素之和。
在本题当中我们要小于某个和的子序列长度最大,也就是说要找到同等长度和最小的子序列。同等长度和最小的子序列当然是排序之后从小到大的元素组合。所以我们只需要将nums
数组排序,长度为1的就是最小的元素,长度为2的序列之和就是最小的两个元素之和,以此类推。
这样,我们只用一次排序之后就可以得到1到n之间所有长度的最小序列和。由于nums
中的所有元素都大于0,所以对应的各个长度的序列和也是递增的。这样我们就可以使用二分法来快速查找了。
class Solution {
public:
vector<int> answerQueries(vector<int>& nums, vector<int>& queries) {
int n = nums.size();
int m = queries.size();
vector<long long> sums(n+2, 0x3f3f3f3f3f3f3f);
sums[0] = 0;
sort(nums.begin(), nums.end());
// 求出任意长度的序列和
for (int i = 0; i < n; i++) {
sums[i+1] = sums[i] + nums[i];
}
vector<int> ret;
for (int i = 0; i < m; i++) {
// 二分法查找大于等于queries[i]的下标
int cur = lower_bound(sums.begin(), sums.begin() + n + 1, queries[i]) - sums.begin();
// 如果序列和大于queries[i],那么长度-1
ret.push_back(cur-(sums[cur] > queries[i]));
}
return ret;
}
};
复制代码
从字符串中移除星号
给你一个包含若干星号 *
的字符串 s
。
在一步操作中,你可以:
- 选中
s
中的一个星号。 - 移除星号 左侧 最近的那个 非星号 字符,并移除该星号自身。
返回移除 所有 星号之后的字符串**。**
注意:
- 生成的输入保证总是可以执行题面中描述的操作。
- 可以证明结果字符串是唯一的。
题解
模拟题,我们可以把字符串当做是栈来操作。当遇到*
时弹出栈顶字符。
class Solution {
public:
string removeStars(string s) {
string ret = "";
for (auto c : s) {
if (c == '*') {
ret.pop_back();
}else ret.push_back(c);
}
return ret;
}
};
复制代码
收集垃圾的最少总时间
给你一个下标从 0 开始的字符串数组 garbage
,其中 garbage[i]
表示第 i
个房子的垃圾集合。garbage[i]
只包含字符 'M'
,'P'
和 'G'
,但可能包含多个相同字符,每个字符分别表示一单位的金属、纸和玻璃。垃圾车收拾 一 单位的任何一种垃圾都需要花费 1
分钟。
同时给你一个下标从 0 开始的整数数组 travel
,其中 travel[i]
是垃圾车从房子 i
行驶到房子 i + 1
需要的分钟数。
城市里总共有三辆垃圾车,分别收拾三种垃圾。每辆垃圾车都从房子 0
出发,按顺序 到达每一栋房子。但它们 不是必须 到达所有的房子。
任何时刻只有 一辆 垃圾车处在使用状态。当一辆垃圾车在行驶或者收拾垃圾的时候,另外两辆车 不能 做任何事情。
请你返回收拾完所有垃圾需要花费的 最少 总分钟数。
题解
同样是模拟题,要处理移动的时间,也要考虑处理垃圾的时间,叠加起来显得有些麻烦。
实际上我们冷静分析会发现,不论是什么品种的垃圾车,移动同样距离的耗时是不变的。所以我们完全可以把垃圾处理和垃圾车移动分开考虑,首先求出垃圾车需要移动的最远距离。再对每一堆垃圾堆进行单独处理即可,因为所有垃圾的处理时间也一样,都是1个单位时间。
因为数据量级不大,基本怎么操作都可以,不用担心超时。
class Solution {
public:
int garbageCollection(vector<string>& gbg, vector<int>& tra) {
int ret = 0;
int n = gbg.size();
vector<int> tot(n+1, 0);
// 前缀和维护移动到坐标i的耗时
for (int i = 0; i < n-1; i++) {
tot[i+1] = tot[i] + tra[i];
}
int last_g = 0, last_p = 0, last_m = 0;
for (int i = 0; i < n; i++) {
auto &gbr = gbg[i];
// 维护每种垃圾车需要移动的最远距离
for (auto c: gbr) {
if (c == 'G') last_g = i;
if (c == 'M') last_m = i;
if (c == 'P') last_p = i;
}
// 直接加上当前这堆垃圾的数量
ret += gbr.length();
}
ret += (tot[last_g] + tot[last_m] + tot[last_p]);
return ret;
}
};
复制代码
给定条件下构造矩阵
给你一个 正 整数 k
,同时给你:
- 一个大小为
n
的二维整数数组rowConditions
,其中rowConditions[i] = [abovei, belowi]
和 - 一个大小为
m
的二维整数数组colConditions
,其中colConditions[i] = [lefti, righti]
。
两个数组里的整数都是 1
到 k
之间的数字。
你需要构造一个 k x k
的矩阵,1
到 k
每个数字需要 恰好出现一次 。剩余的数字都是 0
。
矩阵还需要满足以下条件:
- 对于所有
0
到n - 1
之间的下标i
,数字abovei
所在的 行 必须在数字belowi
所在行的上面。 - 对于所有
0
到m - 1
之间的下标i
,数字lefti
所在的 列 必须在数字righti
所在列的左边。
返回满足上述要求的 任意 矩阵。如果不存在答案,返回一个空的矩阵。
题解
拓扑排序的裸题,我们要在行和列两个方面保证满足限制条件。所以我们要在行和列两个维度做两次拓扑排序,由于我们只需要返回一个合法结果,所以并不用穷举所有的情况。
拓扑排序算法是用来解决依赖叠加问题的算法,当有若干个任务互相依赖时,我们需要找到一个执行顺序,使得我们可以保证所有任务都能被完成。如果若干个任务之间存在互相之间的循环依赖,拓扑排序算法也可以判断出这种情况。
具体的原理也比较简单,通过有向图来判断。如果a任务依赖b任务,可以抽象成从b连出一条指向a的边。同时,我们维护每个点的入度。可以知道,入度为0的点即没有任何依赖的点,即当前可以完成的任务。我们将它完成之后,将以它为初始点的边删除,并且将指向点的入度减一。如果指向的点入度减一之后为0,说明该点也没有了依赖。
我们可以使用一个队列来维护所有当前可以执行的任务,队列弹出的顺序即为一个合法的拓扑排序的序列。如果队列为空时依然存在一些边没有被删除, 则说明图中存在环,即互相循环依赖的任务。对应本题当中,如果有环,说明无解。
class Solution {
public:
vector<vector<int>> buildMatrix(int k, vector<vector<int>>& rowC, vector<vector<int>>& colC) {
// 拓扑排序
auto top = [&](vector<vector<int>> &data) -> vector<int> {
int n = data.size();
map<int, int> ind;
// 使用邻接表建图
vector<vector<int>> graph(k+1, vector<int>());
for (auto &vt : data) {
int u = vt[0], v = vt[1];
ind[v]++;
graph[u].push_back(v);
}
vector<int> ret;
queue<int> que;
// 将入度为0的点放入队列
for (int i = 1; i <= k; i++) {
if (ind.count(i) == 0) {
que.push(i);
}
}
while (!que.empty()) {
int u = que.front();
ret.push_back(u);
que.pop();
// 删除从u连出的所有边,并且将目标点v的入度-1
for (auto v: graph[u]) {
ind[v]--;
// 如果v入度为0,说明v也没有了依赖,加入队列
if (ind[v] == 0) {
que.push(v);
}
// 边的数量-1
n--;
}
}
// 如果还存在边没有被删除,说明有环,无解
if (n > 0) ret.clear();
return ret;
};
auto ret1 = top(rowC);
auto ret2 = top(colC);
if (ret1.size() == 0 || ret2.size() == 0) return vector<vector<int>>();
vector<vector<int>> ret(k, vector<int>(k, 0));
// 组装答案
map<int, int> row_map, col_map;
for (int i = 0; i < k; i++) {
row_map[ret1[i]] = i;
col_map[ret2[i]] = i;
}
for (int i = 1; i <= k; i++) {
int r = row_map[i], c = col_map[i];
ret[r][c] = i;
}
return ret;
}
};
复制代码
因为昨天比赛的时候我在高铁上,所以没能实时参与,我是赛后进行的模拟赛。这场的题都是一次通过,比我预想的还要简单一些。最后一题的拓扑排序可能相对比较冷门,但它在日常工作场景当中的出现频率却不低,很多地方都能用得上。所以当做一个学习和练手的机会也不错。