绪论
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个都是)显然是一个解。
通过这种构造方法:Tn=Tn−1+1T_n = T_{n-1} + 1Tn=Tn−1+1,T1=1T_1 = 1T1=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<=ca<=b<=ca<=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(c−a)−(b−a)−1=c−b−1
但是我忘记了,也有可能是A、B一起消耗C,这样重复的个数是c−b−a−1c-b-a-1c−b−a−1,显然比上面小。
但是有一个问题就是,如果a+b>ca+b>ca+b>c怎么办?这个时候我们可以采取以下策略:首先让A、B重复出现使得(a+b)(a+b)(a+b)每次减少2。因为当c−(a+b)c-(a+b)c−(a+b)为1或者0的时候都没有重复,所以我们总能够通过这种策略不出现重复,因此最小重复次数为max(c−a−b−1,0)max(c-a-b-1,0)max(c−a−b−1,0)
上面的思路最后是AC的。但是不免还有一个疑问?为什么我们能够保证在minminmin和maxmaxmax之间的重复次数都能够出现?
也就是说我们可以采取一种策略,对m个重复的排列C1C_1C1,通过删除插入将其转换成m-1个重复的排列C2C_2C2。
这种策略如下:我们可以将某个重复中的元素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
这道题比赛的时候没做出来,但是我的思路是对的,只是在实现的时候当时已经十二点了,脑袋已经不转了,糊涂了。也有一方面的原因是自己有一些想当然:对于一个数组a0,a1,a2,...,an−1a_0,a_1,a_2,...,a_{n-1}a0,a1,a2,...,an−1,和一个区间[a,b][a,b][a,b],我认为如果a0<ba_0 < ba0<b且 an−1>aa_{n-1} > aan−1>a则一定有元素在[a,b][a,b][a,b]区间内。。。现在发现这个错误后就AC了,呜呜呜,如果这道题做出来说不定我都上分了。
解题思路
题目的意思是,有一个数组,其和为sum,要求ai+c1⩾x&sum−ai+c2⩾ya_i + c_1 \geqslant x \And sum-a_i+c_2\geqslant yai+c1⩾x&sum−ai+c2⩾y,求最小的c1+c2c_1+c_2c1+c2
通过对问题的分析,进行分类讨论(看起来很复杂的问题有可能通过分类讨论变得清晰起来)
- sum⩽ysum \leqslant ysum⩽y
这个时候无论取出哪个元素用来和x比较,都会导致sum
更小,因此总需要花钱,
1.1. sum⩽xsum \leqslant xsum⩽x
说明任何一个元素都小于x,那么我们需要的钱为x−ai+y−(sum−ai)=x+y−sumx-a_i+y-(sum-a_i)=x+y-sumx−ai+y−(sum−ai)=x+y−sum。这真是令人振奋的消息,也就是说在这种情况下我们无论取哪个元素花费都是一样的
1.2。 sum>xsum > xsum>x
这个时候问题又变得复杂起来了,如果里面小于等于x的元素,花费和上面一样为y−sum+xy-sum+xy−sum+x,对于其中大于x的元素,需要的钱为y−sum+aiy-sum+a_iy−sum+ai,也就是说aia_iai越小越好,但还是大于xxx
综上,当sum⩽ysum \leqslant ysum⩽y时,如果该数组中存在一个小于等于x的元素,最优解就为y−sum+xy-sum+xy−sum+x,如果全部都大于x,最优解为y−sum+min(ai)y-sum+min({a_i})y−sum+min(ai),为了方便做到这一点,我们不妨对数组进行排序,通过判断a0a_0a0与x的大小判断解为y−sum+xy-sum+xy−sum+x还是y−sum+a0y-sum+a_0y−sum+a0 - sum>ysum>ysum>y
这个时候的情形更加复杂,因为存在可能不花钱的状况。因此,我们不妨再对这种情况进行分类:
2.1. sum−ai⩾y&ai⩾xsum-a_i\geqslant y \And a_i \geqslant xsum−ai⩾y&ai⩾x
这种情况下我们不用付钱,要求∃ai,x⩽ai⩽sum−y\exists a_i,x\leqslant a_i\leqslant sum-y∃ai,x⩽ai⩽sum−y
2.2. sum−ai<y&ai⩾xsum-a_i < y \And a_i \geqslant xsum−ai<y&ai⩾x
这种情况要付钱y−sum+aiy-sum+a_iy−sum+ai,要求∃ai,ai⩾x&ai>sum−y\exists a_i,a_i\geqslant x \And a_i > sum - y∃ai,ai⩾x&ai>sum−y
2.3. sum−ai⩾y&ai<xsum-a_i\geqslant y \And a_i < xsum−ai⩾y&ai<x
这种情况要付钱x−aix-a_ix−ai,要求∃ai,ai<x&ai⩽sum−y\exists a_i,a_i < x \And a_i \leqslant sum - y∃ai,ai<x&ai⩽sum−y
2.4. sum−ai<y&ai<xsum-a_i<y \And a_i <xsum−ai<y&ai<x
这种情况要付钱y−sum+ai+x−ai=x+y−sumy-sum+a_i+x-a_i=x+y-sumy−sum+ai+x−ai=x+y−sum,要求∃ai,sum−y<ai<x\exists a_i,sum-y<a_i< x∃ai,sum−y<ai<x
我们发现,在这种情况下另一个重要的量sum−ysum-ysum−y经常出现,因此我们另z=sum−yz=sum-yz=sum−y,然后将x和z之间的关系进行分类讨论。
2.5.x⩽zx\leqslant zx⩽z
这个时候2.4不可能发生,只剩下了三种情况,我们可以分别对三种情况进行快速求解,对于满足2.1的aia_iai,ans=0ans=0ans=0,对于满足2.2的aia_iai,我们要找到满足ai>za_i>zai>z的最小aia_iai,ans=ai−zans=a_i-zans=ai−z,对于满足2.3的aia_iai,我们要找到满足ai<xa_i<xai<x的最大aia_iai,ans=x−aians=x-a_ians=x−ai,即这个情况的解为三种解的最小值
2.6. x>zx > zx>z
这个时候2.1不可能发生,剩下三种情况的讨论与2.5相同
上面的求值在一个排好序的数组中都可以使用lower_boundlower\_boundlower_bound和upper_boundupper\_boundupper_bound函数快速解决
每次查询的复杂度为O(logn)O(log_n)O(logn),总复杂度为O((n+m)logn)O((n+m)log_n)O((n+m)logn),前者是排序的复杂度,这对2e52e52e5的复杂度是可以接受的
实现上面的程序需要对二分查找非常熟悉,但是自己实现的二分查找非常容易出现Bug,使用STL中的lower_boundlower\_boundlower_bound和upper_boundupper\_boundupper_bound就成了不二之选,这要求我们对这两个函数非常熟悉。基本的用法很简单,lower_boundlower\_boundlower_bound返回大于等于关键字的第一个迭代器,upper_boundupper\_boundupper_bound返回大于关键字的第一个迭代器,两者之间的范围就是等于关键字的范围,如果要访问元素一定要判断是否等于尾后迭代器。如何求小于和小于等于有一个小技巧,对于小于,也就是大于等于的前一个元素,我们将lower_boundlower\_boundlower_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好像做智力游戏,也挺有意思的。至于数据结构、算法,可以刷紫书嘛。