*【学习笔记】(3) 动态DP
动态 DP 简称 DDP(Dynamic Dynamic Programming),其本质是用 矩阵 维护带修改的动态规划问题。
1.算法介绍:树链剖分写法
模板:P4719 【模板】"动态 DP"&动态树分治
给定一棵
个点的树。 号点的点权为 。有 次操作,每次操作给定 ,表示修改点 的权值为 。你需要在每次操作之后求出这棵树的最大权独立集的权值大小。
广义 矩阵乘法
只需满足
不带修改操作
若没有修改操作,本题是经典的 树上最大独立集 问题,详见没有上司的舞会。设
首先根据动态规划的转移方程可以发现,我们修改了一个点的点权,只会更改从这个点到根这条路径上节点的 DP 值,其他值是不会发生更改的。这时候如果我们要对整棵树重新求一遍最大权独立集,难免会 T。所以我们希望能够更改这条链上的 DP 值。
想在树上一条链上进行快速修改,无疑我们可以使用树链剖分。
首先,对树 进行树链剖分,记
我们保持
感觉不太优美,将
因为不满足结合律,想办法构造矩阵用矩阵快速幂来加速。
考虑维护一个
现在我们要从一个点的重儿子
新定义一个运算符
FJC operator * (const FJC &B){ FJC res; res.clear(); for(int i = 1; i <= 2; ++i) for(int j = 1; j <= 2; ++j) for(int k = 1; k <= 2; ++k) res.c[i][j] = max(res.c[i][j], c[i][k] + B.c[k][j]); return res; }
至于为什么这个具有结合律。一种感性的理解:由于
将转移方程做个变形:
最后得到
这样子,我们对于一条重链,我们的叶子节点就存储了最初始的值,链上每个节点都对应着一个转移矩阵。我们发现这个转移矩阵和重链信息是没有任何关系的,且因为这个矩阵满足结合率,对于一条重链,我们可以之间线段树维护区间“*”积。然后直接跳到了一条重链链头,因为这个点是它父亲的轻儿子,我们需要更新它父亲节点所在的点的转移矩阵。这样子一直跳到根节点就可以了。
重链剖分剖出的 DFS 序,由于先访问了链头,所以这个区间中,链头在区间左端,链尾在区间右端。我们存储的初始信息在叶子节点(也就是链尾)上,因此我们的矩阵
#include<bits/stdc++.h> #define N 100005 #define ls u << 1 #define rs u << 1 | 1 using namespace std; int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } struct FJC{ int c[3][3]; void clear(){memset(c, 0xcf, sizeof(c));} FJC operator * (const FJC &B){ FJC res; res.clear(); for(int i = 1; i <= 2; ++i) for(int j = 1; j <= 2; ++j) for(int k = 1; k <= 2; ++k) res.c[i][j] = max(res.c[i][j], c[i][k] + B.c[k][j]); return res; } }opt[N]; int n, m, tot, t; int Head[N], to[N << 1], Next[N << 1]; int a[N], fa[N], d[N], sz[N], maxson[N]; int dfn[N], top[N], ed[N], f[N][2], id[N]; void add(int u, int v){ to[++tot] = v, Next[tot] = Head[u], Head[u] = tot; } struct Segment{ FJC v[N << 2]; void PushUp(int u){ v[u] = v[ls] * v[rs]; } void Build(int u, int l, int r){ if(l == r) return v[u] = opt[id[l]], void(); int mid = (l + r) >> 1; Build(ls, l, mid), Build(rs, mid + 1, r); PushUp(u); } void UpDate(int u, int l, int r, int x){ if(l == r) return v[u] = opt[id[l]], void(); int mid = (l + r) >> 1; if(x <= mid) UpDate(ls, l, mid, x); else UpDate(rs, mid + 1, r, x); PushUp(u); } FJC Query(int u, int l, int r, int L, int R){ if(L <= l && r <= R) return v[u]; int mid = (l + r) >> 1; if(R <= mid) return Query(ls, l, mid, L, R); else if(L > mid) return Query(rs, mid + 1, r, L, R); return Query(ls, l, mid, L, R) * Query(rs, mid + 1, r, L, R); } }tr; void dfs1(int x){ sz[x] = 1; for(int i = Head[x]; i; i = Next[i]){ int y = to[i]; if(y == fa[x]) continue; fa[y] = x, d[y] = d[x] + 1, dfs1(y); sz[x] += sz[y]; if(sz[y] > sz[maxson[x]]) maxson[x] = y; } } void dfs2(int x, int topf){ dfn[x] = ++t, id[t] = x, top[x] = topf, ed[topf] = max(ed[topf], t); f[x][0] = 0, f[x][1] = a[x]; opt[x].clear(); opt[x].c[1][1] = opt[x].c[1][2] = 0; opt[x].c[2][1] = a[x]; if(!maxson[x]) return ; dfs2(maxson[x], topf); f[x][0] += max(f[maxson[x]][0], f[maxson[x]][1]); f[x][1] += f[maxson[x]][0]; for(int i = Head[x]; i; i = Next[i]){ int y = to[i]; if(y == fa[x] || maxson[x] == y) continue; dfs2(y, y); f[x][0] += max(f[y][0], f[y][1]), f[x][1] += f[y][0]; opt[x].c[1][2] = (opt[x].c[1][1] += max(f[y][0], f[y][1])); opt[x].c[2][1] += f[y][0]; } } void UpDate_Path(int x, int y){ opt[x].c[2][1] += y - a[x], a[x] = y; FJC lst, now; while(x){ lst = tr.Query(1, 1, n, dfn[top[x]], ed[top[x]]); tr.UpDate(1, 1, n, dfn[x]); now = tr.Query(1, 1, n, dfn[top[x]], ed[top[x]]); x = fa[top[x]]; opt[x].c[1][2] = (opt[x].c[1][1] += max(now.c[1][1], now.c[2][1]) - max(lst.c[1][1], lst.c[2][1])); opt[x].c[2][1] += now.c[1][1] - lst.c[1][1]; } } int main(){ n = read(), m = read(); for(int i = 1; i <= n; ++i) a[i] = read(); for(int i = 1; i < n; ++i){ int u = read(), v = read(); add(u, v), add(v, u); } dfs1(1), dfs2(1, 1); tr.Build(1, 1, n); for(int i = 1; i <= m; ++i){ int x = read(), y = read(); UpDate_Path(x, y); FJC ans = tr.Query(1, 1, n, dfn[1], ed[1]); printf("%d\n", max(ans.c[1][1], ans.c[2][1])); } return 0; }
例题
Ⅰ. P5024 [NOIP2018 提高组] 保卫王国
板题,最小权覆盖集 = 全集 - 最大权独立集,不得驻扎将点权加上正无穷,否则反之。
点击查看代码
#include<bits/stdc++.h> #define ls u << 1 #define rs u << 1 | 1 #define INF 1e9 #define ll long long using namespace std; const int N = 2e5 + 67; int read(){ int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();} while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();} return x * f; } struct FJC{ ll c[3][3]; void clear(){memset(c, 0xcf, sizeof(c));} FJC operator * (const FJC B){ FJC res; res.clear(); for(int i = 1; i <= 2; ++i) for(int j = 1; j <= 2; ++j) for(int k = 1; k <= 2; ++k) res.c[i][j] = max(res.c[i][j], c[i][k] + B.c[k][j]); return res; } }op[N]; int n, m, opt, tot, cnt; ll sum; int a[N], sz[N], fa[N], d[N], son[N]; int dfn[N], top[N], id[N], ed[N]; ll f[N][2]; int Head[N], to[N], Next[N]; void add(int u, int v){ to[++tot] = v, Next[tot] = Head[u], Head[u] = tot; } struct Segment{ FJC v[N << 2]; void pushup(int u){ v[u] = v[ls] * v[rs]; } void build(int u, int l, int r){ if(l == r) return v[u] = op[id[l]], void(); int mid = (l + r) >> 1; build(ls, l, mid), build(rs, mid + 1, r); pushup(u); } void update(int u, int l, int r, int x){ if(l == r) return v[u] = op[id[l]], void(); int mid = (l + r) >> 1; if(x <= mid) update(ls, l, mid, x); else update(rs, mid + 1, r, x); pushup(u); } FJC query(int u, int l, int r, int L, int R){ if(L <= l && r <= R) return v[u]; int mid = (l + r) >> 1; if(R <= mid) return query(ls, l, mid, L, R); if(L > mid) return query(rs, mid + 1, r, L, R); return query(ls, l, mid, L, R) * query(rs, mid + 1, r, L, R); } }tr; void dfs1(int x){ sz[x] = 1; for(int i = Head[x]; i; i = Next[i]){ int y = to[i]; if(y == fa[x]) continue; fa[y] = x, d[y] = d[x] + 1, dfs1(y); if(sz[y] > sz[son[x]]) son[x] = y; sz[x] += sz[y]; } } void dfs2(int x, int topf){ dfn[x] = ++cnt, id[cnt] = x, top[x] = topf, ed[topf] = max(ed[topf], cnt); f[x][0] = 0, f[x][1] = a[x]; op[x].clear(); op[x].c[1][1] = op[x].c[1][2] = 0; op[x].c[2][1] = a[x]; if(!son[x]) return ; dfs2(son[x], topf); f[x][0] += max(f[son[x]][0], f[son[x]][1]); f[x][1] += f[son[x]][0]; for(int i = Head[x]; i; i = Next[i]){ int y = to[i]; if(y == fa[x] || y == son[x]) continue; dfs2(y, y); f[x][0] += max(f[y][0], f[y][1]), f[x][1] += f[y][0]; op[x].c[1][2] = (op[x].c[1][1] += max(f[y][0], f[y][1])); op[x].c[2][1] += f[y][0]; } } void update_path(int x, ll y){ op[x].c[2][1] += y; FJC lst, now; while(x){ lst = tr.query(1, 1, n, dfn[top[x]], ed[top[x]]); tr.update(1, 1, n, dfn[x]); now = tr.query(1, 1, n, dfn[top[x]], ed[top[x]]); x = fa[top[x]]; op[x].c[1][2] = (op[x].c[1][1] += max(now.c[1][1], now.c[2][1]) - max(lst.c[1][1], lst.c[2][1])); op[x].c[2][1] += now.c[1][1] - lst.c[1][1]; } } int main(){ n = read(), m = read(), opt = read(); for(int i = 1; i <= n; ++i) a[i] = read(), sum += a[i]; for(int i = 1; i < n; ++i){ int u = read(), v = read(); add(u, v), add(v, u); } dfs1(1), dfs2(1, 1); tr.build(1, 1, n); while(m--){ int x = read(), stx = read(); int y = read(), sty = read(); if(stx == 0 && sty ==0 && (fa[x] == y || fa[y] == x)){ printf("-1\n"); continue; } update_path(x, stx ? -INF : INF); update_path(y, sty ? -INF : INF); sum += ((stx ^ 1) + (sty ^ 1)) * INF; FJC ans = tr.query(1, 1, n, dfn[1], ed[1]); printf("%lld\n", sum - max(ans.c[1][1], ans.c[2][1])); update_path(x, stx ? INF : -INF); update_path(y, sty ? INF : -INF); sum -= ((stx ^ 1) + (sty ^ 1)) * INF; } return 0; }
Ⅱ. P6573 [BalticOI 2017] Toll
Ⅲ. P7359 「JZOI-1」旅行
参考资料:OI WiKi,DP 优化方法大杂烩 I.
,Tweetuzki 的题解。
本文作者:Aurora-JC
本文链接:https://www.cnblogs.com/jiangchen4122/p/17417620.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步