动态DP学习笔记

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

1.引子

让我们从一个常见的问题入手吧:

给定一棵 \(n\) 个点的树,点带点权,\(u\)点的点权为\(au\),求出这棵树的最大权独立集的权值大小。
Tip

复制代码
树上最大独立集问题是指在给定的树中,找到一个具有最大节点数量的独立集。独立集是指在树中选择一组节点,使得这些节点两两之间没有边相连。换句话说,任何两个选择的节点之间都不存在直接连接。

解法显然,设dp\[u\]\[0/1\]分别表示以u为根节点的子树,不选/选择u节点的最大独立集权值。由定义可推出状态转移方程:

\\\begin{align} dp\[u0&=\sum_{v\in child(u)}^{} max(dpv0,dpv1)\\ dpu1&=\sum_{v\in child(u)}^{} dpv0 \end{align} \]

边界条件为:

\\\begin{align} dp\[u0&=0\\ dpu1&=au \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\)节点的儿子分为重儿子\(wsonu\)和轻儿子,重儿子贡献直接累积到\(u\)节点,轻儿子的单独新开一个数组\(gu0/1\),分别表示

所有\(u\)轻儿子对 \(dpu0\) 的贡献以及所有轻儿子对 \(dpu1\) 的贡献再加上\(u\)自己的点权(之所以加上是为了方便转移),则:

\\\begin{align} g\[u0&=\sum_{v\in child(u),v\neq wsonu}^{}=max(dpv0,dpv1)\\ gu1&=\sum_{v\in child(u),v\neq wsonu}^{}=au+dpv0 \end{align} \]

则:

\\\begin{align} dp\[u0&=gu0+max(dpwson\[u]0,dpwson\[u]1)\\ dpu1&=gu1+dpwson\[u]0 \end{align} \]

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

则:

\\\begin{bmatrix} dp\[u0\\ dpu1 \end{bmatrix} = \begin{bmatrix} gu0&gu0\\ gu1&-\infty \end{bmatrix} \begin{bmatrix} dpwson\[u]0\\ dpwson\[u]1 \end{bmatrix} \]

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

首先,可以通过线段树维护重链上的转移矩阵,并同时维护区间乘积,但是需要注意运算顺序。此处因为进行了树链剖分,则有 \(dfnu=dfnwson\[u]-1\),因此线段树维护时"左乘右"即可。

修改 \(gi1\) 时,只需要在线段树上单点修改转移矩阵即可。

之后便是算出 \(\begin{bmatrix}ftop\[u]0\\ftop\[u]1\end{bmatrix}\),从而利用 \(fTop\[u]0,fTop\[u]1\) 更新 \(fatherTop\[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大佬的博客解决的(才不是因为别人的界面比我好看呢,哼😕!)

大佬的博客

相关推荐
hansang_IR10 天前
【题解】P14468 [COCI 2025/2026 #1] 和谐 / Harmonija(线段树 DDP 版本)
c++·算法·动态dp
XuYueming1 年前
圆方树学习笔记 —— 一种关于点双连通分量的思考方式
题解·lca·缩点·tarjan·圆方树·点双连通分量·树链剖分·记录 & 心得·仙人掌·理论 / 算法·bzoj·hydro·图的连通性·动态 dp / ddp
keysky2 年前
「SPOJ2666」QTREE4 - Query on a tree IV
线段树··树链剖分
Code920072 年前
Codeforces Round 975 (Div. 1) D. Max Plus Min Plus Size(思维题 并查集/动态dp 线段树维护状态合并)
并查集·思维题·动态dp
rgw20102 年前
P10789 [NOI2024] 登山
数据结构·动态规划·树上倍增·主席树·树链剖分·前缀优化
AnRain安林2 年前
#8. 「模板」树链剖分
树链剖分