6.12 ACM-ICPC动态规划算法 动态 DP
动态DP问题在现代算法竞赛中是一个非常有趣且具有挑战性的领域,尤其是在解决涉及复杂数据结构和操作的问题时。本篇博客旨在深入探讨猫锟在WC2018所介绍的“动态DP”黑科技,这是一种常用于解决树上带有点权(或边权)修改操作的动态规划问题。
前置知识
在深入动态DP之前,我们需要了解一些基础的数据结构和算法技术,特别是矩阵运算和树链剖分。矩阵提供了一种强大的数学工具来处理结构化数据,而树链剖分则允许我们在树结构上进行高效的修改和查询操作。
动态DP的定义和应用
动态DP主要用于解决树结构上的动态问题,其中节点权值可以随时间修改,而我们需要在每次修改后快速计算某个特定的动态规划状态。这类问题的一个典型例子是计算树的最大权独立集大小,尤其是在多次修改后。
例题分析:洛谷 P4719【模板】动态 DP
考虑一个具体的例题,一棵有n个节点的树,每个节点有一个权值。进行m次操作,每次操作修改一个节点的权值。要求每次操作后输出树的最大权独立集的权值大小。
广义矩阵乘法
在动态DP中,我们将使用一种特殊的矩阵乘法,称为广义矩阵乘法。定义如下: 𝐶𝑖,𝑗=max𝑘=1𝑛(𝐴𝑖,𝑘+𝐵𝑘,𝑗)Ci,j=maxk=1n(Ai,k+Bk,j) 这里的操作将普通的矩阵乘法中的乘法替换为加法,加法替换为max操作。
由于广义矩阵乘法满足结合律,我们可以利用矩阵快速幂来加速计算过程。
不带修改操作的动态规划
定义 f_{i,0}
和 f_{i,1}
:
f_{i,0}
表示不选择节点i的最大答案f_{i,1}
表示选择节点i的最大答案
根据这些定义,我们可以建立以下DP方程:
f_{i,0} = \sum_{son} \max(f_{son,0}, f_{son,1})
f_{i,1} = w_i + \sum_{son} f_{son,0}
答案是 max(f_{root,0}, f_{root,1})
。
带修改操作的动态规划
为了处理修改操作,首先对树进行树链剖分。通过这种方法,我们可以将树分解为多条链,并在每条链上建立数据结构(如线段树)来支持快速修改和查询。
对于每个节点,我们定义:
g_{i,0}
和g_{i,1}
分别表示不选择和选择节点i的情况下,只考虑其轻子树的最大答案。
对于修改操作,我们只需要更新受影响的节点和它们的父链上的矩阵即可。通过树链剖分和线段树,我们可以有效地处理这些修改,并快速重新计算答案。
代码实现
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 500010;
const int INF = 0x3f3f3f3f;
int Begin[maxn], Next[maxn], To[maxn], e, n, m;
int sz[maxn], son[maxn], top[maxn], fa[maxn], dis[maxn], p[maxn], id[maxn],
End[maxn];
// p[i]表示i树剖后的编号,id[p[i]] = i
int cnt, tot, a[maxn], f[maxn][2];
struct matrix {
int g[2][2];
matrix() { memset(g, 0, sizeof(g)); }
matrix operator*(const matrix &b) const // 重载矩阵乘
{
matrix c;
for (int i = 0; i <= 1; i++)
for (int j = 0; j <= 1; j++)
for (int k = 0; k <= 1; k++)
c.g[i][j] = max(c.g[i][j], g[i][k] + b.g[k][j]);
return c;
}
} Tree[maxn], g[maxn]; // Tree[]是建出来的线段树,g[]是维护的每个点的矩阵
void PushUp(int root) { Tree[root] = Tree[root << 1] * Tree[root << 1 | 1]; }
void Build(int root, int l, int r) {
if (l == r) {
Tree[root] = g[id[l]];
return;
}
int Mid = l + r >> 1;
Build(root << 1, l, Mid);
Build(root << 1 | 1, Mid + 1, r);
PushUp(root);
}
matrix Query(int root, int l, int r, int L, int R) {
if (L <= l && r <= R) return Tree[root];
int Mid = l + r >> 1;
if (R <= Mid) return Query(root << 1, l, Mid, L, R);
if (Mid < L) return Query(root << 1 | 1, Mid + 1, r, L, R);
return Query(root << 1, l, Mid, L, R) *
Query(root << 1 | 1, Mid + 1, r, L, R);
// 注意查询操作的书写
}
void Modify(int root, int l, int r, int pos) {
if (l == r) {
Tree[root] = g[id[l]];
return;
}
int Mid = l + r >> 1;
if (pos <= Mid)
Modify(root << 1, l, Mid, pos);
else
Modify(root << 1 | 1, Mid + 1, r, pos);
PushUp(root);
}
void Update(int x, int val) {
g[x].g[1][0] += val - a[x];
a[x] = val;
// 首先修改x的g矩阵
while (x) {
matrix last = Query(1, 1, n, p[top[x]], End[top[x]]);
// 查询top[x]的原本g矩阵
Modify(1, 1, n,
p[x]); // 进行修改(x点的g矩阵已经进行修改但线段树上的未进行修改)
matrix now = Query(1, 1, n, p[top[x]], End[top[x]]);
// 查询top[x]的新g矩阵
x = fa[top[x]];
g[x].g[0][0] +=
max(now.g[0][0], now.g[1][0]) - max(last.g[0][0], last.g[1][0]);
g[x].g[0][1] = g[x].g[0][0];
g[x].g[1][0] += now.g[0][0] - last.g[0][0];
// 根据变化量修改fa[top[x]]的g矩阵
}
}
void add(int u, int v) {
To[++e] = v;
Next[e] = Begin[u];
Begin[u] = e;
}
void DFS1(int u) {
sz[u] = 1;
int Max = 0;
f[u][1] = a[u];
for (int i = Begin[u]; i; i = Next[i]) {
int v = To[i];
if (v == fa[u]) continue;
dis[v] = dis[u] + 1;
fa[v] = u;
DFS1(v);
sz[u] += sz[v];
if (sz[v] > Max) {
Max = sz[v];
son[u] = v;
}
f[u][1] += f[v][0];
f[u][0] += max(f[v][0], f[v][1]);
// DFS1过程中同时求出f[i][0/1]
}
}
void DFS2(int u, int t) {
top[u] = t;
p[u] = ++cnt;
id[cnt] = u;
End[t] = cnt;
g[u].g[1][0] = a[u];
g[u].g[1][1] = -INF;
if (!son[u]) return;
DFS2(son[u], t);
for (int i = Begin[u]; i; i = Next[i]) {
int v = To[i];
if (v == fa[u] || v == son[u]) continue;
DFS2(v, v);
g[u].g[0][0] += max(f[v][0], f[v][1]);
g[u].g[1][0] += f[v][0];
// g矩阵根据f[i][0/1]求出
}
g[u].g[0][1] = g[u].g[0][0];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n - 1; i++) {
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
dis[1] = 1;
DFS1(1);
DFS2(1, 1);
Build(1, 1, n);
for (int i = 1; i <= m; i++) {
int x, val;
scanf("%d%d", &x, &val);
Update(x, val);
matrix ans = Query(1, 1, n, 1, End[1]); // 查询1所在重链的矩阵乘
printf("%d\n", max(ans.g[0][0], ans.g[1][0]));
}
return 0;
}
通过这样的深入解析和示例,希望能帮助你更好地理解和运用动态DP来解决复杂的动态问题。