省流版
- A. 考虑存在相邻两个数组成三角形即可
- B. 仅考虑唯一取值的元素是否占满了当前元素的所有取值
- C. 分阶段考虑贡献,每阶段长度减半,贡献是中点值*区间数量+总偏移量和,维护总偏移量
- D. 最大值取于俩数组从小到大排序。对于操作,等价于修改有序数组的最右边的数,维护答案
- E. 两种必胜情况,一种是q在叶子上,p不在叶子上,另一种是q的邻居能一步到叶子,考虑p的取值,统计非叶子非好点的数量即可
A - Tender Carpenter (cf2053 A)
题目大意
给定一个长度为 \(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 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';elsecout << "NO" << '\n';}return 0;
}
B - Outstanding Impressionist (cf2053 B)
题目大意
有一 \(n\) 个元素的数组,但你只记得第 \(i\) 个元素的值的范围是 \([l_i,r_i]\)。
现在对于每个元素,是否存在一种情况,它的值是独一无二的,即其他元素的值都不等于它。
解题思路
考虑当前元素 \(a_i\),它的范围是 \([l_i,r_i]\),如果它不是独一无二的,那么 \([l_i,r_i]\) 中的每个数在其他位置都一定出现了。
而其他位置的元素的范围是 \([l_j,r_j]\),如果 \(l_j == r_j\),那么该位置只能取一个值,否则它可以避免和 \(a_i\)取同样的值。
因此,我们只关注那些只能取一个值(\(l_j == r_j\))的元素,并记录一下\(l_j\)这个值出现了,记 \(forbid[l_j] = 1\)。
然后对于 \(a_i\),如果 \([l_i,r_i]\) 中的每个数都在之前出现过(\(\sum_{j=l_i}^{r_i} forbid_j == r_i - l_i + 1\)),那么 \(a_i\) 就不是独一无二的。用前缀和即可\(O(1)\)判断上述条件。
注意要消除 \(a_i\) 本身的影响,即若 \(l_i == r_i\),那么要去除它对 \(forbid[l_i]\) 的贡献。
这里就需要记录两个数组:\(cnt\) 和 \(forbid\),前者记录每个值出现的次数,后者记录当前值是否出现,即 \(forbid[i] = (cnt[i] > 0)\),前缀和维护 \(forbid\) 数组。注意去除贡献时不需要修改前缀和数组。
神奇的代码
#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)
题目大意
一个长度为 \(n\) 的全排列,\(1,2,3,\dotsc,n\),给定 \(k\),对区间 \([1,n]\) 进行以下操作。
考虑当前区间\([l,r]\),长度\(len = r - l + 1\)
- 若 \(len < k\),停止。
- 若 \(len\) 为奇数,幸运值增加\(m = \lfloor \frac{l + r}{2} \rfloor\),考虑区间 \([l, m - 1]\) 和 \([m + 1, r]\)。
- 若 \(len\) 为偶数,考虑区间 \([l, m]\) 和 \([m + 1, r]\)。
问最终的幸运值是多少。初始时幸运值为 \(0\)。
解题思路
- 第一阶段,会考虑一个区间:\([1,n]\)。
- 第二阶段,会考虑两个区间:\([1,\frac{n}{2}]\) 和 \([\frac{n}{2} + 1,n]\)。
- 第三阶段,会考虑四个区间:\([1,\frac{n}{4}]\)、\([\frac{n}{4} + 1,\frac{n}{2}]\)、\([\frac{n}{2} + 1,\frac{3n}{4}]\)、\([\frac{3n}{4} + 1,n]\)。
- 第四阶段,会考虑八个区间:\([1,\frac{n}{8}]\)、\([\frac{n}{8} + 1,\frac{n}{4}]\)、\([\frac{n}{4} + 1,\frac{3n}{8}]\)、\([\frac{3n}{8} + 1,\frac{n}{2}]\)、\([\frac{n}{2} + 1,\frac{5n}{8}]\)、\([\frac{5n}{8} + 1,\frac{3n}{4}]\)、\([\frac{3n}{4} + 1,\frac{7n}{8}]\)、\([\frac{7n}{8} + 1,n]\)。
- ......(上述可能会因为奇数的原因有所偏差)
- 区间长度 \(< k\),停止。
最终的幸运值就是每个阶段的贡献和。
注意到,无论是哪个阶段,所考虑的区间的长度都是一样的:从 \(n\),变成了 \(\frac{n}{2}\),再变成了 \(\frac{n}{4}\),再变成了 \(\frac{n}{8}\)...,这意味着,如果当前阶段的区间长度是奇数,那么它们对幸运值都有贡献,区别只是偏移量不同。
即如果第二阶段的区间长度是奇数,那么第二阶段的两个区间都会对幸运值有贡献,区别只是偏移量不同:第一个区间的贡献是 \(m\),第二个区间的贡献则是 \(shift + m\)。而这个偏移量 \(shift\) 是来自第一阶段的中点。
从中点值、偏移量的角度考虑每个阶段的贡献部分,则可以分成两个部分:中点值 \(\times\) 区间数量 + 总偏移量和。
对于当前的第 \(i\) 阶段,区间长度是 \(n\),则中点值是 \(\frac{n + 1}{2}\),区间数量是 \(2^{i - 1}\),考虑总偏移量和怎么求,从上述举例的分析可以得知它可以从上一阶段的总偏移量和中得到。
第 \(i - 1\) 阶段的总偏移量和是 \(add[i - 1]\),考虑每个偏移量的来源,像上述的第二阶段中的 \(shift\),它作用在区间\([\frac{n}{2} + 1,n]\)上,到了第三阶段,该区间就分成两个区间,每个区间算贡献时都要带上这个偏移量,因此该偏移量的贡献就会翻倍。由此实际上每个偏移量的贡献都会翻倍,即 \(add[i] = 2 \times add[i - 1]\)。
同时还有新的偏移量出现:考虑区间\([1,\frac{n}{2}]\),到了第 \(i\) 阶段,该区间会被一分为二,第二个区间就新增偏移量 \(\frac{n}{2}\),而一共有 \(2^{i - 1}\) 个区间,因此新增的偏移量和就是 \(2^{i - 1} \times \frac{n}{2}\)。
因此,从第 \(i - 1\) 阶段到第 \(i\) 阶段,总偏移量和就是 \(add[i] = 2 \times add[i - 1] + 2^{i - 1} \times \frac{n}{2}\)。注意这里的\(n\)是当前阶段的区间长度,不是题目给定的\(n\)。
由此,当前阶段的贡献:中点值 \(\times\) 区间数量 + 总偏移量和,即 \(\frac{n + 1}{2} \times 2^{i - 1} + add[i]\),都能\(O(1)\)得到,而阶段数是\(O(\log n)\)的,因此一次询问的时间复杂度是\(O(\log n)\)的。
神奇的代码
#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)
题目大意
给定两个数组 \(a,b\),长度均为 \(n\)。
可以打乱数组\(b\),最大化 \(\prod_{i=1}^{n} min(a_i, b_i)\)。
维护 \(q\) 次操作,每次操作,将让 一个数组的一个元素加一,然后回答其最大值。
结果对 \(998244353\) 取模。
解题思路
首先考虑如何最大化乘积。可以发现,两者都从小到大排序时,乘积最大。
可以这么考虑:我们从所有数的大到小考虑,对于每个数,比如是 \(a_i\),如果它对答案有贡献,那么要从 \(b\) 中找到一个数 \(b_j\),使得 \(b_j \ge a_i\),这样才能使得 \(min(a_i,b_j) = a_i\),如果没有这样的数,它对答案没有贡献,我们就把它放在 \(a\)的池子里。接下来考虑的数比如 \(b_i\),如果它对答案有贡献,那么要从 \(a\) 中找到一个数 \(a_j\),使得 \(a_j \ge b_i\),这样才能使得 \(min(a_j,b_i) = b_i\),而此时 \(a\) 的池子里的数一定都比 \(b_i\) 大,因此我们可以直接取出来与 \(b_i\) 匹配。
即维护两个数组,一个是 \(a\) 的池子,一个是 \(b\) 的池子,然后从大到小考虑,如果当前数对答案有贡献,就从另一个数组的池子里找一个数匹配,否则就放到自己的池子里。容易发现这样匹配的结果,就是两个数组从小到大排序的结果。
知道最优情况怎么求了,接下来考虑如何维护操作。
每次操作只会让一个数加一,看着变化不大,容易想到一个朴素的维护方法:加一后,与右边的数比较,如果比右边的数大,就交换,直到不再比右边的数大为止。交换的同时维护答案。但这样的时间复杂度有问题。
考虑数组\(a\):\(1,1,1,1,1,1,2\),第一次操作将第一个数加一,则变成\(1,1,1,1,1,2,2\),经历了\(O(n)\)次交换,然后再让第一个数加一,变成\(1,1,1,1,2,2,2\),又经历了\(O(n)\)次交换,这样的时间复杂度是\(O(qn)\),会超时。
怎么办呢?考虑上述朴素做法的问题:当我们第一个数加一时,不断交换位置时,其实答案是不变的:因为交换的两个数是同一个值。而从操作前和操作后的结果对比,其实它等价于让当前数的最右边的数加一,就没了。
因此,对于当前操作,假设对数组\(a\)操作,以及一个排了序的数组\(sorta\),当前对\(a_i = p\)加一,它等价于在\(sorta\)中找到最右边的\(sorta_i = p\),然后让\(sorta_{i}\)加一,同时维护答案(即先除以原来的数,操作后再乘以新的数)。
如何找到最右边的\(sorta_i = p\)呢?因为\(sorta\)是排好序的,可以用二分查找,upper_bound
的前一个就是最右边的\(p\)。
这样的时间复杂度是\(O(q\log n)\)的。
神奇的代码
#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)
题目大意
给定一棵树,对于一对点\((p, q)\),定义一个毛毛虫序列:\(p \to q\)的路径上的点,其中\(p\)是毛毛虫的头,\(q\)是毛毛虫的尾。
两人博弈,先手拉着毛毛虫的头,后手拉着毛毛虫的尾,两人轮流执行操作。每次可以选择一个点,然后将毛毛虫的头或尾连同身子往前移动到这个点的子节点上。
如果头到了叶子节点,先手胜利,如果尾到了叶子节点,后手胜利,如果始终无法到达叶子节点,平局。
两人都会采取最优策略,问\((p,q)\)的数量,使得后手必胜。
解题思路
考虑后手必胜的情况,容易想到的一个是:
- \(q\)在叶子节点,\(p\)不在叶子节点,那么后手必胜。这个统计一下叶子数量即可得到。
还有一种情况是,先手移动后,\(q\)在一个特别的位置,它可以一步移动到叶子节点,这样后手必胜。
我们定义能一步到叶子节点的点为好点,对于\(q\)点,考虑其每一个邻居\(u\),如果\(u\)是好点,那么\((u,q)\)这对点对答案有贡献,考虑贡献怎么求。
由于先手移动一步后,\(q \to u\),那么\(p\)得在\(u\)的子节点上,这样先手移动才有\(q \to u\),考虑\(p\)的取值:
- 不能在叶子上
- 不能在好点上
- 其他点都可以
因此就是统计一个子树里非叶子非好点的点的数量。一个简单的计数问题,可以用DFS解决。
选定一个点为根后,对于每个点\(q\),考虑其儿子\((u,q)\),还有父亲\((fa, q)\),计算它们的贡献即可。
时间复杂度是\(O(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 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;
}