Codeforces Round #748 (Div. 3) 题解
A. Elections
题意
已知竞选中三个候选人的当前得票数 a , b , c a,b,c a,b,c,现在可以增加任何一个人的得票数,要求对于每个人,计算出能使得其得票数大于其余两人的最少增加票数。
思路
按照题意计算即可,假如要使得 a a a的票数最高,只需让 a a a加上 b , c b,c b,c中票数较高的人与 a a a的票数只差,再加上1即可。需要注意的是,如果某个人的得票数已经大于其余两人,应当输出0,而不是负数。
时间复杂度
O ( 1 ) O(1) O(1)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
A - Elections | GNU C++17 | Accepted | 15 ms | 0 KB |
#include <bits/stdc++.h>
using namespace std;
void solve() {
int a, b, c;
scanf("%d%d%d", &a, &b, &c); //每一个都独立计算
printf("%d %d %d\n", a > max(b, c) ? 0 : max(b, c) + 1 - a,
b > max(a, c) ? 0 : max(a, c) + 1 - b,
c > max(a, b) ? 0 : max(a, b) + 1 - c);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
B. Make it Divisible by 25
题意
给定一个十进制正整数,每次可以删除其中的任何一位,如果操作后会产生前导0,则会自动去除前导0。问至少删除多少位后,该数不i变成一个能被25整除的正整数。
思路
被25整除的正整数最大的特点是:十位和个位只有4种情况,即00、25、50、75。因此只需从后往前找,对于每种情况,找到第一个匹配的位置即可。
时间复杂度
O ( n ) O(n) O(n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
B - Make it Divisible by 25 | GNU C++17 | Accepted | 0 ms | 0 KB |
#include <bits/stdc++.h>
using namespace std;
void solve() {
char s[22];
scanf("%s", s);
int n = strlen(s);
int a, b, p1, p2;
p1 = p2 = -1;
for (int i = n - 1; i >= 0; --i) { //后两位25和75可以合并一起找
if (p1 == -1 && s[i] == '5') {
p1 = i;
continue;
}
if (p1 != -1 && (s[i] == '2' || s[i] == '7')) {
p2 = i;
break;
}
}
a = n - 1 - p2 - 1;
p1 = p2 = -1;
for (int i = n - 1; i >= 0; --i) { //后两位00和50可以合并一起找
if (p1 == -1 && s[i] == '0') {
p1 = i;
continue;
}
if (p1 != -1 && (s[i] == '0' || s[i] == '5')) {
p2 = i;
break;
}
}
b = n - 1 - p2 - 1;
printf("%d\n", min(a, b)); //题目保证有解,所以不需要担心答案是0或者无解的情况
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
C. Save More Mice
题意
在一个坐标轴上,有很多个老鼠,一只猫和一个洞,猫的起始位置是0,洞的位置是 n n n,所有老鼠的起始位置在0和 n n n之间,同一个位置可以有多个老鼠。每一个时刻,你可以将一个老鼠向右移动一个单位(坐标值+1),随后猫将向右移动一个单位,并吃掉这个位置上的所有老鼠。如果老鼠移动到了洞里,就不会再移动,也不会被吃掉。问最多能有多少个老鼠存活。
思路
在任何一个时刻,被选中的老鼠一定存活。在 n n n个时刻后,猫会吃掉所有能吃掉的老鼠。因此,我们只需尽可能多的将老鼠移动到洞里。采取贪心策略,排序后选择离洞最近的尽可能多的老鼠,使得它们与洞的距离之和小于 n n n。
时间复杂度
O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
C - Save More Mice | GNU C++17 | Accepted | 124 ms | 1600 KB |
#include <bits/stdc++.h>
using namespace std;
int a[400005];
void solve() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < k; ++i) {
scanf("%d", &a[i]);
}
sort(a, a + k, greater<int>()); //要选择离洞近的,所以从大到小排序
int ans = 0, s = n; //ans记录有几个老鼠存活,s记录还剩多少距离
for (int i = 0; i < k; ++i) {
if (s > n - a[i]) ++ans, s -= n - a[i];
else break;
}
printf("%d\n", ans);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
D1. All are Same
题意
给出一个序列,要求选定一个正整数 k k k,每次操作可以选择序列中的任何一个元素,并将其减少 k k k,这一操作可以进行任意次数。要求在经过若干操作之后,序列中的每一个元素都相等,求最大的可能的 k k k,若 k k k可以取任意大的值,输出-1。
思路
首先我们可以发现,当序列元素在初始状态下就已经相等时, k k k可以任意大。
对于存在不同值的序列,在若干次操作后,序列中的元素一定可以变成序列最小值。
证明:设最终相等的值为 x x x,序列最小值为 m m m:若 x > m x>m x>m,由于 k > 0 k>0 k>0,序列元素只减不增,则一定存在一项 m ′ ≤ m m'\le m m′≤m,一定不能满足 m ′ = x m'=x m′=x;若 x < m x<m x<m,则一定有 x = m − n ⋅ k , n ∈ N ∗ x=m-n\cdot k,n\in N^* x=m−n⋅k,n∈N∗,因此取 x ′ = m x'=m x′=m同样符合题意。
对于任意序列元素 a i ≠ m a_i\not=m ai=m,当满足 k ∣ ( a i − m ) k|(a_i-m) k∣(ai−m)(|表示整除)时,可以经过若干次操作使 a i a_i ai变为 m m m。为了求出满足所所有序列元素的最大 k k k值,我们只要求出 a i − m a_i-m ai−m的最大公因数即可。求最大公因数(gcd)可以手写欧几里得算法,也可以直接调用C++库函数__gcd(x,y),该函数在<algorithm>头文件中。
时间复杂度
O ( n l o g 2 a i ) O(nlog_2a_i) O(nlog2ai)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
D1 - All are Same | GNU C++17 | Accepted | 15 ms | 0 KB |
#include <bits/stdc++.h>
using namespace std;
int a[100];
void solve() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
int minm = a[0], maxm = a[0]; //找到序列最大最小值
for (int i = 1; i < n; ++i) {
minm = min(minm, a[i]);
maxm = max(maxm, a[i]);
}
if (minm == maxm) { //最大最小值相等意味着序列元素完全相同
puts("-1");
return;
}
int ans = maxm - minm; //使用最大值-最小值可以避免ans初值为0的问题
for (int i = 0; i < n; ++i) {
if (a[i] != minm) ans = __gcd(ans, a[i] - minm); //求最大公因数
}
printf("%d\n", ans);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
D2. Half of Same
题意
给出一个序列,要求选定一个正整数 k k k,每次操作可以选择序列中的任何一个元素,并将其减少 k k k,这一操作可以进行任意次数。要求在经过若干操作之后,序列中的至少一半的元素相等,求最大的可能的 k k k,若 k k k可以取任意大的值,输出-1。
思路
这题和上一题的大致思路是相似的,但是难度大很多。观察发现序列长度 n n n的范围只有40,考虑枚举操作结束后相等元素的值(也就是枚举相当于上一题中最小值的那个元素),设计状态 d p [ i ] dp[i] dp[i],表示在最终有 i i i个相等的数的情况下,可能的最大公因数值的集合,随后找出 i ≥ n 2 i\ge \frac{n}{2} i≥2n时的最大值。
时间复杂度
O ( n 4 l o g 2 a i ) / O ( n 4 l o g 2 n l o g 2 a i ) O(n^4log_2a_i)/O(n^4log_2nlog_2a_i) O(n4log2ai)/O(n4log2nlog2ai)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
D2 - Half of Same | GNU C++17 | Accepted | 30 ms | 200 KB |
#include <bits/stdc++.h>
using namespace std;
int a[100];
set<int> dp[100]; //其实是应该用unordered_set的,但当时用了set
map<int, int> mp;
void solve() {
mp.clear();
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
++mp[a[i]]; //统计每个数初始状态下有多少个
}
for (auto &it: mp) {
if (it.second >= n / 2) { //初始状态下已经满足条件,k可取任何值
puts("-1");
return;
}
}
int ans = 0;
for (int i = 0; i < n; ++i) { //这层循环枚举最小的数
for (int j = 0; j < n; ++j) dp[j].clear();
int num = 0; //统计原本就等于这个数的元素数量
for (int j = 0; j < n; ++j) if (a[j] == a[i]) ++num;
for (int j = 0; j < n; ++j) {
if (a[j] > a[i]) { //对于每个可能减小到a[i]的元素都进行处理
for (int k = n - 1; k > 0; --k) { //必须从大到小迭代相等元素的个数,进行递推
for (int l: dp[k]) { //集合元素的枚举,也就是状态的转移过程
dp[k + 1].insert(__gcd(l, a[j] - a[i])); //(unordered_)set可以去重,加速运算
}
}
dp[1].insert(a[j] - a[i]);
}
}
for (int j = n / 2 - num; j <= n; ++j) { //由于我是set,所以最后一个元素就是最大值,如果用的是unordered_set,就需要遍历所有元素取最大值
if (!dp[j].empty()) ans = max(ans, *--dp[j].end());
}
}
printf("%d\n", ans);
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
E. Gardener and Tree
题意
给出一棵树,每次操作删除树上的所有叶子节点,问经过 k k k次操作后,还剩多少节点。(树是无根树,空树经过操作后还是空树)
关于树和叶子节点,以及下面提到的度的概念,不理解者请自行找资料解决,在此不做说明。
思路
我们维护树上所有节点的度数,除了仅有一个节点的树以外,当且仅当节点的度数为1时,该节点是叶子节点。考虑类似拓扑排序的方法,使用bfs解决本题。初状态下将所有1度节点加入队列,然后处理队列中的所有节点,删除与之相连的边(只有一条),并检查与之相连的另一节点在删边后的度数,如果删边后该节点变为1度节点,则新增叶子节点,加入队列。另外需要记录节点的bfs层数,由于只操作 k k k次,当bfs层数达到 k k k后,就不需要再进行操作。
题目坑点:只有1个节点的情况需要特判!
时间复杂度
O ( n ) O(n) O(n)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
E - Gardener and Tree | GNU C++17 | Accepted | 467 ms | 18100 KB |
#include <bits/stdc++.h>
using namespace std;
vector<int> g[400005]; //g是邻接表,存储图的信息
int vis[400005], in[400005]; //in记录的是节点的度(我沿用了拓扑排序中入度的写法)
void solve() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) g[i].clear();
memset(vis, 0, (n + 5) << 2);
memset(in, 0, (n + 5) << 2);
int x, y;
for (int i = 0; i < n - 1; ++i) {
scanf("%d%d", &x, &y); //图的读入
g[x].push_back(y); //加边
g[y].push_back(x);
}
queue<int> q;
int ans = 0; //ans是被删掉的点的数量
for (int i = 1; i <= n; ++i) {
in[i] = g[i].size(); //初始化度数
if (in[i] <= 1) { //bfs的初始节点集
vis[i] = 1;
++ans;
q.push(i);
}
}
while (!q.empty()) {
int p = q.front();
q.pop();
if (vis[p] >= k) continue; //剪枝
for (int i: g[p]) {
if (!vis[i]) {
--in[i]; //删边
if (in[i] == 1) {
vis[i] = vis[p] + 1;
++ans;
q.push(i);
}
}
}
}
printf("%d\n", n - ans); //输出的时候要用总节点数减去删掉的节点数
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
F. Red-Black Number
题意
给出一个位数为 n n n的十进制非负整数 x x x(可能有前导0),另给两个正整数 A A A和 B B B,要求将 x x x的每一位涂成红色或黑色,每种颜色都至少有一位数字,使得同种颜色的数字按原本顺序组合后,红色数字组成的数能被 A A A整除,黑色数字组成的数能被 B B B整除。答案输出使得红色数字个数与黑色数字个数最接近的一种涂色方式,若有多个答案可以输出任意一个,若无解,输出-1。
思路
观察发现 n , A , B n,A,B n,A,B的范围均只有40,考虑 n 4 n^4 n4的四维dp。设计状态 d p [ i ] [ j ] [ k ] [ l ] dp[i][j][k][l] dp[i][j][k][l]:对于前 i i i个数字,红色数字组成的数模 A A A得 j j j,黑色数字组成的数模 B B B得 k k k,其中红色数字有 l l l个。初始状态为 d p [ 0 ] [ 0 ] [ 0 ] [ 0 ] dp[0][0][0][0] dp[0][0][0][0]。
递推时,将 i i i进行迭代,每次迭代枚举每一个有效的 j , k , l j,k,l j,k,l,并枚举这一位涂的颜色(红或黑),记录操作(涂的颜色)以及来源路径(上一层中的位置),详见代码
完成递推后,枚举红色数字个数与黑色数字个数之差的绝对值,若存在有效状态 d p [ n ] [ 0 ] [ 0 ] [ l ] dp[n][0][0][l] dp[n][0][0][l],则说明该情况成立,通过前序dfs回溯路径,输出答案。
时间复杂度
O ( n 4 ) O(n^4) O(n4)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
F - Red-Black Number | GNU C++17 | Accepted | 109 ms | 22100 KB |
#include <bits/stdc++.h>
using namespace std;
int x[50];
pair<int, int> dp[41][41][41][41]; //dp状态,first记录涂红色还是黑色,second记录路径来源
int n, a, b;
void display(int i, int j, int k, int l) { //前序遍历回溯路径
if (!i) return;
if (dp[i][j][k][l].first) display(i - 1, dp[i][j][k][l].second, k, l - 1);
else display(i - 1, j, dp[i][j][k][l].second, l);
printf("%c", dp[i][j][k][l].first ? 'R' : 'B');
}
void solve() {
scanf("%d%d%d", &n, &a, &b);
for (int i = 0; i < n; ++i) scanf("%1d", &x[i]); //输入时直接按位分解存储
memset(dp, -1, sizeof dp); //清空数组
dp[0][0][0][0].first = -2;
for (int i = 0; i < n; ++i) { //枚举数位
for (int j = 0; j < a; ++j) { //枚举红色数的模数
for (int k = 0; k < b; ++k) { //枚举黑色数的模数
for (int l = 0; l <= i; ++l) { //枚举红色数字个数
if (dp[i][j][k][l].first != -1) { //尝试转移
if (dp[i + 1][(j * 10 + x[i]) % a][k][l + 1].first == -1) //涂红色
dp[i + 1][(j * 10 + x[i]) % a][k][l + 1] = make_pair(1, j);
if (dp[i + 1][j][(k * 10 + x[i]) % b][l].first == -1) //涂黑色
dp[i + 1][j][(k * 10 + x[i]) % b][l] = make_pair(0, k);
}
}
}
}
}
for (int i = 0; i <= n / 2; ++i) { //枚举红黑数字个数差的绝对值
if (n / 2 + i < n) {
if (dp[n][0][0][n / 2 + i].first != -1) {
display(n, 0, 0, n / 2 + i);
puts("");
return;
}
}
if (n / 2 - i > 0) {
if (dp[n][0][0][n / 2 - i].first != -1) {
display(n, 0, 0, n / 2 - i);
puts("");
return;
}
}
}
printf("-1\n");
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
G. Changing Brackets
题意
给出一个字符串 s s s,仅由字符’(’,’)’,’[’,’]‘组成,允许以0的代价翻转括号(也就是’(‘和’)‘互换,’[‘和’]'互换),允许以1的代价将任何形式的圆括号变成方括号,但不允许将方括号变成圆括号。
q q q次询问,每次询问,求对于给定的正整数 l , r l,r l,r,将子串 s [ l . . r ] s[l..r] s[l..r]转换为正则括号序列(括号完全匹配)的最小代价。
思路
Idea by REXWind
首先给出结论:统计奇数位置和偶数位置上方括号的个数,当且仅当子串中奇数位置的方括号数等于偶数位置的方括号数时,序列可以形成正则括号序列。
不难发现,括号的方向不影响答案(因为代价是0)。由于圆括号可以变成方括号,所以对答案造成影响的应当是方括号的位置和数量,圆括号根据方括号的需求进行变换。
假设有两个方括号,都位于奇数位置上(或都位于偶数位置上),那么这两个方括号之间的括号序列不可能形成正则序列。
证明:两个方括号之间的这段序列的长度是奇数,因此方括号和圆括号的个数中一定有一个是奇数,奇数个括号不可能完成匹配。
假设有两对方括号,其中两个在奇数位置,两个在偶数位置:
- 如果是嵌套关系(例如方括号位置是 1 , 5 , 8 , 10 1,5,8,10 1,5,8,10),如果仅考虑内层两个方括号之间的序列( s [ 5..8 ] s[5..8] s[5..8]),若该序列是正则序列,则整个序列也是正则序列(因为内外层之间的圆括号数( s [ 2..4 ] + s [ 9..9 ] s[2..4]+s[9..9] s[2..4]+s[9..9])是偶数,且中间没有插入单独的方括号,一定可以完成匹配);
- 如果是并列关系(例如方括号位置是 1 , 4 , 7 , 12 1,4,7,12 1,4,7,12),如果两对方括号之内的序列( s [ 1..4 ] s[1..4] s[1..4]和 s [ 7..12 ] s[7..12] s[7..12])是正则序列,则整个序列也是正则序列(因为两个正则序列之间的圆括号数( s [ 5..6 ] s[5..6] s[5..6])是偶数,且中间没有插入单独的方括号,一定可以完成匹配)。
使用归纳法可证,如果一个序列中奇数位置的方括号数和偶数位置的圆括号数相等,则序列是正则序列。
回到询问,暴力统计子序列中的奇数和偶数位置方括号数显然不可取,前缀和优化之。
每次询问,对前缀和作差,求出奇偶位置的方括号数,作差取绝对值,即是答案。
时间复杂度
O ( n + q ) O(n+q) O(n+q)
AC代码
Problem | Lang | Verdict | Time | Memory |
---|---|---|---|---|
G - Changing Brackets | GNU C++17 | Accepted | 140 ms | 8800 KB |
#include <bits/stdc++.h>
using namespace std;
char s[1000005];
int a[2][1000005]; //奇偶位置方括号的前缀和
void solve() {
scanf("%s", s + 1);
int n = strlen(s + 1);
for (int i = 1; i <= n; ++i) { //预处理前缀和
if (s[i] == '[' || s[i] == ']') a[i & 1][i] = a[i & 1][i - 1] + 1;
else a[i & 1][i] = a[i & 1][i - 1];
a[i & 1 ^ 1][i] = a[i & 1 ^ 1][i - 1];
}
int q;
scanf("%d", &q);
while (q--) {
int l, r;
scanf("%d%d", &l, &r); //每次询问O(1)输出
printf("%d\n", abs((a[0][r] - a[0][l - 1]) - (a[1][r] - a[1][l - 1])));
}
}
int main() {
// freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
后记
本FW又双叒叕AK失败了呜呜呜。。