Codeforces Round #764 (Div. 3)
比赛相关信息
比赛信息
比赛名称:Codeforces Round #764 (Div. 3)
比赛地址:Codeforces
比赛过程回顾
A | B | C | D | E | F | G | |
---|---|---|---|---|---|---|---|
提交次数 | 1 | 1 | 3 | 0 | 0 | 0 | 0 |
首次提交时间 | 00:04:33 | 00:26:05 | 00:59:02 | ||||
首A时间 | 00:04:33 | 00:26:05 | 01:45:47 | ||||
最终通过数 | 19279 | 11931 | 7753 | 3499 | 228 | 184 | 242 |
总体而言,是一场相当有含金量的比赛。
部分题解与小结
A - Plus One on the Subset
小评
非常水的一题,想到了能够很快的完成。码完代码后多测了几个没必要的样例,导致过题时间较晚。
⇔数学规律、⇔入门级(*800)
题意
给定一个序列 \(a[1…n]\) ,每次可以选出一个子序列,使得其中所有的值 \(+1\) 。输出让所有元素的值都相等需要的最小操作次数。
思路
首先应当想到的是,元素的值只增不减——当所有元素都等于最大值时结束操作。其次应该想到的是,选出任意的一个子序列——每次只需将值最小的元素选出并进行操作。再化简,最大的操作次数即为:最小值增长到最大值需要的次数。
AC代码
点击查看代码
void Solve() {
cin >> n;
FOR(i, 1, n) cin >> a[i];
sort(a + 1, a + 1 + n);
cout << a[n] - a[1] << endl;
}
错误次数
无。
B - Make AP
小评
比较水的一题,速度较慢的原因在于思考未完全(推导出一个没用,需要考虑较多细节,导致解题速度下降。
⇔数学规律、⇔入门级(*900)
题意
给定三个正整数 \(a,b,c\) ,你可以选择其中的任意一个,将其乘上一个正整数 \(m\) ,使得这三个数字恰好构成一个等差数列。
思路
使用等差数列公式: \(a_{i-1} + a_{i+1} = 2 * a_i\)
核心思路在于,使用公式计算出目标值,判断目标值是否大于 \(0\)(由于原数字大于 \(0\) ,乘数大于 \(0\) ,故目标值一定大于 \(0\) ),判断乘数 \(m\) 是否为正整数(即目标值 \(\%\) 原值是否等于 \(0\) )。
计算公差
核心思路在于,计算出公差后目标值 ,其余同上。
AC代码
点击查看代码
void Solve() {
int x, y, z;
cin >> x >> y >> z;
if(2 * y - z > 0 && (2 * y - z) % x == 0) yes;
else if((x + z) % 2 == 0 && (x + z) / 2 > 0 && (x + z) / 2 % y == 0) yes;
else if(2 * y - x > 0 && (2 * y - x) % z == 0) yes;
else no;
}
错误次数
无。
C - Division by Two and Permutation
小评
思维题,其实难度不是很高。类似这题这样的全排列是否存在问题还有很多,下放给出比对链接。
⇔构造性算法、⇔数学规律、⇔入门级(*1100)
题意
对于给定的序列 \(a[1…n]\) ,一次操作可以使得任意的 \(a_i\) 变成 \(\left \lfloor \frac{a_i}{2} \right \rfloor\) ,求解最终能否使得序列成为 \(1\) 到 \(n\) 的一个全排列。
思路
由于需要得到一个全排列,所以序列的每一个数字都有用。可以考虑两种做法:快排、桶排序计算。
(题解做法)快排
先将序列降序排列,然后建立判断数组 \(used[1…n]\)(用于判断 \(1\) 到 \(n\) 是否有元素匹配)。现在开始遍历 \(a\) 序列——令 \(x = a[i]\) ,只要 \(x \ge n\) 或是 \(used[x] = \tt{false}\) (已有元素匹配),则将 \(x\) 整除 \(2\) 。存在下述两种情况:
- 得到 \(x = 0\) ,说明 \(a[i]\) 这个元素没有匹配,错误,跳出;
- 发现 \(used[x] = \tt{true}\) (暂无元素匹配),那么将其置为 \(\tt{false}\) 。
遍历完成后若未跳出,则正确。
(自己做法)桶排计算
先建立一个桶 \(count[1…n]\) ,用于记录数字的数量。先将读入的数字处理到 \(\le n\) ,然后放入桶中。现在开始降序遍历桶,存在下述两种情况:
- 如果某个桶为空,说明这个元素没有匹配,错误,跳出。
- 如果某个桶不为空,留下一个,剩余 \(count[i] - 1\) 个的全部加到 \(count[\left \lfloor \frac{a_i}{2} \right \rfloor]\) 桶中。
遍历完成后若未跳出,则正确。
AC代码1(快排)
点击查看代码
void Solve() {
cin >> n;
vector<int> a(n), used(n + 1, true);
for(auto &it : a) cin >> it;
sort(a.begin(), a.end(), [] (int x, int y) {
return x > y;
});
for(auto it : a) {
int x = it;
while(x > n || used[x] == false) x /= 2;
if(x > 0) used[x] = false;
else Ans = false;
}
if(Ans == false) no;
else yes;
}
AC代码2(桶排)
点击查看代码
void Solve() {
int count[55] = {};
cin >> n;
FOR(i, 1, n) {
cin >> x;
while(x > n) x /= 2;
count[x] ++;
}
FORD(i, 1, n) {
if(count[i] == 0) Ans = false;
count[i / 2] += count[i] - 1;
}
if(Ans == false) no;
else yes;
}
错误次数
(比赛 2 次)思路错误,尝试把一个数字能够变换出的所有数字都记录在桶中,若被使用,则从桶中全部删去。
D - Palindromes Coloring
小评
这是道题面非常恐怖的题目,乍一看涉及到了填色问题和回文子串的判断问题,导致我在卡 C 题不得,打算先做这道题的时候,看了几眼题面就果断放弃了,但实际上这道题的难度不大,与染色问题、回文子串问题关联都不大。所以开题的时候还是需要多观察观察再做决定。
⇔贪心、⇔排序、⇔字符串、⇔提高级(*1400)
题意
给定一串字符串,根据以下要求对字符进行染色:
- 一共能且只能染 \(k\) 种颜色;
- 有一些字符可以不染色,但你要尽可能多的染色;
- 取出相同颜色的字符,经过任意次排列后应当可以形成一个回文子串。
要求使得所有颜色的字符串中最短的那串的长度最长,输出这个长度。
思路
贪心(自己做法):
转化下题目:要求从给定的字符串中取出一些字符,组成 \(k\) 个回文子串。我们首先考虑回文子串的组成:若干偶数对字符 + 至多一个单个字符(如 \(\tt{aacbcaa}\) )。所以,我们先统计给定字符串中各个字符的数量,然后将偶数对平均的分给 \(k\) 个串(对于多余的偶数对,我们将其拆分成单个),再判断单个的字符串能否够每个串分到一个。
二分答案:
AC代码(贪心)
点击查看代码
void Solve() {
int n, k, count[30] = {}, num1 = 0, num0 = 0;
char x;
cin >> n >> k;
FOR(i, 1, n) {
cin >> x;
count[x - 'a' + 1] ++;
}
FOR(i, 1, 26) {
num1 += count[i] % 2;
num0 += count[i] / 2;
}
ans = num0 / k * 2; //将偶数对平均的分给 k 个串
num1 += num0 % k * 2; //多余的拆成单个
cout << ans + bool(num1 / k) << endl;
}
错误次数
(补题 4 次)未将多余的偶数对拆分。
G - MinOr Tree
小评
最后一题,但实际上难度不是很大,属于应该做出来的范围主要考察的是最小生成树的生成原理。
⇔位运算、⇔并查集、⇔图论、⇔贪心、⇔铜牌级(*1900)
题意
给定一张图,要求生成一棵生成树,使得这棵树的边权按位或最小。
思路
由于或运算是有一出一,所以要使得按位或最小,即要让选中边所含的 \(1\) 的数量最少,且位数越低越好。故我们先假设所有位数都是 \(1\) ,再按位数从高到低进行循环,寻找可以置为 \(0\) 的位置——假设某一位可以置为 \(0\) ,那么只可以选择这一位为 \(0\) 的边,我们只需判断(使用并查集),使用这些边能否组成连通图,若能,假设成立,删去未选择的边。
最后剩下的未被删去的边一定能组成一张连通图,且由于按位数从高到低循环,得到的答案一定是最优的。
AC代码1(使用 \(\tt{bitset}\) )
点击查看代码
//====================
LL n, num, fa[MAX], m;
bool Ans;
bitset<32> ans;
struct node{
int u, v;
bool flag;
bitset<32> B;
}ver[MAX];
//====================
void Clear() {
ans = 0; Ans = true;
ans.set();
}
void Prepare(int n) {//初始化
FOR(i, 1, n) fa[i] = i;
}
int get(int x) {
if(x == fa[x]) return x;
return fa[x] = get(fa[x]);
}
void merge(int x, int y) {
fa[get(x)] = get(y);
}
void Solve() {
cin >> n >> m;
FOR(i, 1, m) {
cin >> ver[i].u >> ver[i].v >> num;
ver[i].B = num;
ver[i].flag = true;
}
FORD(T, 0, 31) {
Ans = true;
Prepare(n);
FOR(i, 1, m) {
if(ver[i].B[T] == 0 && ver[i].flag == true) {
merge(ver[i].u, ver[i].v);
}
}
FOR(i, 2, n) {
if(get(i) != get(i - 1)) {
Ans = false;
}
}
if(Ans == false) continue; //不可以被删除,继续查找
ans.flip(T);
FOR(i, 1, m) {
if(ver[i].B[T] == 1) {
ver[i].flag = false;
}
}
}
cout << ans.to_ullong() << endl;
}
AC代码2(使用位运算)
点击查看代码
//====================
LL n, m, num, fa[MAX], ans;
bool Ans;
struct node{
int v, u, w;
bool flag;
}ver[MAX];
//====================
void Clear() {
ans = 0; Ans = true;
}
void Prepare(int n) {
FOR(i, 1, n) fa[i] = i;
}
int get(int x) {
if(fa[x] == x) return x;
return fa[x] = get(fa[x]);
}
void merge(int x, int y) {
fa[get(x)] = get(y);
}
void Solve() {
cin >> n >> m;
FOR(i, 1, m) {
cin >> ver[i].u >> ver[i].v >> ver[i].w;
ver[i].flag = true;
}
FORD(bit, 0, 31) {
Ans = true;
Prepare(n);
int judge = (1LL << bit);
FOR(i, 1, m) {
if(ver[i].flag == true && (ver[i].w & judge) == 0) {
merge(ver[i].u, ver[i].v);
}
}
FOR(i, 2, n) {
if(get(i) != get(i - 1)) {
Ans = false;
}
}
if(Ans == false) continue;
FOR(i, 1, m) {
if((ver[i].w & judge) != 0) {
ver[i].flag = false;
}
}
}
FOR(i, 1, m) {
if(ver[i].flag == true) {
ans |= ver[i].w;
}
}
cout << ans << endl;
}
错误次数
(补题,方法1,1 次)先将不符合的边删去后再计算是否能组成连通图,若不能,再恢复——这会导致此前已经确定删除的边被还原。
(补题,方法2,1 次)判断是否被删除的数组( \(\tt{flag} [1…n]\) )初始化错误。
文 / WIDA
2022.01.14 成文
首发于WIDA个人博客,仅供学习讨论
更新日记:
2022.01.14 成文