Educational Codeforces Round 114总结

绪论

https://codeforces.com/contest/1574/
以前想要打CF,总是觉得没有时间,要做这个,要做那个,现在时间充裕了一些,想要多打一些CF,但是光打比赛不总结是没有什么帮助的,这是我从以前的ACM训练中吸取的惨痛教训。从这篇文章开始准备好好总结一些比赛心得。

这场比赛是for div2的,因此对我来讲有些难度,我发挥的不是很好,然后理所当然就掉分了(掉了50+,哭哭)

A

解题思路

要求构造正确的括号组合,即每一个左括号有一个相应匹配的右括号,要求给定括号对数n,输出其n种组合方式。

刚开始思考了一下将左括号看作1,右括号看作-1,任何时刻整个表达式的和为非负数:要求每出现一个右括号的时候都有其对应的左括号。通过
这种方式进行构造。

但是很快,我发现其中的递推关系:
一对括号:()
两对括号:()()、(())
三对括号:()()()、(())()、((()))

我们不难发现,对n对括号的情况,其前2n-2个位置可以是n-1对括号的所有情况,然后最后再加一对括号
但是有一种情况不能由n-1对括号得来:最后两个是))的时候,简单起见,前n个都是(,后n个都是)显然是一个解。

通过这种构造方法: T n = T n − 1 + 1 T_n = T_{n-1} + 1 Tn=Tn1+1 T 1 = 1 T_1 = 1 T1=1,我们总能够造出n个解。因为n最多是50,我们只需要首先通过递推构造出所有的解,然后直接输出即可。

但是显然n对括号不止n种解(当时比赛的时候心中有这个疑惑,但是没有时间去仔细思考):
对三对括号而言,还有一种解:()(())。

AC代码

// Copyright(C), Edward-Elric233
// Author: Edward-Elric233
// Version: 1.0
// Date: 2021/9/20
// Description: 

#include <vector>
#include <string>
#include <iostream>

using namespace std;

const int MAXN = 51;
vector<vector<string>> ans;

int main() {
    ios::sync_with_stdio(false);
    ans.push_back(vector<string>());
    ans.push_back(vector<string>());
    ans[1].push_back("()");
    for (int i = 2; i < MAXN; ++i) {
        ans.push_back(ans[i - 1]);
        for (auto &s :ans[i]) {
            s.append("()");
        }
        ans[i].push_back(string(i, '(') + string(i, ')'));
    }
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        for (int i = 0; i < n; ++i) {
            cout << ans[n][i] << "\n";
        }
    }
    return 0;
}

B

解题思路

给定a、b、c个数的A、B、C,要求其相邻两个重复的次数为m次
我当时初步的想法是求出最小重复次数和最大重复次数,如果m在两者之间则YES,否则则NO。
最大重复次数显然是所有的A都出现完了后B再出现,然后C再出现,因为都放在一起出现一个重复的代价是1个字母(除了第一个),但是一旦A结束重复,还想A出现相邻的重复,又要消耗一个没有意义的字母作为第一个。
在求最小重复次数的时候我犯了错导致WA了一发。当时想着,假设 a < = b < = c a<=b<=c a<=b<=c,让A、B、C循环出现,肯定先将A消耗完,然后B、C再重复出现,最后就只剩下C不得不重复,因此重复的个数是 ( c − a ) − ( b − a ) − 1 = c − b − 1 (c-a)-(b-a)-1=c-b-1 (ca)(ba)1=cb1
但是我忘记了,也有可能是A、B一起消耗C,这样重复的个数是 c − b − a − 1 c-b-a-1 cba1,显然比上面小。
但是有一个问题就是,如果 a + b > c a+b>c a+b>c怎么办?这个时候我们可以采取以下策略:首先让A、B重复出现使得 ( a + b ) (a+b) (a+b)每次减少2。因为当 c − ( a + b ) c-(a+b) c(a+b)为1或者0的时候都没有重复,所以我们总能够通过这种策略不出现重复,因此最小重复次数为 m a x ( c − a − b − 1 , 0 ) max(c-a-b-1,0) max(cab1,0)

上面的思路最后是AC的。但是不免还有一个疑问?为什么我们能够保证在 m i n min min m a x max max之间的重复次数都能够出现?
也就是说我们可以采取一种策略,对m个重复的排列 C 1 C_1 C1,通过删除插入将其转换成m-1个重复的排列 C 2 C_2 C2

这种策略如下:我们可以将某个重复中的元素X取出,则重复变为m-1,剩下我们要做的就是将X再插入排列,我们只要插入和其左右都不同,且左右不同的一个位置。因为数据保证A、B、C都至少出现一次,且更少重复的排列是存在的,我们应该总能够找到这样一个位置。

好吧,我承认我有些证明不过来了,不过大概就是上面那样。

AC代码

// Copyright(C), Edward-Elric233
// Author: Edward-Elric233
// Version: 1.0
// Date: 2021/9/20
// Description: 

#include <iostream>

using namespace std;

