[dp 进阶] 动态 dp
[dp 进阶] 动态 dp
动态 dp(DDP, dynamic dynamic programming)一般用来解决带有点权/边权修改操作的树上问题。
以下题为例讲解 ddp 的过程:
(P4719 【模板】动态 DP)给定一棵带点权的树,多次修改某个点的点权,每次修改后求出树的最大权独立集。
如果不带修,这是经典的树形 dp。设 \(f(u, 1/0)\) 表示选择/不选择 \(u\) 时,\(T(u)\) 中的最大权独立集,有转移
如果每次修改都 dp 一遍,效率太低,我们考虑信息复用。一个观察是,修改一个点 \(u\) 的点权,只会改变 \(u\) 到根节点路径上的点的 dp 值。扩展这个想法,把树重链剖分,那么每次修改后,只有 \(O(\log n)\) 条链上的 dp 值会受影响。
为了更好地利用重链剖分,设状态 \(g(u, 1/0)\) 表示 \(u\) 节点删去重子以后的 dp 值。设 \(v = hson_u\),则下式成立:
设计这个状态的意义在于:修改点 \(u\) 的点权,不会改变 \(u\) 重链中除了 \(u\) 的点的 \(g\) 值。(当然,\(f\) 的值可能会被修改)设 \(p\) 是 \(u\) 所在重链的链顶,如果能快速求出 \(f(p, 0/1)\),就可以用 \(p\) 的 \(f\) 值更新 \(fa_p\) 的 \(g\) 值。也就是说我们一直在重复这个过程:
- 修改某个点 \(u\) 的 \(g\) 值。
- 求出 \(u\) 所在重链的链顶 \(p\) 的 \(f\) 值,用来更新 \(fa_p\) 的 \(g\) 值。这一步可以 \(O(1)\) 完成。
- 如果 \(u = 1\),结束修改过程,否则令 \(u \gets fa_p\),并回到第 1 步。
那么如何快速求出某个点的 \(f\) 值呢?套路性地考虑把转移写成矩阵的形式,然后用线段树维护区间矩阵积。这里的矩阵乘法定义为 \((\max, +)\) 矩阵乘法。
其中 \(\begin{bmatrix} g(u, 0) & g(u, 0) \\ g(u, 1) & -\infty \end{bmatrix}\) 可视作转移矩阵,记为 \(A_u\)。为了便于实现,对于叶子节点 \(u\),设 $f(hson_u, 0) =0 \(,\)f(hson_u, 1) = -\infty$。(可以发现这样设计是符合转移方程的)那么,要求出 \(f(u, 0/1)\),只要从 \(u\) 所在重链的链底开始,向上依次乘经过节点的转移矩阵。设 \(p\) 是 \(u\) 所在重链的链底,则
其中 \(\delta(p, u)\) 表示 \(p\) 到 \(u\) 的简单路径,枚举点 \(v\) 的顺序是自底向上。
那么在重链剖分之后,在线段树上查询 \([\operatorname{dfn}(u), \operatorname{dfn}(p)]\) 的矩阵乘积,就可以求出 \(f(u, 1/0)\)。令 \(u = 1\) 即可求出全局的最大独立集。对于修改操作,依照上文中的过程,修改某个点 \(u\) 的 \(g\) 值时就在线段树上修改该点的转移矩阵,查询 \(f\) 值也可以在线段树上完成。因此单次修改的时间复杂度为 \(O(\log^2 n)\)。
Code
#include<bits/stdc++.h>
using namespace std;
constexpr int INF = 0x3f3f3f3f;
struct Matrix {
static const int n = 2;
int a[n][n];
Matrix() {
memset(a, 0xc0, sizeof(a));
}
int* operator [] (int x) {
return a[x];
}
const int* operator [] (int x) const {
return a[x];
}
friend Matrix operator * (const Matrix &A, const Matrix &B) {
Matrix C;
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
for(int k = 0; k < n; k++) {
C[i][j] = max(C[i][j], A[i][k] + B[k][j]);
}
}
}
return C;
}
};
struct SGT {
#define lson (id << 1)
#define rson (id << 1 | 1)
int n;
vector<Matrix> info;
void update(int id) {
info[id] = info[lson] * info[rson];
}
void init(int _n, const vector<Matrix> &a) {
n = _n, info.resize(n << 2);
function<void(int, int, int)> build = [&](int id, int l, int r) {
if(l == r) {
info[id] = a[l];
return;
}
int mid = (l + r) >> 1;
build(lson, l, mid), build(rson, mid + 1, r);
update(id);
};
build(1, 1, n);
}
void change(int id, int l, int r, int pos, const Matrix &x) {
if(l == r) {
info[id] = x;
return;
}
int mid = (l + r) >> 1;
if(pos <= mid) {
change(lson, l, mid, pos, x);
} else {
change(rson, mid + 1, r, pos, x);
}
update(id);
}
void change(int pos, const Matrix &x) {
change(1, 1, n, pos, x);
}
Matrix query(int id, int l, int r, int L, int R) {
if(L == l && R == r) {
return info[id];
}
int mid = (l + r) >> 1;
if(R <= mid) {
return query(lson, l, mid, L, R);
} else if(L > mid) {
return query(rson, mid + 1, r, L, R);
} else {
return query(lson, l, mid, L, mid) * query(rson, mid + 1, r, mid + 1, R);
}
}
Matrix query(int L, int R) {
return query(1, 1, n, L, R);
}
#undef lson
#undef rson
}tr;
int n, m;
vector<vector<int>> G;
vector<int> a;
vector<array<int, 2>> f, g;
vector<int> hson, fa, top, bot, dfn, sz;
int dn;
void dfs1(int u = 1) {
sz[u] = 1, f[u][1] = a[u];
for(int v: G[u]) {
if(v == fa[u]) continue;
fa[v] = u;
dfs1(v);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
sz[u] += sz[v];
if(sz[v] > sz[hson[u]]) {
hson[u] = v;
}
}
}
void dfs2(int u = 1, int _top = 1) {
dfn[u] = ++dn;
top[u] = _top, bot[u] = dn;
g[u][0] = f[u][0], g[u][1] = f[u][1];
if(hson[u]) {
int v = hson[u];
dfs2(v, _top);
g[u][0] -= max(f[v][0], f[v][1]);
g[u][1] -= f[v][0];
bot[u] = bot[v];
}
for(int v: G[u]) {
if(v == fa[u] || v == hson[u]) continue;
dfs2(v, v);
}
}
Matrix make(const array<int, 2> &arr) {
Matrix b;
b[0][0] = arr[0], b[0][1] = arr[0];
b[1][0] = arr[1], b[1][1] = -INF;
return b;
}
void update(int u, int x) {
g[u][1] += x - a[u];
a[u] = x;
while(u) {
int p = top[u];
auto lst = tr.query(dfn[p], bot[p]);
tr.change(dfn[u], make(g[u]));
auto now = tr.query(dfn[p], bot[p]);
u = fa[p];
int x0 = lst[0][0], x1 = lst[1][0];
int y0 = now[0][0], y1 = now[1][0];
g[u][0] += max(y0, y1) - max(x0, x1);
g[u][1] += y0 - x0;
}
}
int solve() {
auto res = tr.query(1, bot[1]);
return max(res[0][0], res[1][0]);
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n >> m;
a.resize(n + 1);
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
G.resize(n + 1);
for(int i = 1, u, v; i < n; i++) {
cin >> u >> v;
G[u].push_back(v), G[v].push_back(u);
}
f.resize(n + 1), g = f;
hson.resize(n + 1), top = fa = bot = sz = dfn = hson;
dfs1(), dfs2();
vector<Matrix> init(n + 1);
for(int i = 1; i <= n; i++) {
init[dfn[i]] = make(g[i]);
}
tr.init(n, init);
for(int i = 1, u, x; i <= m; i++) {
cin >> u >> x;
update(u, x);
cout << solve() << '\n';
}
return 0;
}
例题
I. P5024 [NOIP 2018 提高组] 保卫王国
和模板极其相似。
设 \(f(u, 1/0)\) 表示选择/不选择 \(u\) 时,\(T(u)\) 中的最小点覆盖,\(g(u, 1/0)\) 表示删去重子之后的 dp 值。
首先列出最小点覆盖的状态转移方程,推出转移矩阵。某个点 \(u\) 必须选的限制,等价于令 \(f(u, 0) \gets +\infty\),同理不选的限制相当于令 \(f(u, 1) \gets +\infty\)。由于线段树维护的是 \(g\),所以我们应该分别令 \(g(u, 0) \gets +\infty\) 和 \(g(u, 1) \gets +\infty\),这是等价的。剩下的部分和模板相同。