出题人题解(补档,之前修改后被吞了)
A mutsumi的质数合数
数学
做法一:观察数据范围为 1 到 100 ,因此我们可以直接手算或用程序算出所有的质数和合数,然后算出质数数量和合数数量,最后相加。
做法二:根据质数和合数的性质,只有 1 不是质数,也不是合数,因此质数数量加合数数量就是数字个数减 1 的个数。
原本没有这题,然后删除了另一个水题(那个题,不需要了),就把这题加进来让大家开心(WA)一下。
#include<bits/stdc++.h> using namespace std; int main(){ int n; cin >> n; int ans = 0; for(int i = 1; i <= n; i++){ int x; cin >> x; if(x != 1) ans++; } cout << ans << endl; }
B tomorin的字符串迷茫值
数数,字符串
首先,只有"mygo","m_ygo","my_go","myg_o","m_y_go","m_yg_o","my_g_o","m_y_g_o"这 8 种子串可以通过删除得到"mygo"子串。
那么,我们可以枚举上述 8 种子串,子串内的删除方法是唯一确定的,子串两边可以任意删除,这个子串对答案的贡献就是两边任意删除的方案数相乘。而子串两边任意删除的方案数可以通过动态规划预处理(其实是斐波那契数列)。
(前面的区域,以后再来探索吧!)
#include<bits/stdc++.h> using namespace std; const int M = 1e9 + 7; int main(){ string s; cin >> s; int n = s.size(); s = " " + s + "BanG Dream! It's MyGO!!!!!"; vector<int> f(n + 1, 1); f[1] = 2; for(int i = 2; i <= n; i++){ f[i] = (f[i - 1] + f[i - 2]) % M; } vector<string> t = {"mygo", "m ygo", "my go", "myg o", "m y go", "m yg o", "my g o", "m y g o"}; int ans = 0; for(int i = 1; i <= n; i++){ for(auto &j : t){ auto x = s.substr(i, j.size()); auto check = [&](auto s, auto t){ for(int i = 0; i < s.size(); i++){ if(t[i] == ' ') continue; if(s[i] != t[i]) return 0; } return 1; }; if(check(x, j)) ans = (ans + 1ll * f[i - 1] * f[n - (i + j.size() - 1)] % M) % M; } } cout << ans << endl; }
C anon的私货
贪心
一个贪心的想法是在第 1 个数字前面夹带 个 0 ,然后 变成 1 。接下来第两个数字开始,在两个数字之间夹带 个 0 (计为 ),并且将 修改为 。最后在最后一个数字后夹带 个 0 。
证明:显然,易证,不会,看不懂,能过就是对的。
夹带私货,看这场的主题就能看出来夹带了多少。
#include<bits/stdc++.h> using namespace std; int main(){ int n; cin >> n; vector<int> a(n + 2); auto ans = 0ll; for(int i = 1; i <= n; i++){ cin >> a[i]; } a[0] = a[n + 1] = 2e9; for(int i = 1; i <= n; i++){ int l = min(a[i], a[i - 1]); ans += l - 1; a[i] -= l - 1; a[i - 1] -= l - 1; int r = min(a[i], a[i + 1]); ans += r - 1; a[i] -= r - 1; a[i + 1] -= r - 1; } cout << ans << endl; }
D soyorin的树上剪切
贪心,图论,搜索,DP
首先算出 到 的经过的边数,计为 。
1) 在 小于 时,我们可以从大到小贪心删掉经过的边中边权最大的边。
2) 在 等于 时,我们留下了一条经过的边中,边权最小的边,并且剩余了一次操作。
3) 在 等于 时,根据上一种情况,剩余了两次操作,我们可以通过两次操作把距离 或 不超过 1 的边置换到 和 之间。
如图,先选择 ,删除 连接 ,再选择 ,删除 连接 ,此时 1 到 2 的距离为 1 。
4) 当 大于 时,我们可以留两次操作用于上述置换,多余操作(计为 次操作)可以将距离 或 不超过 的节点缩到距离 或 不超过1的位置(类似第一类操作)。
本题为2023ICPC沈阳J题改编。原题题意是无边权无自环无重边的无向图,A和B轮流操作,每次按这个题意操作一条边,无法操作的人输,要求每次操作必须改变树的形状,与编号无关。
剪切线为什么是神?
#include<bits/stdc++.h> using namespace std; int main(){ int n, s, t; cin >> n >> s >> t; vector ve(n + 1, vector<pair<int, int>>()); for(int i = 1; i < n; i++){ int u, v, w; cin >> u >> v >> w; ve[u].push_back({v, w}); ve[v].push_back({u, w}); } vector<int> c(n + 1); c[t] = 1; vector<int> dis; auto dfs = [&](auto dfs, int u, int fa) -> void{ //先染红必经之路 if(c[u]) return; for(auto &[i, w] : ve[u]){ if(i == fa) continue; dfs(dfs, i, u); if(c[i]){ c[u] = 1; dis.push_back(w); } } }; dfs(dfs, s, 0); for(int i = 1; i <= n; i++){ //把除s外所有红点的相邻非红色点加入s的邻边 if(c[i] && i != s){ for(auto &[j, w] : ve[i]){ if(c[j]) continue; ve[s].push_back({j, w}); } } } vector<int> dp(n + 1, 1e9), deep(n + 1); //dp[i]为距离s不超过i,最短的路径长度 auto dfs1 = [&](auto dfs1, int u, int fa) -> void{ for(auto &[i, w] : ve[u]){ if(i == fa) continue; if(c[i]) continue; deep[i] = deep[u] + 1; dp[deep[i]] = min(dp[deep[i]], w); dfs1(dfs1, i, u); } }; dfs1(dfs1, s, 0); sort(dis.begin(), dis.end()); auto ans = accumulate(dis.begin(), dis.end(), 0ll); int k = dis.size(); dis.push_back(0); while(dis.size() > 1){ //先删必经路径上最大的 ans -= dis.back(); dis.pop_back(); cout << ans << " "; } for(int i = k; i <= n; i++){ //经过两次额外交换,能换进来的最小的边 ans = min(ans, 1ll * dp[i - k]); cout << ans << " "; } cout << endl; }
E soyorin的数组操作(easy)
贪心
如果数组长度是偶数,则一定可以,每次选择整个数组就可以使相邻两项后一项减前一项的差值增加 1 。
如果数组长度是奇数,则倒数第二个数最多能被操作的次数可以被计算出来,操作后,倒数第四个数最多能被操作的次数也可以被计算出来,依次类推,所有偶数位最多能被操作的次数都能计算出来。所有操作完成后,判断数组是否有序即可。
当然,我们每次操作不能直接暴力加,而是要记录一个操作次数 ,遍历到 时就是加上 。比较推荐从后往前操作,因为前面操作次数可能比后面多。
正着也能做,但似乎会爆 ,要用 。
#include <bits/stdc++.h> using namespace std; using LL = long long; int main() { int T; cin >> T; while (T--) { int n; cin >> n; vector<LL> a(n + 1); for (int i = 1; i <= n; i++) { cin >> a[i]; } LL cnt = 0; if (n % 2 == 0) goto yes; for (int i = n - 1; i >= 2; i -= 2) { a[i] += cnt * i; a[i - 1] += cnt * (i - 1); if (a[i] > a[i + 1]) goto no; LL t = (a[i + 1] - a[i]) / i; a[i] += t * i; a[i - 1] += t * (i - 1); cnt += t; } if (is_sorted(a.begin(), a.end())) goto yes; goto no; if (0) yes: cout << "YES" << endl; if (0) no: cout << "NO" << endl; } }
F soyorin的数组操作(hard)
贪心
如果数组长度是偶数,按上述结论一定可以,那么我们每次都操作整个数组即可,这个次数也很容易确定。
如果数组长度时奇数,那么在有解的情况下,上述操作求出的次数 是一个可行的最大值,也就是说,我们的操作次数最多为 ,每个数能被加到的最大数字也能计算出来。那么,我们枚举到 时,只需要增加 的必要的操作次数即可。
在有解的情况下,直接枚举 的最大值就是答案,这种写法就不需要对数组进行操作。
CF某一题改的,原题挺简单的,具体题意忘了。
#include <bits/stdc++.h> using namespace std; using LL = long long; int main() { int T; cin >> T; while(T--){ int n; cin >> n; vector<LL> a(n + 1); for (int i = 1; i <= n; i++) { cin >> a[i]; } if (n & 1) { LL cnt = 0; LL t = 0; auto b = a; { LL cnt = 0; for (int i = n - 1; i >= 2; i -= 2) { b[i] += cnt * i; b[i - 1] += cnt * (i - 1); if (b[i] > b[i + 1]) goto no; LL t = (b[i + 1] - b[i]) / i; b[i] += t * i; b[i - 1] += t * (i - 1); cnt += t; } if (!is_sorted(b.begin(), b.end())) goto no; } for (int i = n - 1; i > 1; i--) { t = max(t, a[i - 1] - a[i]; } cout << t << endl; } else { LL cnt = 0; for (int i = n; i >= 1; i--) { cnt = max(cnt, a[i - 1] - a[i]); } cout << cnt << endl; } if (0) no: cout << -1 << endl; } }
G sakiko的排列构造(easy)
图匹配
我们可以把数和位置分成左右两个部分,就可以在数和位置之和为质数的两端连接一条边,之后我们直接跑网络流进行最大匹配(或者匈牙利)即可。
如果 为偶数,若存在合法的排列,那么一定是奇数和偶数位置、偶数和奇数位置进行配对。若存在奇数和奇数位置配对,那么一定会存在偶数和偶数位置配对,那么构造一定会失败。那么奇数和偶数位置、偶数和奇数位置进行配对一定是对称的,因此我们其实只需要将奇数和偶数分成两边,这样进行最大匹配会更快且更方便。
如果 为奇数,那么在奇数和偶数位置、偶数和奇数位置配对的前提下,一定会有一个奇数和奇数位置配对,这个奇数和位置之和一定是偶数,是质数的偶数有且仅有一个 2 ,因此这个奇数和奇数位置必须都是 1 。在排除1的情况下,直接按 是偶数进行处理即可。
#include <bits/stdc++.h> using namespace std; template <typename T> class Dinic{ public: struct edge{ int to; //到达点 T flow; //边权流量 int re; //反向边下标 }; vector<vector<edge>> ve; vector<vector<int>> true_edge; vector<int> deep, cur; //深度,以及弧优化 int n, S, E; const T inf = INT_MAX; Dinic(int n, int S, int E) : n(n) , S(S) , E(E){ ve.resize(n + 1); } void add(int u, int v, T w){ true_edge.push_back({u, v, (int)ve[u].size()}); //正向边及下标,用于求最小割 ve[u].push_back({v, w, (int)ve[v].size()}); //正向边 ve[v].push_back({u, 0, (int)ve[u].size()-1}); //反向边 } bool bfs(){ deep = move(vector<int>(n + 1, 0)); //初始化,因为每次 bfs 后 deep 不一定相同 cur = move(vector<int>(n + 1, 0)); //弧优化归 0 queue<int> q; q.push(S); deep[S] = 1; while(q.size()){ auto u = q.front(); q.pop(); if(u == E) return true; //可达,则有增广路,存在流量 for(auto &[i, j, k] : ve[u]){ if(deep[i] || !j) continue; deep[i] = deep[u] + 1; q.push(i); } } return false; } T dfs(int u, T in){ if(u == E) return in; //增广路最终的有效流量 T out = 0; //流出的流量 for(int i = cur[u]; i < ve[u].size(); i++){ //从还有流量的边开始走 auto &[v, flow, re] = ve[u][i]; cur[u] = i; //更新弧优化 if(!flow || deep[v] != deep[u] + 1) continue; T res = dfs(v, min(in , flow)); //向下一层流量限制为获得的流量和可输出的流量中小的那一个 flow -= res; //本条边可输出的流量减少了 res ve[v][re].flow += res; //反向路径可输出流量增加了 res in -= res; //获得的流量消耗了 res out += res; //当前结点可输出的流量增加了 res if(!in) break; //获得流量消耗完了,没必要往下走了 } if(!out) deep[u] = 0; //当前结点输出不了流量了,下次不用进来了 return out; //返回输出的流量 } T max_flows(){ T sum = 0; while(bfs()){ sum += dfs(S,inf); } return sum; } vector<pair<int, int>> min_cuts(){ vector<pair<int, int>> ans; for(auto &v : true_edge){ //v[0]~v[2] 分别记录有向边起点,终点,边的下标 if(!ve[v[0]][v[2]].flow) //当正向边流量为0时,说明是最小割 ans.push_back({v[0], v[1]}); } return ans; } }; int main(){ int n; cin >> n; Dinic<int> dinic(n + 2, 0, n + 1); int start = 1; int d = -1; if(n & 1){ start = 3; d = 1; } for(int i = 2; i <= n; i += 2){ dinic.add(0, i, 1); dinic.add(i + d, n + 1, 1); for(int j = start; j <= n; j += 2){ int x = i + j; int isp = 1; for(int i = 3; i * i <= x; i++){ if(x % i == 0) isp = 0; } if(isp) dinic.add(i, j, 1); } } assert(dinic.max_flows() == n / 2); auto v = dinic.min_cuts(); vector<int> a(n + 1, 1); for(auto &[x, y] : v){ if(x <= n && x > 0 && y <= n && y > 0){ a[x] = y; a[y] = x; } } for(int i = 1; i <= n; i++){ cout << a[i] << " "; } cout << endl; }
H sakiko的排列构造(hard)
打表
方法一:
1) 若 为 2 ,或 为 4 ,由于 3 和 5 都是质数,那么我们可以发现,只需要构造 到 即可;若 为 3 ,或 为 5 ,由于 5 和 7 都是质数,那么我们可以发现,在排除掉 1 后,只需要构造 到 2 即可。也就是说,如果 中有一个质数,我们就可以直接得到答案。
2) 若 为 8 ,由于 3 和 11 都是质数,我们可以先构造 2 到 1 ,再构造 8 到 3 即可;若 为 7 ,由于 5 和 11 都是质数,排除掉 1 后,我们可以先构造 3 到 2 ,再构造 7 到 4 即可。也就是说,如果能找到一个断点 ,使得 两个数都是质数,则我们就可以直接得到答案。
根据打表可以发现,若 不满足第一种情况,则一定满足第二种情况,而且断点出现的位置不会超过1000,所以可以直接暴力判断是否为质数,不用使用筛法。
方法二:
由于 到 $2n$ 之间一定有质数,因此我们可以找到 $n$ 后面第一个质数 $x$ ,把 $n$ 与 $x-n$ 配对, $n-1$ 与 $x-n+1$ 配队,将整个问题规约成一个子问题,再对子问题进行递归。而且实际上 $x$ 和 $n$ 非常接近,最多只会差不到 100 ,因此不会超时。
兰子出过一题使得 都不是质数的排列构造,然后反过来这题存了一年多了。
#include<bits/stdc++.h> using namespace std; int main(){ int n; cin >> n; auto check = [&](int x){ for(int i = 2; i * i <= x; i++){ if(x % i == 0) return 0; } return 1; }; int t = 1; if(n & 1){ cout << 1 << " "; t++; } for(int i = t; i < n; i++){ if(check(t + i) && check(i + 1 + n)){ for(int j = i; j >= t; j--){ cout << j << " "; } for(int j = n; j > i; j--){ cout << j << " "; } cout << endl; return 0; } } for(int i = n; i >= t; i--){ cout << i << " "; } cout << endl; }
I rikki的最短路
分类讨论
1) rikki走到 的过程看不到 ,那么先走到 ,再走到 ,再走到 。距离为 。
2) rikki走到 的过程看到了 ,那么先走到 ,再走到 。距离为 。
注意不爆 即可。
本来是出给米哈游的,把 rikki 换成旅行者, anon 换成仙灵, tomorin 换成仙灵装置就是原来的题目,然后想了想换过来了,事实上可能给米哈游会更好一点,并且把 anon 换成 rana 变成抓猫猫可能更合适一点。这场的主题完全没有 rana ,主要是 rana 的台词不够劲。
#include<bits/stdc++.h> using namespace std; using LL = long long; int main(){ LL t, a, k; cin >> t >> a >> k; LL ans = 0; if(t * a >= 0){ t = abs(t); a = abs(a); if(a <= t) ans = t; else ans = t + (a - t) * 2; } else{ if(a > k || a < -k) ans = abs(t) * 3 + abs(a) * 2; else ans = abs(a) * 2 + abs(t); } cout << ans << endl; }
J rikki的数组陡峭值
贪心+DP
如果所有区间的交集不为空,则在交集中任取一个值陡峭值都会为0。
如果所有区间的交集为空,那么可以将其拆分成一些连续且独立的交集,并且相邻两个交集之间一定不相交(否则这两个交集可以直接合并成一个交集),那么每个交集内部的陡峭值都是0。由于相邻交集是不相交的,因此我们可以用动态规划解决交集之间的陡峭值。
现在的问题是:怎么拆分成这些连续且独立的交集?我们可以贪心的按顺序选择交集。首先,第一个区间一定在第一个交集里,第二个区间如果和第一个交集相交,则我们更新第一个交集,否则我们将第二个区间变成第二个交集,以此类推。
证明:不会,类似取区间,与后面求交一定不会更优,与前面求交一定不会更劣。结论是对于一个区间,如果前后两个交集都已知,这个区间既跟前一个交集相交又跟后一个交集相交,但前后两个交集不相交。那么这个区间一定包含了在前后两个交集之间空出的一部分,所以这个区间放在前面和后面都不会影响两个交集之间的陡峭值,但如果放在后面,可能会导致交集变小,导致后续增加区间的数量减少。
兰子已经出过一万个数组陡峭值了,所以可能大家非常熟悉。
#include<bits/stdc++.h> using namespace std; int main(){ int n; cin >> n; vector<pair<int, int>> ve(n); for(auto &[l, r] : ve){ cin >> l >> r; } int L = 1, R = 1e9; vector<vector<int>> v; for(auto &[l, r] : ve){ if(L > r || R < l){ v.push_back({L, R}); L = l; R = r; } L = max(L, l); R = min(R, r); } v.push_back({L, R}); vector dp(v.size(), vector(2,0ll)); for(int i = 1; i < v.size(); i++){ dp[i][0] = min(dp[i - 1][0] + abs(v[i][0] - v[i - 1][0]), dp[i - 1][1] + abs(v[i][0] - v[i - 1][1])); dp[i][1] = min(dp[i - 1][0] + abs(v[i][1] - v[i - 1][0]), dp[i - 1][1] + abs(v[i][1] - v[i - 1][1])); } cout << min(dp.back()[0], dp.back()[1]) << endl; }
K soyorin的通知
DP
由于人数只有1000,那么 实际有效的范围只有1000左右,并且,soyorin至少要花一次 的代价将消息通知给 1 个人,然后再让这个人去将消息通知给剩下的 个人。
那么问题就转化成了:将消息通知给 个人的最小代价,将消息通知给 个人需要花费 的代价,且 能用多次,也就是一个完全背包。
我是来结束乐队的.jpg
#include <bits/stdc++.h> using namespace std; int main() { int n, p; cin >> n >> p; vector<int> dp(n + 1, 1e9); dp[0] = 0; for (int i = 1; i <= n; i++) { int a, b; cin >> a >> b; if (b >= n) b = n - 1; for (int j = 0; j <= n; j++) { dp[j] = min(dp[j], dp[max(0, j - b)] + a); } } int ans = 1e9; for (int i = 0; i < n; i++) { ans = min(ans, dp[i] + (n - i) * p); } cout << ans << endl; }
L anon的星星
数学
做法一:直接枚举赢和输的场数即可。
做法二:假设赢了 局,输了 局,那么 ,联立一下就是 ,直接解方程即可。
嘤嘤打王者狂输有感。
#include <bits/stdc++.h> using namespace std; int main() { int n, x; cin >> n >> x; if((n + x) & 1) cout << -1 << endl; else{ int t = n - abs(x) >> 1; if(x > 0) cout << n-t << " " << t << endl; else cout << t << " " << n - t <<endl; } }
M mutsumi的排列连通
分类讨论
1) 如果 ,且 不是 1 或 ,那么只操作一次即可。
2) 如果 或 (都不越界),那么只操作一次即可。
3) 如果上述两个条件都不满足,且 大于 2 ,则操作两次即可(把 都删除即可)。
4) 如果 等于 ,若不满足第二个条件,则无解。
5) 如果 等于 1 ,则无解。
忘记这题怎么来的了,剪成两半,正好适合小睦被soyorin切割(退黄瓜)。
#include<bits/stdc++.h> using namespace std; int main(){ int T; cin >> T; while(T--){ int n; cin >> n; vector<int> a(n + 2), b = a; for(int i = 1; i <= n; i++){ cin >> a[i]; } for(int i = 1; i <= n; i++){ cin >> b[i]; } if(n == 1) cout << -1 << endl; else if(n == 2){ if(a[1] == b[1]) cout << -1 << endl; else cout << 1 << endl; } else{ int tar = 0; for(int i = 1; i <= n; i++){ if(a[i] == b[i] && i != 1 && i != n) tar++; if(a[i] == b[i - 1]) tar++; if(a[i] == b[i + 1]) tar++; } if(tar) cout << 1 << endl; else cout << 2 << endl; } } }
花絮
这里是出题人——网恋粉毛被骗一万的世界第一可爱的毒瘤王嘤嘤
由于为了题目简洁,导致有些题用了专业名词,但是解释并不到位,非常抱歉,下次不解释了QAQ
以及 I 题阅读理解有点抽象,非常抱歉TAT
本场比赛主要以思维为主,除D的DFS和K的DP外,几乎没有考察算法,更没有高深数据结构,对学习算法和数据结构较多的选手可能并不友好,反而是一些真正的萌新有着不错的发挥。然后榜就被带歪了,尤其是刚开始全在冲CEGH,没人做I和M,后期K的榜也有点歪。
很喜欢emo哥哥的一句话:选手一整场都能有事做,总有一款适合你的牢
求求你去看《BanG Dream! It's MyGO!!!!!》吧!只要是我能做的,我什么都愿意做!
本场比赛的出题费将资助嘤嘤买MyGO!!!!!的BD(什么?题出的太差不发出题费了?没钱买BD这种事情不要啊!求求你让我买BD吧,只要是我能做的我什么都愿意做!)
今天是要乐奈的生日,转发这条消息到五个群,猫猫就会去你家给你演奏春日影,我试过了,是假的,还会被骂看MyGO看的,但今天真的是乐奈酱的生日