int main() {
    ios::sync_with_stdio(false);
    int T, a, b, c, m;
    cin >> T;
    while (T--) {
        cin >> a >> b >> c >> m;
        if (a > c) std::swap(a, c);
        if (b > c) std::swap(b, c);
        if (a > b) std::swap(a, b);
        int min = std::max(c - b - a - 1, 0);
        int max = a + b + c - 3;
        if (m >= min && m <= max) {
            cout << "YES\n";
        } else {
            cout << "NO\n";
        }
    }
    return 0;
}

C

这道题比赛的时候没做出来,但是我的思路是对的,只是在实现的时候当时已经十二点了,脑袋已经不转了,糊涂了。也有一方面的原因是自己有一些想当然:对于一个数组 a 0 , a 1 , a 2 , . . . , a n − 1 a_0,a_1,a_2,...,a_{n-1} a0,a1,a2,...,an1,和一个区间 [ a , b ] [a,b] [a,b],我认为如果 a 0 < b a_0 < b a0<b a n − 1 > a a_{n-1} > a an1>a则一定有元素在 [ a , b ] [a,b] [a,b]区间内。。。现在发现这个错误后就AC了,呜呜呜,如果这道题做出来说不定我都上分了。

解题思路

题目的意思是,有一个数组,其和为sum,要求 a i + c 1 ⩾ x & s u m − a i + c 2 ⩾ y a_i + c_1 \geqslant x \And sum-a_i+c_2\geqslant y ai+c1x&sumai+c2y,求最小的 c 1 + c 2 c_1+c_2 c1+c2
通过对问题的分析,进行分类讨论(看起来很复杂的问题有可能通过分类讨论变得清晰起来)

  1. s u m ⩽ y sum \leqslant y sumy
    这个时候无论取出哪个元素用来和x比较,都会导致sum更小,因此总需要花钱,
    1.1. s u m ⩽ x sum \leqslant x sumx
    说明任何一个元素都小于x,那么我们需要的钱为 x − a i + y − ( s u m − a i ) = x + y − s u m x-a_i+y-(sum-a_i)=x+y-sum xai+y(sumai)=x+ysum。这真是令人振奋的消息,也就是说在这种情况下我们无论取哪个元素花费都是一样的
    1.2。 s u m > x sum > x sum>x
    这个时候问题又变得复杂起来了,如果里面小于等于x的元素,花费和上面一样为 y − s u m + x y-sum+x ysum+x,对于其中大于x的元素,需要的钱为 y − s u m + a i y-sum+a_i ysum+ai,也就是说 a i a_i ai越小越好,但还是大于 x x x
    综上,当 s u m ⩽ y sum \leqslant y sumy时,如果该数组中存在一个小于等于x的元素,最优解就为 y − s u m + x y-sum+x ysum+x,如果全部都大于x,最优解为 y − s u m + m i n ( a i ) y-sum+min({a_i}) ysum+min(ai),为了方便做到这一点,我们不妨对数组进行排序,通过判断 a 0 a_0 a0与x的大小判断解为 y − s u m + x y-sum+x ysum+x还是 y − s u m + a 0 y-sum+a_0 ysum+a0
  2. s u m > y sum>y sum>y
    这个时候的情形更加复杂,因为存在可能不花钱的状况。因此,我们不妨再对这种情况进行分类:
    2.1. s u m − a i ⩾ y & a i ⩾ x sum-a_i\geqslant y \And a_i \geqslant x sumaiy&aix
    这种情况下我们不用付钱,要求 ∃ a i , x ⩽ a i ⩽ s u m − y \exists a_i,x\leqslant a_i\leqslant sum-y ai,xaisumy
    2.2. s u m − a i < y & a i ⩾ x sum-a_i < y \And a_i \geqslant x sumai<y&aix
    这种情况要付钱 y − s u m + a i y-sum+a_i ysum+ai,要求 ∃ a i , a i ⩾ x & a i > s u m − y \exists a_i,a_i\geqslant x \And a_i > sum - y ai,aix&ai>sumy
    2.3. s u m − a i ⩾ y & a i < x sum-a_i\geqslant y \And a_i < x sumaiy&ai<x
    这种情况要付钱 x − a i x-a_i xai,要求 ∃ a i , a i < x & a i ⩽ s u m − y \exists a_i,a_i < x \And a_i \leqslant sum - y ai,ai<x&aisumy
    2.4. s u m − a i < y & a i < x sum-a_i<y \And a_i <x sumai<y&ai<x
    这种情况要付钱 y − s u m + a i + x − a i = x + y − s u m y-sum+a_i+x-a_i=x+y-sum ysum+ai+xai=x+ysum,要求 ∃ a i , s u m − y < a i < x \exists a_i,sum-y<a_i< x ai,sumy<ai<x
    我们发现,在这种情况下另一个重要的量 s u m − y sum-y sumy经常出现,因此我们另 z = s u m − y z=sum-y z=sumy,然后将x和z之间的关系进行分类讨论。
    2.5. x ⩽ z x\leqslant z xz
    这个时候2.4不可能发生,只剩下了三种情况,我们可以分别对三种情况进行快速求解,对于满足2.1的 a i a_i ai a n s = 0 ans=0 ans=0,对于满足2.2的 a i a_i ai,我们要找到满足 a i > z a_i>z ai>z的最小 a i a_i ai a n s = a i − z ans=a_i-z ans=aiz,对于满足2.3的 a i a_i ai,我们要找到满足 a i < x a_i<x ai<x的最大 a i a_i ai a n s = x − a i ans=x-a_i ans=xai,即这个情况的解为三种解的最小值
    2.6. x > z x > z x>z
    这个时候2.1不可能发生,剩下三种情况的讨论与2.5相同
    上面的求值在一个排好序的数组中都可以使用 l o w e r _ b o u n d lower\_bound lower_bound u p p e r _ b o u n d upper\_bound upper_bound函数快速解决

