*【学习笔记】(3) 动态DP

动态 DP 简称 DDP(Dynamic Dynamic Programming),其本质是用 矩阵 维护带修改的动态规划问题。

1.算法介绍:树链剖分写法

模板:P4719 【模板】"动态 DP"&动态树分治

给定一棵 \(n\) 个点的树。\(i\) 号点的点权为 \(a_i\) 。有 \(m\) 次操作,每次操作给定 \(u, w\),表示修改点 \(u\) 的权值为 \(w\)。你需要在每次操作之后求出这棵树的最大权独立集的权值大小。

广义 矩阵乘法

\(\qquad \qquad \qquad \qquad C_{i,j}=\bigoplus\limits_{k =1}^{n} A_{i,k}\otimes B_{k,j}\)
只需满足 \(\bigotimes\) 具有 结合律,且 \(\bigotimes\)\(\bigoplus\) 有 分配律,则存在结合律。

不带修改操作

若没有修改操作,本题是经典的 树上最大独立集 问题,详见没有上司的舞会。设 \(f_{i,0/1}\) 分别表示 不选 或 选 点 \(i\) 的最大权值,有
\(\qquad \qquad \qquad \qquad f_{i,0} = \sum\limits_{x \in son_i}\max(f_{x,0}, f_{x,1})\)
\(\qquad \qquad \qquad \qquad f_{i,1} = a_i + \sum\limits_{x \in son_i}f_{x,0}\)

首先根据动态规划的转移方程可以发现,我们修改了一个点的点权,只会更改从这个点到根这条路径上节点的 DP 值,其他值是不会发生更改的。这时候如果我们要对整棵树重新求一遍最大权独立集,难免会 T。所以我们希望能够更改这条链上的 DP 值。

想在树上一条链上进行快速修改,无疑我们可以使用树链剖分。

首先,对树 进行树链剖分,记 \(mx_i\)\(i\) 的重儿子。

我们保持 \(f\) 数组的定义不变。为了迎合重链剖分划分出了轻重儿子,我们定义$ g$ 数组:\(g_{i, 1}\) 表示 \(i\) 号点的所有轻儿子,都不取的最大权独立集;\(g_{i, 0}\) 表示 \(i\) 号点的所有轻儿子,可取可不取形成的最大权独立集。将 DP 式子简化成:

\[f_{i,0} = g_{i,0} + max(f_{j,0},f_{j,1}) \]

\[f_{i,1} = g_{i, 1} + a_i + f_{j, 0} \]

\(j\)\(i\) 的重儿子。对于叶子节点 \(g_{i,0} = g_{i,1} = 0\)
感觉不太优美,将 \(g_{i,1}\)\(a_i\) 合并起来。得到 \(g_{i,1}\) 新的定义 :表示 \(i\) 号点只考虑轻儿子的取自己的最大权独立集。那么这时候,第二个方程就可以变为 \(f_{i, 1} = g_{i, 1} + f_{j, 0}\)​。

因为不满足结合律,想办法构造矩阵用矩阵快速幂来加速。

考虑维护一个 \(1 \times 2\) 的矩阵

\[∣ ​f_{i,0​} \ \ \ \ \ f_{i,1}​​ ∣ \]

现在我们要从一个点的重儿子 \(j\) 转移到 \(i\) 上,也就是说我们需要构造出一个转移矩阵使得 \(\begin{vmatrix} f_{j, 0} & f_{j, 1} \end{vmatrix}\)​ 能够转移到 \(\begin{vmatrix} f_{i, 0} & f_{i, 1} \end{vmatrix}\)

新定义一个运算符 \(*\) ,对于矩阵 \(A,B\), $A * B $结果 \(C\) 满足:

\[C_{i,j} = max(A_{i,k} + B_{k,j}) \]

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; 
	}

至于为什么这个具有结合律。一种感性的理解:由于 \(⁡\max\) 操作和加法操作都是满足结合率的,所以这个运算满足结合率。

将转移方程做个变形:

\[f_{i,0} = max(g_{i,0} + f_{j,0}, g_{i,0} + f_{j,1}) \]

\[f_{i,1} = max(g_{i, 1} + a_i + f_{j, 0}, −∞) \]

最后得到

\[\begin{vmatrix} f_{j, 0} & f_{j, 1} \end{vmatrix} * \begin{vmatrix} g_{i, 0} & g_{i, 1} \\ g_{i, 0} & -\infty \end{vmatrix} = \begin{vmatrix} f_{i, 0} & f_{i, 1} \end{vmatrix} \]

这样子,我们对于一条重链,我们的叶子节点就存储了最初始的值,链上每个节点都对应着一个转移矩阵。我们发现这个转移矩阵和重链信息是没有任何关系的,且因为这个矩阵满足结合率,对于一条重链,我们可以之间线段树维护区间“*”积。然后直接跳到了一条重链链头,因为这个点是它父亲的轻儿子,我们需要更新它父亲节点所在的点的转移矩阵。这样子一直跳到根节点就可以了。
重链剖分剖出的 DFS 序,由于先访问了链头,所以这个区间中,链头在区间左端,链尾在区间右端。我们存储的初始信息在叶子节点(也就是链尾)上,因此我们的矩阵 \(*\) 法应当是转移矩阵在前,要维护的值矩阵在后。我们要把这个矩阵前后换个顺序,再转个个儿,加上一些推算,可以变形成:

\[\begin{vmatrix} g_{i, 0} & g_{i, 0} \\ g_{i, 1} & -\infty \end{vmatrix} * \begin{vmatrix} f_{j, 0} \\ f_{j, 1} \end{vmatrix} = \begin{vmatrix} f_{i, 0} \\ f_{i, 1} \end{vmatrix} \]

#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 WiKiDP 优化方法大杂烩 I.
Tweetuzki 的题解

posted @ 2023-08-31 22:06  Aurora-JC  阅读(22)  评论(0编辑  收藏  举报