【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 1m,n100,最大组合数为 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 kykx>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. 两个相邻的矩形块得有重叠部分,重叠高度至少为1

  2. 第一个和最后一个矩形块的底部贴地

  3. 夹在中间的矩形块的底部与地面的距离在 [ 0 , k − 1 ] [0, k-1] [0,k1] 之间

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+k1],且第 i − 1 i-1 i1 个矩形块的高度区间为 [ 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] [lbk+1,ub+k1],我们对两个高度区间取交集: [ 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+k1] [lbk+1,ub+k1] 并赋值给 [ 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 0hi108 2 ≤ k ≤ 1 0 8 2 \le k\le10^8 2k108,而通过加减运算得到的最大数据区间为 [ − 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 n1 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 3n2×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/ayax/ay+1axayax>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,...,an1,an],我们可以从前到后选用 { i , i + 1 } \{ i, i+1 \} {i,i+1} ( 3 ≤ i ≤ n − 1 3 \le i \le n-1 3in1),使用取整函数 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,...,an1,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 logn/2,题目只给我们额外 5 5 5 次,显然当 n ≥ 2 6 − 1 = 127 n \ge 2^6-1 = 127 n261=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. 把序列区间 [ 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]

  2. 执行向上取整操作两次: 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]

  3. 忽略后半全为 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

题目链接


欢迎大佬们批评指正,或者分享解题思路~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
"educational codeforces round 103 (rated for div. 2)"是一个Codeforces平台上的教育性比赛,专为2级选手设计评级。以下是有关该比赛的回答。 "educational codeforces round 103 (rated for div. 2)"是一场Codeforces平台上的教育性比赛。Codeforces是一个为程序员提供竞赛和评级的在线平台。这场比赛是专为2级选手设计的,这意味着它适合那些在算法和数据结构方面已经积累了一定经验的选手参与。 与其他Codeforces比赛一样,这场比赛将由多个问题组成,选手需要根据给定的问题描述和测试用例,编写程序来解决这些问题。比赛的时限通常有两到三个小时,选手需要在规定的时间内提交他们的解答。他们的程序将在Codeforces的在线评测系统上运行,并根据程序的正确性和效率进行评分。 该比赛被称为"educational",意味着比赛的目的是教育性的,而不是针对专业的竞争性。这种教育性比赛为选手提供了一个学习和提高他们编程技能的机会。即使选手没有在比赛中获得很高的排名,他们也可以从其他选手的解决方案中学习,并通过参与讨论获得更多的知识。 参加"educational codeforces round 103 (rated for div. 2)"对于2级选手来说是很有意义的。他们可以通过解决难度适中的问题来测试和巩固他们的算法和编程技巧。另外,这种比赛对于提高解决问题能力,锻炼思维和提高团队合作能力也是非常有帮助的。 总的来说,"educational codeforces round 103 (rated for div. 2)"是一场为2级选手设计的教育性比赛,旨在提高他们的编程技能和算法能力。参与这样的比赛可以为选手提供学习和进步的机会,同时也促进了编程社区的交流与合作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值