Luogu P4719 【模板】動態 DP

題目大意

%  給定一棵 nn 個點的樹,點帶點權。有 mm 次操作,每次操作給定 x,yx,y,表示修改點 xx 的權值爲 yy,輸出每次修改後樹的最大權獨立集大小。
%  數據範圍 1n,m1051\leqslant n,m\leqslant 10^5

題解

%   首先如果不考慮修改操作,很容易想到動態規劃,首先是定義


%   fu,0f_{u,0} 表示不取 uu 節點時,以 uu 爲根的子樹的最大獨立集大小。
  fu,1f_{u,1} 表示 uu 節點時,以 uu 爲根的子樹的最大獨立集大小。


%   定義 uu 的所有兒子的集合爲 SuS_u,自身的權值爲 aua_u,則轉移如下(其實是廢的)1

fu,0=vSumax(fv,0,fv,1)fu,1=au+vSufv,0\begin{aligned} f_{u,0}&=\sum_{v\in S_u}\max(f_{v,0},f_{v,1})\\ f_{u,1}&=a_u+\sum_{v\in S_u}f_{v,0} \end{aligned}

%  可以發現,每次的修改操作只會影響到某個點到根節點上所有點的 ff 數組,這是樹上的其中一條鏈,接下來我們可以考慮樹鏈剖分。爲了適應樹鏈剖分的定義,我們再從 ff 中分離出一部分信息,我們定義 uu 的重兒子爲 son(u)son(u),以它爲根的子樹爲 Tson(u)T_{son(u)},則可以定義


%   gu,0g_{u,0} 表示不取 uu 節點時,以 uu 爲根的子樹,在不考慮子樹 Tson(u)T_{son(u)}的前提下,的最大獨立集大小。
  gu,1g_{u,1} 表示 uu 節點時,以 uu 爲根的子樹,在不考慮子樹 Tson(u)T_{son(u)}的前提下,的最大獨立集大小。


%   簡而言之,gg 就是不考慮重兒子的貢獻的 ffgg 的轉移如下


gu,0=vSu,vson(u)max(fv,0,fv,1)gu,1=au+vSu,vson(u)fv,0\begin{aligned} g_{u,0}=\sum_{v\in S_u,v\not=son(u)}\max(f_{v,0},f_{v,1})\\ g_{u,1}=a_u+\sum_{v\in S_u,v\not=son(u)}f_{v,0}\\ \end{aligned}


%  同樣地,ff 的轉移可以更簡單。


fu,0=gu,0+max(fson(u),0,fson(u),1)=max(gu,0+fson(u),0,gu,0 +fson(u),1)fu,1=gu,1+fson(u),0=max(gu,1+fson(u),0,+fson(u),1)\begin{aligned} f_{u,0}&=g_{u,0}+\max(f_{son(u),0},f_{son(u),1})&&=\max(g_{u,0}+f_{son(u),0},g_{u,0}\ +f_{son(u),1})\\ f_{u,1}&=g_{u,1}+f_{son(u),0}&&=\max(g_{u,1}+f_{son(u),0},-\infty+f_{son(u),1})\\ \end{aligned}


%  上面的轉移中,第一個等於號的右側應該是顯然的,第二個等於號後面是喫飽了撐的,但絕不是無聊之作。考慮樹鏈剖分的操作,我們需要一種數據結構來維護這些信息,如果仍然採用線段樹的話,我們需要一種滿足結合律的運算,使得我們能保證結果的正確。
%  觀察一下,第二個等於號後面的式子有取最大值,有相加操作,還設計到多個元素的運算,顯然不可能爲簡單的四則運算,但其形式和矩陣乘法有些類似。於是,我們定義新矩陣乘法爲 (A×B)i,j=maxk=1nAi,k+Bk,j(A\times B)_{i,j}=\max_{k=1}^n A_{i,k}+B_{k,j}  可以證明,這樣的定義滿足結合律,仍然不滿足交換率。那麼上面的轉移可以寫成


[gu,0gu,0gu,1]×[fson(u),0fson(u),1]=[fu,0fu,1] \begin{bmatrix}g_{u,0}&g_{u,0}\\g_{u,1}&-\infty\end{bmatrix} \times\begin{bmatrix}f_{son(u),0}\\f_{son(u),1}\end{bmatrix} =\begin{bmatrix}f_{u,0}\\f_{u,1}\end{bmatrix}


