CodeTON Round 2 题解

· · 个人记录

也许更好的阅读体验

写在前面

时隔近两年,这个 Codeforces赛后题解 系列续更了(
但是估计后面很长一段时间又要咕咕咕了

终于没掉分了,还上了一波小分,感动。可惜 E 题没能完成绝杀。还是太菜了 /kk

CF1704A Two 0-1 Sequences

首先不要看错题,只有 a 序列是可以操作的,b 序列是不会变的。而 a 的操作又要删除元素,所以 m\le n 才能使 a 有可能 变成 b。(赛时看题目浏览得比较匆忙,没注意到题目保证 m \le n,所以代码中有一处特判,删去不影响)

由于最后 a 的长度从 n 变为 m,而每次操作只能改变第一个元素,所以原序列 ab 的后 m-1 位必须相同。

时间复杂度 $O(n)$,代码如下: ```cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 55; int n, m; char a[maxn], b[maxn]; void work() { scanf("%d%d", &n, &m); scanf("%s%s", a + 1, b + 1); if(n < m) //前面提到的特判,删去不影响。 { puts("No"); return; } for(int i = n; i >= n - m + 2; --i) if(a[i] != b[m + i - n]) { puts("No"); return; } for(int i = n - m + 1; i >= 1; --i) if(a[i] == b[1]) { puts("Yes"); return; } puts("No"); return; } int main() { int T; cin >> T; while(T--) work(); return 0; } ``` # [CF1704B Luke is a Foodie](https://www.luogu.com.cn/problem/CF1704B) 本题和 [CF1304C Air Conditioner](https://www.luogu.com.cn/problem/CF1304C) 的思想比较相似,不妨在切完本题后去切一下这题。 把题意抽象并且形象化一下,就是说,有一个区间 $[v-x, v+x]$ 可以通过改变 $v$ 的值移来移去,我们要尽可能地把 $a_i$ 框进去。 为了把 $a_1$ 框进去,$v$ 的范围为 $[a_1-x, a_1+x]$; 在不考虑 $a_1$ 的情况下,为了把 $a_2$ 框进去,$v$ 的范围为 $[a_2-x,a_2+x]$。 那么如果这两个限制区间有交集,我们就可以通过调整 $v$ 的初始值把 $a_1, a_2$ 一次性框进去,$v$ 的范围就是这两个区间的交集; 如果这两个限制区间没有交集,那么就必须改变 $v$ 的值,$ans$ 增加 $1$,总的限制区间设置为 $[a_2-x,a_2+x]$。 此后类似。 总结一下思路:计算把 $a_1,a_2,\cdots,a_k$ 都框进去的 $v$ 的限制区间 $[L,R]$。对其后的 $a_{k+1}$,若 $[a_{k+1}-x,a_{k+1}+x]$ 与 $[L,R]$ 有交集,那么就调整 $L$ 和 $R$;若没有交集,那么 `ans++`,$[L,R]$ 设为 $[a_{k+1}-x,a_{k+1}+x]$。 代码如下,时间复杂度 $O(n)$。 ```cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 2e5 + 10; int n; ll a[maxn], x; inline int read(); void work() { n = read(); x = read(); for(int i = 1; i <= n; ++i) a[i] = read(); ll L, R; int ans = 0; L = a[1] - x; R = a[1] + x; for(int i = 2; i <= n; ++i) { ll l = a[i] - x, r = a[i] + x; if(r < L || l > R) { ans++; L = l; R = r; continue; } L = max(L, l); R = min(R, r); } printf("%d\n", ans); return; } int main() { int T; cin >> T; while(T--) work(); return 0; } inline int read() { int x = 0; bool f = true; char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') f = false; ch = getchar(); } while(isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } return f ? x : -x; } ``` # [CF1704C Virus](https://www.luogu.com.cn/problem/CF1704C) 思考一下传染的过程:$m$ 个初始感染点会把 $n$ 个点分隔成 $m$ 个区间(当然有的区间长度为 $0$),每个区间在不加任何干预的情况下会每次感染最左端和最右端的点,不断向中间推进直至全部感染。 要让感染的点最少,也就是让健康的点最多。 怎么样才能让健康的点最多呢? ~~古人云:“求木之长者,必固其根本;欲流之远者,必浚其泉源;思国之安者,必积其德义。”要从源头抓起防患于未然。~~ 只要在每一段先堵上最左边的传染源再堵上最右边的传染源,就能让中间的点全部不被感染。 那么不难想到一个贪心的思想:先保最长的区间。 时间复杂度 $O(m\log m)$,代码见下: ```cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxm = 1e5 + 10; int n, m, a[maxm], b[maxm]; inline int read(); void work() { n = read(); m = read(); for(int i = 1; i <= m; ++i) a[i] = read(); sort(a + 1, a + m + 1); b[1] = n - a[m] + a[1] - 1; for(int i = 2; i <= m; ++i) b[i] = a[i] - a[i - 1] - 1; sort(b + 1, b + m + 1); reverse(b + 1, b + m + 1); //懒人式排序,从大到小 //其实是赛时智障了一不小心搞成从小到大排序了 //于是直接加了个reverse补救一下 int ans = 0, t = 0; for(int i = 1; i <= m; ++i) { ll inf = 2LL * t; //infected if(b[i] - inf <= 0) continue; if(b[i] - inf <= 2) //这种情况只能保一个点 { ans++; t++; continue; } ans += b[i] - inf - 1; //先堵上左边,然后损失一个点,再堵右边 t += 2; } cout << n - ans << endl; return; } int main() { int T; cin >> T; while(T--) work(); return 0; } inline int read() { int x = 0; bool f = true; char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') f = false; ch = getchar(); } while(isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } return f ? x : -x; } ``` # [CF1704D Magical Array](https://www.luogu.com.cn/problem/CF1704D) ~~一道 _Never Gonna Give You Up_ 式的 Rick Astley 诈骗题~~ 一道思维好题。 乍一看很难找到突破口——复原数组 $b$ 显然是不现实、不可行的。 那么思考的重点便是: * 所有由操作 $1$ 得到的数组,有什么共同的特征? * 由操作 $2$ 得到的数组,如何与操作 $1$ 得到的数组区别开来? * 既然要计数,那么能否把数组映射为一个数,从而既达到判定又达到计数的目的? 目标:将数组处理成一个值,使得进行一次操作 $1$ 不会改变这个值,而进行一次操作 $2$ 会将这个值增加 $1$。 这种想法属实是妙手偶得的神来之笔。 这其实是一个 hash 的思想,也就是,构建映射。 那么思路呼之欲出了:计算 $\sum\limits_{j=1}^n j \times c_j$。这个值满足我们的目标。 时间复杂度 $O(nm)$,代码如下: ```cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 1e5 + 5; int n, m; vector<ll> c[maxn]; ll v[maxn]; void work() { cin >> n >> m; for(int i = 1; i <= n; ++i) { c[i].clear(); c[i].resize(m + 1); } for(int i = 1; i <= n; ++i) for(int j = 1; j <= m; ++j) cin >> c[i][j]; fill(v + 1, v + n + 1, 0); for(int i = 1; i <= n; ++i) for(int j = 1; j <= m; ++j) v[i] += j * c[i][j]; ll mx = 0; int idx; for(int i = 1; i <= n; ++i) { if(v[i] > mx) { idx = i; mx = v[i]; } } cout << idx << ' '; for(int i = 1; i <= n; ++i) if(v[i] != mx) { ll ans = mx - v[i]; cout << ans << endl; break; } return; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin >> T; while(T--) work(); return 0; } ``` # [CF1704E Count Seconds](https://www.luogu.com.cn/problem/CF1704E) 赛时把题意读错了,凉了。 整个流程就好比是一个漫水的过程。 众所周知,在没有什么思路的时候,比较好的办法是手玩模拟一下样例。 不妨先思考没有点权为 $0$ 的情况:入度为 $0$ 的点只减不增,把自己的点权贡献给出边连向的点,点权减到 $0$ 之后删去,从而又有新的一批点只减不增…… **可不就是拓扑排序吗!** 只要把拓扑序在前面的点的点权一直传递下去到汇点,就结束了,这部分很好算。 但问题出现了:我们发现汇点有可能点权为0,这个时候不会把点权排出去,不能用上面的方法直接算。 然而经过 $n$ 轮后,所有的点的点权一定可以传递到汇点,假如在这前 $n$ 轮中不是所有点的点权都变成了 $0$,那么一定会使得汇点点权 $>0$。 上代码,复杂度 $O(nm+n^2)$。一些细节见代码。 ```cpp #include <bits/stdc++.h> using namespace std; const int maxn = 1010; const int mod = 998244353; int n, m; vector<int> g[maxn]; int in[maxn], a[maxn], add[maxn]; int ans; bool del[maxn]; queue<int> q; int tpo[maxn], tim; inline int read(); void bruteforce() /* 这里a和add的值都不要取模 因为可能涉及a是998244353的倍数的情况,取模后会出现错误 而且此处不取模是不会溢出的 */ { ans++; fill(add + 1, add + n + 1, false); for(int i = 1; i <= n; ++i) if(a[i] > 0) for(int v : g[i]) add[v]++; for(int i = 1; i <= n; ++i) if(a[i] > 0) a[i]--; for(int i = 1; i <= n; ++i) a[i] += add[i]; return; } void toposolve() { tim = 0; for(int i = 1; i <= n; ++i) if(in[i] == 0) q.push(i); while(!q.empty()) { int p = q.front(); q.pop(); tpo[++tim] = p; for(int v : g[p]) if((--in[v]) == 0) q.push(v); } for(int i = 1; i <= n; ++i) a[i] %= mod; for(int i = 1; i <= n; ++i) { for(int j : g[tpo[i]]) a[j] = (a[j] + a[tpo[i]]) % mod; } ans = (ans + a[tpo[n]]) % mod; return; } void work() { ans = 0; n = read(); m = read(); fill(in + 1, in + n + 1, 0); fill(del + 1, del + n + 1, false); for(int i = 1; i <= n; ++i) a[i] = read(); for(int i = 1; i <= n; ++i) g[i].clear(); for(int i = 1; i <= m; ++i) { int x, y; x = read(); y = read(); g[x].push_back(y); in[y]++; } for(int i = 1; i <= n; ++i) { bool flag = true; for(int j = 1; j <= n; ++j) if(a[j] != 0) { flag = false; break; } //记得check,可能没跑满n轮就结束了 if(flag) { cout << ans << endl; return; } bruteforce(); } toposolve(); cout << ans << endl; return; } int main() { int T; T = read(); while(T--) work(); return 0; } inline int read() { int x = 0; bool f = true; char ch = getchar(); while(!isdigit(ch)) { if(ch == '-') f = false; ch = getchar(); } while(isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } return f ? x : -x; } ``` # 写在后面 太难了后面的题, 大钟楼下送快递, 一边开摆一边寄。 ~~(押韵成功)~~ 做不来,不写了。