训练总结 1
CF1899G
首先先求出 dfs 序,然后就相当于每次询问 \([l, r]\) 中是否存在 \(y \in [L_x, R_x]\)。
常见套路:把询问离线,我们只需要求出 \([1, r]\) 和 \([1, l - 1]\) 中 属于 \([L_x, R_x]\) 的点的个数,如果相等,那么说明 \([l, r]\) 中没有新增属于 \([L_x, R_x]\) 中的点,也就是不存在。
或者更直接的做法,把每个点的标号按照 dfs 序插到线段树中,然后固定询问的右端点,在线段树中查询 \([L_x, R_x]\) 中的最大值是否大于等于询问中的 \(l\)。
void solve() {
int n, q; std::cin >> n >> q;
std::vector<std::vector<int>> G(n + 1);
for (int i = 1; i < n; ++i) {
int u, v; std::cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
std::vector<int> p(n + 1);
for (int i = 1; i <= n; ++i) {
std::cin >> p[i];
}
std::vector<int> L(n + 1), R(n + 1);
int dfc = 0;
auto dfs = [&](auto &self, int u, int f) -> void {
L[u] = ++dfc;
for (auto v : G[u]) {
if (v == f) {
continue;
}
self(self, v, u);
}
R[u] = dfc;
};
dfs(dfs, 1, 0);
std::vector<std::vector<std::array<int, 3>>> qa(n + 1);
for (int i = 1; i <= q; ++i) {
int l, r, x; std::cin >> l >> r >> x;
qa[l - 1].push_back({x, i, 0});
qa[r].push_back({x, i, 1});
}
Fenwick<int> fen(n + 1);
std::vector<std::array<int, 2>> ans(q + 1);
for (int i = 1; i <= n; ++i) {
fen.add(L[p[i]], 1);
for (auto [x, id, y] : qa[i]) {
ans[id][y] = fen.rangeSum(L[x], R[x]);
}
}
for (int i = 1; i <= q; ++i) {
std::cout << (ans[i][0] == ans[i][1] ? "No\n" : "Yes\n");
}
}
CF1902D
注意到对于翻转区间,是否翻转对终点的位置是没有影响的。进一步画图观察可以得到:设未翻转时第 \(i\) 步走到的点为 \((pre_{i, 0}, pre_{i, 1})\),对于一个要翻转的区间,如果翻转前经过了 \((x, y)\),那么翻转后一定会经过 \((pre_{l - 1, 0} + pre_{r, 0} - x, pre_{l - 1, 1} + pre_{r, 1} - y)\)。
这里蕴含着一种对称性,对于网格问题有的时候可以从对称性入手思考,且网格路径的走法重新排列对终点是没有影响的。
CF1902E
正难则反,算贡献。
设 \(s_i\) 和 \(s_j\) 的后缀和前缀的最长匹配长度为 \(l\),那么\(|C(s_i, s_j)| = |s_i| + |s_j| - l\)。
问题的关键在于求 \(l\) 对答案的贡献。固定 \(s_i\),将 \(s_i\) 翻转,那么对于翻转后的 \(s_i\) 的每一个前缀,相当于我们要计算出有多少个 \(s_j\) 的前缀能匹配,减掉他们(贡献类似与一层一层叠上去)。这个可以用字典树解决。
CF1905D
注意到题目要求前缀 \(\text{mex}\) 的和,那么正着肯定不好处理。注意到题目给出的是一个排列,所以我们倒着考虑,发现题目转化为求一个排列循环移位后 后缀 \(\min\) 的和 的最大值。
注意到后缀 \(\min\) 一定是单调不减的,同时是分段的,即某一段的 \(\min\) 都是一个值,可以合并计算。那么我们正着维护一个单调不减的单调栈,破坏单调性要 pop 的时候减去之前的贡献,push 一个新元素的时候加上新贡献,那么把原数组倍长用单调栈维护取答案的 \(\max\) 就行了。
void solve() {
int n; std::cin >> n;
std::vector<int> p(n * 2 + 2, -1);
for (int i = 1; i <= n; ++i) {
std::cin >> p[i];
p[i + n] = p[i];
}
std::vector<int> stk;
stk.push_back(0);
i64 res = 0, ans = 0;
for (int i = 1; i <= n * 2; ++i) {
while (stk.size() && p[stk.back()] > p[i]) {
int u = stk.back(); stk.pop_back();
res -= 1ll * (u - stk.back()) * p[u];
}
res += 1ll * (i - stk.back()) * p[i];
stk.push_back(i);
if (i > n) {
ans = std::max(ans, res + 1ll * n);
}
}
std::cout << ans << "\n";
}
CF2072F
实际上要求的就是 \(\binom{n - 1}{i} \mod 2\) 的值。
直接的做法就是预处理 \(1 - N\) 中每个数有多少个 2,然后做一个前缀和。直接判断 2 的个数是否等于 0 即可。
更简单的做法:组合数模 2 的性质
ABC391F
设 \(f(i, j, k)=A_iB_j + B_jC_k + C_kA_i\),把 a, b, c 从小到大排序,那么有 \(f(i, j, k) \geq f(i - 1, j, k)\),\(f(i, j, k) \geq f(i, j - 1, k)\),\(f(i, j, k) \geq f(i, j, k - 1)\)。
所以可以一步一步地推出第 k 大。具体来说,小于当前值 \(f(i, j, k)\) 的较大值在上述三个值中取到。那么维护一个优先队列,每次弹出最大值,并将对应的可能的较大值插到队列中,做 k 次就能得到第 k 大值。
对于某些求前 k 大值的问题,有可能是能够递推求解的。
ABC359F
直觉上的结论:\(n\) 个节点的树的度数之和为 \(2n - 2\)。
问题转化为一个经典的贪心:使当前点 \(u\) 的度数 \(i\) 变为 \(i+1\) 对答案的贡献为 \((2i+1) \times a_u\),那么把贡献当作关键字插到 pq 里,每次挑出贡献最大的加入答案。
每个点的度数至少为 1,所以先对每个点做一次,然后再把剩余的 \(n-2\) 度数分配。
ABC390F
算贡献。

浙公网安备 33010602011771号