%  定義上面方程左側含有 gg 的矩陣爲矩陣 GuG_u,等式右側的矩陣爲 FuF_u,具體地,原樹中每個非葉子節點存儲的信息爲 GuG_u,特別地,葉子節點存儲的信息爲 FuF_u。每個節點的信息均可以用一次普通樹形DP求出。
  如此,我們想要知道根節點的 FF 矩陣,只需要將根節點所在的鏈上所有節點的矩陣全部乘起來(從深度小的乘到深度大的)。這個過程需要我們先對原樹進行重鏈剖分,然後重編號,再用維護區間乘法的線段樹維護每條重鏈上的矩陣的信息。
  然後,我們就成功地把一個線性的算法優化到了帶一個log的級別。 我們開始考慮修改操作。
  我們定義 top(u)top(u) 表示 uu 所在的節點的重鏈的頂部的節點,fa(u)fa(u) 表示節點 uu 的父親,修改點 uu 的權值後,gu,1g_{u,1} 的值會隨之更改,這個修改十分簡單,直接在線段樹上修改,這樣會改變 Ftop(u)F_{top(u)} 矩陣,進而影響到 Gfa(u)G_{fa(u)}
  每次都按照定義重新計算一次 Gfa(u)G_{fa(u)} 顯然不現實,於是我們考慮在修改 gu,1g_{u,1} 之前,先將 Ftop(u)F_{top(u)} 的信息存下來,然後完修改 gu,1g_{u,1} 後,讓Gfa(u)G_{fa(u)} 加上 Ftop(u)F_{top(u)} 的變化量(先扣除原來的 Ftop(u)F_{top(u)},再加上新的 Ftop(u)F_{top(u)})。
  令整顆樹的根節點爲 rootroot,則最後的答案便是 max(froot,0,froot,1)\max(f_{root,0},f_{root,1})

代碼

%  代碼本身不算長,和普通樹剖差不多,下面的代碼只是註釋和空行比較多。

#include<bits/stdc++.h>
using namespace std;
#define maxn 400010
struct matrix{//矩陣類 
	int mat[2][2];	
	matrix(){memset(mat,-63,sizeof mat);}//初始化爲無窮小 
	inline int* operator[](const int x){return mat[x];}//重載中括號運算符 
	inline matrix operator*(const matrix &x)const{//新·矩陣乘法 
		matrix c;
		for(int i=0;i<=1;i++)
			for(int j=0;j<=1;j++)
				for(int k=0;k<=1;k++)
					c[i][j]=max(c[i][j],mat[i][k]+x.mat[k][j]);
		return c;
	}
};

struct edge{//鄰接表 
	int v,next;
}edges[maxn<<1];
int head[maxn],cnte=0;

void ins(int u,int v){//建邊 
	edges[++cnte]=(edge){v,head[u]};
	head[u]=cnte;
}

int fa[maxn],size[maxn],son[maxn];
//父節點,子樹大小,重兒子 

void dfs(int u){//深搜求出基本信息 
	size[u]=1;
	for(int i=head[u],v;i;i=edges[i].next)
		if((v=edges[i].v)!=fa[u]){
			fa[v]=u; dfs(v);
			size[u]+=size[v];
			if(size[son[u]]<size[v])
				son[u]=v;
		}
}

int n,q,a[maxn];

int idx[maxn],rank[maxn],cntv=0,top[maxn],end[maxn];
//idx:新編號  rank:原編號  top:重鏈頂部的原編號  end:重鏈尾的新編號 

int f[maxn][2],g[maxn][2]; 

