Luogu P4719 【模板】动态 DP

题目大意

%   给定一棵 n n n 个点的树,点带点权。有 m m m 次操作,每次操作给定 x , y x,y x,y,表示修改点 x x x 的权值为 y y y,输出每次修改后树的最大权独立集大小。
%   数据范围  1 ⩽ n , m ⩽ 1 0 5 1\leqslant n,m\leqslant 10^5 1n,m105

题解

%   首先如果不考虑修改操作,很容易想到动态规划,首先是定义


%    f u , 0 f_{u,0} fu,0 表示不取 u u u 节点时,以 u u u 为根的子树的最大独立集大小。
   f u , 1 f_{u,1} fu,1 表示 u u u 节点时,以 u u u 为根的子树的最大独立集大小。


%   定义 u u u 的所有儿子的集合为 S u S_u Su,自身的权值为 a u a_u au,则转移如下(其实是废的)1

f u , 0 = ∑ v ∈ S u max ⁡ ( f v , 0 , f v , 1 ) f u , 1 = a u + ∑ v ∈ S u f v , 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} fu,0fu,1=vSumax(fv,0,fv,1)=au+vSufv,0

%   可以发现,每次的修改操作只会影响到某个点到根节点上所有点的 f f f 数组,这是树上的其中一条链,接下来我们可以考虑树链剖分。为了适应树链剖分的定义,我们再从 f f f 中分离出一部分信息,我们定义 u u u 的重儿子为 s o n ( u ) son(u) son(u),以它为根的子树为 T s o n ( u ) T_{son(u)} Tson(u),则可以定义


%    g u , 0 g_{u,0} gu,0 表示不取 u u u 节点时,以 u u u 为根的子树,在不考虑子树 T s o n ( u ) T_{son(u)} Tson(u)的前提下,的最大独立集大小。
   g u , 1 g_{u,1} gu,1 表示 u u u 节点时,以 u u u 为根的子树,在不考虑子树 T s o n ( u ) T_{son(u)} Tson(u)的前提下,的最大独立集大小。


%   简而言之, g g g 就是不考虑重儿子的贡献的 f f f g g g 的转移如下


g u , 0 = ∑ v ∈ S u , v ≠ s o n ( u ) max ⁡ ( f v , 0 , f v , 1 ) g u , 1 = a u + ∑ v ∈ S u , v ≠ s o n ( u ) f v , 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} gu,0=vSu,v=son(u)max(fv,0,fv,1)gu,1=au+vSu,v=son(u)fv,0


%   同样地, f f f 的转移可以更简单。


f u , 0 = g u , 0 + max ⁡ ( f s o n ( u ) , 0 , f s o n ( u ) , 1 ) = max ⁡ ( g u , 0 + f s o n ( u ) , 0 , g u , 0   + f s o n ( u ) , 1 ) f u , 1 = g u , 1 + f s o n ( u ) , 0 = max ⁡ ( g u , 1 + f s o n ( u ) , 0 , − ∞ + f s o n ( 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} fu,0fu,1=gu,0+max(fson(u),0,fson(u),1)=gu,1+fson(u),0=max(gu,0+fson(u),0,gu,0 +fson(u),1)=max(gu,1+fson(u),0,+fson(u),1)


%   上面的转移中,第一个等于号的右侧应该是显然的,第二个等于号后面是吃饱了撑的,但绝不是无聊之作。考虑树链剖分的操作,我们需要一种数据结构来维护这些信息,如果仍然采用线段树的话,我们需要一种满足结合律的运算,使得我们能保证结果的正确。
%   观察一下,第二个等于号后面的式子有取最大值,有相加操作,还设计到多个元素的运算,显然不可能为简单的四则运算,但其形式和矩阵乘法有些类似。于是,我们定义新矩阵乘法为 ( A × B ) i , j = max ⁡ k = 1 n A i , k + B k , j (A\times B)_{i,j}=\max_{k=1}^n A_{i,k}+B_{k,j} (A×B)i,j=k=1maxnAi,k+Bk,j  可以证明,这样的定义满足结合律,仍然不满足交换率。那么上面的转移可以写成


[ g u , 0 g u , 0 g u , 1 − ∞ ] × [ f s o n ( u ) , 0 f s o n ( u ) , 1 ] = [ f u , 0 f u , 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} [gu,0gu,1gu,0]×[fson(u),0fson(u),1]=[fu,0fu,1]


%   定义上面方程左侧含有 g g g 的矩阵为矩阵 G u G_u Gu,等式右侧的矩阵为 F u F_u Fu,具体地,原树中每个非叶子节点存储的信息为 G u G_u Gu,特别地,叶子节点存储的信息为 F u F_u Fu。每个节点的信息均可以用一次普通树形DP求出。
  如此,我们想要知道根节点的 F F F 矩阵,只需要将根节点所在的链上所有节点的矩阵全部乘起来(从深度小的乘到深度大的)。这个过程需要我们先对原树进行重链剖分,然后重编号,再用维护区间乘法的线段树维护每条重链上的矩阵的信息。
  然后,我们就成功地把一个线性的算法优化到了带一个log的级别。 我们开始考虑修改操作。
  我们定义 t o p ( u ) top(u) top(u) 表示 u u u 所在的节点的重链的顶部的节点, f a ( u ) fa(u) fa(u) 表示节点 u u u 的父亲,修改点 u u u 的权值后, g u , 1 g_{u,1} gu,1 的值会随之更改,这个修改十分简单,直接在线段树上修改,这样会改变 F t o p ( u ) F_{top(u)} Ftop(u) 矩阵,进而影响到 G f a ( u ) G_{fa(u)} Gfa(u)
  每次都按照定义重新计算一次 G f a ( u ) G_{fa(u)} Gfa(u) 显然不现实,于是我们考虑在修改 g u , 1 g_{u,1} gu,1 之前,先将 F t o p ( u ) F_{top(u)} Ftop(u) 的信息存下来,然后完修改 g u , 1 g_{u,1} gu,1 后,让 G f a ( u ) G_{fa(u)} Gfa(u) 加上 F t o p ( u ) F_{top(u)} Ftop(u) 的变化量(先扣除原来的 F t o p ( u ) F_{top(u)} Ftop(u),再加上新的 F t o p ( u ) F_{top(u)} Ftop(u))。
  令整颗树的根节点为 r o o t root root,则最后的答案便是 max ⁡ ( f r o o t , 0 , f r o o t , 1 ) \max(f_{root,0},f_{root,1}) max(froot,0,froot,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. 有两条分割线分割的是最终使用的版本。 ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值