AtCoder Beginner Contest 337
A - Scoreboard (abc337 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 n; cin >> n; LL a = 0, b = 0; while (n--) { int x, y; cin >> x >> y; a += x; b += y; } if (a > b) cout << "Takahashi" << '\n'; else if (a < b) cout << "Aoki" << '\n'; else cout << "Draw" << '\n'; return 0; }
B - Extended ABC (abc337 B)
题目大意
给定一个字符串,问是否形如AAAA..BBBB...CCCC
。
解题思路
如果形如上述,那么通过unique
函数去重后就是有序的,因此去重排序比较即可。
当然也可以朴素找出A B 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); string s; cin >> s; s.erase(unique(s.begin(), s.end()), s.end()); auto t = s; ranges::sort(t); if (s == t) cout << "Yes" << '\n'; else cout << "No" << '\n'; return 0; }
C - Lining Up 2 (abc337 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; cin >> n; map<int, int> pos; for (int i = 0; i < n; i++) { int x; cin >> x; pos[x] = i + 1; } int st = pos[-1]; cout << st; for (int i = 0; i < n - 1; i++) { st = pos[st]; cout << ' ' << st; } cout << '\n'; return 0; }
D - Cheating Gomoku Narabe (abc337 D)
题目大意
给定的二维网格,格子上有 xo.
三种之一。
问最少进行的操作数,使得网格有连续(水平或垂直)个格子都是 o
。
操作为:将一个为.
的格子改为o
。
解题思路
只有 ,因此可以枚举这连续个格子的左端点 ,然后往右的个格子里,不能有 x
,然后对.
的数量取个最小值。事先预处理一下关于x
和.
数量的前缀和,即可判断和求答案。当然也可以滑动窗口直接维护这两字符的数量。
水平判一次后,将原图旋转 度再判一次即可。时间复杂度是。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; const int inf = 1e9; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int h, w, k; cin >> h >> w >> k; vector<string> s(h); for (auto& i : s) cin >> i; vector<string> t(w); for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { t[i] += s[j][i]; } } auto solve = [&](vector<string>& s) { int ans = inf; for (auto& i : s) { vector<int> preo(i.size(), 0), prex(i.size(), 0); preo[0] = i[0] == 'o'; prex[0] = i[0] == 'x'; for (int j = 1; j < i.size(); ++j) { preo[j] = preo[j - 1] + (i[j] == 'o'); prex[j] = prex[j - 1] + (i[j] == 'x'); } for (int j = 0; j + k - 1 < i.size(); ++j) { if (prex[j + k - 1] - (j ? prex[j - 1] : 0) == 0) { ans = min(ans, k - (preo[j + k - 1] - (j ? preo[j - 1] : 0))); } } } return ans; }; int ans = min(solve(s), solve(t)); if (ans == inf) ans = -1; cout << ans << '\n'; return 0; }
E - Bad Juice (abc337 E)
题目大意
交互题。
个果汁,一个是坏的,喝了第二天会拉肚子。现在你要找出这个坏果汁是哪个。
你需要找最少数量的朋友,然后给他们一些果汁喝。一瓶果汁可以给多个朋友喝,一个朋友也可以喝多个果汁。
如果有朋友喝到坏的果汁,则第二天会告诉你他肚子疼。
你需要根据朋友第二天是否肚子疼的情况判断是哪个果汁坏了。
解题思路
一个经典的判断膺品的题。
考虑对果汁编号,考虑其二进制。对于坏的果汁,我们需要确定的二进制下,哪些数位是 。
对于第 个朋友(编号从 开始),它的作用就是判断的第位是否为。即把 中,所有二进制下第 位为 的都给第 个朋友喝,如果它第二天肚子疼了,说明 的第 位为 。
最终需要 个朋友。
神奇的代码
#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; int num = 0; while ((1 << num) < n) num++; cout << num << '\n'; for (int i = 0; i < num; ++i) { vector<int> candi; for (int j = 0; j < n; ++j) { if ((j >> i) & 1) candi.push_back(j); } cout << candi.size() << ' '; for (auto x : candi) cout << x + 1 << ' '; cout << '\n'; } cout.flush(); string s; cin >> s; int ans = 0; for (int i = 0; i < s.size(); ++i) { if (s[i] == '1') ans += (1 << i); } cout << ans + 1 << endl; return 0; }
F - Usual Color Ball Problems (abc337 F)
题目大意
给定个球的颜色, 个盒子。
回答 个询问。
第 个(从开始)询问 ,将前个球放到最后。
然后依次考虑每个球,
- 若有已经放了个球的盒子,且数量不超过 ,则放入
- 否则,若有空盒子,则放入空盒子标号中,多个空盒子就放到标号最小的那个。
- 否则,吃了它。
问放入盒子的球的数量。
解题思路
先对第个询问模拟求出答案。
注意到转移到下一个询问时,其实只有一个盒子的球会变动,然后感觉暴力模拟下就好了,但感觉写不明白一觉醒来感觉明白了。
首先整成个球,后面 个球的颜色与前面相同,这样对于第个询问,就考虑区间的球。
然后预处理出表示颜色为 的球的下标数组。注意到某个颜色被放入盒子的话,它始终是 的一个前缀(当然是从当前位置开始)。因此我们再维护 表示中下标在的球被放入了盒子里。很显然答案
考虑我们求得了第 个询问的答案,考虑如何求第 个询问 ,即如何更新数组和 数组。
考虑此时的第一个球的颜色,它将要放到最后面,这会带来什么影响。
- 首先,该球原来肯定放到了某个盒子里,因为它要被放到后面,所以这个盒子空了,首先。
- 然后再减去该盒子对该颜色球的影响,此时放入盒子的颜色的球会减少,思考减少多少。
- 假设该颜色球放入盒子数为个,实际上会减少 ,即会减少个,如果 则减少 个,即该颜色球的最后一个盒子(数量最少)的数量。
- 考虑这个盒子将会被什么颜色球占领。很显然将会是取得对应的颜色,取对应数量的颜色球放入即可,因此还得开一个
set
维护的最小值。
注意更新数组时要同步更新 $ans和set
。
按上述模拟,总时间复杂度是,只是细节可能有亿点多。
神奇的代码
#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, k; cin >> n >> m >> k; vector<int> c(2 * n); for (int i = 0; i < n; ++i) { cin >> c[i]; c[i]--; c[i + n] = c[i]; } vector<int> l(n, 0), r(n, 0); vector<vector<int>> col(n); set<int> minn; int ans = 0; for (int i = 0; i < 2 * n; ++i) { col[c[i]].push_back(i); } for (int i = 0; i < n; ++i) { if (!col[i].empty()) { minn.insert(col[i][r[i]]); } } auto modify_r = [&](int color, int R) { if (r[color] < col[color].size()) minn.erase(col[color][r[color]]); ans -= r[color] - l[color]; r[color] = R; ans += r[color] - l[color]; if (r[color] < col[color].size()) minn.insert(col[color][r[color]]); }; auto push_box = [&](int pos, int R) { int color = c[pos]; int r1 = min((int)col[color].size(), r[color] + k); int r2 = ranges::lower_bound(col[color], R) - col[color].begin(); modify_r(color, min(r1, r2)); }; auto pop_box = [&](int pos, int L, int R) { int color = c[pos]; int cnt = (r[color] - l[color]) % k; if (cnt == 0) cnt = k; l[color]++; ans--; modify_r(color, r[color] + 1); modify_r(color, r[color] - cnt); }; auto not_in_box = [&](int pos) { int color = c[pos]; return not(col[color][l[color]] <= pos and pos < col[color][r[color]]); }; for (int i = 0; i < n; ++i) { if (not_in_box(i) && m) { push_box(i, n); --m; } } cout << ans << '\n'; for (int i = 0; i < n - 1; ++i) { pop_box(i, i, i + n); if (!minn.empty()) { int pos = *minn.begin(); push_box(pos, i + 1 + n); } cout << ans << '\n'; } return 0; }
G - Tree Inversion (abc337 G)
题目大意
给定一棵树,对每个点,求 ,表示为 的数量,使得 的路径上有点 。
解题思路
这里涉及到三个点,依次考虑枚举哪个,发现枚举 不会求,而枚举 能求。
考虑枚举 ,其有 个儿子和一个父亲。然后考虑 在哪里:
- 若在的一个儿子子树,里面点标号小于 的都是 ,假设数量有 个,那么 的其他分支(包括 )的所有点 的 。但这个其他分支的加法不太好做,我们可以维护全局加法,然后 这个儿子子树的所有点都,这样就可以了。
- 若在 的父亲分支那部分,假设有 个,那 的所有儿子(包括 )的所有点 的 ,这是一个子树的加法,直接加就好了。
当然这里的子树加法不是子树的每个点都,注意到这里就多次修改,一次查询,因此树上差分就好了,或者说对要操作的子树的根打个标记,最后来一次求答案时再传递标记即可。
剩下的问题就是如何求,怎么求的一个儿子子树中,标号小于的数量。
代码里写的比较复杂(不过板子是贴的),用的动态开点的权值线段树+线段树合并。求一个数在一堆区间的排名,一般可以转换成一个权值线段树的前缀和。由于在DFS时,每个点都要开一棵线段树,普通线段树会爆空间,但由于点权值是稀疏的,得用动态开点线段树。然后DFS合并两个儿子的权值线段树时,还得线段树合并。
在 父亲部分时求 的数量,可以转换成求 所有儿子中小于 的数量, 这样。就和第一种情况统一了。
如何求,还有个更简单的办法,先对树求一个序,与此同时还有一个初始为的数组。然后我们按照点编号从小到大依次考虑,假设当前考虑点 ,让 ,即点所在的 序上的位置 。然后要求的一个儿子 的子树中标号小于 的数量,则为这个子树对应的一个 序的 的区间和(因为 都加进来了)。
此处为单点修改和区间查询,用线段树或树状数组维护这个 即可。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; const int maxn = 2e5 + 7; int n; int rt[maxn], ls[maxn * 20], rs[maxn * 20], num[maxn * 20], cnt; inline void pushup(int x) { num[x] = num[ls[x]] + num[rs[x]]; } void update(int& x, int l, int r, int pos, int val = 1) { if (!x) x = ++cnt; if (l == r) { num[x] += val; return; } int mid = (l + r) >> 1; if (pos <= mid) update(ls[x], l, mid, pos, val); else update(rs[x], mid + 1, r, pos, val); pushup(x); } int merge(int x1, int x2, int l, int r) { if ((!x1) || (!x2)) return x1 + x2; if (l == r) { num[x1] += num[x2]; return x1; } int mid = (l + r) >> 1; ls[x1] = merge(ls[x1], ls[x2], l, mid); rs[x1] = merge(rs[x1], rs[x2], mid + 1, r); pushup(x1); return x1; } int query(int x, int l, int r, int L, int R) { if (!x) return 0; if (L <= l && r <= R) return num[x]; int mid = (l + r) >> 1, ans = 0; if (L <= mid) ans += query(ls[x], l, mid, L, R); if (R > mid) ans += query(rs[x], mid + 1, r, L, R); return ans; } int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n; vector<vector<int>> edge(n); for (int i = 0; i < n - 1; ++i) { int u, v; cin >> u >> v; --u, --v; edge[u].push_back(v); edge[v].push_back(u); } LL tot = 0; vector<LL> p(n, 0); auto DFS = [&](auto self, int u, int fa) -> void { for (auto& v : edge[u]) { if (v == fa) continue; self(self, v, u); LL tmp = query(rt[v], 1, n, 1, u + 1); tot += tmp; p[v] -= tmp; rt[u] = merge(rt[u], rt[v], 1, n); } update(rt[u], 1, n, u + 1); p[u] += u + 1 - query(rt[u], 1, n, 1, u + 1); }; DFS(DFS, 0, 0); vector<LL> ans(n, 0); auto solve = [&](auto self, int u, int fa, LL pre) -> void { ans[u] += tot + p[u] + pre; for (auto& v : edge[u]) { if (v == fa) continue; self(self, v, u, pre + p[u]); } }; solve(solve, 0, 0, 0); for (auto& i : ans) cout << i << ' '; cout << '\n'; return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2023-01-21 AtCoder Beginner Contest 286