/*

f[u][0]表示不取點u時,以u爲根的子樹的最大獨立集大小
f[u][1]表示取點u時,以u爲根的子樹的最大權獨立集大小 
g[u][0]表示不選點u,也不考慮重兒子所在子樹的前提下,以u爲根的子樹的最大權獨立集 
g[u][1]表示選點u,不考慮重兒子所在子樹的前提下,以u爲根的子樹的最大權獨立集 

g[u][0]=sigma{max(f[i][0],f[i][1])|i是u的兒子,i≠son[u]}
g[u][1]=a[i]+sigma{f[i][0]|i是u的兒子,i≠son[u]}

f[u][0]=max(g[u][0]+f[son[u]][0],g[u][0]+f[son[u]][1])
f[u][1]=max(g[i][1]+f[son[u]][0],-inf+f[son[u]][1])

|g[i][0]	g[i][0]	|	*	|f[son][0]|	=	|f[i][0]|
|g[i][1]	-inf	|		|f[son][1]|		|f[i][1]|

對於葉子節點,其沒有重兒子,因而其f和g的值沒有區別

g[leaf][0]=f[leaf][0]=0
g[leaf][1]=f[leaf][1]=a[leaf] 

因而不需要特判 
*/

matrix val[maxn];
//每個節點的矩陣 

void recode(int u,int chain){//重編號+基本dp 
	rank[idx[u]=++cntv]=u;	//標號 
	end[top[u]=chain]=cntv;	//更新重鏈信息 
	
	g[u][0]=f[u][0]=0;	//初始化 
	g[u][1]=f[u][1]=a[u];
	
	if(son[u]){	//先遍歷重兒子 
		recode(son[u],chain);
		f[u][0]+=max(f[son[u]][1],f[son[u]][0]);	//計算f數組 
		f[u][1]+=f[son[u]][0];	 
		//g數組不考慮重兒子 
	}
	
	for(int i=head[u],v;i;i=edges[i].next)
		if((v=edges[i].v)!=fa[u]&&v!=son[u]){
			recode(v,v);
			//計算f和g數組 
			f[u][0]+=max(f[v][0],f[v][1]);
			g[u][0]+=max(f[v][0],f[v][1]);
			f[u][1]+=f[v][0];
			g[u][1]+=f[v][0];
		}
	//複製到val中 
	val[u][0][1]=val[u][0][0]=g[u][0];
	val[u][1][0]=g[u][1];
	val[u][1][1]=-1e9;
}

struct node{
	node *left,*right;
	int l,r;
	matrix d;
	
	node(int tl=0,int tr=0):l(tl),r(tr){//建樹 
		if(tl==tr){
			d=val[rank[tl]];//線段樹葉子節點存儲每個點儲存的矩陣 
			left=right=NULL;
			return;
		}
		
		int mid=(l+r)>>1;
		left=new node(l,mid);
		right=new node(mid+1,r);
		d=left->d*right->d;
	}
	
	void change(int x){//更新 
		if(l==r){
			d=val[rank[l]];//更新每個點儲存的矩陣 
			return;
		}
		
		if(x<=left->r) left->change(x);
		else right->change(x);
		d=left->d*right->d;
	}
	
	matrix query(int x,int y){//查詢 
		if(l==x&&r==y) return d;
		if(y<=left->r) return left->query(x,y);
		if(x>=right->l) return right->query(x,y);
		return left->query(x,left->r)*right->query(right->l,y);
	}
}t;

void change(int u,int d){//修改 
	val[u][1][0]+=d-a[u];//加上差值 
	a[u]=d;
	while(u!=0){
		matrix before=t.query(idx[top[u]],end[top[u]]);//計算出原來的 
		t.change(idx[u]);//修改 
		matrix after=t.query(idx[top[u]],end[top[u]]);//計算出後來的 
		
		u=fa[top[u]];//到另一條鏈上 
		val[u][0][0]+=max(after[0][0],after[1][0])-max(before[0][0],before[1][0]);//加上差值 
		val[u][0][1]=val[u][0][0];
		val[u][1][0]+=after[0][0]-before[0][0];
	}
}

int main(){
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1,u,v;i<n;i++){
		scanf("%d %d",&u,&v);
		ins(u,v),ins(v,u);
	}
	
	dfs(1);
	recode(1,1);
	t=node(1,n);
	
	for(int i=1,u,d;i<=q;i++){
		scanf("%d %d",&u,&d);
		change(u,d);
		matrix ans=t.query(idx[1],end[top[1]]);
		printf("%d\n",max(ans[0][0],ans[1][0]));
	}
	return 0;
}

  1. 有兩條分割線分割的是最終使用的版本。 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章