20230414 训练记录:前后缀
时间过得真快啊
去年这个时候打 ZUCC 校赛同步赛时还在牛客写了两个题解,里面有句话:
一年后的我已经学会了 lca,那现在算不算长大了呢?
这两篇题解也还挺有意思的,搬到这里好了!
Sum of Numerators
给定 \(N, K\),求解将序列 \(\left\{\dfrac{i}{2^K}\right\}\) 中元素全部约分后的分子和,其中 \(i\) 遍历 \(1 \sim N\)。
\(N \in [1, 10^9], K \in [0, 10 ^ 9]\)
首先注意到 \(\gcd(2^K, \mathrm{odd}) = 1\),于是我们先将所有奇数和算入答案,我们知道:
接下来考虑偶数与 \(2 ^ K\) 约分的过程,偶数可写作 \(X = x2 ^ t,\;\mathtt{where}\;\gcd(2, x) = 1\)。于是 \(2 ^ K\) 将会约分所有 \(t \le K\) 的部分,使得其变为一段奇数和,使用上述算式求解即可。
最后再加上没有被约分的偶数和即可,即
展开代码
using i64 = long long;
void solve() {
int n, k;
std::cin >> n >> k;
i64 ans = 0;
for (k += 1; k -- && n; n -= (n + 1) / 2) {
ans += 1LL * ((n + 1) / 2) * ((n + 1) / 2);
(!k) && (ans += 1LL * (n / 2) * (n / 2 + 1));
}
std::cout << ans << '\n';
}
Disjoint Path On Tree
给定一棵 \(N\) 个节点的树,求解二元组 \((u, v)\) 的个数,其中 \(u, v\) 是俩不相交的简单路径。
路径 \((i, j)\) 与 \((j, i), \,i \ne j\) 视作同一路径。\(N \in [1, 2 \times 10 ^ 5]\)
显然,树上的路径均为简单路径。容易知道 \(N\) 个节点的树的简单路径条数为:
而 \((i, j)\) 与 \((j, i), \,i \ne j\) 视作一样的也没关系,求出所有二元组后除以二即可。
另一方面,所求也等价于 \(\Big((F(N) ^ 2 -\) 相交组数 \(\Big)\),设两条路径交于 \(u\),即选择
讨论 \(x, y\) 的来源:
- \(x \in u\),即 \(\mathrm{cnt}_1 = F(u) - \displaystyle \sum_{to \;\in\;u} F(to)\)。
- \(x \notin u\),即 \(\mathrm{cnt}_2 = \mathrm{size}_u \times (n - \mathrm{size}_u)\)。
注意不能是两点均来自 \(x \notin u\)。组合起来:
展开代码
Z f(int n) { return 1LL * n * (n + 1) / 2; }
void solve() {
int n;
std::cin >> n;
std::vector<std::vector<int>> G(n);
for (int i = 1, u, v; i < n; ++i) {
std::cin >> u >> v;
-- u, -- v;
G[u].push_back(v);
G[v].push_back(u);
}
Z ans = f(n) * f(n);
std::vector<int> size(n);
std::function<void(int, int)> dfs = [&](int u, int p) {
size[u] = 1;
for (auto &&to : G[u]) if (to != p) {
dfs(to, u);
size[u] += size[to];
}
Z cnt1 = f(size[u]);
Z cnt2 = 1LL * size[u] * (n - size[u]);
for (auto &&to : G[u]) if (to != p) {
cnt1 -= f(size[to]);
}
ans -= (cnt1 + cnt2) * (cnt1 + cnt2) - cnt2 * cnt2;
};
dfs(0, -1);
std::cout << ans / 2 << '\n';
}
至多删除一/两段的最大子段和
集训队小伙伴问的面试题,挺有意思的。中途还断断续续地去请教了 tarjen 大佬,感激不尽。
至多一段
注意到,最终的答案是两段拼起来的:枚举 \(i\),求出 \(i\) 左侧的最大子段和、右侧的最大子段和,作为候选答案。实际上问题转换为求出前缀、后缀的最大子段和的最大值。这种前后缀拼起来贡献答案的题挺常见的。
展开代码
#include <bits/stdc++.h>
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
std::vector<int> a(n + 1), fp(n + 1), fs(n + 2);
for (int i = 1; i <= n; i++) std::cin >> a[i];
for (int i = 1; i <= n; i++) fp[i] = std::max(fp[i - 1], 0) + a[i];
for (int i = 1; i <= n; i++) fp[i] = std::max(fp[i], fp[i - 1]);
for (int i = n; i >= 1; i--) fs[i] = std::max(fs[i + 1], 0) + a[i];
for (int i = n; i >= 1; i--) fs[i] = std::max(fs[i], fs[i + 1]);
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = std::max(ans, fp[i - 1] + fs[i + 1]);
}
std::cout << std::max(ans, fp[n]) << '\n';
return 0;
}
至多两段
我首先想的是,是否等价于做两次上面的那个问题。然后被群友 Hack 了:
![](https://img2023.cnblogs.com/blog/2993672/202304/2993672-20230415014232145-143974901.png)
这时候 tarjen 好哥哥说直接 \(\mathcal O(n \log n)\) 扫一遍就好了,我却没有理解到,接着加好友学了一波。答案实际上也是两段,不过这次是前区间和最小的两段,问题转换为求出所有前缀中,区间和最小的一段和为多少。区间和是两段前缀和之差。即对于当前遍历到的 \(i\),前缀和为 \(s_i\),找出所有前缀和 \(s_j\,(j \lt i)\) 中的最大值,和 \(s_i\) 的差就是前缀中最小的那段区间和了。
举一些例子来描述这个过程:
-1 -1 -1 -1 -1 [-1]
当前遍历到的前缀和为 \(-6\),前缀和集合为 \(\color{red}{0}\) \(-1, -2, -3, -4, -5\) 最大的是 \(0\),因此当前最小区间和为 \(-6\)。
1 2 3 -6 5 [-6]
当前遍历到的前缀和为 \(-4\),前缀和集合为 \(0, 1, 3, 5, 6\),所以求出最小的区间和为 \(-1 - 6 = -7\)。
展开代码
#include <bits/stdc++.h>
using ll = long long;
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
std::vector<int> a(n + 1);
std::vector<ll> ps(n + 1), ss(n + 2);
for (int i = 1; i <= n; i++) std::cin >> a[i];
for (int i = 1; i <= n; i++) ps[i] = ps[i - 1] + a[i];
for (int i = n; i >= 1; i--) ss[i] = ss[i + 1] + a[i];
std::set<ll, std::greater<ll>> s;
constexpr ll inf = 1e18;
std::vector<ll> fp(n + 1), fs(n + 2);
fp[0] = fs[n + 1] = inf;
for (int i = 1; i <= n; i++) {
s.insert(ps[i - 1]);
fp[i] = std::min(fp[i - 1], ps[i] - *s.begin());
}
s.clear();
for (int i = n; i >= 1; i--) {
s.insert(ss[i + 1]);
fs[i] = std::min(fs[i - 1], ss[i] - *s.begin());
}
fp[0] = fs[n + 1] = 0;
ll ans = 0;
for (int i = 1; i <= n; i++) {
ans = std::min(ans, fp[i - 1] + fs[i + 1]);
}
std::cout << std::max<ll>(0, ps[n] - ans) << '\n';
return 0;
}
闲话:学弟一天就写了个单调队列模板题然后十分得意,想说一通可是基本功实力不如他,讨厌自己。