【CodeForces】Educational Round 101 (for div#2) 复盘
这是我在CodeForces上打的第四场比赛,也是我第一次想尝试复盘一下这场比赛的题目。之前都是做了就让它过去了,结果就是每次比赛只做出两道题,没有啥提升。
我只是个菜鸟,当时花了1小时就做了前两题外加第三题的审题,第三题看了10多分钟后才看懂,然后就没继续往下做了…
A. Regular Bracket Sequence
1. 问题描述
这题目乍一看像是我们熟悉的有效括号匹配问题,但事实上没有那么"简单",但也没有那么复杂(这句话好像可以形容很多CodeForces上的题目)。根据题目描述,我们简单提取一下问题:
给定具有如下约束的字符串,看看这个字符串是否有可能合法,是则输出"YES",否则输出"NO"。
-
字符串中字符只由
(
、)
和?
构成,其中?
可以表示(
或)
-
字符串中有且仅有一对括号,这意味着字符串中其余部分都是
?
或者为空串
2. 思路
理解题意后,似乎很好办了,我们就按照样例给的字符串找规律,总结如下:
-
如果字符个数为奇数,则不可能获得所有括号的匹配,输出“NO”;
-
在字符总数为偶数的前提下,如果字符串中仅存的一对括号"相匹配",即
(
在)
的左边,那么一定存在合法的括号匹配情况; -
在字符总数为偶数的前提下,如果字符串中仅存的一对括号"不匹配",即
(
在)
的右边,那么当且仅当)
的左边 和(
的右边不存在字符时输出“NO”,其余情况总能找到合法的括号匹配情况。
3. 代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int t;
cin >> t;
while (t--){
string s;
cin >> s;
int n = s.size();
if(n&1 || s[0] == ')' || s[n-1] == '(')
cout << "NO" << endl;
else
cout << "YES" << endl;
}
}
时间复杂度: O ( 1 ) O(1) O(1)
空间复杂度: O ( 1 ) O(1) O(1)
B. Red and Blue
1. 问题描述
这题目故意设了个情景(实际上CodeForces里面的题基本都这样),绕来绕去其实就是这么个问题:给定两个序列,现今要把它们两个进行合并,但不能打乱各个元素在各自序列中的相对顺序(从原文加粗字以及 restore 一词可以看出),求出所有可能的序列中 f ( a ) f(a) f(a) 的最大值,其中 f ( a ) f(a) f(a) 其实就是合并序列的所有前缀和中的最大值(别忘了还有一个0)。
2. 思路
为了便于描述,我们把两个数组分别命名为 A A A 和 B B B,注意到无论如何排列元素,两个数组的元素顺序是固定的,那么对于 [ A 1 , A 2 , . . . , A n ] [A_1, A_2, ..., A_n] [A1,A2,...,An] 与 [ B 1 , B 2 , . . . , B m ] [B_1, B_2, ..., B_m] [B1,B2,...,Bm],新序列的排列总数为 A m + n m + n / ( A m m × A n n ) = C m + n n A_{m+n}^{m+n} / (A_m^m × A_n^n) = C_{m+n}^n Am+nm+n/(Amm×Ann)=Cm+nn,根据数据规模 1 ≤ m , n ≤ 100 1 \le m, n \le 100 1≤m,n≤100,最大组合数为 C 200 100 C_{200}^{100} C200100,如果想知道这个数是个什么概念,你用下面的程序试试就知道了,总之就是暴力方式不可取。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
// 求组合数
LL getCombine(int total, int select){
LL ans = 1;
for (int i=total-select+1, j=1; i <= total; ++i, ++j){
ans *= i / j;
}
return ans;
}
int main() {
cout<<getCombine(200, 100)<<endl;
return 0;
}
暴力不行,那么肯定有我们没有利用的条件。注意到合并序列的最大前缀和,它一定是由 A A A 的前缀和以及 B B B 的前缀和构成的,当然其中可能不存在 A A A 或 B B B 中的元素。这样,不论构成这些前缀和的序列如何排列,答案都不会因此改变,因而我们不需要讨论排列的具体细节。
我们可以简单证明一下为什么答案一定是由 A A A 和 B B B 的前缀和构成的,用反证法:假设答案序列 [ a 1 , a 2 , . . . , a i + j ] [a_1, a_2, ..., a_{i+j}] [a1,a2,...,ai+j] 是由非前缀序列 [ A k 1 , A k 2 , . . . , A k i ] [A_{k1}, A_{k2}, ..., A_{ki}] [Ak1,Ak2,...,Aki] 和 [ B q 1 , B q 2 , . . . , B q j ] [B_{q1}, B_{q2}, ..., B_{qj}] [Bq1,Bq2,...,Bqj] 构成,根据题意, A k 1 A_{k1} Ak1 一定是 A A A 中最前面的元素,如果 k 1 ≠ 1 k_1 \ne 1 k1=1,那么说明还有比 A k 1 A_{k1} Ak1 更前面的元素,矛盾,说明 A A A 的子序列从头开始;对于序列中的元素 < A k x , A k y > <A_{kx}, A_{ky}> <Akx,Aky>,如果 k y − k x > 1 ky - kx > 1 ky−kx>1,即 A k x + 1 A_{kx+1} Akx+1 没出现,则在 A A A 中其后面的元素都不应该出现,矛盾,故有 k y = k x + 1 ky = kx + 1 ky=kx+1,说明序列中两两元素是相邻的。综合这两点,序列 [ A k 1 , A k 2 , . . . , A k i ] ⟺ [ A 1 , A 2 , . . . , A i ] [A_{k1}, A_{k2}, ..., A_{ki}] \iff [A_1, A_2, ..., A_i] [Ak1,Ak2,...,Aki]⟺[A1,A2,...,Ai] ,同理有 [ B q 1 , B q 2 , . . . , B q j ] ⟺ [ B 1 , B 2 , . . . , B j ] [B_{q1}, B_{q2}, ..., B_{qj}] \iff [B_1, B_2, ..., B_j] [Bq1,Bq2,...,Bqj]⟺[B1,B2,...,Bj],所以答案是两个前缀和组成。
这样我们就可以在线性时间复杂度下解决问题了:首先求两个序列各自的前缀和,找到各自序列前缀和的最大值,然后与 0 比较求较大值 (我在这里吃了两次WA),最后相加即得到答案。
3. 代码
#include<bits/stdc++.h>
using namespace std;
int a[101], b[101];
int solve(int a[], int b[], int n, int m){
int sumA = 0, sumB = 0;
int maxSumA = 0, maxSumB = 0;
for (int i = 0; i < n; ++i) {
sumA += a[i];
maxSumA = max(maxSumA, sumA);
}
for (int i = 0; i < m; ++i) {
sumB += b[i];
maxSumB = max(maxSumB, sumB);
}
return maxSumA + maxSumB;
}
int main(){
int t;
cin >> t;
while (t--){
int n, m;
cin >> n;
for (int i = 0; i < n; ++i)
cin >> a[i];
cin >> m;
for (int i = 0; i < m; ++i)
cin >> b[i];
cout << solve(a, b, n, m)<<endl;
}
}
时间复杂度: O ( m a x ( n , m ) ) O(max(n, m)) O(max(n,m)), n n n、 m m m 分别代表两个子序列的长度。
空间复杂度: O ( 1 ) O(1) O(1)
从现在开始,我要开始龟了…
C. Building a Fence
1. 问题描述
这个题目审了十多分钟就为了理解 “stand on the corresponding ground levels”,其实意思就是"贴地放"。翻译一下题目:有
n
n
n 个相同的矩形条,它们宽为
1
1
1,长为
k
k
k,现把它们安置在高低不平的地面上,地面的高度用一个数组序列
[
h
1
,
h
2
,
.
.
.
,
h
n
]
[h_1, h_2, ..., h_n]
[h1,h2,...,hn] 依次给出,现在考虑是否有同时满足下面三种情况的安放方法:
-
两个相邻的矩形块得有重叠部分,重叠高度至少为1
-
第一个和最后一个矩形块的底部贴地
-
夹在中间的矩形块的底部与地面的距离在 [ 0 , k − 1 ] [0, k-1] [0,k−1] 之间
2. 思路
按顺序从前向后遍历,根据约束条件 2,第 1 个矩形块贴地放置,所以其可放置的高度区间范围为 [ h 1 , h 1 ] [h_1, h_1] [h1,h1] ,遍历到第 i i i 个矩形块时 ( i > 1 i \gt 1 i>1),根据约束条件 3,它的合法高度区间为 [ h i , h i + k − 1 ] [h_i, h_i+k-1] [hi,hi+k−1],且第 i − 1 i-1 i−1 个矩形块的高度区间为 [ l b , u b ] [lb, ub] [lb,ub],根据约束条件 1,可以确定第 i i i 个矩形块的区间范围是 [ l b − k + 1 , u b + k − 1 ] [lb-k+1, ub+k-1] [lb−k+1,ub+k−1],我们对两个高度区间取交集: [ h i , h i + k − 1 ] ⋂ [ l b − k + 1 , u b + k − 1 ] [h_i, h_i+k-1] \bigcap \ [lb-k+1, ub+k-1] [hi,hi+k−1]⋂ [lb−k+1,ub+k−1] 并赋值给 [ l b , u b ] [lb, ub] [lb,ub]。如果交集为空,即 l b > u b lb > ub lb>ub,那么说明不存在符合条件的安放方法,直接退出循环并输出"NO";到了最后一个矩形块时,根据约束条件 2,看看是否满足 h n ∈ [ l b , u b ] h_n \in [lb, ub] hn∈[lb,ub],如果满足,说明可以贴地放,那么就输出"YES",否则输出"NO"。
另外注意一下数据范围,比赛的时候如果不确定是否溢出,用 long long 准没问题,但在这里还是分析一下是否需要:注意到 0 ≤ h i ≤ 1 0 8 0\le h_i \le10^8 0≤hi≤108, 2 ≤ k ≤ 1 0 8 2 \le k\le10^8 2≤k≤108,而通过加减运算得到的最大数据区间为 [ − 1 0 8 , 2 × 1 0 8 ] [-10^8, 2\times10^8] [−108,2×108],没有超过 int 的范围,所以按照上述算法用 int 是不会溢出的。
3. 代码
#include<bits/stdc++.h>
using namespace std;
#define YES cout << "YES" << endl
#define NO cout << "NO" << endl
int main(){
int t;
cin >> t;
while (t--){
int n, k;
cin >> n >> k;
vector<int> H(n);
for (int i = 0; i < n; ++i)
cin >> H[i];
int lb = H[0], ub = H[0];
bool ok = true;
for (int i = 1; i < n; ++i){
lb = max(lb - k + 1, H[i]);
ub = min(ub + k - 1, H[i] + k - 1);
if(lb > ub){
ok = false;
break;
}
if(i == n-1 && (lb > H[i] || ub < H[i]))
ok = false;
}
ok ? YES : NO;
}
}
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1),这里没算用于存储地面高度的数组
从D题开始,就是我未曾触及的领域了,因此后续解题思路都是参考顶级大神 jiangly 的代码推出来的。
D. Ceil Divisions
1. 问题描述
翻译一下问题:给定一个由
n
n
n 个数构成的连续序列
[
a
1
,
a
2
,
a
3
,
.
.
.
,
a
n
]
→
[
1
,
2
,
3
,
.
.
.
,
n
]
[a_1, a_2, a_3, ..., a_n] \rightarrow [1, 2, 3, ..., n]
[a1,a2,a3,...,an]→[1,2,3,...,n],我们下面要进行若干个步骤,使得该序列最终由
n
−
1
n-1
n−1 个
1
1
1 和
1
1
1 个
2
2
2 构成,每一步进行如下操作:
-
任意选取序列中的两个数 a x , a y ( x ≠ y ) a_x, a_y (x \ne y) ax,ay(x=y),其中 a x = x , a y = y a_x = x, a_y = y ax=x,ay=y
-
使用向上取整函数对 a x a_x ax 取整: a x = ⌈ a x a y ⌉ a_x = \lceil \frac{a_x}{a_y} \rceil ax=⌈ayax⌉
完成以上任务的步骤数不超过 n + 5 n+5 n+5,并且不一定考虑最少步骤数。对于任意 n n n ( 3 ≤ n ≤ 2 × 1 0 5 3 \le n \le 2\times10^5 3≤n≤2×105),总能保证有满足约束的构造方案。试输出步骤总数,并按照步骤顺序打印每一步选取的元素下标 [ x , y ] [x, y] [x,y] 或 [ y , x ] [y, x] [y,x]。
2. 思路
这是一个数学问题,需要我们找到一种固定的构造方式,能满足对于数据范围内的所有 n n n ,操作步数都不超过 n + 5 n+5 n+5。
为此,我们先看看取整函数的可能取值:
a x = ⌈ a x a y ⌉ = { 1 a x ≤ a y a x / a y a x > a y a n d a x m o d a y = 0 ⌊ a x / a y ⌋ + 1 a x > a y a n d a x m o d a y ≠ 0 a_x = \lceil \frac{a_x}{a_y} \rceil = \begin{cases} 1& a_x\le a_y\\ a_x / a_y & a_x > a_y & and & a_x \ mod \ a_y = 0\\ \lfloor a_x / a_y \rfloor + 1 & a_x > a_y & and & a_x \ mod \ a_y \ne0 \end{cases} ax=⌈ayax⌉=⎩⎪⎨⎪⎧1ax/ay⌊ax/ay⌋+1ax≤ayax>ayax>ayandandax mod ay=0ax mod ay=0
如果不考虑步数限制,我们的一种可行方式为:
从 3 3 3 开始,即 [ a 3 , a 4 , . . . , a n − 1 , a n ] [a_3, a_4, ..., a_{n-1}, a_n] [a3,a4,...,an−1,an],我们可以从前到后选用 { i , i + 1 } \{ i, i+1 \} {i,i+1} ( 3 ≤ i ≤ n − 1 3 \le i \le n-1 3≤i≤n−1),使用取整函数 a i = ⌈ a i / a i + 1 ⌉ = 1 a_i = \lceil a_i / a_{i+1} \rceil = 1 ai=⌈ai/ai+1⌉=1,这样以后的序列变为 [ a 1 , a 2 , a 3 , a 4 , . . . , a n − 1 , a n ] → [ 1 , 2 , 1 , 1 , . . . , 1 , a n ] [a_1, a_2, a_3, a_4, ..., a_{n-1}, a_n] \rightarrow [1, 2, 1, 1, ..., 1, a_n] [a1,a2,a3,a4,...,an−1,an]→[1,2,1,1,...,1,an],然后选取 { 2 , n } \{2, n\} {2,n},即 a n = ⌈ a n / a 2 ⌉ a_n = \lceil a_n / a_2 \rceil an=⌈an/a2⌉,循环执行直到 a n a_n an 变为 1 1 1,显然 a n a_n an 从 ⌈ n / 2 ⌉ \lceil n / 2 \rceil ⌈n/2⌉ 降至 1 1 1 的过程就是额外次数产生的过程,每一次都使得数字减半,故额外次数为 ⌊ l o g ⌈ n / 2 ⌉ ⌋ \lfloor log\lceil n / 2 \rceil \rfloor ⌊log⌈n/2⌉⌋,题目只给我们额外 5 5 5 次,显然当 n ≥ 2 6 − 1 = 127 n \ge 2^6-1 = 127 n≥26−1=127 时,这种方式的总操作次数就超过 n + 5 n+5 n+5 次了。
我们能很轻易地用一步操作就让大片的序列元素变为 1 1 1,但却苦于序列最后的 a n a_n an 太大,只能用 2 2 2 来持续对它减半,这样的额外步骤我们承受不起。直觉告诉我们:如果要控制额外步数,我们一定要让最大数 a n a_n an 在一个步骤内降低得更多一些,一次操作减半太慢了,那比它更快的操作是什么呢?似乎除了开根号也没有什么常见的了,而在开根号中用得最多的就是开平方根。
我们刚才的想法都是一次性搞定 [ 1 , . . . , n ] [1, ..., n] [1,...,n],如果我们把区间划分一下呢?例如进行如下步骤:
-
把序列区间 [ 1 , 2 , . . . , n ] [1, 2, ..., n] [1,2,...,n] 拆分成 [ 1 , 2 , . . . , ⌈ n ⌉ ] ⋃ [ ⌈ n ⌉ + 1 , . . . , n ] [1, 2, ..., \lceil\sqrt{n}\rceil] \bigcup \ [ \lceil\sqrt{n}\rceil + 1, ..., n] [1,2,...,⌈n⌉]⋃ [⌈n⌉+1,...,n] (每个区间至少两个元素),我们先对较大区间处理,很容易得到 [ 1 , 2 , . . . , ⌈ n ⌉ ] ⋃ [ 1 , 1 , . . . , 1 , n ] [1, 2, ..., \lceil\sqrt{n}\rceil] \bigcup \ [ 1, 1, ..., 1, n] [1,2,...,⌈n⌉]⋃ [1,1,...,1,n]
-
执行向上取整操作两次: a n = ⌈ n / ⌈ n ⌉ ⌉ a_n = \lceil n / \lceil\sqrt{n}\rceil \rceil an=⌈n/⌈n⌉⌉,由此得 [ 1 , 2 , 3 , . . . , ⌈ n ⌉ ] ⋃ [ 1 , 1 , . . . , 1 , 1 ] [1, 2, 3, ..., \lceil\sqrt{n}\rceil] \bigcup \ [ 1, 1, ..., 1, 1] [1,2,3,...,⌈n⌉]⋃ [1,1,...,1,1]
-
忽略后半全为 1 1 1 的区间,如果区间 [ 1 , 2 , . . . , ⌈ n ⌉ ] [1, 2, ..., \lceil\sqrt{n}\rceil] [1,2,...,⌈n⌉] 只有两个元素,则过程完成;否则继续对该区间重复步骤 1, 2
上述过程中唯有步骤 2 2 2 可能产生额外步骤开销,开销次数即步骤的重复次数,对数据规模上限 a n = 2 × 1 0 5 a_n = 2\times10^5 an=2×105 连续开 5 5 5 次平方根后的结果为 1.46 1.46 1.46,而 ⌈ 1.46 ⌉ = 2 \lceil1.46\rceil = 2 ⌈1.46⌉=2,另外别忘了 a n a_n an 本身有一次操作还没有用,因而最后恰好能使用 n + 3 n+3 n+3 次操作满足要求 (其中原本的 1 1 1 和 2 2 2 不需要进行操作)。
可见这个 n + 5 n+5 n+5 于此构造方式还是算宽松的,其最大操作次数为 n + 3 n+3 n+3。
3. 代码
#include<bits/stdc++.h>
using namespace std;
int main(){
int t;
cin >> t;
while (t--){
int n;
cin >> n;
vector<pair<int, int>> ret;
while (n > 2){
int s = ceil(sqrt(n));
for (int i = s + 1; i < n; ++i){
ret.push_back(make_pair(i, i+1));
}
ret.push_back(make_pair(n, s));
ret.push_back(make_pair(n, s));
n = s;
}
cout << ret.size() << endl;
for (auto p: ret){
cout << p.first << " " << p.second << endl;
}
}
}
时间复杂度: O ( n ) O(n) O(n),对于 [ 1 , 2 , . . . , n ] [1, 2, ..., n] [1,2,...,n] 中的 O ( n ) O(n) O(n) 个元素,每个都进行 O ( 1 ) O(1) O(1) 的操作,因此总体来说是 O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n),操作次数上限为 n + 3 n+3 n+3
后面的题目刚不动了,就复盘到这里吧…
E. A Bit Similar
F. Power Sockets
欢迎大佬们批评指正,或者分享解题思路~