这篇文章小结一下271场周赛的题目和解析。这场比赛在这里
No.1 环和杆
总计有 n 个环,环的颜色可以是红、绿、蓝中的一种。这些环分布穿在 10 根编号为 0 到 9 的杆上。
给你一个长度为 2n 的字符串 rings ,表示这 n 个环在杆上的分布。rings 中每两个字符形成一个 颜色位置对 ,用于描述每个环:
第 i 对中的 第一个 字符表示第 i 个环的 颜色(‘R’、‘G’、‘B’)。
第 i 对中的 第二个 字符表示第 i 个环的 位置,也就是位于哪根杆上(‘0’ 到 ‘9’)。
例如,“R3G2B1” 表示:共有 n == 3 个环,红色的环在编号为 3 的杆上,绿色的环在编号为 2 的杆上,蓝色的环在编号为 1 的杆上。
找出所有集齐 全部三种颜色 环的杆,并返回这种杆的数量。
示例 1:
输入:rings = “B0B6G0R6R0R6G9”
输出:1
解释:
- 编号 0 的杆上有 3 个环,集齐全部颜色:红、绿、蓝。
- 编号 6 的杆上有 3 个环,但只有红、蓝两种颜色。
- 编号 9 的杆上只有 1 个绿色环。
因此,集齐全部三种颜色环的杆的数目为 1 。
提示:
r
i
n
g
s
.
l
e
n
g
t
h
=
=
2
∗
n
rings.length == 2 * n
rings.length==2∗n
1
<
=
n
<
=
100
1 <= n <= 100
1<=n<=100
如
i
是
偶
数
,
则
r
i
n
g
s
[
i
]
的
值
可
以
取
′
R
′
、
′
G
′
或
′
B
′
(
下
标
从
0
开
始
计
数
)
如 i 是 偶数 ,则 rings[i] 的值可以取 'R'、'G' 或 'B'(下标从 0 开始计数)
如i是偶数,则rings[i]的值可以取′R′、′G′或′B′(下标从0开始计数)
如
i
是
奇
数
,
则
r
i
n
g
s
[
i
]
的
值
可
以
取
′
0
′
到
′
9
′
中
的
一
个
数
字
(
下
标
从
0
开
始
计
数
)
如 i 是 奇数 ,则 rings[i] 的值可以取 '0' 到 '9' 中的一个数字(下标从 0 开始计数)
如i是奇数,则rings[i]的值可以取′0′到′9′中的一个数字(下标从0开始计数)
解析
本题输入一个字符串,字符串从下标0开始每两个连续字符构成一组,表示哪一根杆上有什么颜色的环。让我们返回有多少杆包含全部颜色的环,返回这个数量。
杆的编号是’0’-'9’的字符,可见杆的数量只有10个。而环的颜色只有RGB三种。所以我们可以建一个
10
∗
3
10*3
10∗3的数组rec,
r
e
c
[
i
]
[
j
]
rec[i][j]
rec[i][j]表示标号为i的杆上是否有颜色为j的环。数组首先初始化为全0,遍历输入的字符串,按照其规则得到杆和环颜色的编号,将rec数组对应位置设为1,表示这根杆存在对应的颜色。最后遍历数组,记录多少杆上三种颜色都有即可。
C++代码如下:
//No 1
int countPoints(string rings) {
vector<vector<int>>rec(10, vector<int>(3, 0));
unordered_map<char, int>color{ {'R',0},{'G',1},{'B',2} };
int n = rings.size() / 2;
for (int i = 0; i < n; ++i) {
int c = color[rings[2 * i]];
int p = rings[2 * i + 1] - '0';
rec[p][c] = 1;
}
int count = 0;
for (int i = 0; i < rec.size(); ++i) {
if (rec[i][0] == 1 && rec[i][1] == 1 && rec[i][2] == 1)++count;
}
return count;
}
No.2 子数组范围和
给你一个整数数组 nums 。nums 中,子数组的 范围 是子数组中最大元素和最小元素的差值。
返回 nums 中 所有 子数组范围的 和 。
子数组是数组中一个连续 非空 的元素序列。
示例 1:
输入:nums = [1,2,3]
输出:4
解释:nums 的 6 个子数组如下所示:
[1],范围 = 最大 - 最小 = 1 - 1 = 0
[2],范围 = 2 - 2 = 0
[3],范围 = 3 - 3 = 0
[1,2],范围 = 2 - 1 = 1
[2,3],范围 = 3 - 2 = 1
[1,2,3],范围 = 3 - 1 = 2
所有范围的和是 0 + 0 + 0 + 1 + 1 + 2 = 4
提示:
1
<
=
n
u
m
s
.
l
e
n
g
t
h
<
=
1000
1 <= nums.length <= 1000
1<=nums.length<=1000
−
1
0
9
<
=
n
u
m
s
[
i
]
<
=
1
0
9
-10^9 <= nums[i] <= 10^9
−109<=nums[i]<=109
解析
本题首先定了一个数组的范围,即数组最大元素减去最小元素的差值。现在输入一个数组,求他所有的子数组的范围之和。也就是对于这个数组所有的子数组,求最大最小值之差,然后都加起来返回。
暴力解决这个问题,需要通过 O ( N 2 ) O(N^2) O(N2)找到所有的子数组,再从中找到最大最小值,时间复杂度为 O ( N 3 ) O(N^3) O(N3)。但是显然,找到子数组的过程中就可以直接确定最大最小值。
每个子数组都有一个起始位置和结束位置。我们用第一层循环遍历所有起始位置,第二层循环遍历终止位置。首先起始和终止都是一个位置,当前的最大最小值也就是这个位置的值。终止位置向后推进一个位置,就等于引入一个新元素,用新元素的值对比当前的最大最小值,就能更新最大最小值,从而计算出范围。
这样我们就通过保存记录的最大最小值,在
O
(
N
)
O(N)
O(N)时间内计算了以任意一个位置开始的所有子数组范围和,而第一层循环遍历了所有的起始位置,那么就实现了
O
(
N
2
)
O(N^2)
O(N2)遍历所有子数组并计算范围求和,满足题意不会超时。
C++代码如下:
在这里插入代码片
No.3 给植物浇水 II
Alice 和 Bob 打算给花园里的 n 株植物浇水。植物排成一行,从左到右进行标记,编号从 0 到 n - 1 。其中,第 i 株植物的位置是 x = i 。
每一株植物都需要浇特定量的水。Alice 和 Bob 每人有一个水罐,最初是满的 。他们按下面描述的方式完成浇水:
Alice 按 从左到右 的顺序给植物浇水,从植物 0 开始。Bob 按 从右到左 的顺序给植物浇水,从植物 n - 1 开始。他们 同时 给植物浇水。
如果没有足够的水 完全 浇灌下一株植物,他 / 她会立即重新灌满浇水罐。
不管植物需要多少水,浇水所耗费的时间都是一样的。
不能 提前重新灌满水罐。
每株植物都可以由 Alice 或者 Bob 来浇水。
如果 Alice 和 Bob 到达同一株植物,那么当前水罐中水更多的人会给这株植物浇水。如果他俩水量相同,那么 Alice 会给这株植物浇水。
给你一个下标从 0 开始的整数数组 plants ,数组由 n 个整数组成。其中,plants[i] 为第 i 株植物需要的水量。另有两个整数 capacityA 和 capacityB 分别表示 Alice 和 Bob 水罐的容量。返回两人浇灌所有植物过程中重新灌满水罐的 次数 。
示例 1:
输入:plants = [2,2,3,3], capacityA = 5, capacityB = 5
输出:1
解释:
- 最初,Alice 和 Bob 的水罐中各有 5 单元水。
- Alice 给植物 0 浇水,Bob 给植物 3 浇水。
- Alice 和 Bob 现在分别剩下 3 单元和 2 单元水。
- Alice 有足够的水给植物 1 ,所以她直接浇水。Bob 的水不够给植物 2 ,所以他先重新装满水,再浇水。
所以,两人浇灌所有植物过程中重新灌满水罐的次数 = 0 + 0 + 1 + 0 = 1 。
提示:
n
=
=
p
l
a
n
t
s
.
l
e
n
g
t
h
n == plants.length
n==plants.length
1
<
=
n
<
=
1
0
5
1 <= n <= 10^5
1<=n<=105
1
<
=
p
l
a
n
t
s
[
i
]
<
=
1
0
6
1 <= plants[i] <= 10^6
1<=plants[i]<=106
m
a
x
(
p
l
a
n
t
s
[
i
]
)
<
=
c
a
p
a
c
i
t
y
A
,
c
a
p
a
c
i
t
y
B
<
=
1
0
9
max(plants[i]) <= capacityA, capacityB <= 10^9
max(plants[i])<=capacityA,capacityB<=109
解析
本题给定一个数组,表示一排植物,数组记录了每个植物需要浇水的量,两个人拿证两个水壶分别从左右,即0和n-1位置向中间依次浇水,浇水这一操作就是用当前水壶剩余减去当前位置的植物需求量,剩余水不足时需要补满。当所有植物都浇完水后,求两个人总的补水操作数量。
按照题设,两个人浇水速度是一致的,补水不需要时间。因此等价于求两个人分别从左右向中间移动,每次同时移动一个位置,直到相遇,这是一个双指针的问题。
我们用
i
a
,
i
b
ia,ib
ia,ib指示两人所处位置,初始分别位于0和n-1,
c
a
,
c
b
ca,cb
ca,cb表示当前水量,
c
o
u
n
t
count
count记录补水次数。显然需要讨论的范围是
(
i
a
<
=
i
b
)
(ia<=ib)
(ia<=ib),二者相同时表示相遇位置需要浇水。在相遇之前,只需要ab分别判断水是否够用,不够的补充,并对计数器加1,然后减去当前位置植物的需求更新水量,同时移动指针。
二者相遇时,选取其中较大的水量,判断是否够用,不够对计数器加一。相遇处浇完水表示全都交过了,因此无需维护是谁浇的水和浇完后的水量。
C++代码如下:
//No 3
int minimumRefill(vector<int>& plants, int capacityA, int capacityB) {
int n = plants.size();
int ia = 0, ib = n - 1,count=0,ca= capacityA,cb= capacityB;
while (ia <= ib) {
if (ia < ib) {
if (ca < plants[ia]) {
ca = capacityA;
++count;
}
ca -= plants[ia];
++ia;
if (cb < plants[ib]) {
cb = capacityB;
++count;
}
cb -= plants[ib];
--ib;
}
else {
int cm = max(ca, cb);
if (cm < plants[ia]) {
++count;
}
++ia;
--ib;
}
}
return count;
}
No.4 摘水果
在一个无限的 x 坐标轴上,有许多水果分布在其中某些位置。给你一个二维整数数组 fruits ,其中 fruits[i] = [positioni, amounti] 表示共有 amounti 个水果放置在 positioni 上。fruits 已经按 positioni 升序排列 ,每个 positioni 互不相同 。
另给你两个整数 startPos 和 k 。最初,你位于 startPos 。从任何位置,你可以选择 向左或者向右 走。在 x 轴上每移动 一个单位 ,就记作 一步 。你总共可以走 最多 k 步。你每达到一个位置,都会摘掉全部的水果,水果也将从该位置消失(不会再生)。
返回你可以摘到水果的 最大总数 。
示例 1:
输入:fruits = [[2,8],[6,3],[8,6]], startPos = 5, k = 4
输出:9
解释:
最佳路线为:
- 向右移动到位置 6 ,摘到 3 个水果
- 向右移动到位置 8 ,摘到 6 个水果
移动 3 步,共摘到 3 + 6 = 9 个水果
示例 2:
输入:fruits = [[0,9],[4,1],[5,7],[6,2],[7,4],[10,9]], startPos = 5, k = 4
输出:14
解释:
可以移动最多 k = 4 步,所以无法到达位置 0 和位置 10 。
最佳路线为:
- 在初始位置 5 ,摘到 7 个水果
- 向左移动到位置 4 ,摘到 1 个水果
- 向右移动到位置 6 ,摘到 2 个水果
- 向右移动到位置 7 ,摘到 4 个水果
移动 1 + 3 = 4 步,共摘到 7 + 1 + 2 + 4 = 14 个水果
提示:
1
<
=
f
r
u
i
t
s
.
l
e
n
g
t
h
<
=
105
1 <= fruits.length <= 105
1<=fruits.length<=105
f
r
u
i
t
s
[
i
]
.
l
e
n
g
t
h
=
=
2
fruits[i].length == 2
fruits[i].length==2
0
<
=
s
t
a
r
t
P
o
s
,
p
o
s
i
t
i
o
n
i
<
=
2
∗
1
0
5
0 <= startPos, positioni <= 2 * 10^5
0<=startPos,positioni<=2∗105
对
于
任
意
i
>
0
,
p
o
s
i
t
i
o
n
i
−
1
<
p
o
s
i
t
i
o
n
i
均
成
立
(
下
标
从
0
开
始
计
数
)
对于任意 i > 0 ,positioni-1 < positioni 均成立(下标从 0 开始计数)
对于任意i>0,positioni−1<positioni均成立(下标从0开始计数)
1
<
=
a
m
o
u
n
t
i
<
=
1
0
4
1 <= amounti <= 10^4
1<=amounti<=104
0
<
=
k
<
=
2
∗
1
0
5
0 <= k <= 2 * 10^5
0<=k<=2∗105
解析
本题给了一些列数轴上存放水果的位置与数量信息,预计初始位置与总步数,要求我们计算从初始位置出发、最多走总步数k,能获得的最多水果数量。
显然在一维数轴上走,我们只有沿某一方向走以及掉头这两种操作。
如果调头次数为0,也即一直沿某个方向走下去,例如从S走到A,可以获得[S,A]范围内所有的水果;
如果调头次数为1,例如从S出发先向左走到A,然后掉头走到S右边的B,可以获得[A,B]范围内所有水果,但是S-A的路程走了两次。这样仍然可能是最优结果,例如题目的示例2.原因是向右B点右侧下一个水果点距离要比S-A的2倍还大,即使不掉头一直向右也无法到达,不如消耗一些步数拿到左边的水果。
但是,当调头次数超过1,例如从S到左边的A,再到右边的B,再到左边的C,这一段流程。这样做实际上不如直接从S到B再到C,同样吃到了[C,B]之间所有的水果,还节省了A-B直接的路程,能够走的更远。
换言之, 调 头 次 数 > 1 调头次数>1 调头次数>1获得的最大水果,不可能优于 调 头 次 数 < = 1 调头次数<=1 调头次数<=1的情况。任何反复横跳,都相当于先走到最左边再掉头走到最右边(或先右后左),而调头次数少反而能扩大最左和最右的范围。而 调 头 次 数 = 1 调头次数=1 调头次数=1可能取得比 调 头 次 数 = 0 调头次数=0 调头次数=0更优的结果。
因此本题的最优解只有4种情况:
一路向左(右)不回头一直走,取得其中所有水果;
先向左(或右)走p的距离,再回头走(k-p)的距离;
也就是调头次数为0或1,首次前进方向为左或右的4种情况。
由于本题水果位置限制是 0 < = s t a r t P o s , p o s i t i o n i < = 2 ∗ 1 0 5 0 <= startPos, positioni <= 2 * 10^5 0<=startPos,positioni<=2∗105,所以可以构建一个数组,记录从位置0(含)开始到位置i(含)即 [ 0 , i ] [0,i] [0,i]范围内的水果总数,这一过程 O ( N ) O(N) O(N)可以完成,实现后,通过查询累积水果表,再相减可以在 O ( 1 ) O(1) O(1)复杂度内计算位置范围内任意两点之间 [ i , j ] [i,j] [i,j]水果总数,这样我们就可以根据走过的最左边和最右边坐标,直接算出获得的水果数量。
这里要注意,题目说数轴是无限长的,但显然,超过水果存放坐标范围以外的区域虽然可以一直走下去,但没有意义,因为水果数量不会改变,所以我们使用有限长的数组是可行的。
另外,为了方便计算,我们加入-1位置累积水果存量(值为0),方便计算闭区间水果总数。
F
[
i
,
j
]
=
F
[
0
,
j
]
−
F
[
0
,
i
−
1
]
F[i,j]=F[0,j]-F[0,i-1]
F[i,j]=F[0,j]−F[0,i−1]被减数是左端点前一个位置。
这样就会导致数组下标比真实位置多1.对于真实位置
l
e
f
t
,
r
i
g
h
t
left, right
left,right,对应的累计水果应当是
F
[
l
e
f
t
+
1
]
,
F
[
r
i
g
h
t
+
1
]
F[left+1],F[right+1]
F[left+1],F[right+1],而计算
[
l
e
f
t
,
r
i
g
h
t
]
[left, right]
[left,right]的水果累计数量,需要对左端点减一,即
F
[
r
i
g
h
t
+
1
]
−
F
[
l
e
f
t
+
1
−
1
]
=
F
[
r
i
g
h
t
+
1
]
−
F
[
l
e
f
t
]
F[right+1]-F[left+1-1]=F[right+1]-F[left]
F[right+1]−F[left+1−1]=F[right+1]−F[left]
明确了这一计算过程,我们首先对水果累计数组初始化,思路很直接,如果一个位置没有存放水果,其累计数量与前一个位置相同;否则等于前一个位置的数量加上这个位置存放的数量。
之后我们分别对上述的4中可能产生最优解的情况做计算,计算方式就是找出每种情况的最左边界和最右边界。这里要注意,理论上题目规定数轴左右边界应当是
±
∞
\pm \infty
±∞,实际上由于水果位置限制,小于0和超过最大位置
m
a
x
P
maxP
maxP并没有意义,因为后面没有水果了,完全可以截止在0和
m
a
x
P
maxP
maxP,我们的累计水果数量表也只要这个范围以内就可以。
所以左边界截止在0,右边界截止在
m
a
x
P
maxP
maxP。
对于上述四种情况:
一路向左:
l
e
f
t
=
m
a
x
(
0
,
s
t
a
r
t
−
k
)
,
r
i
g
h
t
=
s
t
a
r
t
left=max(0,start-k), right=start
left=max(0,start−k),right=start
一路向右:
l
e
f
t
=
s
t
a
r
t
,
r
i
g
h
t
=
m
i
n
(
m
a
x
P
,
s
t
a
r
t
+
k
)
left=start, right=min(maxP, start+k)
left=start,right=min(maxP,start+k)
向左p后折返:
l
e
f
t
=
m
a
x
(
0
,
s
t
a
r
t
−
p
)
,
r
i
g
h
t
=
m
i
n
(
m
a
x
P
,
s
t
a
r
t
+
k
−
2
p
)
left=max(0,start-p), right=min(maxP, start+k-2p)
left=max(0,start−p),right=min(maxP,start+k−2p)
向右p后折返:
l
e
f
t
=
m
a
x
(
0
,
s
t
a
r
t
−
k
+
2
p
)
,
r
i
g
h
t
=
m
i
n
(
m
a
x
P
,
s
t
a
r
t
+
k
)
left=max(0,start-k+2p), right=min(maxP, start+k)
left=max(0,start−k+2p),right=min(maxP,start+k)
其中折返的情况p代表向一个方向走的距离,这个值为0时退化到向相反方向走到头,而最大值取
k
/
2
k/2
k/2即可,超过
k
/
2
k/2
k/2就走不会起始位置了,完全被该方向一直走这种情况所涵盖。
因此我们先计算一直沿一个方向走的两种情况,只需计算左右边界,查表相减即可,时间复杂度
O
(
1
)
O(1)
O(1);
然后遍历p从0到
k
/
2
k/2
k/2,计算一次调头的结果,遍历全部的p,复杂度
O
(
N
)
O(N)
O(N)。所有情况中最大的即为所求,且根据数据范围不会超时。
C++代码如下:
//No 4
int maxTotalFruits(vector<vector<int>>& fruits, int startPos, int k) {
int maxP = 2e5+10;
long long ans = 0;
vector<long long>vp(maxP + 1, 0);
unordered_map<int, int>fm;
for (auto f : fruits) {
fm[f[0]] = f[1];
}
for (int i = 1; i <= maxP; ++i) {
long long cur = 0;
if (fm.count(i - 1)) cur = fm[i - 1];
vp[i] = vp[i - 1] + cur;
}
int left = max(0, startPos - k);
ans = max(ans, vp[startPos + 1] - vp[left]);
int right = min(maxP-1,startPos + k);
ans = max(ans, vp[right + 1] - vp[startPos]);
for (int p = 0; p < k / 2; ++p) {
left = max(0, startPos - p);
right= min(maxP - 1, startPos + k-2*p);
ans = max(ans, vp[right + 1] - vp[left]);
left = max(0, startPos - k+2*p);
right = min(maxP - 1, startPos + p);
ans = max(ans, vp[right + 1] - vp[left]);
}
return ans;
}
我这里其实先把数组形式的位置与水果数量信息转化成了哈希表。但由于这个数组是排好序的,完全可以通过添加一个位置记录的指示来实现累计水果数量表的生成,可以省一些空间。