Good Bye 2024
省流版
- A. 考虑存在相邻两个数组成三角形即可
- B. 仅考虑唯一取值的元素是否占满了当前元素的所有取值
- C. 分阶段考虑贡献,每阶段长度减半,贡献是中点值*区间数量+总偏移量和,维护总偏移量
- D. 最大值取于俩数组从小到大排序。对于操作,等价于修改有序数组的最右边的数,维护答案
- E. 两种必胜情况,一种是q在叶子上,p不在叶子上,另一种是q的邻居能一步到叶子,考虑p的取值,统计非叶子非好点的数量即可
A - Tender Carpenter (cf2053 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; bool ok = false; for (int i = 0; i < n - 1; ++i) { int minn = min(a[i], a[i + 1]); int maxx = max(a[i], a[i + 1]); ok |= (minn + minn > maxx); } if (ok) cout << "YES" << '\n'; else cout << "NO" << '\n'; } return 0; }
B - Outstanding Impressionist (cf2053 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> forbid(2 * n + 1); vector<int> cnt(2 * n + 1); vector<array<int, 2>> a(n); auto add = [&](int x) { if (cnt[x] == 0) forbid[x] = 1; cnt[x]++; }; auto remove = [&](int x) { cnt[x]--; if (cnt[x] == 0) forbid[x] = 0; }; for (auto& [l, r] : a) { cin >> l >> r; if (l == r) { add(l); } } vector<int> presum(2 * n + 1); partial_sum(forbid.begin(), forbid.end(), presum.begin()); auto get_sum = [&](int l, int r) { return presum[r] - (l == 0 ? 0 : presum[l - 1]); }; string s(n, '0'); for (int i = 0; i < n; ++i) { auto [l, r] = a[i]; if (l == r) { remove(l); } auto sum = get_sum(l, r); if (sum != (r - l + 1) || forbid[l] == 0) s[i] = '1'; if (l == r) add(l); } cout << s << '\n'; } return 0; }
C - Bewitching Stargazer (cf2053 C)
题目大意
一个长度为 的全排列,,给定 ,对区间 进行以下操作。
考虑当前区间,长度
- 若 ,停止。
- 若 为奇数,幸运值增加,考虑区间 和 。
- 若 为偶数,考虑区间 和 。
问最终的幸运值是多少。初始时幸运值为 。
解题思路
- 第一阶段,会考虑一个区间:。
- 第二阶段,会考虑两个区间: 和 。
- 第三阶段,会考虑四个区间:、、、。
- 第四阶段,会考虑八个区间:、、、、、、、。
- ......(上述可能会因为奇数的原因有所偏差)
- 区间长度 ,停止。
最终的幸运值就是每个阶段的贡献和。
注意到,无论是哪个阶段,所考虑的区间的长度都是一样的:从 ,变成了 ,再变成了 ,再变成了 ...,这意味着,如果当前阶段的区间长度是奇数,那么它们对幸运值都有贡献,区别只是偏移量不同。
即如果第二阶段的区间长度是奇数,那么第二阶段的两个区间都会对幸运值有贡献,区别只是偏移量不同:第一个区间的贡献是 ,第二个区间的贡献则是 。而这个偏移量 是来自第一阶段的中点。
从中点值、偏移量的角度考虑每个阶段的贡献部分,则可以分成两个部分:中点值 区间数量 + 总偏移量和。
对于当前的第 阶段,区间长度是 ,则中点值是 ,区间数量是 ,考虑总偏移量和怎么求,从上述举例的分析可以得知它可以从上一阶段的总偏移量和中得到。
第 阶段的总偏移量和是 ,考虑每个偏移量的来源,像上述的第二阶段中的 ,它作用在区间上,到了第三阶段,该区间就分成两个区间,每个区间算贡献时都要带上这个偏移量,因此该偏移量的贡献就会翻倍。由此实际上每个偏移量的贡献都会翻倍,即 。
同时还有新的偏移量出现:考虑区间,到了第 阶段,该区间会被一分为二,第二个区间就新增偏移量 ,而一共有 个区间,因此新增的偏移量和就是 。
因此,从第 阶段到第 阶段,总偏移量和就是 。注意这里的是当前阶段的区间长度,不是题目给定的。
由此,当前阶段的贡献:中点值 区间数量 + 总偏移量和,即 ,都能得到,而阶段数是的,因此一次询问的时间复杂度是的。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = unsigned long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); auto solve = [&](int n, int k) { LL ans = 0; __int128 nn = 1; __int128 add = 0; while (n) { if (n < k) break; if (n & 1) { ans += (n + 1) / 2 * nn + add; } add = add * 2 + (n + 1) / 2 * nn; nn *= 2; n /= 2; } return ans; }; int t; cin >> t; while (t--) { int n, k; cin >> n >> k; LL ans = solve(n, k); cout << ans << '\n'; } return 0; }
D - Refined Product Optimality (cf2053 D)
题目大意
给定两个数组 ,长度均为 。
可以打乱数组,最大化 。
维护 次操作,每次操作,将让 一个数组的一个元素加一,然后回答其最大值。
结果对 取模。
解题思路
首先考虑如何最大化乘积。可以发现,两者都从小到大排序时,乘积最大。
可以这么考虑:我们从所有数的大到小考虑,对于每个数,比如是 ,如果它对答案有贡献,那么要从 中找到一个数 ,使得 ,这样才能使得 ,如果没有这样的数,它对答案没有贡献,我们就把它放在 的池子里。接下来考虑的数比如 ,如果它对答案有贡献,那么要从 中找到一个数 ,使得 ,这样才能使得 ,而此时 的池子里的数一定都比 大,因此我们可以直接取出来与 匹配。
即维护两个数组,一个是 的池子,一个是 的池子,然后从大到小考虑,如果当前数对答案有贡献,就从另一个数组的池子里找一个数匹配,否则就放到自己的池子里。容易发现这样匹配的结果,就是两个数组从小到大排序的结果。
知道最优情况怎么求了,接下来考虑如何维护操作。
每次操作只会让一个数加一,看着变化不大,容易想到一个朴素的维护方法:加一后,与右边的数比较,如果比右边的数大,就交换,直到不再比右边的数大为止。交换的同时维护答案。但这样的时间复杂度有问题。
考虑数组:,第一次操作将第一个数加一,则变成,经历了次交换,然后再让第一个数加一,变成,又经历了次交换,这样的时间复杂度是,会超时。
怎么办呢?考虑上述朴素做法的问题:当我们第一个数加一时,不断交换位置时,其实答案是不变的:因为交换的两个数是同一个值。而从操作前和操作后的结果对比,其实它等价于让当前数的最右边的数加一,就没了。
因此,对于当前操作,假设对数组操作,以及一个排了序的数组,当前对加一,它等价于在中找到最右边的,然后让加一,同时维护答案(即先除以原来的数,操作后再乘以新的数)。
如何找到最右边的呢?因为是排好序的,可以用二分查找,upper_bound
的前一个就是最右边的。
这样的时间复杂度是的。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; const int mo = 998244353; int qpower(int a, int b) { int qwq = 1; while (b) { if (b & 1) qwq = 1ll * qwq * a % mo; a = 1ll * a * a % mo; b >>= 1; } return qwq; } int inv(long long x) { return qpower(x, mo - 2); } int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t; cin >> t; while (t--) { int n, q; cin >> n >> q; array<vector<int>, 2> a{vector<int>(n), vector<int>(n)}; for (auto& x : a) for (auto& y : x) cin >> y; auto sorta = a; for (auto& x : sorta) sort(x.begin(), x.end()); LL ans = 1; auto get = [&](int x) { return min(sorta[0][x], sorta[1][x]); }; for (int i = 0; i < n; ++i) { ans = ans * get(i) % mo; } cout << ans << ' '; while (q--) { int o, x; cin >> o >> x; --o; --x; int val = a[o][x]; int pos = prev(upper_bound(sorta[o].begin(), sorta[o].end(), val)) - sorta[o].begin(); ans = ans * inv(get(pos)) % mo; a[o][x] = val + 1; sorta[o][pos] = val + 1; ans = ans * get(pos) % mo; cout << ans << " \n"[q == 0]; } } return 0; }
E - Resourceful Caterpillar Sequence (cf2053 E)
题目大意
给定一棵树,对于一对点,定义一个毛毛虫序列:的路径上的点,其中是毛毛虫的头,是毛毛虫的尾。
两人博弈,先手拉着毛毛虫的头,后手拉着毛毛虫的尾,两人轮流执行操作。每次可以选择一个点,然后将毛毛虫的头或尾连同身子往前移动到这个点的子节点上。
如果头到了叶子节点,先手胜利,如果尾到了叶子节点,后手胜利,如果始终无法到达叶子节点,平局。
两人都会采取最优策略,问的数量,使得后手必胜。
解题思路
考虑后手必胜的情况,容易想到的一个是:
- 在叶子节点,不在叶子节点,那么后手必胜。这个统计一下叶子数量即可得到。
还有一种情况是,先手移动后,在一个特别的位置,它可以一步移动到叶子节点,这样后手必胜。
我们定义能一步到叶子节点的点为好点,对于点,考虑其每一个邻居,如果是好点,那么这对点对答案有贡献,考虑贡献怎么求。
由于先手移动一步后,,那么得在的子节点上,这样先手移动才有,考虑的取值:
- 不能在叶子上
- 不能在好点上
- 其他点都可以
因此就是统计一个子树里非叶子非好点的点的数量。一个简单的计数问题,可以用DFS解决。
选定一个点为根后,对于每个点,考虑其儿子,还有父亲,计算它们的贡献即可。
时间复杂度是。
神奇的代码
#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); vector<int> du(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); du[u]++; du[v]++; } if (n == 2) { cout << 0 << '\n'; continue; } int leaves = 0; vector<int> good(n); for (int i = 0; i < n; ++i) { if (du[i] == 1) { leaves++; for (int v : edge[i]) { good[v] = 1 && (du[v] != 1); } } } int goods = accumulate(good.begin(), good.end(), 0); LL ans = 1ll * leaves * (n - leaves); auto dfs = [&](auto& dfs, int u, int fa) -> array<int, 3> { int sum = good[u]; int sz = 1; int leave = du[u] == 1; for (int v : edge[u]) { if (v == fa) continue; auto [nsum, nleave, nsz] = dfs(dfs, v, u); if (good[v]) { ans += nsz - nleave - nsum; } sum += nsum; sz += nsz; leave += nleave; } if (u != fa && du[u] != 1) { if (good[fa]) { int ano_sum = goods - sum; int ano_sz = n - sz; int ano_leave = leaves - leave; ans += ano_sz - ano_leave - ano_sum; } } return {sum, leave, sz}; }; int root = 0; while (root < n && du[root] == 1) root++; dfs(dfs, root, root); cout << ans << '\n'; } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!