动态DP学习笔记

动态DP(居然是毒瘤DP,那就不学了(bushi)

1.引子

让我们从一个常见的问题入手吧:
给定一棵 \(n\) 个点的树,点带点权,\(u\)点的点权为\(a[u]\),求出这棵树的最大权独立集的权值大小。

$Tip$
树上最大独立集问题是指在给定的树中,找到一个具有最大节点数量的独立集。独立集是指在树中选择一组节点,使得这些节点两两之间没有边相连。换句话说,任何两个选择的节点之间都不存在直接连接。
解法显然,设$dp[u][0/1]$分别表示以u为根节点的子树,不选/选择u节点的最大独立集权值。由定义可推出状态转移方程:

\[\begin{align} dp[u][0]&=\sum_{v\in child(u)}^{} max(dp[v][0],dp[v][1])\\ dp[u][1]&=\sum_{v\in child(u)}^{} dp[v][0] \end{align} \]

边界条件为:

\[\begin{align} dp[u][0]&=0\\ dp[u][1]&=a[u] \end{align} \]

所以这道题就做完了......吗?

2.序

让我们再来看看丧心病狂的出题人干的好事:有 m 次操作,每次操作给定 \(x\),\(y\),表示修改点 \(x\) 的权值为 \(y\)

\(1\le n \le 10^5, 1\le m \le 10^5\)(加强版甚至为 \(1\le n \le 10^6,1\le m \le 10^6\),朴素树剖都过不去!!!)

\(OMG!!!\) 出生出题人为了恶心\(OIers\)竟然丧心病狂到把静态数据变为动态数据了!!!

如果我们每次修改权值后都暴力再跑一次\(DP\),那么时间复杂度就会来到惊人的\(O(nm)\),显然是过不了这道题的。

那么我们只能另寻他法了。

注意到,时间复杂度之所以如此之高,是因为每次修改后,我们只会傻乎乎地全局跑一次\(DP\),但事实上,每次修改只会影响被修改的点到根节点的这一条,所以不难想到使用树链剖分对其进行处理。(当然全局平衡二叉树、Toptree、LCT这个常数太大还是算了吧等也可以进行处理,甚至时间复杂度为单\(log(n)\),但是谁又能拒绝好想又好写的树剖呢?)

3.正题

终于讲到这里了qwq

\(u\)节点的儿子分为重儿子\(wson[u]\)和轻儿子,重儿子贡献直接累积到\(u\)节点,轻儿子的单独新开一个数组\(g[u][0/1]\),分别表示
所有\(u\)轻儿子对 \(dp[u][0]\) 的贡献以及所有轻儿子对 \(dp[u][1]\) 的贡献再加上\(u\)自己的点权(之所以加上是为了方便转移),则:

\[\begin{align} g[u][0]&=\sum_{v\in child(u),v\neq wson[u]}^{}=max(dp[v][0],dp[v][1])\\ g[u][1]&=\sum_{v\in child(u),v\neq wson[u]}^{}=a[u]+dp[v][0] \end{align} \]

则:

\[\begin{align} dp[u][0]&=g[u][0]+max(dp[wson[u]][0],dp[wson[u]][1])\\ dp[u][1]&=g[u][1]+dp[wson[u]][0] \end{align} \]

于是,我们就可以愉快地进行矩阵优化了!定义\(max-plus\)为广义上的矩阵乘法(只要满足结合律即可)(\(max\)相当于\(+\),\(+\)相当于\(*\)

则:

\[\begin{bmatrix} dp[u][0]\\ dp[u][1] \end{bmatrix} = \begin{bmatrix} g[u][0]&g[u][0]\\ g[u][1]&-\infty \end{bmatrix} \begin{bmatrix} dp[wson[u]][0]\\ dp[wson[u]][1] \end{bmatrix} \]

因此,对于每条链,若定义\(Top[u],End[u]\)\(u\)所在链的顶端和链顶端所在链的结束位置,则对于 \(u\) 所在的一条重链,只有链顶 \(Top[u]\) 是轻节点。那么修改节点 \(u\) 时,链内有且仅有 \(Top[u]\) 的父节点 \(father[Top[u]]\) 的转移矩阵会被修改。考虑会修改成什么样。

首先,可以通过线段树维护重链上的转移矩阵,并同时维护区间乘积,但是需要注意运算顺序。此处因为进行了树链剖分,则有 \(dfn[u]=dfn[wson[u]]-1\),因此线段树维护时“左乘右”即可。
修改 \(g[i][1]\) 时,只需要在线段树上单点修改转移矩阵即可。
之后便是算出 \(\begin{bmatrix}f[top[u]][0]\\f[top[u]][1]\end{bmatrix}\),从而利用 \(f[Top[u]][0],f[Top[u]][1]\) 更新 \(father[Top[u]]\) 的转移矩阵。

4.C++代码

动态DP模板(数据加强版)

点击查看代码
//注意,这是加强版的,会有常数优化
#include<bits/stdc++.h>
using namespace std;

const int N=1e6+5,INF=0x3f3f3f3f;
int n,m;
int a[N];
int head[N],to[N<<1],nxt[N<<1],tot;
int fa[N],siz[N],wson[N],dfn[N],Top[N],End[N],rev[N],idx,root[N];
//父亲、子树大小、重儿子、dfs序、链顶、链尾、dfs序的反映射、编号、链顶在线段树上的编号
int f[N][2];
struct matrix{
	int mat[2][2];
	friend matrix operator *(matrix A,matrix B){
		matrix C;
		//手动展开,优化常数
		C.mat[0][0]=max(A.mat[0][0]+B.mat[0][0],A.mat[0][1]+B.mat[1][0]);
		C.mat[0][1]=max(A.mat[0][0]+B.mat[0][1],A.mat[0][1]+B.mat[1][1]);
		C.mat[1][0]=max(A.mat[1][0]+B.mat[0][0],A.mat[1][1]+B.mat[1][0]);
		C.mat[1][1]=max(A.mat[1][0]+B.mat[0][1],A.mat[1][1]+B.mat[1][1]);
		return C;
	}
};
matrix val[N],tr[N<<2];
int ls[N<<2],rs[N<<2],cnt;//动态开点,优化常数

void addedge(int u,int v){
	to[++tot]=v;
	nxt[tot]=head[u];
	head[u]=tot;
}

inline void pushup(int u){
	tr[u]=tr[ls[u]]*tr[rs[u]];
	//注意,在推导矩阵形式的状态转移方程时,我们的转移矩阵是从左乘到右的,所以更新也必须是这个顺序(否则必须从右乘到左)
}

void build(int &u,int l,int r){
	if(!u) u=++cnt;
	if(l==r){
		tr[u]=val[rev[l]];
		return ;
	}
	int mid=(l+r)>>1;
	build(ls[u],l,mid);
	build(rs[u],mid+1,r);
	pushup(u);
}

void update(int u,int l,int r,int pos){
	//单点修改
	if(l==r){
		tr[u]=val[rev[pos]];
		return ;
	}
	int mid=(l+r)>>1;
	if(pos<=mid) update(ls[u],l,mid,pos);
	else update(rs[u],mid+1,r,pos);
	pushup(u);
}

void dfs1(int u){
	siz[u]=1;
	for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(v==fa[u]) continue;
		fa[v]=u;
		dfs1(v);
		siz[u]+=siz[v];
		if(siz[v]>siz[wson[u]]) wson[u]=v;
	}
}

void dfs2(int u,int topchain){
	dfn[u]=++idx;
	rev[dfn[u]]=u;
	Top[u]=topchain;
	End[topchain]=max(End[topchain],dfn[u]);
	
	//初始化
	f[u][0]=0;
	f[u][1]=a[u];
	
	//初始化
	val[u].mat[0][0]=val[u].mat[0][1]=0;
	val[u].mat[1][0]=a[u];
	val[u].mat[1][1]=-INF;
	
	if(wson[u]){
		dfs2(wson[u],topchain);
		f[u][0]+=max(f[wson[u]][0],f[wson[u]][1]);
		f[u][1]+=f[wson[u]][0];
	}
	for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(v==fa[u] || v==wson[u]) continue;
		dfs2(v,v);
		//根据定义进行状态转移
		f[u][0]+=max(f[v][0],f[v][1]);
		f[u][1]+=f[v][0];
		val[u].mat[0][0]+=max(f[v][0],f[v][1]);
		val[u].mat[0][1]=val[u].mat[0][0];
		val[u].mat[1][0]+=f[v][0];
	}
}

