大家好,我是梁唐。
今天是周一,我们照惯例来聊聊昨天的LeetCode周赛。
这一场是LeetCode周赛的第320场,由诺基亚贝尔赞助。前200名的同学可以获得简历内推的机会。
这一场的比赛赛题质量还可以,但翻译上有一点缺陷,如果读的是中文的题面在理解上可能会存在一点小问题。。。
很多人吐槽了这个问题,我做题的时候也蒙了一会,错了一次之后才找到原因……
好了,废话不多说,我们来看题吧。
数组中不等三元组的数目
给你一个下标从 0 开始的正整数数组 nums 。请你找出并统计满足下述条件的三元组 (i, j, k) 的数目:
-
0 <= i < j < k < nums.length
-
nums[i]、nums[j] 和 nums[k] 两两不同 。
-
- 换句话说:nums[i] != nums[j]、nums[i] != nums[k] 且 nums[j] != nums[k] 。
返回满足上述条件三元组的数目*。*
题解
水题,范围太小,直接暴力循环搞定。
class Solution {
public:
int unequalTriplets(vector<int>& nums) {
int ret = 0;
int n = nums.size();
for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
for (int k = j+1; k < n; k++) {
if (nums[i] != nums[j] && nums[i] != nums[k] && nums[j] != nums[k]) ret++;
}
}
}
return ret;
}
};
复制代码
二叉搜索树最近节点查询
给你一个 二叉搜索树 的根节点 root ,和一个由正整数组成、长度为 n 的数组 queries 。
请你找出一个长度为 n 的 二维 答案数组 answer ,其中 answer[i] = [mini, maxi] :
- mini 是树中小于等于 queries[i] 的 最大值 。如果不存在这样的值,则使用 -1 代替。
- maxi 是树中大于等于 queries[i] 的 最小值 。如果不存在这样的值,则使用 -1 代替。
返回数组 answer 。
题解
这题想到解法不难,无非是二分搜索找到答案。但扯的是,题目给定的虽然是二叉搜索树,但不保证平衡。也就是说会存在蜕化成链表的极端case。如果直接在二叉搜索树上搜索会超时,所以只能先通过中序遍历的方式拿到所有的元素,再通过二分来找答案。
但这么一来二叉搜索树的数据结构就显得多余了,即使我们拿到元素不是按照递增或递减的顺序,也只需要排序一次即可。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 中序遍历
void dfs(vector<int> &vt, TreeNode* u) {
if (u == nullptr) return ;
dfs(vt, u->left);
vt.push_back(u->val);
dfs(vt, u->right);
}
vector<vector<int>> closestNodes(TreeNode* root, vector<int>& queries) {
vector<vector<int>> ret;
vector<int> vt;
dfs(vt, root);
for (auto q : queries) {
// lower_bound找到第一个大于等于q的位置
auto it = lower_bound(vt.begin(), vt.end(), q);
if (it == vt.end()) {
ret.push_back({vt.back(), -1});
}else if (*it == q) {
ret.push_back({q, q});
}else if (it == vt.begin()) {
ret.push_back({-1, *it});
}else {
ret.push_back({*prev(it), *it});
}
}
return ret;
}
};
复制代码
到达首都的最少油耗
给你一棵 n 个节点的树(一个无向、连通、无环图),每个节点表示一个城市,编号从 0 到 n - 1 ,且恰好有 n - 1 条路。0 是首都。给你一个二维整数数组 roads ,其中 roads[i] = [ai, bi] ,表示城市 ai 和 bi 之间有一条 双向路 。
每个城市里有一个代表,他们都要去首都参加一个会议。
每座城市里有一辆车。给你一个整数 seats 表示每辆车里面座位的数目。
城市里的代表可以选择乘坐所在城市的车,或者乘坐其他城市的车。相邻城市之间一辆车的油耗是一升汽油。
请你返回到达首都最少需要多少升汽油。
题解
首先题目给定我们树是以边的形式,我们最好将它使用邻接表或者其他数据结构进行转化,这样方便我们进行递归。
其次,经过简单分析,可以发现距离根节点也就是节点0最远的节点,也就是叶子节点,这些节点只有一种解法,就是乘坐本节点的车去往更上层的节点。我们继续分析,对于叶子节点上一层的节点而言,它们的代表就有两种方式,一种是搭便车,另外一种是单独再开一辆车继续往上。
我们要使得总体耗油量越少,显然开出的车数量越少,消耗的油就越少。这里有一个坑点,题目中没有明确说可以在节点处换乘,比如有10个人乘坐10辆车来了当前节点,而一辆车就能装下10个人,那么就合乘一辆车出发,这样可以省油。
这里我使用了一个pair
来记录当前节点开出往根节点进发的车的数量和乘客的数量,之后只需要递归即可。
class Solution {
public:
typedef pair<long long, long long> pll;
pll dfs(vector<vector<int>>& graph, int u, int f, int seats, long long &ret) {
int cars = 0, persons = 1;
for (auto v: graph[u]) {
if (v == f) continue;
auto cur = dfs(graph, v, u, seats, ret);
cars += cur.first;
persons += cur.second;
// 从子节点开来车的数量就是耗油的数量
ret += cur.first;
}
// 换乘,尽可能少得开车
cars = (persons + seats - 1) / seats;
return pll(cars, persons);
}
long long minimumFuelCost(vector<vector<int>>& roads, int seats) {
int n = roads.size();
// 邻接表建树
vector<vector<int>> graph(n+1, vector<int>());
for (auto &vt: roads) {
int u = vt[0], v = vt[1];
graph[u].push_back(v);
graph[v].push_back(u);
}
long long ret = 0;
dfs(graph, 0, -1, seats, ret);
return ret;
}
};
复制代码
完美分割的方案数
给你一个字符串 s ,每个字符是数字 '1' 到 '9' ,再给你两个整数 k 和 minLength 。
如果对 s 的分割满足以下条件,那么我们认为它是一个 完美 分割:
- s 被分成 k 段互不相交的子字符串。
- 每个子字符串长度都 至少 为 minLength 。
- 每个子字符串的第一个字符都是一个 质数 数字,最后一个字符都是一个 非质数 数字。质数数字为 '2' ,'3' ,'5' 和 '7' ,剩下的都是非质数数字。
请你返回 s 的 完美 分割数目。由于答案可能很大,请返回答案对 109 + 7 取余 后的结果。
一个 子字符串 是字符串中一段连续字符串序列。
题解
很显然,这是一道动态规划问题。
我们使用dp[i][k]
表示以下标i结尾(下标从1开始),将字符串分割成k段,满足题意的解法的数量。我们要求dp[i][k]
就需要先找到一个更早的状态dp[j][k-1]
,并且满足字符串s[j+1:i]
满足题意。这样dp[i][k] += dp[j][k-1]
。我们遍历所有可能的j,就能求出答案。
理解了状态转移方程之后,整个代码非常简单,是这样的:
class Solution {
public:
int beautifulPartitions(string s, int k, int minLength) {
long long Mod = 1e9 + 7;
int n = s.length();
vector<vector<long long>> dp(n+2, vector<long long>(k+2, 0));
dp[0][0] = 1;
set<char> prime {'2', '3', '5', '7'};
// 字符串向右移动一格,将下标0表示空串
s = ' ' + s;
if (!prime.count(s[1])) return 0;
// 字符串长度需要大于等于minLength,所以i从minLength开始
for (int i = minLength; i <= n; i++) {
// s[i]不能是质数
if (prime.count(s[i])) continue;
// 遍历所有可能的j,保证s[j:i]长度大于等于minLength
for (int j = 1; j <= i-minLength+1; j++) {
// s[j] 不能是合数
if (!prime.count(s[j])) continue;
for (int _k = 1; _k <= k; _k++) {
dp[i][_k] = (dp[i][_k] + dp[j-1][_k-1]) % Mod;
}
}
}
return dp[n][k];
}
};
复制代码
但这样并不能通过,因为复杂度高达,会超时。所以我们需要对这个计算过程进行优化,怎么优化呢?
观察一下代码,可以发现我们遍历j
的时候,它的范围和i
的范围是挂钩的。我们可以使用前缀和来进行优化,用pres[i][k]
来记录j
从1遍历到i
对应的dp[j][k]
的和。因为j
的遍历范围和i
是对应的,我们只需要在每一轮循环结束之后累加更新pres
即可。
可以比较上下两段代码,感受一下前缀和的妙用。
class Solution {
public:
int beautifulPartitions(string s, int k, int minLength) {
long long Mod = 1e9 + 7;
int n = s.length();
vector<vector<long long>> dp(n+2, vector<long long>(k+2, 0)), pres(n+2, vector<long long>(k+2, 0));
dp[0][0] = 1;
pres[0][0] = 1;
set<char> prime {'2', '3', '5', '7'};
s = ' ' + s;
if (!prime.count(s[1])) return 0;
for (int i = 1; i <= n; i++) {
// s[i]要是合数,且s[i+1]必须是质数
if (i >= minLength && !prime.count(s[i]) && (i == n || prime.count(s[i+1]))) {
// 前缀和即是答案
for (int _k = 1; _k <= k; _k++) {
dp[i][_k] = pres[i-minLength][_k-1];
}
}
// 更新前缀和准备下一轮遍历
for (int _k = 0; _k <= k; _k++) {
pres[i][_k] = (pres[i-1][_k] + dp[i][_k]) % Mod;
}
}
return dp[n][k];
}
};
复制代码
我个人感觉这一场的题目质量还是不错的,尤其是最后两题,涉及的解法和思路之前很少出现。非常适合大家练习。