Codeforces Round 881 (Div. 3)
A - Sasha and Array Coloring (CF1843 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 t; cin >> t; while(t--){ int n; cin >> n; vector<int> a(n); for(auto &i : a) cin >> i; sort(a.begin(), a.end()); int ans = -accumulate(a.begin(), a.begin() + n / 2, 0) + accumulate(a.end() - n / 2, a.end(), 0); cout << ans << '\n'; } return 0; }
B - Long Long (CF1843 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 t; cin >> t; while(t--){ int n; cin >> n; vector<int> a(n); for(auto &i : a) cin >> i; LL ans = 0; for(auto &i : a) ans += abs(i); int cnt = 0; bool neg = (a[0] < 0); for(auto &i : a){ if (i > 0 && neg){ cnt ++; neg = false; }else if (i < 0) neg = true; } cnt += neg; cout << ans << ' ' << cnt << '\n'; } return 0; }
C - Sum in Binary Tree (CF1843 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 t; cin >> t; while(t--){ LL n; cin >> n; LL ans = 0; while(n){ ans += n; n >>= 1; } cout << ans << '\n'; } return 0; }
D - Apple Tree (CF1843 D)
题目大意
给定一棵有根树。树上有两个苹果。每一时刻,每个苹果可以往其中一个儿子节点移动。最终移动到叶子处。
设两个苹果最终所处的叶子节点为,问 的情况数。
解题思路
如果一个苹果在节点,那么它能移动的叶子处就是以 为根的叶子节点,如果有 个叶子,那么该苹果的可移动到的叶子数就是 。
现在有两个苹果,假设它们最终可移动到的叶子节点数分别为,那最终的情况数就是 。
因此事先预处理表示以为根的叶子数量,回答即可。
神奇的代码
#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 t; cin >> t; while(t--){ int n; cin >> n; vector<vector<int>> edge(n); for(int i = 1; i < n; ++ i){ int u, v; cin >> u >> v; -- u, -- v; edge[u].push_back(v); edge[v].push_back(u); } vector<int> son(n, 0); function<void(int, int)> dfs = [&](int u, int fa){ bool leaf = true; for(auto &v : edge[u]){ if (v == fa) continue; leaf = false; dfs(v, u); son[u] += son[v]; } son[u] += leaf; }; dfs(0, 0); int q; cin >> q; while(q--){ int u, v; cin >> u >> v; -- u, -- v; cout << 1ll * son[u] * son[v] << '\n'; } } return 0; }
E - Tracking Segments (CF1843 E)
题目大意
给定一个有个的数组,以及 个区间。
一个区间如果是好区间
,则其间 的个数严格大于 的个数。
现依次进行 次操作,每次操作将 。
问最早进行了多少次操作后,出现好区间
。
解题思路
我们可以依次枚举,得到操作后的数组,接下来就是判断每个区间是不是 好区间
,即判断的个数和 的个数的大小关系。朴素判断是,但我们可以事先对 的个数求一遍数组的前缀和 ,这样每个区间的判断都可以在 得出结果,判断的复杂度降为。
由于我们是枚举的,此时时间复杂度为 ,但可以发现 与是否存在 好区间
是一个单调的函数,即如果存在 好区间
,那么也一定存在好区间
,因此我们可以二分这个 q
,得到其临界点。
最终时间复杂度降为
神奇的代码
#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 t; cin >> t; while(t--){ int n, m; cin >> n >> m; vector<array<int, 2>> seg(m); for(auto &i : seg){ cin >> i[0] >> i[1]; -- i[0], -- i[1]; } int q; cin >> q; vector<int> pos(q); for(auto &i : pos){ cin >> i; -- i; } auto check = [&](int x){ vector<int> cnt(n, 0); for(int i = 0; i < x; ++ i) cnt[pos[i]] = 1; partial_sum(cnt.begin(), cnt.end(), cnt.begin()); for(auto &[l, r] : seg){ int one = cnt[r]; if (l > 0) one -= cnt[l - 1]; int zero = r - l + 1 - one; if (one > zero) return true; } return false; }; int l = 0, r = q; while(l + 1 < r){ int mid = (l + r) >> 1; if (check(mid)) r = mid; else l = mid; } if (!check(r)) r = -1; cout << r << '\n'; } return 0; }
F1 - Omsk Metro (simple version) (CF1843 F1)
题目大意
给定一棵点权为 或的树,点数为。
给定个询问,依次回答从这条路径中,是否存在一个子区间,其和为 。
该题
解题思路
子区间和
可以转换成两个前缀和作差
,那么在一般的问题里,我们可以枚举每个前缀和,若其值为,那就看之前的前缀和是否有值为 的。
很显然本题里,复杂度是,无法通过。但本题有个奇特点为点权仅为或 ,这其间或许有什么性质。
容易发现,假设一个区间的最大字段和为 ,最小字段和为 ,那么 区间的数都可以取到,即都存在对应的子区间。因为点权仅为或 ,一个子区间左右扩张,其值只会加一或者减一,不会跳跃,而初始值为 。
因此问题就转换成维护一个区间的子区间的最大值和最小值即可。这是一个简单的,即设 表示以 结尾的最大后缀和,然后再维护一个全局的最大值。最小值同理。
由于本题中 ,因此可以直接从 开始 ,维护这个 即可。撤销相当于回溯。
神奇的代码
#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 t; cin >> t; while(t--){ int n; cin >> n; vector<vector<int>> edge(n + 1); vector<int> val(n + 1); vector<vector<int>> target(n + 1); vector<int> q; val[0] = 1; int cur = 1; for(int i = 0; i < n; ++ i){ string op; cin >> op; if (op[0] == '+'){ int v, x; cin >> v >> x; -- v; edge[v].push_back(cur); edge[cur].push_back(v); val[cur] = x; ++ cur; }else{ int u, v, k; cin >> u >> v >> k; -- u, -- v; assert(u == 0); target[v].push_back(int(q.size())); q.push_back(k); } } vector<int> ans(q.size()); function<void(int, int, int, int, int, int)> dfs = [&](int u, int fa, int maxx, int minn, int gmaxx, int gminn){ for(auto &t : target[u]){ ans[t] = (q[t] <= gmaxx && q[t] >= gminn); } for(auto &v : edge[u]){ if (v == fa) continue; int nmaxx = max({maxx + val[v], val[v], 0}); int nminn = min({minn + val[v], val[v], 0}); int ngmaxx = max(gmaxx, nmaxx); int ngminn = min(gminn, nminn); dfs(v, u, nmaxx, nminn, ngmaxx, ngminn); } }; dfs(0, 0, 1, 0, 1, 0); for(auto &i : ans) if (i) cout << "YES" << '\n'; else cout << "NO" << '\n'; } return 0; }
F2 - Omsk Metro (hard version) (CF1843 F2)
题目大意
给定一棵点权为 或的树,点数为。
给定个询问,依次回答从这条路径中,是否存在一个子区间,其和为 。
解题思路
本题。
在上一题中,我们将问题转换成维护一个区间的子区间的最大值和最小值。
注意到这个信息是可合并
的。以最大值为例。
假设我们有一个左区间和一个右区间,现在我们要将其合并成一个大区间,并求其子区间的最大值。
那么该最大值只有三种情况:
- 左区间的最大值
- 右区间的最大值
- 左区间的最大后缀和+右区间的最大前缀和
因此我们只需要额外增加最大前缀和
和最大后缀和
这两个信息,我们就可以将两个区间合并,得到新区间的最大值。
而为了维护最大前缀和
和最大后缀和
,还需要区间和
这个信息,因为新区间的最大前缀和
有两种情况:
- 左区间的
最大前缀和
- 左区间的
区间和
+右区间的最大前缀和
最大后缀和
同理,因此我们只要维护一个区间的
- 最大值、最小值
- 最大前缀和、最小前缀和
- 最大后缀和、最小后缀和
- 区间和
这些信息,我们就可以将两个区间合并,得到一个大区间的信息。
可合并有什么用呢?
每次询问其实就是询问一个区间的最大子区间和
和最小子区间和
,因为这些区间的数量级是,我们不可能一一计算,而是通过只计算某些区间,这样其他区间都可以通过这些区间,以较少的代价合并得到。
回想下 表,它可以 回答出任意一个区间的最值,但它预处理的复杂度只有 ,也就是说它可以通过计算 个区间的最值,然后以的代价通过 这些计算好的区间,合并出任意区间的最值(注意最值
这个信息也是可合并
的)。而它用到的方法就是倍增
。
同样,既然我们要维护的信息是可合并
的,我们可以运用倍增
的思想,即树上倍增
,对于每个点,维护以这个点往父亲方向,一共个点所组成的序列的信息,然后对于每个询问所对应的路径,通过 倍增
的方式可以拆成个区间进行合并 。
假设询问,因为在树上,我们可以在它们的点拆开,这样, 就是一条链,通过倍增
合并得到其信息(跟倍增
求lca
是一样的),最后再将和合并(注意信息的前后缀要反转过来)即得到的信息。
这样每次询问的复杂度就是了。
当然因为是可合并的,树链剖分将一个路径剖分成若干条轻链和重链合并也不是不行
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; struct info{ int max, premax, sufmax; int min, premin, sufmin; int sum; }; info operator+(const info& a, const info& b){ info c; c.max = max({a.max, b.max, a.sufmax + b.premax}); c.premax = max(a.premax, a.sum + b.premax); c.sufmax = max(b.sufmax, b.sum + a.sufmax); c.min = min({a.min, b.min, a.sufmin + b.premin}); c.premin = min(a.premin, a.sum + b.premin); c.sufmin = min(b.sufmin, b.sum + a.sufmin); c.sum = a.sum + b.sum; return c; } info reverse(const info& a){ info c = a; swap(c.premax, c.sufmax); swap(c.premin, c.sufmin); return c; } int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t; cin >> t; while(t--){ int n; cin >> n; vector<int> val(n + 1); val[0] = 1; int cur = 1; vector<array<int, 3>> q; vector<vector<int>> fa(32, vector<int>(n + 1)); vector<int> deep(n + 1); for(int i = 0; i < n; ++ i){ string op; cin >> op; if (op[0] == '+'){ int v, x; cin >> v >> x; -- v; val[cur] = x; fa[0][cur] = v; deep[cur] = deep[v] + 1; ++ cur; }else{ int u, v, k; cin >> u >> v >> k; -- u, -- v; q.push_back({u, v, k}); } } vector<vector<info>> up(32, vector<info>(n)); for(int i = 0; i < n; ++ i){ if (val[i] == 1){ up[0][i] = info{1,1,1,0,0,0,1}; }else{ up[0][i] = info{0,0,0,-1,-1,-1,-1}; } } for(int i = 1; i < 32; ++ i){ for(int j = 0; j < n; ++ j){ fa[i][j] = fa[i - 1][fa[i - 1][j]]; if (fa[i - 1][j] != fa[i][j]) up[i][j] = up[i - 1][j] + up[i - 1][fa[i - 1][j]]; } } auto find = [&](int u, int v){ if (deep[u] < deep[v]) swap(u, v); int dis = deep[u] - deep[v]; info l{}, r{}; for(int i = 0; i < 32; ++ i){ if (dis & 1){ l = l + up[i][u]; u = fa[i][u]; } dis >>= 1; } if (u == v){ return l + reverse(up[0][u]); } for(int i = 31; i >= 0; -- i){ if (fa[i][u] != fa[i][v]){ l = l + up[i][u]; r = r + up[i][v]; u = fa[i][u]; v = fa[i][v]; } } int lca = fa[0][u]; l = l + up[0][u]; r = r + up[0][v]; return l + up[0][lca] + reverse(r); }; auto solve = [&](int u, int v, int k){ auto res = find(u, v); return res.min <= k && k <= res.max; }; for(auto &[u, v, k] : q) if (solve(u, v, k)) cout << "YES" << '\n'; else cout << "NO" << '\n'; } return 0; }
本文作者:~Lanly~
本文链接:https://www.cnblogs.com/Lanly/p/17496398.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步