AtCoder Beginner Contest 355
A - Who Ate the Cake? (abc355 A)
题目大意
三人有偷吃蛋糕的嫌疑,现告诉两个目击证人的澄清对象。问谁偷吃蛋糕。
解题思路
两个对象相同就不知道是谁,否则就是剩下的那一个了。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int a, b; cin >> a >> b; if (a == b) cout << "-1" << '\n'; else cout << 1 + 2 + 3 - a - b << '\n'; return 0; }
B - Piano 2 (abc355 B)
题目大意
给定两个数组a,b,定义数组 c,为数组 a,b拼接后排序。
问c中相邻两个数,均在数组 a中出现的数量。
解题思路
预处理出exist[i]表示数字 i是否在数组 a出现过,然后枚举 c的相邻数判断即可。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, m; cin >> n >> m; vector<int> a(n), b(m); for (auto& i : a) cin >> i; for (auto& i : b) cin >> i; vector<int> c = a; c.insert(c.end(), b.begin(), b.end()); sort(c.begin(), c.end()); vector<int> ina(*max_element(a.begin(), a.end()) + 1, 0); for (auto i : a) ina[i] = 1; bool ok = false; for (int i = 0; i < n + m - 1; ++i) { ok |= ina[c[i]] && ina[c[i + 1]]; } if (ok) cout << "Yes" << endl; else cout << "No" << endl; return 0; }
C - Bingo 2 (abc355 C)
题目大意
二维网格,有t次操作,每次操作涂黑一个格子。
问多少操作后,有一行或一列或对角线的格子全部被涂黑。
解题思路
分别记录每行、每列、对角线中被涂黑的格子数量row[i],col[i],diag1,diag2,每次操作后检查该格子对应的行列对角线的黑格子数是否是 n即可。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, t; cin >> n >> t; vector<int> row(n), col(n); int diag1 = 0, diag2 = 0; int ans = -1; auto tr = [&](int x) -> pair<int, int> { return {x / n, x % n}; }; for (int i = 0; i < t; i++) { int x; cin >> x; --x; auto [r, c] = tr(x); row[r]++; col[c]++; if (r + c == n - 1) diag1++; if (r == c) diag2++; if (row[r] == n || col[c] == n || diag1 == n || diag2 == n) { ans = i + 1; break; } } cout << ans << '\n'; return 0; }
D - Intersecting Intervals (abc355 D)
题目大意
给定n个区间[li,ri],问俩俩区间有重叠的数量。交点重叠也算重叠。
解题思路
将这些区间按照左端点排序,然后依次枚举区间,考虑如何计算重叠数量cntj。
假设当前枚举的第j个区间,我要求 i<j的数量,满足第 i个区间与第 j个区间有重叠。
考虑满足什么条件算重叠,由于li≤lj,所以只要ri≥lj,那么第 i个区间就与第 j个区间重叠。
因此cntj就是满足 i<j,ri≥lj的数量。
这是一个二维偏序问题,但与之前不同的是条件lj是单调递增的。因此我们可以维护一个小根堆的优先队列,将 i<j的 ri丢进去, 一旦堆顶的ri<lj,说明不重叠,之后也不会重叠,就丢弃。 cntj就是优先队列里的元素个数。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n; cin >> n; vector<array<int, 2>> a(n); for (auto& x : a) { cin >> x[0] >> x[1]; } sort(a.begin(), a.end()); priority_queue<int, vector<int>, greater<int>> team; LL ans = 0; for (int i = 0; i < n; i++) { while (!team.empty() && team.top() < a[i][0]) { team.pop(); } ans += team.size(); team.push(a[i][1]); } cout << ans << '\n'; return 0; }
E - Guess the Sum (abc355 E)
题目大意
交互题。
有一个长度为2n的隐藏数组a,你需要用最少的询问,问出 ∑ri=lai的和。对 100取模。
每次询问给定i,j,回答 l=2ij,r=2i(j+1),∑r−1i=lai的和,对 100取模。
解题思路
一开始参考abc349d,结果wa了,应该是询问的次数不是最少。
可以考虑一反例,询问 [1,7],如果按照上面的思路,则是询问 [1,1]+[2,3]+[4,7] 共三个询问,即(0,1)(1,1)(2,1)。
而实际上可以做到两个:询问[0,7]−[0,0] 。即(3,0)(0,0)
因此此处除了区间加,还有可能区间减。换句话说就像是先退一步(1→0),才能走的更远 (0→7)。
由此我们可以假想在一张有2n个点的无向图上,我们从点l出发,花最小的步数到达点 r+1。点与点之间通过询问连边。比如我们可以询问 (3,0),即区间 [0,8),因此点0和点 8就连一条无向边。而询问数只有 O(n2n),也即边数,点数有 O(2n),边权均为 1。因此在该图上直接从 点l进行一次 BFS,记录转移点,然后询问即可。
按上面的为例,我们从点 1出发,最终到达点8,一个最短的方案是 1→0→8,第一条就是询问 [0,1),要减去该值,第二条就是询问 [0,8),要加上该值。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, l, r; cin >> n >> l >> r; int up = (1 << n); vector<vector<int>> edge(up + 1); for (int i = 0; i <= n; ++i) { for (int l = 0; l < up; l += (1 << i)) { int r = l + (1 << i); edge[l].push_back(r); edge[r].push_back(l); } } queue<int> team; vector<int> pre(up + 1, -1); pre[l] = 0; team.push(l); while (!team.empty()) { int cur = team.front(); team.pop(); if (cur == r + 1) { break; } for (int next : edge[cur]) { if (pre[next] == -1) { pre[next] = cur; team.push(next); } } } vector<array<int, 2>> query; for (int i = r + 1; i != l; i = pre[i]) { query.push_back({pre[i], i}); } reverse(query.begin(), query.end()); int ans = 0; for (auto& [a, b] : query) { int sign = 1; if (a > b) { sign = -1; swap(a, b); } int i = __builtin_ctz(b - a); int j = a >> i; cout << "? " << i << " " << j << endl; int sum; cin >> sum; ans += sum * sign; } ans = (ans % 100 + 100) % 100; cout << "! " << ans << endl; return 0; }
F - MST Query (abc355 F)
题目大意
给定一棵树,边有边权,不断加边,求每次加边后的最小生成树的边权和。
边权≤10。
解题思路
当给树加了一条边,此时就出现了个环,而只要去除环上边权最大的边,就又变回一棵树,同时也是一个最小生成树。
因此我们要动态维护一棵树,维护俩点之间的边权最大值,这是Link-Cut Tree所擅长的——动态连边、断边、快速求出一条路径的权值最大值。
LCT
#include <bits/stdc++.h> using namespace std; using LL = long long; const int maxn = 1e6; const int inf = 1e9 + 7; struct Splay { int ch[maxn][2], fa[maxn], tag[maxn], val[maxn], maxx[maxn]; void clear(int x) { ch[x][0] = ch[x][1] = fa[x] = tag[x] = val[x] = maxx[x] = 0; } int getch(int x) { return ch[fa[x]][1] == x; } int isroot(int x) { return ch[fa[x]][0] != x && ch[fa[x]][1] != x; } void maintain(int x) { if (!x) return; maxx[x] = x; if (ch[x][0]) { if (val[maxx[ch[x][0]]] > val[maxx[x]]) maxx[x] = maxx[ch[x][0]]; } if (ch[x][1]) { if (val[maxx[ch[x][1]]] > val[maxx[x]]) maxx[x] = maxx[ch[x][1]]; } } void pushdown(int x) { if (tag[x]) { if (ch[x][0]) tag[ch[x][0]] ^= 1, swap(ch[ch[x][0]][0], ch[ch[x][0]][1]); if (ch[x][1]) tag[ch[x][1]] ^= 1, swap(ch[ch[x][1]][0], ch[ch[x][1]][1]); tag[x] = 0; } } void update(int x) { if (!isroot(x)) update(fa[x]); pushdown(x); } void print(int x) { if (!x) return; pushdown(x); print(ch[x][0]); printf("%d ", x); print(ch[x][1]); } void rotate(int x) { int y = fa[x], z = fa[y], chx = getch(x), chy = getch(y); fa[x] = z; if (!isroot(y)) ch[z][chy] = x; ch[y][chx] = ch[x][chx ^ 1]; fa[ch[x][chx ^ 1]] = y; ch[x][chx ^ 1] = y; fa[y] = x; maintain(y); maintain(x); if (z) maintain(z); } void splay(int x) { update(x); for (int f = fa[x]; f = fa[x], !isroot(x); rotate(x)) if (!isroot(f)) rotate(getch(x) == getch(f) ? f : x); } void access(int x) { for (int f = 0; x; f = x, x = fa[x]) splay(x), ch[x][1] = f, maintain(x); } void makeroot(int x) { access(x); splay(x); tag[x] ^= 1; swap(ch[x][0], ch[x][1]); } int find(int x) { access(x); splay(x); while (ch[x][0]) x = ch[x][0]; splay(x); return x; } void link(int x, int y) { makeroot(x); fa[x] = y; } void cut(int x, int y) { makeroot(x); access(y); splay(y); ch[y][0] = fa[x] = 0; maintain(y); } } st; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, q; cin >> n >> q; int ans = 0; for (int i = 1; i <= n; ++i) st.val[i] = -inf, st.maintain(i); int id = n + 1; vector<array<int, 2>> edge; for (int i = 0; i < n - 1; ++i) { int u, v, w; cin >> u >> v >> w; st.val[id] = w; st.maintain(id); st.link(u, id); st.link(id, v); edge.push_back({u, v}); ++id; ans += w; } while (q--) { int u, v, w; cin >> u >> v >> w; edge.push_back({u, v}); st.val[id] = w; st.maintain(id); st.makeroot(u); st.access(v); st.splay(v); int x = st.maxx[v]; int maxx = st.val[x]; auto [l, r] = edge[x - n - 1]; if (w < maxx) { st.cut(l, x); st.cut(x, r); st.link(u, id); st.link(id, v); ans -= maxx; ans += w; } ++id; cout << ans << '\n'; } return 0; }
不过这题有个特别的地方就是边权≤10,会有不需要 lct的做法。
由于边权个数只有10,我们可以考虑每个边权选择的数量之类的,这样可以算出生成树边权,但删边的话无从下手,因为不知道加边的两个点之间边权。
或许可以维护边权为 i的边所形成的连通性, 这样加一条边权为i时,看有没有边权 >i的使得那两点连通,从而决定删除哪条边。但会发现不好维护,边权为 i的边所形成的连通性会依赖于边权<i的选择结果。
为了不依赖其他情况,我们可以维护边权 ≤i的边所形成的连通性情况,这用一个并查集di维护,这样我们就建立 10个这样的并查集,并查集之间的依赖关系就没有了。
每加一条边就只用更新这O(10)个并查集的连通性,而如何计算最小生成树的边权呢?关键是求出每个边权的使用个数。
由于每加一条边,连通块的个数就会少一。要求出边权为i使用的个数,那就看 di维护的连通块数量与di−1的连通块数量,两者的差值就是边权为i的使用个数。
这样求答案的时间也是 O(10)。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; class dsu { public: vector<int> p; int n; int block; dsu(int _n) : n(_n), block(_n) { p.resize(n); iota(p.begin(), p.end(), 0); } inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); } inline bool unite(int x, int y) { x = get(x); y = get(y); if (x != y) { p[x] = y; --block; return true; } return false; } }; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, q; cin >> n >> q; vector<dsu> d(10, dsu(n)); vector<array<int, 3>> edge(n - 1); for (auto& [u, v, w] : edge) { cin >> u >> v >> w; --u; --v; } sort(edge.begin(), edge.end(), [](auto& a, auto& b) { return a[2] < b[2]; }); for (int i = 0; i < 10; ++i) { int up = i + 1; auto& di = d[i]; for (auto& [u, v, w] : edge) { if (w > up) break; di.unite(u, v); } } while (q--) { int u, v, w; cin >> u >> v >> w; --u, --v, --w; for (int i = w; i < 10; ++i) { d[i].unite(u, v); } int ans = 0, la = 0; for (int i = 0; i < 10; ++i) { int use = n - d[i].block; ans += (use - la) * (i + 1); la = use; } cout << ans << '\n'; } return 0; }
G - Baseball (abc355 G)
题目大意
给定一个关于n的排列p,高桥从中取k个数xi。青木则以一定概率取一个数y∈[1,n],取的y=i的概率是 pi∑pj。
青木的分数即为min。
求高桥的最优策略,使得青木的期望分数最小。输出对应的最小分数的\sum p_j倍。
解题思路
<++>
神奇的代码
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】博客园携手 AI 驱动开发工具商 Chat2DB 推出联合终身会员
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个适用于 .NET 的开源整洁架构项目模板
· 【开源】C#上位机必备高效数据转换助手
· .NET 9.0 使用 Vulkan API 编写跨平台图形应用
· MyBatis中的 10 个宝藏技巧!
· [.NET] 使用客户端缓存提高API性能