每次查询的复杂度为 O ( l o g n ) O(log_n) O(logn),总复杂度为 O ( ( n + m ) l o g n ) O((n+m)log_n) O((n+m)logn),前者是排序的复杂度,这对 2 e 5 2e5 2e5的复杂度是可以接受的

实现上面的程序需要对二分查找非常熟悉,但是自己实现的二分查找非常容易出现Bug,使用STL中的 l o w e r _ b o u n d lower\_bound lower_bound u p p e r _ b o u n d upper\_bound upper_bound就成了不二之选,这要求我们对这两个函数非常熟悉。基本的用法很简单, l o w e r _ b o u n d lower\_bound lower_bound返回大于等于关键字的第一个迭代器, u p p e r _ b o u n d upper\_bound upper_bound返回大于关键字的第一个迭代器,两者之间的范围就是等于关键字的范围,如果要访问元素一定要判断是否等于尾后迭代器。如何求小于和小于等于有一个小技巧,对于小于,也就是大于等于的前一个元素,我们将 l o w e r _ b o u n d lower\_bound lower_bound向前移动一个就是最后一个小于关键字的元素,小于等于同理,不过我们需要注意的是要判断迭代器是否是开始迭代器,如果是开始迭代器则说明不存在小于或者小于等于的元素。

这个思路是我在比赛的时候想出来的,后来再看发现还是很复杂,惊叹自己当时竟然能够想这么多。不过比较可惜的是在判断2.1-2.4的时候我的脑袋已经糊涂了,导致最终没能AC。这也提醒我千里之堤毁于蚁穴,行百里半九十,对一个程序来说每一个细节都是致命的,可能平时很瞧不起,觉得很简单 ,但是他们其实是平等的,要对每一个小细节怀有敬畏之心。

AC代码

// Copyright(C), Edward-Elric233
// Author: Edward-Elric233
// Version: 1.0
// Date: 2021/9/20
// Description: 

#include <vector>
#include <array>
#include <iostream>
#include <algorithm>
#include <climits>

using namespace std;

constexpr int MAXN = 2e5 + 5;
using ll = long long;
array<ll, MAXN> a;
int n, m;
ll x, y, sum, ans, z;

int main() {
    ios::sync_with_stdio(false);
    cin >> n;
    sum = 0;
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        sum += a[i];
    }
    auto begin = a.begin();
    auto end = a.begin() + n;
    std::sort(begin, end);
    cin >> m;
    while (m--) {
        ans = LONG_LONG_MAX;
        cin >> x >> y;
        if (sum <= y) {
            if (a[0] > x) {
                ans = y + a[0] - sum;
            } else {
                ans = y + x - sum;
            }
        } else {
            z = sum - y;
            if (x <= z) {
                if (a[0] > z) {
                    ans = a[0] - z;
                } else if (a[n - 1] < x) {
                    ans = x - a[n - 1];
                } else {
                    ans = LONG_LONG_MAX;
                    auto it1 = std::upper_bound(begin, end, z);
                    if (it1 != end) {
                        ans = std::min(ans, *it1 - z);
                    }

                    auto it2 = std::lower_bound(begin, end, x);
                    if (it2 != end && *it2 <= z) {
                        ans = 0;
                    }

                    if (it2 > begin) {
                        --it2;
                        ans = std::min(ans, x - *it2);
                    }

                }
            } else {
                if (a[0] >= x) {
                    ans = a[0] - z;
                } else if (a[n - 1] <= z) {
                    ans = x - a[n - 1];
                } else {
                    ans = LONG_LONG_MAX;

                    auto it1 = std::lower_bound(begin, end, x);
                    if (it1 != end) {
                        ans = std::min(ans, *it1 - z);
                    }
                    auto it2 = std::upper_bound(begin, end, z);
                    if (it2 != end && *it2 < x) {
                        ans = std::min(ans, x - z);
                    }

                    if (it2 > a.begin()) {
                        --it2;
                        ans = std::min(ans, x - *it2);
                    }

                }
            }
        }
        cout << ans << "\n";
    }
    return 0;
}

后面的题目我没有看,我发现做出来的人很少。也不准备去做,题目无穷无尽,不应该去追逐题目,而应该做一题会一题,从有限的题目中提升自己的思维能力。觉得做CF好像做智力游戏,也挺有意思的。至于数据结构、算法,可以刷紫书嘛。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值