void updatepath(int u,int nval){
	val[u].mat[1][0]+=nval-a[u];
	a[u]=nval;
	
	while(u){
		matrix bef=tr[root[Top[u]]];
		update(root[Top[u]],dfn[Top[u]],End[Top[u]],dfn[u]);
		matrix aft=tr[root[Top[u]]];
		//这就是为什么要记录链顶在线段树上的编号的原因。若不记录,则需额外调用两次查询函数求出链顶的值,这会增大常数。实际上,我们只需要链顶的值,并不需要复杂的查询操作
		//所以,这样即可优化常数
		u=fa[Top[u]];
		if(!u) break;
		val[u].mat[0][0]+=max(aft.mat[0][0],aft.mat[1][0])-max(bef.mat[0][0],bef.mat[1][0]);
		val[u].mat[0][1]=val[u].mat[0][0];
		val[u].mat[1][0]+=aft.mat[0][0]-bef.mat[0][0];
		//根据定义进行更新
	}
}

int main(){
	cin.tie(nullptr)->sync_with_stdio(false);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		addedge(u,v);
		addedge(v,u);
	}
	
	dfs1(1);
	dfs2(1,1);
	for(int i=1;i<=n;i++)
		if(Top[i]==i) build(root[i],dfn[i],End[i]);//对于每条重链单独建树
	
	int lst=0;
	while(m--){
		int x,y;
		cin>>x>>y;
		updatepath(x^lst,y);
		matrix ans=tr[root[1]];
		lst=max(ans.mat[0][0],ans.mat[1][0]);
		cout<<lst<<'\n';
	}
	return 0;
}

5.后记

呼呼,终于写完了这篇博客。
推荐大家去阅读一下TH911大佬的博客,我有很多疑问都是被TH911大佬的博客解决的(才不是因为别人的界面比我好看呢,哼😕!
大佬的博客

posted on 2026-08-25 17:30  _CENSORED  阅读(63)  评论(0)    收藏  举报

导航