大家好,我是梁唐。
今天是周一,我们照惯例来看看昨天的LeetCode周赛。
昨天是LeetCode周赛第322场,由阿里巴巴质量创新大会赞助。前5名的同学可以获得精美的小礼品……
总体上来说这一场比赛的题目质量不错,比之前手速场要好不少,有一定难度,比较锻炼人。
回环句
句子 是由单个空格分隔的一组单词,且不含前导或尾随空格。
- 例如,
"Hello World"
、"HELLO"
、"hello world hello world"
都是符合要求的句子。
单词 仅 由大写和小写英文字母组成。且大写和小写字母会视作不同字符。
如果句子满足下述全部条件,则认为它是一个 回环句 :
- 单词的最后一个字符和下一个单词的第一个字符相等。
- 最后一个单词的最后一个字符和第一个单词的第一个字符相等。
例如,"leetcode exercises sound delightful"
、"eetcode"
、"leetcode eats soul"
都是回环句。然而,"Leetcode is cool"
、"happy Leetcode"
、"Leetcode"
和 "I like Leetcode"
都 不 是回环句。
给你一个字符串 sentence
,请你判断它是不是一个回环句。如果是,返回 true
;否则,返回 false
。
题解
水题,为了操作字符串方便,这里使用了Python。
class Solution:
def isCircularSentence(self, sentence: str) -> bool:
words = sentence.split(' ')
n = len(words)
for i in range(n):
nxt = (i+1) % n
if words[i][-1] != words[nxt][0]:
return False
return True
复制代码
划分技能点相等的团队
给你一个正整数数组 skill
,数组长度为 偶数 n
,其中 skill[i]
表示第 i
个玩家的技能点。将所有玩家分成 n / 2
个 2
人团队,使每一个团队的技能点之和 相等 。
团队的 化学反应 等于团队中玩家的技能点 乘积 。
返回所有团队的 化学反应 之和,如果无法使每个团队的技能点之和相等,则返回 -1
。
题解
题目要求所有队伍技能点数之和相等,所以我们可以事先算出满足条件时每一对队伍的技能点数之和,接着维护每个数字出现的次数。
如果出现不能整除,或者是不能组到n/2
组队伍时,说明无解,返回-1,否则累加求和。
class Solution {
public:
long long dividePlayers(vector<int>& skill) {
long long tot = 0;
int n = skill.size();
map<int, int> mp;
// 统计每个数字出现的次数
for (auto x: skill) {
tot += x;
mp[x] += 1;
}
n /= 2;
// 如果不能整除,说明无解
if (tot % n > 0) return -1;
tot /= n;
long long ret = 0;
for (auto x: skill) {
// 如果当前元素已经在之前组成队了,跳过
if (mp[x] == 0) continue;
// 如果符合能组队的元素存在,则组队
if (mp.count(tot - x) && mp[tot-x] > 0) {
mp[x] -= 1;
mp[tot-x] -= 1;
ret += (long long)x * (tot - x);
// 否则无解
}else return -1;
}
return ret;
}
};
复制代码
两个城市间路径的最小分数
给你一个正整数 n
,表示总共有 n
个城市,城市从 1
到 n
编号。给你一个二维数组 roads
,其中 roads[i] = [ai, bi, distancei]
表示城市 ai
和 bi
之间有一条 双向 道路,道路距离为 distancei
。城市构成的图不一定是连通的。
两个城市之间一条路径的 分数 定义为这条路径中道路的 最小 距离。
城市 1
和城市 n
之间的所有路径的 最小 分数。
注意:
- 一条路径指的是两个城市之间的道路序列。
- 一条路径可以 多次 包含同一条道路,你也可以沿着路径多次到达城市
1
和城市n
。 - 测试数据保证城市
1
和城市n
之间 至少 有一条路径。
题解
最短路问题的变种,题目已经给定了起点和终点,求中间出现的最短边。
我们用dis[i]
维护从1出发到达i
点经过的最短边。初始化时将dis
数组全部设为无穷大,使用队列来维护所有可能更新其他点的点。初始时队列中只有1,接着我们从1出发,遍历所有与1点相连的点进行松弛操作。假设当前遍历的点是u
,u->v
的边小于dis[v]
。也就是说u->v
这条边能够更新dis[v]
,连带着就有可能更新所有与v
相连的点,所以我们将v
压入队列。
当不存在可以松弛的可能性时,遍历结束,返回dis[n]
即是答案。本质上就是最短路问题,连代码都几乎完全一样。
class Solution {
public:
int minScore(int n, vector<vector<int>>& roads) {
typedef pair<int, int> pii;
vector<vector<pii>> graph(n+2, vector<pii>());
// 使用连接表建图
for (auto vt: roads) {
int u = vt[0], v = vt[1], l = vt[2];
graph[u].push_back(make_pair(v, l));
graph[v].push_back(make_pair(u, l));
}
vector<int> dis(n+2, 0x3f3f3f3f);
queue<int> que;
que.push(1);
// 使用bfs进行松弛操作,spfa算法的核心
while (!que.empty()) {
int u = que.front();
que.pop();
for (auto vt: graph[u]) {
int v = vt.first, l = vt.second;
if (min(dis[u], l) < dis[v]) {
dis[v] = min(dis[u], l);
que.push(v);
}
}
}
return dis[n];
}
};
复制代码
将节点分成尽可能多的组
给你一个正整数 n
,表示一个 无向 图中的节点数目,节点编号从 1
到 n
。
同时给你一个二维整数数组 edges
,其中 edges[i] = [ai, bi]
表示节点 ai
和 bi
之间有一条 双向 边。注意给定的图可能是不连通的。
请你将图划分为 m
个组(编号从 1 开始),满足以下要求:
- 图中每个节点都只属于一个组。
- 图中每条边连接的两个点
[ai, bi]
,如果ai
属于编号为x
的组,bi
属于编号为y
的组,那么|y - x| = 1
。
请你返回最多可以将节点分为多少个组(也就是最大的 m
)。如果没办法在给定条件下分组,请你返回 -1
。
题解
首先, 题目当中说了图不一定连通,也就是说这n个点可能组成若干个连通分量,很容易想到,由于题目要求分组的数量尽量大,我们可以把各个连通分量平铺摆放,这样的分组数量最大,此时的答案为各个连通分量分组数之和。
考虑一个连通分量时,我们可以转换思路,假设有一个外力施加在连通分量的某一个点上,将整个图拎起来。图拎起来之后,我们令这个被拎起来的点是u
,我们把其余各点到u
的距离看做是深度。那么很容易想到,深度相同的点应该属于同一个分组。分组数即到u
距离最远的点和u
的距离+1。
从这个结论出发,又可以想到,选择不同的点拎起来会得到不同的结果,并且可能会出现违反题意的情况,我们需要加以判断。
由于点的数量很少,最多只有500,我们可以枚举连通分量中每个点都被拎起来的情况,从中选择符合题意且最大的作为最终结果。和上一题一样,我们可以利用最短路算法来求连通分量中每个点到u
的距离。维护连通分量中的所有点,以及所有边,来判断是否符合题意。
更多细节可以查看代码。
class Solution {
public:
int magnificentSets(int n, vector<vector<int>>& edges) {
vector<int> graph[n+1];
// 邻接表建图
for (auto &vt : edges) {
int u = vt[0], v = vt[1];
graph[u].push_back(v);
graph[v].push_back(u);
}
// 存储每个连通分量的结果
map<int, int> mp;
auto query = [&](int u) {
queue<int> que;
que.push(u);
// dis[i]为i到u的最短距离
vector<int> dis(n+1, 0x3f3f3f3f);
dis[u] = 0;
// tmp存储连通分量中的最大距离
int tmp = 0;
// cur维护连通分量中序号最小的点,方便维护连通分量中的最大值
int cur = u;
// 存储连通分量中的所有节点
set<int> comp;
comp.insert(u);
while (!que.empty()) {
int u = que.front(); que.pop();
comp.insert(u);
cur = min(cur, u);
for (auto v: graph[u]) {
if (dis[v] > dis[u] + 1) {
dis[v] = dis[u] + 1;
tmp = max(tmp, dis[v]);
que.push(v);
}
}
}
// 初始化mp[cur]=-1,即无解
if (!mp.count(cur)) mp[cur] = -1;
// 枚举所有边
for (auto u: comp) {
for (auto v : graph[u]) {
// 如果相连的两点序号不是相差了1,说明不成立
if (abs(dis[u] - dis[v]) != 1) return ;
}
}
// 如果成立,记录tmp + 1
mp[cur] = max(mp[cur], tmp + 1);
};
int ret = 0;
for (int i = 1; i <= n; i++) {
query(i);
}
for (auto it : mp) {
if (it.second < 0) return -1;
ret += it.second;
}
return ret;
}
};
复制代码
这一场涉及到了一些图论的知识,对于图论不太熟悉的小伙伴做起来可能会比较吃力,尤其是最后一题,要想明白题目中的所有隐藏条件,并且理清思路写出代码还是挺难的。
不过正是因为困难,所以才有成就感,绞尽脑汁之后的AC无疑是最好的回报。