动态规划随笔
可能是做题记录,笔记或者一些感想,总之很随便吧
动态规划
线性动态规划
总之是线性处理的,能在某些方面体现线性。
题目
P2501 [HAOI2006] 数字序列
\(\color{#FFA500}{[NORMAL+]}\)
本题数据随机生成,时间复杂度 \(\mathcal{O}(n^2)\) 也能过
记 \(b_i = a_i - i\) ,第一问即求 \(\{b\}\) 的最长不降子序列的长度。用类似求 LIS 的 \(\mathcal{O}(n\log n)\) 的方式求出。
第二问等价于求 \(\{b\}\) 在改变数目最小的情况下,最小的变化量之和。解决问题的基础是,两个在 \(\{b\}\) 的最长不降子序列里相邻的数 \(b_i, b_{j}\) ,修改后可以满足 \(k \in (i, j), \forall p \in (i, k], q \in (k, j), b_p = b_i, b_q = b_j\),同时,变化量之和最小。
证明参考这篇,反正让我写我也只会抄,我觉得应该是找到一个满足左边上升点个数大于下降点个数,右边反之的临界点。
对于两个确定的点,中间的临界点需要暴力枚举。
最长不降子序列的个数,每种都需要求,考虑动态规划。
定义\(c(i)\) 表示以 \(i\) 为结尾最长不降子序列长度,\(f(i)\) 为将 \([1, i]\) 变为以 \(i\) 为结尾的不降序列的最小变化量。
则有
\(\sum\limits_{p = j + 1}^{k} |a_j - a_p|\) 等容易用前缀和处理,满足 \(c(j) = c(i) - 1\) 的 \(j\) 点,可以用 vector 存,暴力转移即可。这是 \(\mathcal{O}(n^2)\) 的。
一些碎碎念
关于第一问:在理所当然之后,我重新思考为什么第一问是这样做的,为什么不直接用总长减去 LIS 长度呢。
这很显然,LIS 中相邻两点间可能只相差 \(1\),这对于中间的点是无法更改的。
关于证明:我认为李煜东关于这题的证明和本题没有关系,本题是有前置条件:改变的数最少的情况下。
关于复杂度:对于每个能最长不降序列来说,原序列都会被遍历一遍,花费 \(\mathcal{O}(n)\)。定义 \(R\) 表示最长不降序列的数目,时间复杂度约为 \(\mathcal{O}(Rn)\)。
因为数据随机,所以 \(R\) 远小于 \(n\),故能过。
因此也很好卡,生成一个单调下降的序列,可以把许多题解卡到 1.8s左右。
code
const int N = 3.5e4 + 5;
const int inf = 1e9;
int n, a[N], b[N], c[N], cnt;
vector<int> vec[N];
int f[N];
i64 g[N], pre[N], suf[N];
int main() {
read(n);
rep(i, 1, n) read(a[i]), b[i] = a[i] - i;
b[0] = -inf, b[n + 1] = inf;
memset(c, 0x3f, sizeof(c));
vec[0].push_back(0); c[0] = b[0];
vec[1].push_back(1); c[++cnt] = b[1], f[1] = 1;
rep(i, 2, n + 1) {
if (c[cnt] <= b[i]) {
c[++cnt] = b[i];
f[i] = cnt;
vec[cnt].push_back(i);
continue;
}
int pos = upper_bound(c + 1, c + cnt + 1, b[i]) - c;
c[pos] = b[i];
f[i] = pos;
vec[pos].push_back(i);
}
write(n - cnt + 1); puts("");
rep(i, 1, n + 1) g[i] = 1e18;
rep(i, 1, n + 1)
rep(j, 0, (int)vec[f[i] - 1].size() - 1) {
int st = vec[f[i] - 1][j];
if (st >= i || b[st] > b[i]) continue;
pre[st] = suf[i] = 0;
rep(k, st + 1, i - 1) pre[k] = pre[k - 1] + abs(b[k] - b[st]);
per(k, i - 1, st + 1) suf[k] = suf[k + 1] + abs(b[k] - b[i]);
rep(k, st, i - 1) g[i] = min(g[i], g[st] + pre[k] + suf[k + 1]);
}
write(g[n + 1]);
return 0;
}
P1541 [NOIP 2010 提高组] 乌龟棋
\(\color{#39C5BB}{[EASY-]}\)
容易想到状态 \(f(i, j, k, l)\) 表示用了 \(i/j/k/l\) 张标有 \(1/2/3/4\) 的卡牌。转移显然。
code
const int N = 355;
const int M = 125;
const int K = 45;
int a[N], b[M], cnt[5];
int n, m;
int f[K][K][K][K];
int main() {
read(n, m);
rep(i, 1, n) read(a[i]);
rep(i, 1, m) {
read(b[i]);
cnt[b[i]]++;
}
rep(i, 0, cnt[1]) rep(j, 0, cnt[2]) rep(k, 0, cnt[3]) rep(l, 0, cnt[4])
f[i][j][k][l] = -1e9;
f[0][0][0][0] = a[1];
rep(i, 0, cnt[1]) rep(j, 0, cnt[2]) rep(k, 0, cnt[3]) rep(l, 0, cnt[4]) {
int x = i + 2 * j + 3 * k + 4 * l + 1;
if (i + 1 <= cnt[1]) f[i + 1][j][k][l] = max(f[i + 1][j][k][l], f[i][j][k][l] + a[x + 1]);
if (j + 1 <= cnt[2]) f[i][j + 1][k][l] = max(f[i][j + 1][k][l], f[i][j][k][l] + a[x + 2]);
if (k + 1 <= cnt[3]) f[i][j][k + 1][l] = max(f[i][j][k + 1][l], f[i][j][k][l] + a[x + 3]);
if (l + 1 <= cnt[4]) f[i][j][k][l + 1] = max(f[i][j][k][l + 1], f[i][j][k][l] + a[x + 4]);
}
write(f[cnt[1]][cnt[2]][cnt[3]][cnt[4]]);
return 0;
}
背包
二维背包
P8548 小挖的买花
\(\color{#39C5BB}{[EASY+]}\)
考虑到 \(f_j\le 500\),新鲜度的容量开到 \(501\) 即可,用 \(501\) 表示新鲜度大于 \(500\) 的情况。
然后转移很容易。
考虑到背包表示的是恰好的情况,容易用前缀后缀处理出至少和至多的情况。
这题目的边界情况好猎奇
code
const int N = 505;
int n, q, c[N], f[N], b[N];
int g[N][N], suf[N][N], pre[N][N];
int main() {
read(n, q);
rep(i, 1, n) read(c[i], f[i], b[i]);
rep(i, 0, 504) rep(j, 0, 504)
g[i][j] = suf[i][j] = pre[i][j] = -1e9;
g[0][0] = 0;
rep(i, 1, n) per(j, 500, c[i]) {
per(k, 501, 501 - f[i]) g[j][501] = max(g[j][501], g[j - c[i]][k] + b[i]);
per(k, 500, f[i]) g[j][k] = max(g[j][k], g[j - c[i]][k - f[i]] + b[i]);
}
rep(i, 0, 500) per(j, 501, 0)
suf[i][j] = max(suf[i][j + 1], g[i][j]);
rep(j, 0, 501) rep(i, 0, 500)
pre[i][j] = max(pre[max(i - 1, 0)][j], suf[i][j]);
rep(i, 1, q) {
int u, v; read(u, v);
write(pre[u][v] < 0 ? 0 : pre[u][v]); puts("");
}
return 0;
}
分组背包
P3188 [HNOI2007] 梦幻岛宝珠
\(\color{#FFA500}{[NORMAL-]}\)
因为题目保证的特殊条件,导致这题好像不是很难可以想到按 \(2^b\) 分组,背包的容量按 \(a\) 算,这样背包的容量就大大下降了。
定义 \(f(i, j)\) 表示 \(2^i\) 组内的元素,用 \(j\) 的容量的最大价值。处理这个是容易的。
然后考虑分组背包,定义 \(g(i, j)\) 表示用 \(j\) 个 \(2^i\) 的总价值能获得的最大代价。得到状态转移方程
转移用 \(f(i, j)\) 就行了。
关于初始化:
初始化显然应该与定义有关。
\(f(i, j)\) 应全制为 \(0\),因为后面考虑的是能获得的最大价值,所以不需要考不存在的状态。
code
const int N = 105;
const i64 inf = 1e18;
int n, W, p[N], r[N];
i64 f[35][1005], w[N], v[N];
void solve() {
rep(i, 0, 34) rep(j, 0, 1004) f[i][j] = 0;
rep(i, 1, n) {
read(w[i], v[i]);
r[i] = w[i] / (w[i] & -w[i]), p[i] = 0;
for (i64 j = (w[i] & -w[i]) >> 1; j; j >>= 1) p[i]++;
}
rep(i, 1, n) per(j, 1000, r[i])
f[p[i]][j] = max(f[p[i]][j], f[p[i]][j - r[i]] + v[i]);
int len = 0, _W = W >> 1;
while (_W) len++, _W >>= 1;
rep(i, 1, len) {
per(j, 1000, 0) rep(k, 0, j) {
f[i][j] = max(f[i][j], f[i][j - k] + f[i - 1][min(1000, 2 * k + (W >> (i - 1) & 1))]);
}
}
write(f[len][1]); puts("");
}
int main() {
while (read(n, W), n != -1) solve();
return 0;
}
区间动态规划
题目
P2466 [SDOI2008] Sue 的小球
\(\color{#FFA500}{[NORMAL]}\)
记 \(t_i\) 为接到第 \(i\) 个球的时间。答案是 \(\sum\limits_{i = 1}^{n} y_i - t_i\times v_i = \sum\limits_{i = 1}^n y_i - \sum\limits_{i = 1}^nt_i\times v_i\)。
\(\sum\limits_{i = 1}^n y_i\) 为定值,要求最小化 \(\sum\limits_{i = 1}^nt_i\times v_i\)。直接做大概不容易,换种思路,每次移动消耗的时间会令所有未接的球价值减小,从整体看,计算所有小球减少的权值,令其总和最小。
起点看成下落速度和高度为 \(0\) 的小球,将小球按 \(x\) 排序。对于某个时刻,接到的球构成一段区间,所有未接到的球单位时间减少的价值容易用前缀和后缀预处理。
考虑区间 DP,定义 \(f(i, j, 0/1)\) 表示编号 \([i, j]\) 的小球全部接完,停在左/右的最小代价,转移方程显然。
关于初始化
最值开始考虑错了,实际上是 \(1\times10^{13}\) 左右量级的,要开
long long,初始化用0x3f够了。但是我的
ans是int。糖丸了。
code
const int N = 1e3 + 5;
int n, b[N], x0;
struct node { int x, y, v; } a[N];
i64 f[N][N][2], pre[N], suf[N];
bool cmp(node a, node b) {
return a.x == b.x ? a.y < b.y : a.x < b.x;
}
int main() {
read(n, x0);
rep(i, 1, n) read(a[i].x);
rep(i, 1, n) read(a[i].y);
rep(i, 1, n) read(a[i].v);
a[n + 1] = {x0, 0, 0};
rep(i, 1, n + 1) b[i] = a[i].x;
sort(b + 1, b + n + 2);
int len = unique(b + 1, b + n + 2) - b - 1;
rep(i, 1, n + 1) a[i].x = lower_bound(b + 1, b + len + 1, a[i].x) - b;
x0 = a[n + 1].x;
sort(a + 1, a + n + 2, cmp);
memset(f, 0x3f, sizeof(f));
f[x0][x0][0] = f[x0][x0][1] = 0;
rep(i, 1, n + 1) pre[i] = pre[i - 1] + a[i].v;
per(i, n + 1, 1) suf[i] = suf[i + 1] + a[i].v;
rep(l, 1, n + 1) rep(i, 1, (n - l + 2)) {
f[i - 1][i + l - 1][0] = min(f[i - 1][i + l - 1][0], f[i][i + l - 1][0] + (b[a[i].x] - b[a[i - 1].x]) * (pre[i - 1] + suf[i + l]));
f[i][i + l][1] = min(f[i][i + l][1], f[i][i + l - 1][0] + (b[a[i + l].x] - b[a[i].x]) * (pre[i - 1] + suf[i + l]));
f[i - 1][i + l - 1][0] = min(f[i - 1][i + l - 1][0], f[i][i + l - 1][1] + (b[a[i + l - 1].x] - b[a[i - 1].x]) * (pre[i - 1] + suf[i + l]));
f[i][i + l][1] = min(f[i][i + l][1], f[i][i + l - 1][1] + (b[a[i + l].x] - b[a[i + l - 1].x]) * (pre[i - 1] + suf[i + l]));
}
i64 ans = -min(f[1][n + 1][0], f[1][n + 1][1]);
rep(i, 1, n + 1) ans += a[i].y;
printf("%0.3lf", ans / 1000.0);
return 0;
}
P10236 [yLCPC2024] D. 排卡
\(\color{#39C5BB}{[EASY-]}\)
队列是一个不断缩小的区间,考虑区间动态规划。
定义 \(f(i, j, 0/1)\) 表示区间 \([i, j]\) 还未处理,上一个处理的在区间左/右的最大值。
转移显然,不过这样定义处理 \(f(i, i, 0/1)\) 时要直接计算对答案的贡献。
做的时候考虑了下后效性,处理 \([i, j]\) 时,\([1, i - 1]\) 和 \([j + 1, n]\) 的结果已经是最优的,且和 \([i, j]\) 没有关系,与 \([i, j]\) 的结果有关的显然只有 \(a_{i - 1}\) 和 \(a_{j + 1}\)
状态也是基于这设计的
code
const int N = 1e3 + 5;
const i64 inf = 1e18;
const int mod = 998244353;
int T, n, a[N];
i64 f[N][N][2];
i64 ans;
int Pow(int a, int b) {
if (a == 0) return 0;
int res = 1;
for (; b; b >>= 1) {
if (b & 1) res = 1LL * res * a % mod;
a = 1LL * a * a % mod;
}
return res;
}
void solve() {
read(n);
rep(i, 1, n) read(a[i]);
rep(i, 1, n) rep(j, 1, n) rep(k, 0, 1) f[i][j][k] = -inf;
f[1][n - 1][1] = f[2][n][0] = 0, ans = -inf;
per(l, n - 1, 1) {
rep(i, 1, n - l + 1) {
if (l == 1) {
if (i > 1) ans = max(ans, f[i][i][0] + Pow(a[i - 1], a[i]));
if (i < n) ans = max(ans, f[i][i][1] + Pow(a[i + 1], a[i]));
} else {
f[i][i + l - 2][1] = max(f[i][i + l - 2][1], f[i][i + l - 1][0] + Pow(a[i - 1], a[i + l - 1]));
f[i][i + l - 2][1] = max(f[i][i + l - 2][1], f[i][i + l - 1][1] + Pow(a[i + l], a[i + l - 1]));
f[i + 1][i + l - 1][0] = max(f[i + 1][i + l - 1][0], f[i][i + l - 1][0] + Pow(a[i - 1], a[i]));
f[i + 1][i + l - 1][0] = max(f[i + 1][i + l - 1][0], f[i][i + l - 1][1] + Pow(a[i + l], a[i]));
}
}
}
write(ans); puts("");
}
int main() {
read(T);
while (T--) solve();
return 0;
}
// START AT 2025 / 07 / 10 08 : 45 : 58
树形动态规划
树上背包
P12844 [蓝桥杯 2025 国 A] 树
\(\color{#39C5BB}{[EASY+]}\)
令 \(v \in son(u)\),可以想到用 \(f(u, 0/1/2)\) 表示只考虑 \(sub(u)\),选择了 \(u\)/选择了 \(v\)/其余情况的方案数。
\(f(u, 0)\) 只能由 \(f(v, 2)\) 转移而来,\(f(u, 2)\) 从 \(f(v, 1), f(v, 2)\) 转移而来。
\(f(u, 1)\) 的转移稍显复杂,需要从\(f(v, 1)\) 和其他子树中转移出来,转移方程为
可以用前缀积和逆元 \(\mathcal{O}(\log V)\) 处理。
时间复杂度 \(\mathcal{O}(n\log V)\)。
大概是树上背包吧
code
const int N = 3e5 + 5;
const int mod = 998244353;
int head[N], ver[N << 1], Next[N << 1], tot = 1;
int n, f[N][3];
void add(int u, int v) {
Next[++tot] = head[u];
ver[head[u] = tot] = v;
}
int Pow(int a, int b) {
int res = 1;
for (; b; b >>= 1) {
if (b & 1) res = 1LL * res * a % mod;
a = 1LL * a * a % mod;
}
return res;
}
void dfs(int u, int _f) {
f[u][0] = f[u][2] = 1;
int suf = 1, pre = 1;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i]; if (v == _f) continue;
dfs(v, u);
f[u][0] = 1LL * f[u][0] * f[v][2] % mod;
f[u][2] = 1LL * f[u][2] * (f[v][1] + f[v][2]) % mod;
suf = 1LL * suf * (f[v][1] + f[v][2]) % mod;
}
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i]; if (v == _f) continue;
suf = 1LL * suf * Pow(f[v][1] + f[v][2], mod - 2) % mod;
(f[u][1] += 1LL * f[v][0] * suf % mod * pre % mod) %= mod;
pre = 1LL * pre * (f[v][1] + f[v][2]) % mod;
}
}
int main() {
read(n);
rep(i, 1, n - 1) {
int u, v; read(u, v);
add(u, v), add(v, u);
}
dfs(1, 0);
write((1LL * f[1][0] + f[1][1] + f[1][2] - 1 + mod) % mod);
return 0;
}
P12734 理解
\(\color{#FFA500}{[NORMAL]}\)
定义状态 \(f(u, i)\) 表示处理了 \(sub(u)\) 中所有关键节点,任意时刻记住的节点不超过 \(i\) ,不包含处理 \(u\) 的最小时间。
初始化:由实际意义,对于关键节点 \(u\),需要有 \(f(u, 0) = \inf\),其他都为 \(0\)。
大部分的转移简单。需要注意到对于 \(f(u, i)\),可以有 \(v\in son(u)\),从 \(f(v, i)\) 转移到 \(f(u, i)\),因为处理完别的子树后可以把 \(u\) 删了。
状态很差呢……
为什么实际含义是这样的呢,方便转移吧,否则可能需要再开一维吧
code
const int N = 1e5 + 5;
const i64 inf = 1e18;
struct Graph {
int head[N], ver[N << 1], Next[N << 1], tot = 1;
void add(int u, int v) {
Next[++tot] = head[u];
ver[head[u] = tot] = v;
}
void init() {
memset(head, 0, sizeof(head));
tot = 1;
}
} tree;
int T, n, m, k, r[N], t[N], x[N];
i64 f[N][15];
bool tag[N];
void dfs(int u, int _f) {
vector<i64> Min(11, 0);
adj(tree, u, v) if (v != _f) {
dfs(v, u);
f[u][1] += min(f[v][0], f[v][k] + r[v]);
rep(i, 2, k) {
i64 tmin = min(f[v][0], min(f[v][i - 1] + t[v], f[v][k] + r[v]));
chkmin(Min[i], f[v][i] + t[v] - tmin);
f[u][i] += tmin;
}
}
rep(i, 2, k) f[u][i] += Min[i], chkmin(f[u][i], f[u][i - 1]);
if (!tag[u]) f[u][0] = f[u][1];
}
void solve() {
read(n, m, k);
rep(i, 1, n) {
int p; read(p);
tree.add(p, i), tree.add(i, p);
}
rep(i, 1, n) read(r[i]);
rep(i, 1, n) read(t[i]);
rep(i, 1, m) read(x[i]), tag[x[i]] = 1, f[x[i]][0] = inf;
dfs(0, -1);
write(f[0][0], '\n');
tree.init();
rep(i, 1, m) tag[x[i]] = 0;
memset(f, 0, sizeof(f));
}
int main() {
read(T);
while (T--) solve();
return 0;
}
换根 DP
P9437 『XYGOI round 1』一棵树
\(\color{#39C5BB}{[EASY]}\)
从别的节点到该节点和从该节点到别的节点是一样的。然后就好做了。
定义 \(f(u)\) 表示 \(sub(u)\) 中以 \(u\) 为结尾的途径的和。容易得到状态转移方程式
显然 \(root\) 的对答案贡献为 \(f(root)\),考虑换根。
得到 \(f(u)\) 中不经过 \(v\) 的途径的和 \(g(u) = f(u) - f(v) \times 10^{\lg a_u} + size(v) \times a_u\),当 \(v\) 为树的 \(root\) 时,\(sub(u)\) 的对贡献为 \(g(u) \times 10^{\lg{a_v}} + size(u) \times a_v\)。
答案就是 \(\sum\limits_{u\in V} f(u)\)。
code
const int N = 1e6 + 5;
const int mod = 998244353;
int head[N], ver[N << 1], Next[N << 1], tot = 1;
int n, a[N], f[N], sz[N], _10[N], ans;
int Pow(int a, int b) {
int res = 1;
for(; b; b >>= 1) {
if (b & 1) res = 1LL * res * a % mod;
a = 1LL * a * a % mod;
}
return res;
}
void add(int u, int v) {
ver[++tot] = v, Next[tot] = head[u], head[u] = tot;
}
int lg(int x) {
if (x == 0) return 1;
int res = 0;
while (x) res++, x /= 10;
return res;
}
void dfs(int u, int _f) {
sz[u] = 1;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i]; if (v == _f) continue;
dfs(v, u);
sz[u] = (sz[u] + sz[v]) % mod;
f[u] = (f[u] + 1LL * f[v] * _10[u] % mod) % mod;
}
f[u] = (f[u] + 1LL * a[u] * sz[u]) % mod;
}
void _dfs(int u, int _f) {
ans = (ans + f[u]) % mod;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i]; if (v == _f) continue;
f[v] = (f[v] + 1LL * (f[u] - 1LL * f[v] * _10[u] % mod - 1LL * a[u] * sz[v] % mod + mod) % mod * _10[v] % mod + 1LL * (n - sz[v]) * a[v] % mod) % mod;
sz[v] = n;
_dfs(v, u);
}
}
int main() {
read(n);
rep(i, 1, n) read(a[i]);
rep(i, 1, n - 1) {
int p; read(p);
add(p, i + 1), add(i + 1, p);
}
rep(i, 1, n) _10[i] = Pow(10, lg(a[i]));
dfs(1, 0);
_dfs(1, 0);
write(ans);
return 0;
}
P6419 [COCI 2014/2015 #1] Kamp
\(\color{#39C5BB}{[EASY]}\)
明显是换根 DP。
对于 \(u\) 来说,把所有人送回家的路径是一棵树,以下均为此树。
定义 \(len(u)\) 表示 \(sub(u)\) 中 \(u\) 到叶子节点的最长距离。答案是这颗树的边长和 \(sum(u) \times 2\) 减去 \(len(v)\)。
据上述,重要的是 \(sum(u)\) 和 \(len(u)\)。 \(len(u)\) 的转移可能会导致最长链的长度变化,因此需要维护次长距离。
转移都比较简单。
理解错题意地做了 1 个小时,一定要充分理解题目再做题啊!
code
const int N = 5e5 + 5;
int head[N], ver[N << 1], edge[N << 1], Next[N << 1], tot = 1;
int n, K;
bool vis[N];
i64 dis[N], len[N], id[N], _len[N];
int sz[N];
i64 f[N];
void add(int u, int v, int w) {
Next[++tot] = head[u];
ver[head[u] = tot] = v;
edge[tot] = w;
}
void dfs(int u, int _f) {
sz[u] += vis[u];
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i], w = edge[i]; if (v == _f) continue;
dfs(v, u);
if(sz[v]) {
dis[u] += dis[v] + 2LL * w;
if (len[v] + w > len[u]) _len[u] = len[u], len[u] = len[v] + w, id[u] = v;
else if (len[v] + w > _len[u]) _len[u] = len[v] + w;
sz[u] += sz[v];
}
}
}
void _dfs(int u, int _f) {
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i], w = edge[i]; if (v == _f) continue;
if (sz[v] == 0) {
len[v] = len[u] + w, f[v] = f[u] + 2 * w;
} else if (sz[v] == K) {
f[v] = dis[v];
} else {
f[v] = f[u];
if (id[u] == v) {
if (_len[u] + w > len[v]) _len[v] = len[v], len[v] = _len[u] + w, id[v] = u;
else if (_len[u] + w > _len[v]) _len[v] = _len[u] + w;
} else _len[v] = len[v], len[v] = len[u] + w, id[v] = u;
}
_dfs(v, u);
}
}
int main() {
read(n, K);
rep(i, 1, n - 1) {
int u, v, w; read(u, v, w);
add(u, v, w), add(v, u, w);
}
rep(i, 1, K) {
int x; read(x);
vis[x] = 1;
}
dfs(1, 0);
f[1] = dis[1];
_dfs(1, 0);
rep(i, 1, n) write(f[i] - len[i]), puts("");
return 0;
}
P11766 「KFCOI Round #1」遥不可及
\(\color{#FFA500}{[NORMAL-]}\)
Sol 1
注意到对于任意节点,最长边一定经过直径的中心,找出中心,然后对每个节点考虑其所处的两种情况:从子树到外的最长链和从外到子树内的最长链。
code
const int N = 1e6 + 5;
struct Garph {
int head[N], ver[N << 1], edge[N << 1], Next[N << 1], tot = 1;
void add(int u, int v, int w) {
Next[++tot] = head[u];
ver[head[u] = tot] = v;
edge[tot] = w;
}
} tree;
int n, dep[N], fa[N], p, q, rw[N], cnt[N], sz[N];
int core, _core;
i64 dis[N], ans, ml[N];
void dfs(int u, int _f) {
dep[u] = dep[fa[u] = _f] + 1, cnt[u] = sz[u] = 1, ml[u] = dis[u];
if (dis[u] > dis[q]) q = u;
adj(tree, u, v, w) if (v != _f) {
rw[v] = w; dis[v] = dis[u] + w; dfs(v, u);
if (ml[v] > ml[u]) ml[u] = ml[v], cnt[u] = cnt[v];
else if (ml[u] == ml[v]) cnt[u] += cnt[v];
sz[u] += sz[v];
}
if (cnt[u] == 0) cnt[u] = 1;
}
void calc(int u, int w) {
ans += (i64)w * cnt[u];
adj(tree, u, v, _w) if (v != fa[u] && ml[u] == ml[v]) calc(v, w);
}
void _calc(int u, int w) {
ans += (i64)w * sz[u];
adj(tree, u, v, _w) if (v != fa[u]) _calc(v, w);
}
int main() {
read(n);
rep(i, 1, n - 1) {
int u, v, w; read(u, v, w);
tree.add(u, v, w), tree.add(v, u, w);
}
dfs(1, 0); p = q;
dis[p] = 0; dfs(p, 0);
i64 w = 0, u = q;
while (fa[u] != 0) {
w += rw[u];
if (w * 2 == dis[q]) { core = fa[u]; break; }
else if (w * 2 > dis[q]) { core = u, _core = fa[u]; break; }
u = fa[u];
}
if (_core) {
dis[_core] = 0; dfs(_core, 0); int w = cnt[core];
dis[core] = 0; dfs(core, 0); calc(_core, n - sz[_core]); _calc(_core, w);
w = cnt[_core];
dis[_core] = 0; dfs(_core, 0); calc(core, n - sz[core]), _calc(core, w);
} else {
dis[core] = 0; dfs(core, 0); calc(core, 1);
adj(tree, core, u, w) {
if (ml[u] == ml[core]) {
ans += n - sz[u] - 1;
calc(u, n - sz[u] - 1);
}
_calc(u, cnt[core] - (ml[core] == ml[u] ? cnt[u] : 0));
}
}
write(ans);
return 0;
}
Sol 2
更容易想到一个很典的换根 DP,维护最长链,考虑到某些儿子的特殊性,单独维护一个次长链。
code
const int N = 1e6 + 5;
struct Graph {
int head[N], edge[N << 1], Next[N << 1], ver[N << 1], tot = 1;
void add(int u, int v, int w) {
Next[++tot] = head[u];
ver[head[u] = tot] = v;
edge[tot] = w;
}
} tree;
struct node { i64 len; int res, way; } fir[N], sec[N];
int n;
i64 ans;
void tran(int u, i64 len, int res, int way) {
if (len > fir[u].len) sec[u] = fir[u], fir[u] = {len, res + way, way};
else if (len == fir[u].len) fir[u].res += res + way, fir[u].way += way;
else if (len > sec[u].len) sec[u] = {len, res + way, way};
else if (len == sec[u].len) sec[u].res += res + way, sec[u].way += way;
}
void dfs(int u, int _f) {
fir[u] = {0, 1, 1};
adj(tree, u, v, w) if (v != _f) { dfs(v, u); tran(u, fir[v].len + w, fir[v].res, fir[v].way); }
}
void _dfs(int u, int _f) {
adj(tree, u, v, w) if (v != _f) {
if (fir[u].len != fir[v].len + w) tran(v, fir[u].len + w, fir[u].res, fir[u].way);
else if (fir[u].way ^ fir[v].way) tran(v, fir[u].len + w, fir[u].res - fir[v].res - fir[v].way, fir[u].way - fir[v].way);
else tran(v, sec[u].len + w, sec[u].res, sec[u].way);
ans += fir[v].res; _dfs(v, u);
}
}
int main() {
read(n);
rep(i, 1, n - 1) {
int u, v, w; read(u, v, w);
tree.add(u, v, w), tree.add(v, u, w);
}
dfs(1, 0); ans += fir[1].res; _dfs(1, 0);
write(ans, '\n');
return 0;
}
状态压缩动态规划
题目
P6289 [COCI 2016/2017 #1] Vještica
\(\color{#FFA500}{[NORMAL]}\)
注意到公共前缀只和字符的出现次数相关。容易处理出 \(2^n\) 种情况的所有公共前缀。
考虑状压 DP,设 \(f(\mathbb S)\) 表示集合 \(\mathbb S\) 需要的最小节点数,\(b(\mathbb{S})\) 表示 \(\mathbb{S}\) 的最长公共前缀。
对于两个集合 \(\mathbb{P, Q\;(P \cap Q = \varnothing)}\),\(f(\mathbb{P \cup Q}) = f(\mathbb P) + f(\mathbb Q) - |b(\mathbb{P \cup Q})|\)。
有 \(b(\mathbb{P \cup Q}) \subset b(\mathbb P),b(\mathbb{P \cup Q}) \subset b(\mathbb Q)\), 证明从略。
code
const int N = 16;
int n, lcp[(1 << N) + 5];
int ch[N + 5][30], pre[30];
int f[(1 << N) + 5];
int main() {
read(n);
memset(f, 0x3f, sizeof(f));
rep(i, 1, n) {
string s; cin >> s;
for (char e : s) ch[i][e - 'a' + 1]++;
f[1 << (i - 1)] = s.size();
}
rep(i, 1, (1 << n) - 1) {
memset(pre, 0x3f, sizeof(pre));
rep(j, 1, n) if (i >> (j - 1) & 1) {
rep(k, 1, 26) chkmin(pre[k], ch[j][k]);
}
rep(j, 1, 26) lcp[i] += pre[j];
}
rep(S, 1, (1 << n) - 1) {
if (f[S] != 0x3f3f3f3f) continue;
for (int T = S; T; T = (T - 1) & S) {
chkmin(f[S], f[T] + f[S ^ T] - lcp[S]);
}
}
write(f[(1 << n) - 1] + 1);
return 0;
}
// START AT 2025 / 07 / 12 08 : 27 : 01
DP 优化
单调队列优化 DP
P6563 [SBCOI2020] 一直在你身旁
\(\color{#FFA500}{[NORMAL+]}\)
每次购买电线都会缩短猜测区间,考虑区间动态规划。
定义 \(f(i, j)\) 表示猜测区间为 \([i, j]\) 猜出结果需要的最小价值。状态转移方程为
其中 \(f(i, k)\) 单调不降,\(f(k + 1, j)\) 单调不升。转移被划分为两个阶段。设两个阶段为 \([i, pos)\),\([pos, j)\)。
- 当取 \(f(i, k)\) 时,\(f(i, k) + a_k\) 单调不减,取 \(f(i, pos)+a_{pos}\)。
- 当取 \(f(k + 1, j)\) 时并没有单调性,但是可以用单调队列处理,容易发现,随着 \(j\) 增加,\(pos\) 单调不降。
code
const int N = 7.1e3 + 5;
int T, n, a[N];
int deq[N], head, tail;
i64 f[N][N];
void solve() {
read(n);
rep(i, 1, n) read(a[i]);
rep(r, 2, n) {
int head = 0, tail = -1;
deq[++tail] = r;
int pos = r;
per(l, r - 1, 1) {
if (l == r - 1) { f[l][r] = a[l]; continue; }
while (pos > l && f[l][pos - 1] > f[pos][r]) pos--;
f[l][r] = f[l][pos] + a[pos];
while (head <= tail && deq[head] >= pos) head++;
if (head <= tail) chkmin(f[l][r], f[deq[head] + 1][r] + a[deq[head]]);
while (head <= tail && f[deq[tail] + 1][r] + a[deq[tail]] >= f[l + 1][r] + a[l]) tail--;
deq[++tail] = l;
}
}
write(f[1][n], '\n');
}
int main() {
read(T);
while (T--) solve();
return 0;
}
数据结构优化 DP
P9400 「DBOI」Round 1 三班不一般
\(\color{#FFA500}{[NORMAL]}\)
定义 \(f(i, j)\) 表示处理了 \([1, i]\) 所有寝室,有连续 \(j\) 个寝室亮度大于 \(b\) 的方案数。
容易得到
观察下标,相当与一个区间开头加上一个数,末尾删去一个数。
开一个下标区间为 \([1, n + a]\) 的线段树,开始相当于只维护区间 \([n + 1, n + a]\),然后不断左移。区间乘维护简单。
不错的 trick!
数据结构优化 DP 怎么这么千变万化(叹气)
code
const int N = 4e5 + 5;
const int mod = 998244353;
struct Limit { int l, r; } lim[N];
struct SegmentTree {
int mul[N << 2], tr[N << 2];
#define ls (rt << 1)
#define rs (rt << 1 | 1)
void build(int rt, int l, int r) {
mul[rt] = 1;
if (l == r) return;
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
}
void up(int rt) {
tr[rt] = (tr[ls] + tr[rs]) % mod;
}
void down(int rt, int l, int r) {
tr[ls] = 1LL * mul[rt] * tr[ls] % mod;
tr[rs] = 1LL * mul[rt] * tr[rs] % mod;
mul[ls] = 1LL * mul[rt] * mul[ls] % mod;
mul[rs] = 1LL * mul[rt] * mul[rs] % mod;
mul[rt] = 1;
}
void update(int rt, int l, int r, int p, int v) {
if (l == r) return tr[rt] = v, void();
down(rt, l, r);
int mid = (l + r) >> 1;
if (p <= mid) update(ls, l, mid, p, v);
else update(rs, mid + 1, r, p, v);
up(rt);
}
void update(int rt, int l, int r, int L, int R, int v) {
if (L <= l && r <= R) {
mul[rt] = 1LL * mul[rt] * v % mod;
tr[rt] = 1LL * tr[rt] * v % mod;
return;
}
down(rt, l, r);
int mid = (l + r) >> 1;
if (L <= mid) update(ls, l, mid, L, R, v);
if (R > mid) update(rs, mid + 1, r, L, R, v);
up(rt);
}
int query(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) return tr[rt];
down(rt, l, r);
int mid = (l + r) >> 1, res = 0;
if (L <= mid) (res += query(ls, l, mid, L, R)) %= mod;
if (R > mid) (res += query(rs, mid + 1, r, L, R)) %= mod;
return res;
}
} tr;
int n, a, b, m, l, r;
int main() {
read(n, a, b);
m = n + a, l = n + 1, r = n + a;
tr.build(1, 1, m);
tr.update(1, 1, m, l, 1);
rep(i, 1, n) {
read(lim[i].l, lim[i].r);
int x = 1LL * tr.query(1, 1, m, l, r) * (max(min(lim[i].r, b) - lim[i].l + 1, 0)) % mod;
tr.update(1, 1, m, l - 1, x);
x = max(lim[i].r - max(b, lim[i].l - 1), 0);
tr.update(1, 1, m, l, r - 1, x);
l--, r--;
}
write(tr.query(1, 1, m, l, r));
return 0;
}
浙公网安备 33010602011771号