绪论
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=Tn−1+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
(c−a)−(b−a)−1=c−b−1
但是我忘记了,也有可能是A、B一起消耗C,这样重复的个数是
c
−
b
−
a
−
1
c-b-a-1
c−b−a−1,显然比上面小。
但是有一个问题就是,如果
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(c−a−b−1,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,...,an−1,和一个区间 [ a , b ] [a,b] [a,b],我认为如果 a 0 < b a_0 < b a0<b且 a n − 1 > a a_{n-1} > a an−1>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+c1⩾x&sum−ai+c2⩾y,求最小的
c
1
+
c
2
c_1+c_2
c1+c2
通过对问题的分析,进行分类讨论(看起来很复杂的问题有可能通过分类讨论变得清晰起来)
-
s
u
m
⩽
y
sum \leqslant y
sum⩽y
这个时候无论取出哪个元素用来和x比较,都会导致sum
更小,因此总需要花钱,
1.1. s u m ⩽ x sum \leqslant x sum⩽x
说明任何一个元素都小于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 x−ai+y−(sum−ai)=x+y−sum。这真是令人振奋的消息,也就是说在这种情况下我们无论取哪个元素花费都是一样的
1.2。 s u m > x sum > x sum>x
这个时候问题又变得复杂起来了,如果里面小于等于x的元素,花费和上面一样为 y − s u m + x y-sum+x y−sum+x,对于其中大于x的元素,需要的钱为 y − s u m + a i y-sum+a_i y−sum+ai,也就是说 a i a_i ai越小越好,但还是大于 x x x
综上,当 s u m ⩽ y sum \leqslant y sum⩽y时,如果该数组中存在一个小于等于x的元素,最优解就为 y − s u m + x y-sum+x y−sum+x,如果全部都大于x,最优解为 y − s u m + m i n ( a i ) y-sum+min({a_i}) y−sum+min(ai),为了方便做到这一点,我们不妨对数组进行排序,通过判断 a 0 a_0 a0与x的大小判断解为 y − s u m + x y-sum+x y−sum+x还是 y − s u m + a 0 y-sum+a_0 y−sum+a0 -
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 sum−ai⩾y&ai⩾x
这种情况下我们不用付钱,要求 ∃ a i , x ⩽ a i ⩽ s u m − y \exists a_i,x\leqslant a_i\leqslant sum-y ∃ai,x⩽ai⩽sum−y
2.2. s u m − a i < y & a i ⩾ x sum-a_i < y \And a_i \geqslant x sum−ai<y&ai⩾x
这种情况要付钱 y − s u m + a i y-sum+a_i y−sum+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,ai⩾x&ai>sum−y
2.3. s u m − a i ⩾ y & a i < x sum-a_i\geqslant y \And a_i < x sum−ai⩾y&ai<x
这种情况要付钱 x − a i x-a_i x−ai,要求 ∃ 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&ai⩽sum−y
2.4. s u m − a i < y & a i < x sum-a_i<y \And a_i <x sum−ai<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 y−sum+ai+x−ai=x+y−sum,要求 ∃ a i , s u m − y < a i < x \exists a_i,sum-y<a_i< x ∃ai,sum−y<ai<x
我们发现,在这种情况下另一个重要的量 s u m − y sum-y sum−y经常出现,因此我们另 z = s u m − y z=sum-y z=sum−y,然后将x和z之间的关系进行分类讨论。
2.5. x ⩽ z x\leqslant z x⩽z
这个时候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=ai−z,对于满足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=x−ai,即这个情况的解为三种解的最小值
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好像做智力游戏,也挺有意思的。至于数据结构、算法,可以刷紫书嘛。