A. [USACO23JAN] Tractor Paths P
有 \(n\) 个区间,第 \(i\) 个区间为 \([l_i,r_i]\)。保证 \(l_1<l_2<\cdots<l_n\) 且 \(r_1<r_2<\cdots<r_n\)。其中一部分区间是特殊的。
如果第 \(i\) 个区间和第 \(j\) 个区间相交,那么 \(i,j\) 之间有一条边。保证 \(1,n\) 联通。
\(q\) 次询问,每次给定 \(a,b\),求 \(a\) 到 \(b\) 至少要经过多少条边,以及有多少个特殊区间对应的点,使得这个点可能在 \(a\) 到 \(b\) 的最短路径上。
\(n,q \le 2\times 10^5\)。
显然,每个区间能够到达的也是一个区间 \([L_i,R_i]\),并且也满足 \(L_i,R_i\) 递增。
第一问倍增即可。第二问相当于要求有多少个点 \(d(a,x) + d(x,b) = d(a,b)\),我们可以求出 \(f_{i,k}\) 表示从 \(i\) 往右 \(k\) 步能到达的最远点,\(g_{i,k}\) 表示从 \(i\) 往左 \(k\) 步能到达的最远点,我们枚举 \(d(a,x)\),设 \(c(l,r)\) 表示在 \([l,r]\) 之间有多少个关键点,则答案即为 \(c(a,a) + c(b,b) + \sum \limits_{i = 1}^{d(a,b) - 1} c(g_{b,d(a,b) - i}, f_{a,i})\),显然这些区间不交。拆成前缀和用倍增维护即可,时间复杂度 \(\mathcal{O}(n \log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, k; char s[N], t[N];
int a[N], le[N], ri[N], f[N][18], g[N][18], p[N][18], q[N][18];
int qry(int x, int y) {
if (x == y) {
return 0;
}
int d = 0;
for (int i = 17; i >= 0; i--) {
if (f[x][i] >= y || f[x][i] == -1)
continue;
x = f[x][i], d += 1 << i;
}
return d + 1;
}
int main() {
// ios :: sync_with_stdio(false);
// cin.tie(nullptr);
cin >> n >> k;
scanf("%s %s", s + 1, t + 1);
int c = 0, u = 1;
for (int i = 1; i <= 2 * n; i++) {
if (s[i] == 'L') {
c++;
} else {
ri[u] = c, u++;
}
}
c = n + 1, u = n;
for (int i = 2 * n; i >= 1; i--) {
if (s[i] == 'R') {
c--;
} else {
le[u] = c, u--;
}
}
for (int i = 1; i <= n; i++) {
a[i] = a[i - 1] + t[i] - '0';
}
memset(f, -1, sizeof(f));
memset(g, -1, sizeof(g));
for (int i = 1; i < n; i++) {
f[i][0] = ri[i], p[i][0] = a[ri[i]];
}
for (int i = n; i > 1; i--) {
g[i][0] = le[i], q[i][0] = a[le[i] - 1];
}
for (int j = 1; j <= 17; j++) {
for (int i = 1; i < n; i++) {
if (f[i][j - 1] == -1)
continue;
f[i][j] = f[f[i][j - 1]][j - 1];
p[i][j] = p[i][j - 1] + p[f[i][j - 1]][j - 1];
}
}
for (int j = 1; j <= 17; j++) {
for (int i = n; i > 1; i--) {
if (g[i][j - 1] == -1)
continue;
g[i][j] = g[g[i][j - 1]][j - 1];
q[i][j] = q[i][j - 1] + q[g[i][j - 1]][j - 1];
}
}
while (k--) {
int x, y;
cin >> x >> y;
int c = qry(x, y), d = 0;
d += t[x] - '0';
d += t[y] - '0';
c--;
for (int i = 17; i >= 0; i--) {
if (c & (1 << i)) {
d += p[x][i], x = f[x][i];
}
}
for (int i = 17; i >= 0; i--) {
if (c & (1 << i)) {
d -= q[y][i], y = g[y][i];
}
}
c++;
cout << c << " " << d << "\n";
}
return 0;
}
B. [USACO23JAN] Mana Collection P
有 \(N\) 个法力池,第 \(i\) 个法力池每秒可积累 \(m_i\) 法力。有 \(M\) 条有向边 \((a_i,b_i,t_i)\) 连接,你可以在 \(t_i\) 秒内从 \(a_i\) 移动到 \(b_i\) 。每当你出现在一个池子里,你就可以收集储存在那个地方的所有法力,把它清空。在 \(0\) 的时候,所有的法力池都是空的,你可以选择任何一个池子来开始收集。
\(q\) 次询问,每个次给定两个整数 \(s\) 和 \(e\),求如果你在第 \(s\) 秒结束时必须在法力池 \(e\) 处,在 \(s\) 秒内能收集的最大法力值。
\(N \leq 18\),\(q \leq 2 \times 10^5\),时限 \(\text{5.0s}\)。
先用 Floyd 预处理出 \(d(x,y)\) 表示 \(x \to y\) 的最短距离,这步时间复杂度 \(\mathcal{O}(n^3)\)。
容易发现,对于一条路径,收集到的法力仅与路径上所有点的最晚遍历时刻有关,也就是说,我们仅需知道这些点的最晚遍历时刻即可算出收集到的法力大小。形式化地,假设路径经过了 \(c_1,c_2,\cdots,c_k = e\),且 \(c_i\) 最晚遍历的时间是 \(d_i(0 \leq d_1 < d_2 < \cdots < d_k = s)\),那么最终收集到的法力大小为 \(\sum \limits_{i=1}^k f_{c_i} \cdot d_i\)。
一个显然的贪心是,如果确定了一条路径,那么我们的策略一定是,先等一段时间,直到不走就来不及走到 \(e\) 了再开始走,即取 \(d_i = s - \sum \limits_{j = i}^{k-1} d(c_i,c_{i+1})\) 是最优的。那么法力大小即为 \(\sum \limits_{i = 1}^k f_{c_i} \cdot \left( s - \sum \limits_{j = i}^{k-1} d(c_i,c_{i+1}) \right) = s \cdot \sum \limits_{i=1}^k f_{c_i} - \sum \limits_{i=1}^{k-1} d(c_i,c_{i+1}) \sum \limits_{j=1}^{i} f_{c_j}\)。
将 \(\sum \limits_{i = 1}^k f_{c_i}\) 看做斜率,\(- \sum \limits_{i=1}^{k-1} d(c_i,c_{i+1}) \sum \limits_{j=1}^{i} f_{c_j}\) 看做截距,那么它对应了一条直线。进一步地,对于一个确定的点集,它所对应的直线的斜率是一个定值,故我们保留截距最大的那条即可,这可以通过状压 DP 在 \(\mathcal{O}(2^nn^2)\) 的时间复杂度内预处理。
对于每个询问,我们考虑满足 \(c_k = e\) 的点集对应的直线,并求出当前横坐标 \(s\) 对应的最大纵坐标即可,这可以通过单调栈维护凸包来预处理,询问时二分 \(s\) 对应的是哪一段即可。
现在唯一的问题是如何保证 \(d_1 \geq 0\)。但我们发现如果 \(d_1 < 0\) 一定不优,所以可以直接不管了。时间复杂度 \(\mathcal{O}(n^3 + 2^n n^2 + q \log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 18;
const LL inf = 1e18;
int n, m, q, a[N];
LL d[N][N], v[1 << N], f[1 << N][N];
struct dat {
LL x, y;
};
vector <dat> e[N], t[N];
vector <double> b[N];
double calc(dat a, dat b) {
if (a.x == b.x) {
if (a.y < b.y) {
return inf;
} else {
return -inf;
}
}
return 1.0 * (b.y - a.y) / (a.x - b.x);
}
int main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i < (1 << n); i++) {
for (int j = 1; j <= n; j++) {
f[i][j] = inf;
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) {
continue;
}
d[i][j] = inf;
}
}
for (int i = 1, x, y, z; i <= m; i++) {
cin >> x >> y >> z;
d[x][y] = z;
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
for (int s = 1; s < (1 << n); s++) {
for (int i = 1; i <= n; i++) {
if ((s & (1 << i - 1)) == 0) {
continue;
}
v[s] += a[i];
int t = s ^ (1 << i - 1);
if (t == 0) {
f[s][i] = 0;
break;
}
for (int j = 1; j <= n; j++) {
if ((t & (1 << j - 1)) == 0 || d[j][i] == inf) {
continue;
}
f[s][i] = min(f[s][i], f[t][j] + v[t] * d[j][i]);
}
}
}
for (int i = 1; i <= n; i++) {
for (int s = 1; s < (1 << n); s++) {
if ((s & (1 << i - 1)) == 0) {
continue;
}
e[i].push_back((dat){v[s], -f[s][i]});
}
sort(e[i].begin(), e[i].end(), [&](dat &a, dat &b) {
if (a.x == b.x) {
return a.y > b.y;
} else {
return a.x < b.x;
}
});
t[i].push_back((dat){0, 0}), b[i].push_back(-inf);
for (auto x : e[i]) {
while (t[i].size() > 1 && calc(x, t[i].back()) < b[i].back()) {
t[i].pop_back();
b[i].pop_back();
}
double r = calc(x, t[i].back());
t[i].push_back(x);
b[i].push_back(r);
}
}
cin >> q;
while (q--) {
int c, d;
cin >> c >> d;
int i = upper_bound(b[d].begin(), b[d].end(), c) - b[d].begin() - 1;
dat z = t[d][i];
LL ans = c * z.x + z.y;
cout << ans << "\n";
}
return 0;
}
C. [USACO23JAN] Subtree Activation P
有一棵根为 \(1\) 的树,每个顶点最初都是关闭的。在一次操作中,你可以将一个顶点的状态切换。输出操作序列的最小长度,满足对于每一个顶点的子树,都有一个时刻,开启状态顶点的集合恰好是该子树中的顶点,且在所有操作之后,每个顶点都是关闭的。
\(n \leq 2 \times 10^5\)。
第一眼:哎这不是 DSU on Tree 吗(x)
设 \(S_u\) 为 \(u\) 子树的节点组成的集合。如果最后要求所有顶点都开启就可以直接 DSU on Tree 了,但是现在要所有顶点都关闭,那么不妨先考虑一下我们会怎么关。不难发现,假设现在开启的集合是 \(S_u\),那么我们一定会每次选择一个儿子 \(v\),关掉 \(S_u/S_v\),这样就能得到 \(S_v\),重复这样的操作直到所有点关闭。开启的时候也同理。
也就是说,我们的操作大概长成这样:每次从一个叶子 \(u\) 开始点亮,依次覆盖 \(S_u,S_{f_u},\cdots,S_r\),然后从 \(S_r\) 开始关,依次覆盖 \(S_{v' \in \text{son}_r},\cdots,S_{v}\),其中 \(v\) 是另一个叶子。进一步地,每次可以选择两个叶子 \((u,v)\),然后选择一个 \(\text{LCA(u,v)}\) 到根的路径上的节点 \(r\),覆盖 \(u \to r\) 和 \(r \to v\) 上的所有集合。这样一次操作的代价是 \(2 \times s_r\),其中 \(s_r\) 为 \(r\) 的子树大小。
现在问题就变成了:选取若干这样的路径,覆盖树上所有节点,并使得代价最小。可以发现,对于每个 \(r\),我们至多选择一条这样的路径,否则可以拆成两条路径且答案更优。于是可以 DP,设 \(f_{u,0}\) 表示覆盖 \(u\) 子树所需的最小代价,\(f_{u,1}\) 表示剩下一条从 \(u\) 到其子树内某个叶结点的链,\(u\) 子树内其余结点均被覆盖所需的最小代价。转移比较简单,这里就不具体写了,时间复杂度 \(\mathcal{O}(n)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;
const LL inf = 1e18;
int n, sz[N]; LL f[N][2];
vector <int> e[N];
LL max(LL x, LL y) {
return x > y ? x : y;
}
void dfs(int u, int ff) {
sz[u] = 1;
LL sum = 0, mx = 0, mi = inf, _mi = inf;
for (auto v : e[u]) {
if (v == ff) {
continue;
}
dfs(v, u);
sz[u] += sz[v], mx = max(mx, sz[v]), sum += f[v][0];
LL d = f[v][1] - f[v][0];
if (d <= mi) {
_mi = mi, mi = d;
} else {
_mi = min(_mi, d);
}
}
if (sz[u] == 1) {
f[u][0] = 2;
return;
}
f[u][0] = 2 * sz[u] + sum, f[u][1] = sum + mi;
if (_mi < inf) {
f[u][0] += mi + _mi;
}
f[u][0] = min(f[u][0], sum + 2 * (sz[u] - mx));
}
int main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 2, f; i <= n; i++) {
cin >> f;
e[i].push_back(f);
e[f].push_back(i);
}
dfs(1, 0);
cout << f[1][0] << "\n";
return 0;
}