动态DP小结

先摆一道模板题

(里面的最大权独立集也就是指选若干个点,这些点两两之间没有直接连边,然后使点权和最大)

假如这是一道静态的题,大家肯定随手就写出方程了:设 f i , 0 f_{i,0} fi,0 表示不选 i i i 的最优解, f i , 1 f_{i,1} fi,1 表示选 i i i 的最优解,那么有(其中 s o n son son 表示 i i i 的儿子):
{ f i , 0 = ∑ max ⁡ ( f s o n , 0 , f s o n , 1 ) f i , 1 = v a l i + ∑ f s o n , 0 \begin{cases} f_{i,0}=\sum \max(f_{son,0},f_{son,1})\\ f_{i,1}=val_i+\sum f_{son,0} \end{cases} {fi,0=max(fson,0,fson,1)fi,1=vali+fson,0

发现这个东西有点像矩阵乘法的形式,我们改变一下矩阵乘法的规则——原来的规则是这样的:
A × B = C → C i , j = ∑ k = 1 n A i , k × B i , j A\times B=C \to C_{i,j}=\sum_{k=1}^n A_{i,k}\times B_{i,j} A×B=CCi,j=k=1nAi,k×Bi,j

现在改成这样:
A × B = C → C i , j = max ⁡ k = 1 n ( A i , k + B i , j ) A\times B=C \to C_{i,j}=\max_{k=1}^n (A_{i,k}+B_{i,j}) A×B=CCi,j=k=1maxn(Ai,k+Bi,j)

为了方便,先考虑只有一个儿子的情况。那么转移方程可以表示成:
( 0 0 v a l i − i n f ) × ( f s o n , 0 f s o n , 1 ) = ( f i , 0 f i , 1 ) \left( \begin{array}{c} 0 & 0\\ val_{i} & -inf \end{array} \right)\times \left( \begin{array}{c} f_{son,0}\\ f_{son,1} \end{array} \right)= \left( \begin{array}{c} f_{i,0}\\ f_{i,1} \end{array} \right) (0vali0inf)×(fson,0fson,1)=(fi,0fi,1)

但事实上是不可能只有一个儿子的,考虑转化为一个儿子来做,也就是树链剖分了。

(下面的 s o n son son指重儿子)

但是还需要维护非重儿子的信息,我们定义一个新的数组 g g g g i , 0 g_{i,0} gi,0 表示不选 i i i i i i 的非重儿子的子树的最优解, g i , 1 g_{i,1} gi,1 表示选 i i i i i i 的非重儿子的最优解,那么 g g g 的转移方程为( j j j 是在枚举儿子):
{ g i , 0 = ∑ max ⁡ ( f j , 0 , f j , 1 )    ( j ≠ s o n ) g i , 1 = ∑ f j , 0                      ( j ≠ s o n ) \begin{cases} g_{i,0}=\sum \max (f_{j,0},f_{j,1})~~(j\ne son)\\ g_{i,1}=\sum f_{j,0}~~~~~~~~~~~~~~~~~~~~(j\ne son) \end{cases} {gi,0=max(fj,0,fj,1)  (j=son)gi,1=fj,0                    (j=son)

说白了, g g g f f f 的定义差不多,但是不考虑重链而已。

那么转移的矩阵就变成(下面称这条柿子最左边的那个矩阵为 g g g 矩阵):
( g i , 0 g i , 0 v a l i + g i , 1 − i n f ) × ( f s o n , 0 f s o n , 1 ) = ( f i , 0 f i , 1 ) \left( \begin{array}{c} g_{i,0} & g_{i,0}\\ val_{i}+g_{i,1} & -inf \end{array} \right)\times \left( \begin{array}{c} f_{son,0}\\ f_{son,1} \end{array} \right)= \left( \begin{array}{c} f_{i,0}\\ f_{i,1} \end{array} \right) (gi,0vali+gi,1gi,0inf)×(fson,0fson,1)=(fi,0fi,1)

那么我们在每个点上把它对应的 g g g 矩阵存下来,叶子结点特判一下,存一个 f f f 矩阵,那么因为每条重链的底部一定是叶子结点,所以要求某一个点的 f f f 矩阵的话,只需要把他所在的重链的底部的路径上的所有点的矩阵乘起来就可以得到了。

但是这里还可以取取巧,因为叶子结点的 g i , 0 g_{i,0} gi,0 g i , 1 g_{i,1} gi,1 f i , 0 f_{i,0} fi,0 f i , 1 f_{i,1} fi,1 相同,所以叶子结点我们不妨也放它的 g g g 矩阵,这样的效果是不会变的,只是有两个数是废的,问题不大。这样的话就不需要特判了,所有的点都放 g g g 矩阵即可。

修改的话维护 g g g 矩阵即可,显然,只有 l o g n logn logn 个点的 g g g 矩阵会发生改变,所以复杂度可以保证。

具体的修改也就是将这个权值被修改的点沿着重链往上跳,每一条重链的父亲就是要修改的对象,让这个对象根据现在的 f f f 来调整即可。

具体的调整: g g g 矩阵中 g i , 0 g_{i,0} gi,0 的值为所有非重儿子的 max ⁡ ( f j , 0 , f j , 1 ) \max(f_{j,0},f_{j,1}) max(fj,0,fj,1),那么让 g i , 0 g_{i,0} gi,0 减去原来的 max ⁡ \max max 值再加上现在的 max ⁡ \max max 值即可。 g i , 1 g_{i,1} gi,1 f j , 1 f_{j,1} fj,1 之和,那么只需要减去原来的 f j , 1 f_{j,1} fj,1 再加上现在的 f j , 1 f_{j,1} fj,1 即可。 − i n f -inf inf 那一位不需要修改。

那么代码如下:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100010
#define inf 999999999

int n,m;
struct nod{int y,next;};
nod e[maxn*2];
int first[maxn];
void buildroad(int x,int y)
{
	static int len=0;
	e[++len]=(nod){y,first[x]};
	first[x]=len;
}
int val[maxn];
int fa[maxn],size[maxn],mson[maxn];
void dfs1(int x)//树链剖分,找重儿子
{
	size[x]=1;
	for(int i=first[x];i;i=e[i].next)
	{
		int y=e[i].y;
		if(y==fa[x])continue;
		fa[y]=x;
		dfs1(y);
		size[x]+=size[y];
		if(size[y]>size[mson[x]])mson[x]=y;
	}
}
int now[maxn],old[maxn],id=0,top[maxn];
int bottom[maxn],cnt=0,be[maxn];
void dfs2(int x,int tp,int belong)//树链剖分,重新编号
{
	now[x]=++id;old[id]=x;top[x]=tp;be[x]=belong;
	//be[x]是x所处的重链的编号,bottom[x]是编号为x的重链的最低端的结点
	if(mson[x]==0){bottom[belong]=x;return;}
	dfs2(mson[x],tp,belong);
	for(int i=first[x];i;i=e[i].next)
	if(e[i].y!=fa[x]&&e[i].y!=mson[x])dfs2(e[i].y,e[i].y,++cnt);
}
struct matrix{//矩阵
	int a[2][2];
	matrix(){memset(a,-63,sizeof(a));}
	void set(int s[2][2])
	{
		for(int i=0;i<2;i++)
		for(int j=0;j<2;j++)
		a[i][j]=s[i][j];
	}
	matrix operator *(const matrix b)
	{
		matrix c;
		for(int i=0;i<2;i++)
		for(int j=0;j<2;j++)
		for(int k=0;k<2;k++)
		c.a[i][j]=max(c.a[i][j],a[i][k]+b.a[k][j]);//注意这里的矩阵的计算规则是重新定义的
		return c;
	}
};
int g[maxn][2][2],f[maxn][2];//g是记录每个点的g矩阵,f是用来辅助dp的
/*
g[i][0] , g[i][0]
g[i][1] , -MAX
*/
struct node{//线段树
	int l,r;
	matrix z;
	node *zuo,*you;
	node(int x,int y)
	{
		l=x,r=y;
		if(l<r)
		{
			int mid=l+r>>1;
			zuo=new node(l,mid);
			you=new node(mid+1,r);
			z=zuo->z*you->z;//注意是左儿子乘右儿子
		}
		else zuo=you=NULL,z.set(g[old[l]]);
	}
	void change(int x)//更新x的矩阵
	{
		if(l==r){z.set(g[old[l]]);return;}
		int mid=l+r>>1;
		if(x<=mid)zuo->change(x);
		else you->change(x);
		z=zuo->z*you->z;
	}
	matrix ask(int x,int y)//询问x到y的矩阵乘积
	{
		if(l==x&&r==y)return z;
		int mid=l+r>>1;
		if(y<=mid)return zuo->ask(x,y);
		else if(x>=mid+1)return you->ask(x,y);
		else return zuo->ask(x,mid)*you->ask(mid+1,y);
	}
};
node *root=NULL;
void dp(int x)//预处理
{
	g[x][1][1]=-inf;g[x][1][0]=val[x];
	f[x][1]=val[x];//这里的f是原dp方程中的f,用来辅助求出g的
	for(int i=first[x];i;i=e[i].next)
	{
		int y=e[i].y;
		if(y==fa[x])continue;
		dp(y);
		f[x][0]+=max(f[y][0],f[y][1]);
		f[x][1]+=f[y][0];
		if(y!=mson[x])
		{
			g[x][0][0]+=max(f[y][0],f[y][1]);
			g[x][1][0]+=f[y][0];
		}
	}
	g[x][0][1]=g[x][0][0];
}
matrix before,after;
void change(int x,int y)
{
	g[x][1][0]+=y-val[x];//修改
	while(x!=0)
	{
		before=root->ask(now[top[x]],now[bottom[be[x]]]);//先求出原来的
		root->change(now[x]);//更新矩阵
		after=root->ask(now[top[x]],now[bottom[be[x]]]);//求出现在的
		x=fa[top[x]];//往上跳
		g[x][0][0]+=max(after.a[0][0],after.a[1][0])-max(before.a[0][0],before.a[1][0]);
		g[x][1][0]+=after.a[0][0]-before.a[0][0];
		g[x][0][1]=g[x][0][0];//更新这个位置的矩阵
	}
}

int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)
	scanf("%d",&val[i]);
	for(int i=1,x,y;i<n;i++)
	{
		scanf("%d %d",&x,&y);
		buildroad(x,y);
		buildroad(y,x);
	}
	dfs1(1);
	dfs2(1,1,++cnt);
	dp(1);
	root=new node(1,n);
	for(int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		change(x,y);val[x]=y;
		matrix ans=root->ask(now[1],now[bottom[be[1]]]);
		printf("%d\n",max(ans.a[0][0],ans.a[1][0]));
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值