AtCoder Beginner Contest 328
A - Not Too Hard (abc328 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, x; cin >> n >> x; int sum = 0; for (int i = 0; i < n; ++i) { int a; cin >> a; sum += a * (a <= x); } cout << sum << '\n'; return 0; }
B - 11/11 (abc328 B)
题目大意
给定一年的月数和一个月的天数。
问有多少对,表示第 个月的第 日, 的数位上每个数字都是一样的。
解题思路
范围只有,枚举所有的 逐个判断即可。
神奇的代码
#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 ans = 0; auto ok = [&](int x, int y) { int t = x % 10; while (x) { if (t != x % 10) return false; x /= 10; } while (y) { if (t != y % 10) return false; y /= 10; } return true; }; for (int i = 1; i <= n; ++i) { int x; cin >> x; for (int j = 1; j <= x; ++j) { ans += ok(i, j); } } cout << ans << '\n'; return 0; }
C - Consecutive (abc328 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, q; string s; cin >> n >> q >> s; vector<int> sum(n); for (int i = 0; i < n - 1; ++i) { sum[i] = (s[i] == s[i + 1]); if (i) sum[i] += sum[i - 1]; } while (q--) { int l, r; cin >> l >> r; --l, --r; int ans = 0; if (r) ans += sum[r - 1]; if (l) ans -= sum[l - 1]; cout << ans << '\n'; } return 0; }
D - Take ABC (abc328 D)
题目大意
给定一个仅包含ABC
的字符串,每次将最左边的ABC
删除,直至不能删。
问最后的字符串。
解题思路
可以从左到右依次将的每个字符加入栈中,一旦栈顶构成ABC
就弹栈。
模拟即可。
神奇的代码
#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; string st; for (auto& i : s) { st += i; if (st.size() >= 3) { int n = st.size(); if (st.substr(st.size() - 3) == "ABC") { st.pop_back(); st.pop_back(); st.pop_back(); } } } cout << st << '\n'; return 0; }
E - Modulo MST (abc328 E)
题目大意
给定一张图,问模意义下的最小生成树的代价。
解题思路
注意是模意义下的最小代价,在求生成树过程中的每一个值都有可能在加入某条边后超过而变的最小,成为最后的答案。
注意点数只有,边数最多也只有 ,因此总的方案数只有 。暴力可行。即可以保留中间的所有结果。
考虑求生成树做法,从号点不断往外拓展,保留当前最小的结果。我们借用这个想法,但保留当前所有的结果:设 表示所有点与号点连通性为 的情况下,所有生成树的结果(的所有情况都是不合法的,号点肯定与 号点连通)。很显然
然后枚举下一条边连接,更新所有结果即可。由于可能会有重复的值(同一个生成树可以从不同的加边顺序得到),所以用 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; LL k; cin >> n >> m >> k; vector<vector<pair<int, LL>>> edge(n); for (int i = 0; i < m; ++i) { int u, v; LL w; cin >> u >> v >> w; --u, --v; edge[u].push_back({v, w}); edge[v].push_back({u, w}); } int up = (1 << n); int st = n - 1; vector<set<LL>> candi(up); candi[1].insert(0); for (int i = 0; i < up; ++i) { if (~i & 1) continue; for (int u = 0; u < n; ++u) { if ((~i >> u) & 1) continue; for (auto& [v, w] : edge[u]) { if ((i >> v) & 1) continue; for (auto& val : candi[i]) candi[i | (1 << v)].insert((val + w) % k); } } } cout << *ranges::min_element(candi.back()) << '\n'; return 0; }
F - Good Set Query (abc328 F)
题目大意
给定数字,依次给定个条件 。
对于一个条件集合,如果存在一个长度为数组 ,对于这个集合里的所有条件,都满足,那么这个集合是好的。
初始集合为空,依次对每个条件,如果加入到集合后,集合是好的,则加入到集合中。
问最后集合的元素。
解题思路
条件相当于是规定了数组元素之间的差的关系。
对于一个条件,我们可以连一条 的边,边权为 ,反向边的边权为 。
考虑一个集合不是好时,此时形成的图是怎样的。
当加入一个条件,集合可能还是好的,但也可能变得不好,
如果还是好的,有两种情况:
- 一是原先没有什么联系,即不连通,加了条边之后连通了,仅此而已。
- 二是是连通的,加了 这条边后会形成一个环,环的边权和为 ,或者说 存在两条路径,其边权和相等。
在第二种情况下,这个条件就是多余的,我们可以不管这个条件,即不加这条边。此时图就没有环,即是一棵树(或森林)。
树是一个非常好的图,有着树上路径唯一的性质,因此情况二下, 我们可以很容易求出 的路径和,然后与 比较,相等则说明加入这个条件后,集合还是好的。
而如果不相等,则说明不能加入这个条件,即有条件冲突了,说明 存在两条边权和不一样的路径。
所以问题就剩下,如何在动态加边的情况下,求出的长度。
如果是一棵静止的树,一个常用的方法就是预处理每个点到根节点的距离,那么 距离就是 ,注意到反向边的边权是负值,所以到根的距离恰好抵销了。
而当两棵树合并时,有一棵树的 就要全部更新,如果随便选一棵树更新的话,总的时间复杂度可能会是。为降低时间复杂度,可以采用启发式合并的策略,即节点树少的树合并到节点树多的树上,这样每次只用更新节点数少的树的 。更新就是从合并点开始,更新数组。
为计算启发式合并的时间复杂度,可以考虑每个节点的的更新次数——每更新一次,其节点所在的连通块大小至少翻倍,那么每个节点最多更新 次,其所在的连通块就包含了所有的节点,也就不会再更新了,因此启发式合并的复杂度是
用并查集维护连通性,然后树合并时采用启发式合并的策略更新数组 ,时间复杂度是 。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; class dsu { public: vector<int> p; vector<int> sz; vector<LL> dis; vector<vector<array<int, 2>>> edge; int n; dsu(int _n) : n(_n) { p.resize(n); sz.resize(n); dis.resize(n); edge.resize(n); iota(p.begin(), p.end(), 0); fill(sz.begin(), sz.end(), 1); fill(dis.begin(), dis.end(), 0); } inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); } inline void dfs(int u, int fa) { for (auto& [v, w] : edge[u]) { if (v == fa) continue; dis[v] = dis[u] - w; dfs(v, u); } } inline bool unite(int x, int y, int w) { int fx = get(x); int fy = get(y); if (fx != fy) { if (sz[fx] > sz[fy]) { swap(x, y); swap(fx, fy); w = -w; } edge[x].push_back({y, w}); edge[y].push_back({x, -w}); dis[x] = dis[y] + w; dfs(x, y); p[fx] = fy; sz[fy] += sz[fx]; return true; } else { return dis[x] == dis[y] + w; } } }; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, q; cin >> n >> q; dsu ji(n); for (int i = 1; i <= q; ++i) { int a, b, d; cin >> a >> b >> d; --a, --b; if (ji.unite(a, b, d)) cout << i << ' '; } cout << '\n'; return 0; }
好像是带权并查集裸题怪不得这么多人过得这么快
还是借用上面计算的思想,由于反向边边权取反,因此对于任意一条闭合回路,其边权和一定是 。
由此每个点只需记录到根的距离,而不必关心树的形态,分别考虑在路径压缩和合并时距离的更新即可。
压缩的时候,,这里的 是压缩前的父亲, 但是更新后的值。
合并的时候, ,令 ,则 。而 会在路径压缩的时候更新。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; class dsu { public: vector<int> p; vector<int> sz; vector<LL> dis; int n; dsu(int _n) : n(_n) { p.resize(n); sz.resize(n); dis.resize(n); iota(p.begin(), p.end(), 0); fill(sz.begin(), sz.end(), 1); fill(dis.begin(), dis.end(), 0); } inline int get(int x) { if (x != p[x]) { int t = p[x]; p[x] = get(p[x]); dis[x] += dis[t]; } return p[x]; } inline bool unite(int x, int y, int w) { int fx = get(x); int fy = get(y); if (fx != fy) { p[fx] = fy; dis[fx] = -dis[x] + dis[y] + w; sz[fy] += sz[fx]; return true; } else { return dis[x] == dis[y] + w; } } }; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, q; cin >> n >> q; dsu ji(n); for (int i = 1; i <= q; ++i) { int a, b, d; cin >> a >> b >> d; --a, --b; if (ji.unite(a, b, d)) cout << i << ' '; } cout << '\n'; return 0; }
G - Cut and Reorder (abc328 G)
题目大意
给定两个数组,可以进行以下两种操作任意次:
- 选择一个数,将数组 分成个部分,然后重新排序,组成一个新数组。代价为
- 令,代价为
问最小的代价,使得变成 。
解题思路
首先注意到一点,所有的结果都可以转换成一次操作一+若干次操作二。
由此我们只需考虑如何操作一,因为进行完操作一后。操作二的代价是固定的。
考虑朴素做法,即枚举分界点,有种情况,然后对每一个部分进行排序,有 种。其复杂度过大了,期间存在重复计算的情况,考虑如何压缩,设计合适的状态。
考虑重复计算的状态:设想对数组 切分成出一段 ,然后把它放到最前面,其他段任意分。关于 的这个子状态会被重复考虑多次——这个状态是不必要的。
即当我们考虑数组 中的某一段时,我们只关心两个信息。
- 能不能选择这一段,即这一段中有没有和之前选择的段重叠了。
- 能不能放到数组 的某一段,即 某一段有没有和之前放置的部分重叠了。
由此可以设表示将 中的 部分( 表示)放到 中的 部分的最小代价。但这个状态数高达 ,且可能会包含很多非法状态(可能中某连续部分放不到某连续部分上),得转换一下状态。
为保持上面的连续性,可以规定将中的 部分放到 中的最前面,即前 (二进制下的个数)位。即设 表示将 中的 部分放到 中的最前面的最小代价 ,记,即放到 中的前 位。
考虑往后转移,即选择中一段连续的,然后放到 。
当然也可以考虑从前转移,但计算代价部分需要预处理一下操作二代价,否则转移会多出一个 复杂度。而往后转移的代价可以在迭代更新维护。
初始情况即,其余无限大。注意从 往后转移时没有代价 。其余的则有。
关于其时间复杂度,初看可能认为是,但由于转移时枚举连续的段,其复杂度可能会比这小。分别考虑总状态数和总转移数的话,其实是
- 总状态数是
- 总转移数是。注意到每次转移都是由一个连续的段产生的,考虑这些连续的段的转移次数。一个长度为 的连续的段有,能选择该段的状态数有 ,由此 总的转移次数为。
由此总的时间复杂度就是总状态数+总转移数
,即。
神奇的代码
#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; LL c; cin >> n >> c; vector<LL> a(n), b(n); for (auto& i : a) cin >> i; for (auto& i : b) cin >> i; size_t up = (1 << n); vector<LL> dp(up, numeric_limits<LL>::max()); dp[0] = 0; for (size_t i = 0; i < up; ++i) { int cnt = popcount(i); for (int j = 0; j < n; ++j) { if ((i >> j) & 1) continue; LL sum = c * (i != 0); LL nxt = 0; for (int k = j; k < n; ++k) { if ((i >> k) & 1) break; nxt |= (1 << k); sum += abs(a[k] - b[cnt + k - j]); dp[i | nxt] = min(dp[i | nxt], dp[i] + sum); } } } cout << dp.back() << '\n'; return 0; }
本文作者:~Lanly~
本文链接:https://www.cnblogs.com/Lanly/p/17826500.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步