第232场LeetCode周赛
本次周赛地址
No 1. 仅执行一次字符串交换能否使两个字符串相等
给你长度相等的两个字符串 s1 和 s2 。一次 字符串交换 操作的步骤如下:选出某个字符串中的两个下标(不必不同),并交换这两个下标所对应的字符。
如果对 其中一个字符串 执行 最多一次字符串交换 就可以使两个字符串相等,返回 true ;否则,返回 false 。
示例 1:
输入:s1 = “bank”, s2 = “kanb”
输出:true
解释:例如,交换 s2 中的第一个和最后一个字符可以得到 “bank”
示例 2:
输入:s1 = “attack”, s2 = “defend”
输出:false
解释:一次字符串交换无法使两个字符串相等
数据范围:
1
<
=
s
1.
l
e
n
g
t
h
,
s
2.
l
e
n
g
t
h
<
=
100
s
1.
l
e
n
g
t
h
=
=
s
2.
l
e
n
g
t
h
s
1
和
s
2
仅
由
小
写
英
文
字
母
组
成
\begin{aligned} 1 <= s1.length, s2.length <= 100\\ s1.length == s2.length\\ s1 和 s2 仅由小写英文字母组成 \end{aligned}
1<=s1.length,s2.length<=100s1.length==s2.lengths1和s2仅由小写英文字母组成
解析
本题给我们两个字符串,能够对其中一个最多进行一次交换操作,也就是将该字符串两个字符交换位置,返回能否最多一次交换操作使其与另一个字符串相等。
显然,如果能够在最多一次交换使二者相等,必须满足:
- 两个字符串等长,长度要一样;
- 二者完全一致,或存在两个不同位置的字符不同,且不同的这两个位置的字符串交换一下就相同了。换言之假设二者在位置
i
,
j
i,j
i,j上不同,那么只有KaTeX parse error: Expected 'EOF', got '&' at position 14: s1[i]==s2[j] &̲& s1[j]==s2[i]才能实现交换一次相等。
所以本题思路也很简单,首先比较长度,长度一致,就逐一遍历下标,记录两个字符串不等的数量与对应的字符,再检查是否符合上述的条件,满足返回True,其他情况都是False。
C++的代码如下:
// 2021.03.14 leetcode weekly contest 232 - code
//No 1
bool areAlmostEqual(string s1, string s2) {
int countDiff = 0;
vector<char>c1, c2;
int l1 = s1.size(), l2 = s2.size();
if (l1 != l2) return false;
for(int i = 0; i < l1; ++i) {
if (s1[i] != s2[i]) {
++countDiff;
c1.push_back(s1[i]);
c2.push_back(s2[i]);
}
if (countDiff > 2) return false;
}
if (countDiff==0 || (countDiff == 2 && c1[0] == c2[1] && c1[1] == c2[0])) return true;
return false;
}
No 2. 找出星型图的中心节点
有一个无向的 星型 图,由 n 个编号从 1 到 n 的节点组成。星型图有一个 中心 节点,并且恰有 n - 1 条边将中心节点与其他每个节点连接起来。
给你一个二维整数数组 edges ,其中 edges[i] = [ui, vi] 表示在节点 ui 和 vi 之间存在一条边。请你找出并返回 edges 所表示星型图的中心节点。
示例 1:
输入:edges = [[1,2],[2,3],[4,2]]
输出:2
解释:如上图所示,节点 2 与其他每个节点都相连,所以节点 2 是中心节点。
示例 2:
输入:edges = [[1,2],[5,1],[1,3],[1,4]]
输出:1
数据范围:
3
<
=
n
<
=
1
0
5
e
d
g
e
s
.
l
e
n
g
t
h
=
=
n
−
1
e
d
g
e
s
[
i
]
.
l
e
n
g
t
h
=
=
2
1
<
=
u
i
,
v
i
<
=
n
u
i
!
=
v
i
\begin{aligned} 3 <= n <= 10^5\\ edges.length == n - 1\\ edges[i].length == 2\\ 1 <= ui, vi <= n\\ ui != vi \end{aligned}
3<=n<=105edges.length==n−1edges[i].length==21<=ui,vi<=nui!=vi
解析
本题要求我们从输入的星型图中找到中间节点。所谓星型图,是指由n个节点和n-1条边组成,其中中心节点与其它节点之间各存在一条边,除此之外节点不存在其他的边。
那么很显然,我们可以通过每个节点所连接的边数判断中心节点,除了中心节点有n-1条边,其余都只有一条。遍历所有边的集合,给每个节点记录边数,最后找到边数为n-1的就是中心节点。
C++代码如下:
//No 2
int findCenter(vector<vector<int>>& edges) {
int n = edges.size() + 1;
vector<int>deg(n+1, 0);
for (auto e : edges) {
deg[e[0]]++;
deg[e[1]]++;
}
for (int i = 1; i <= n; ++i) {
if (deg[i] > 1) return i;
}
}
No 3. 最大平均通过率
一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试。给你一个二维数组 classes ,其中 classes[i] = [passi, totali] ,表示你提前知道了第 i 个班级总共有 totali 个学生,其中只有 passi 个学生可以通过考试。
给你一个整数 extraStudents ,表示额外有 extraStudents 个聪明的学生,他们 一定 能通过任何班级的期末考。你需要给这 extraStudents 个学生每人都安排一个班级,使得 所有 班级的 平均 通过率 最大 。
一个班级的 通过率 等于这个班级通过考试的学生人数除以这个班级的总人数。平均通过率 是所有班级的通过率之和除以班级数目。
请你返回在安排这 extraStudents 个学生去对应班级后的 最大 平均通过率。与标准答案误差范围在 10-5 以内的结果都会视为正确结果。
示例 1:
输入:classes = [[1,2],[3,5],[2,2]], extraStudents = 2
输出:0.78333
解释:你可以将额外的两个学生都安排到第一个班级,平均通过率为 (3/4 + 3/5 + 2/2) / 3 = 0.78333 。
示例 2:
输入:classes = [[2,4],[3,9],[4,5],[2,10]], extraStudents = 4
输出:0.53485
数据范围:
1
<
=
c
l
a
s
s
e
s
.
l
e
n
g
t
h
<
=
1
0
5
c
l
a
s
s
e
s
[
i
]
.
l
e
n
g
t
h
=
=
2
1
<
=
p
a
s
s
i
<
=
t
o
t
a
l
i
<
=
1
0
5
1
<
=
e
x
t
r
a
S
t
u
d
e
n
t
s
<
=
1
0
5
\begin{aligned} 1 <= classes.length <= 10^5\\ classes[i].length == 2\\ 1 <= passi <= totali <= 10^5\\ 1 <= extraStudents <= 10^5 \end{aligned}
1<=classes.length<=105classes[i].length==21<=passi<=totali<=1051<=extraStudents<=105
解析
本题给了我们一系列班级能通过的考试的人数与总人数,并且有一些一定能通过的学生,让我们分配这些学生使得总的平均通过率最高。
通过率计算自然是通过的学生除以总人数:
r
a
t
i
o
[
i
]
=
p
a
s
s
[
i
]
/
t
o
t
a
l
[
i
]
ratio[i] = {pass[i]}/{total[i]}
ratio[i]=pass[i]/total[i]
将一个通过的学生分配进入班级,则通过率变为
r
a
t
i
o
N
e
w
[
i
]
=
(
p
a
s
s
[
i
]
+
1
)
/
(
t
o
t
a
l
[
i
]
+
1
)
ratioNew[i] = {(pass[i]+1)}/{(total[i]+1)}
ratioNew[i]=(pass[i]+1)/(total[i]+1)
根据相关背景知识,这个值显然大于原来的通过率。
而这些一定通过的学生数量是有限的,如何能够取得最大的平均通过率呢?这里采用贪心的策略。
一个分数分子分母都加1,值肯定大于原来的分数,但提升的量取决于原来的分子分母。
我们定义每个班级的通过率提升值为,加入一个聪明学生后通过率提升的值:
p
r
o
[
i
]
=
r
a
t
i
o
N
e
w
[
i
]
−
r
a
t
i
o
[
i
]
=
(
p
a
s
s
[
i
]
+
1
)
/
(
t
o
t
a
l
[
i
]
+
1
)
−
p
a
s
s
[
i
]
/
t
o
t
a
l
[
i
]
pro[i]=ratioNew[i]-ratio[i]= {(pass[i]+1)}/{(total[i]+1)}-{pass[i]}/{total[i]}
pro[i]=ratioNew[i]−ratio[i]=(pass[i]+1)/(total[i]+1)−pass[i]/total[i]
如果每添加一个学生,都选择当前人员配置下提升最大的那个班级进行添加,那么每个聪明学生都能最大限度的对整体通过率施加一个提升,这样总提升就最大。而初始通过率是一定的(不添加额外学生),那么最大的提升必然带来最大的最终结果。
这里我们计算每个班级初始的提升,并采用优先队列的方式获得有序的班级通过率数组,排序方式为提升大的优先。每次选取提升最大的一个班级,从优先队列弹出,然后向其中添加一个必过学生,再放入队列,直到所有的必过学生都被添加。
构造优先队列
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN),每次添加完放入队列需要
O
(
l
o
g
N
)
O(logN)
O(logN),这样不会超时,找到了每次提升最大的班级并完成添加。
为了简化操作,我们首先将初始每个班级通过人数和总人数都放入数组。优先队列存入 {提升量,班级序号下标}这样的pair,保证既能按照提升量排序,又能够查找到班级人数信息以便添加学生。
由于最终要求的是平均通过率,我们在记录每个班级初始情况时就可以先把各班级初始通过率之和求出来,每次添加学生就把提升量也加上,最后得到各班级通过率之和,再除以班级数即可。这样可以减少一次循环。
C++代码如下:
//No 3
double maxAverageRatio(vector<vector<int>>& classes, int extraStudents) {
double ans = 0.0;
int n = classes.size();
vector<int>pass(n), total(n);
for (int i = 0; i < n; ++i) {
pass[i] = classes[i][0];
total[i] = classes[i][1];
ans += (double)(pass[i]) / (double)(total[i]);
}
auto pro = [&](int id)->double {
return ((double)(pass[id] + 1) / (double)(total[id] + 1)) - ((double)(pass[id]) / (double)(total[id]));
};
priority_queue<pair<double,int>> pq;
for (int i = 0; i < n;++i) {
pq.push({ pro(i),i });
}
while (extraStudents) {
auto c = pq.top();
pq.pop();
int id = c.second;
ans += c.first;
pass[id]++;
total[id]++;
pq.push({ pro(id),id });
extraStudents--;
}
ans /= (double)n;
return ans;
}
No 4. 好子数组的最大分数
给你一个整数数组 nums (下标从 0 开始)和一个整数 k 。
一个子数组 (i, j) 的 分数 定义为 min(nums[i], nums[i+1], …, nums[j]) * (j - i + 1) 。一个 好 子数组的两个端点下标需要满足 i <= k <= j 。
请你返回 好 子数组的最大可能 分数 。
示例 1:
输入:nums = [1,4,3,7,4,5], k = 3
输出:15
解释:最优子数组的左右端点下标是 (1, 5) ,分数为 min(4,3,7,4,5) * (5-1+1) = 3 * 5 = 15 。
示例 2:
输入:nums = [5,5,4,5,4,1,1,1], k = 0
输出:20
解释:最优子数组的左右端点下标是 (0, 4) ,分数为 min(5,5,4,5,4) * (4-0+1) = 4 * 5 = 20 。
数据范围:
1
<
=
n
u
m
s
.
l
e
n
g
t
h
<
=
1
0
5
1
<
=
n
u
m
s
[
i
]
<
=
2
∗
1
0
4
0
<
=
k
<
n
u
m
s
.
l
e
n
g
t
h
\begin{aligned} 1 <= nums.length <= 10^5\\ 1 <= nums[i] <= 2 * 10^4\\ 0 <= k < nums.length \end{aligned}
1<=nums.length<=1051<=nums[i]<=2∗1040<=k<nums.length
解析
本题要求我们找到一个数组的最优子数组,满足子数组包含了下标为k的元素,且子数组最小值与其长度的乘积最大。
本题其实类似一个求矩形面积这样的问题,将数组每个元素视作一个高度等于其数值、宽度为1的矩形柱子,从中找一个矩形,高是包含元素的最小值,宽是包含元素的数量,该矩形跨过下标k且面积最大。
本题暴力的想法就是枚举每一个起始位置i和终止位置j,二者分别在k的两侧,找到之间子数组的最小值并与长度相乘,找到最大值。这时的复杂度是 O ( N 2 ) O(N^2) O(N2)。
那么进一步想,当我们以某一根柱子(某个元素)作为矩形的最小值时,它能构成的矩形宽度是有限的。例如,我们选定下标h,值为
n
u
m
s
[
h
]
nums[h]
nums[h],假设
n
u
m
s
[
i
−
1
]
<
n
u
m
s
[
h
]
,
i
−
1
<
h
nums[i-1]<nums[h],i-1<h
nums[i−1]<nums[h],i−1<h
n
u
m
s
[
j
+
1
]
>
n
u
m
s
[
h
]
,
j
+
1
>
h
nums[j+1]>nums[h],j+1>h
nums[j+1]>nums[h],j+1>h
n
u
m
s
[
h
h
]
<
n
u
m
s
[
h
]
,
i
≤
h
h
≤
j
nums[hh]<nums[h],i\leq hh\leq j
nums[hh]<nums[h],i≤hh≤j
也就是i到j之间的元素最小值就是
n
u
m
s
[
h
]
nums[h]
nums[h],那么很显然,以
n
u
m
s
[
h
]
nums[h]
nums[h]为高度的矩形,最大宽度就是
L
=
j
−
i
+
1
L=j-i+1
L=j−i+1,面积就是
S
=
L
∗
n
u
m
s
[
h
]
S=L*nums[h]
S=L∗nums[h]。
而任何起点大于i、终点小于j的子数组都无需讨论,因为他们跟上边的S比起来,高度一样,而宽度更小。如果我们能找到以每个元素值作为高度时能达到的最大宽度,就等于求出了该高度的最大面积,全局最大面积必然是其中之一。
这里采用两次单调栈的思路,分别求出一个元素(下标为h)左边第一个比他小,和右边第一个比他小的位置,该元素作为矩形高度时的宽度就有这两个位置确定,然后遍历所有元素,确认每个元素作为高度时最大矩形是否包含下标k,如果包含就计算其面积并更新最大面积,这样遍历所有元素后就找到了全局最大的面积。
C++代码如下:
//No 4
int maximumScore(vector<int>& nums, int k) {
int n = nums.size();
vector<int>left(n, -1),right(n,n);
stack<int>s;
int i = 0;
while (i < n) {
while (!s.empty()&&nums[i]<nums[s.top()]) {
s.pop();
}
if (!s.empty()) left[i] = s.top();
s.push(i);
++i;
}
i = n - 1;
while (i >= 0) {
while (!s.empty() && nums[i] < nums[s.top()]) {
s.pop();
}
if (!s.empty())right[i] = s.top();
s.push(i);
--i;
}
for (auto l : left) cout << l << " ";
cout << endl;
for (auto r : right) cout << r << " ";
cout << endl;
int ans = 0;
for (int i = 0; i < n; ++i) {
int l = left[i] + 1, r = right[i] - 1;
if (l <= k && r >= k) ans = max(ans, (nums[i] * (r - l + 1)));
}
return ans;
}
由于两次单调栈和最后一次遍历,总的复杂度为
O
(
3
N
)
O(3N)
O(3N)。
这里是将问题转化成了包含k的最大矩形面积,并枚举所有的矩形高度,通过事先计算的最大宽度,来更新最大面积,省去了非最大宽度的面积求解(这些面积不可能成为最大)。
另一个思路是将k位置考虑在内,枚举包含k位置的所有矩形宽度,在每个宽度下找到最大面积,同样可以找到全局最大。方法是类似双指针的思路,将起点和终点都置于k位置,每次宽度加一,可以选择向左或向右扩展一个位置,每次选取其中较大的方向进行扩展。这个思路的代码可参考本题相关的题解。