P5024 保卫王国(矩阵乘法,动态dp)

P5024 保卫王国

题意概述

给定一棵带权树和 m m m个要求,求树上每条边的两个端点至少选择一个时,最小的总权值. 此外,每个要求都会指定两个点是否能够选择,求在分别满足这些要求的前提下,最小的总权值是多少.

题目分析

首先,如果只看题目的前半部分,就是一个简单的树形 d p dp dp,本题的难点就在于如何在指定两个点的状态的同时做到时间复杂度尽量低.

朴素的想法

考虑最朴素的做法,显然是每次询问时,重新遍历一次整棵树,对指定的两个点进行标记,从而得解.

那我们是怎么标记的呢?不妨考虑我们是怎样设的状态和怎样推的状态转移方程.

f i , 0 f_{i,0} fi,0表示在第i个点不选择时候的子树最小值, f i , 1 f_{i,1} fi,1表示在第i个点选择时的子树最小值.
那么,对于 v − > u v->u v>u,就有:
f u , 0 + = f v , 1 f_{u,0}+=f_{v,1} fu,0+=fv,1
f u , 1 + = m i n ( f v , 1 , f v , 0 ) f_{u,1}+=min(f_{v,1},f_{v,0}) fu,1+=min(fv,1,fv,0)
这两个状态转移方程的正确性是显然的.

不难想到,由于状态转移方程是取 m i n min min的操作,故而可以通过给一个点赋极大值,从而达到“不选择”的目的.

举个例子:
假设 v v v不能选择,那么 f v , 1 = i n f f_{v,1}=inf fv,1=inf,那么它的 n e x t next next节点 u u u也就不能不选择了,我们观察状态转移方程,发现此时算出来的 f u , 0 + = i n f f_{u,0}+=inf fu,0+=inf,的确是极大值,从而该做法可行.
同理 v v v可以选择的情况.

void dfs(int u,int pre,int x,int a,int y,int b){
	f[u][1]=wt[u];f[u][0]=0;
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs(v,u,x,a,y,b);
		f[u][1]+=min(f[v][0],f[v][1]);
		f[u][0]+=f[v][1];
	}
	if(u==x&&a)f[u][0]=inf;
	else if(u==x&&!a)f[u][1]=inf;
	
	if(u==y&&b)f[u][0]=inf;
	else if(u==y&&!b)f[u][1]=inf;
}


	for(int i=1,x,a,y,b;i<=m;i++){
		memset(f,0,sizeof f);
		scanf("%d%d%d%d",&x,&a,&y,&b);
		dfs(1,1,x,a,y,b);
		if(min(f[1][0],f[1][1])>=inf)printf("-1\n");
		else printf("%lld\n",min(f[1][0],f[1][1]));
	}
进一步的思考

可以发现,以上时间复杂度为 O ( n m ) O(nm) O(nm)的朴素做法,慢就慢在重复计算了大量重复的状态,比如说每次在dfs完之后,都得memset一下,然后再重新计算.

而我们知道,实际上每次对于一个点的修改,都只会影响从这个点到根节点的状态.

考虑状态转移方程,一个节点状态只与它的所有儿子的状态有关,而一个节点状态的改变也只会影响其父亲.
也就是说每个节点和其子树都是相对独立的,不会出现同层子树互相干扰的情况.

所以我们考虑在初始化时以 O ( n ) O(n) O(n)的复杂度算出初始状态后,每个要求都用一个什么东西维护修改的两个值,然后恢复初始状态.

这正是我们所熟悉的动态 d p dp dp.

动态dp

树上的动态dp=树链剖分/LCT将树“摊平”+线段树维护矩阵转移方程

模板指路 P4719

其中一篇我比较喜欢的题解

动态dp的主要思想就是利用线段树(或者其他合适的数据结构)维护状态转移方程,从而实现对于一个dp问题进行修改操作.

在树形dp的操作就是套路化的套一个树链剖分或者LCT.

现在就以使用树链剖分为例窝太菜了不会LCT来讲讲怎么实现这一题.

另外,由于蒟蒻不知道大佬们是怎么推出最小权覆盖集=全集-最大权独立集这个结论的,所以只能用常规的求最小权覆盖集的算法(直接求 m i n min min) ,不像一些题解所顾虑的一样,实际上这个方法是完全可行的.


首先,由于使用了树链剖分,同时为了迎合之后状态转移方程改为矩阵形式的需要,状态设计要有所改变.

f i , 0 f_{i,0} fi,0表示第i个点不选时的最小值
f i , 1 f_{i,1} fi,1表示第i个点选择时的最小值
g i , 0 g_{i,0} gi,0表示不选i而i的轻儿子全部选择时的值
g i , 1 g_{i,1} gi,1表示i选定而其轻儿子可以任意选择时的最小值

那么我们就有如下的状态转移方程

f i , 0 = f j , 1 + g i , 0 f_{i,0}=f_{j,1}+g_{i,0} fi,0=fj,1+gi,0
f i , 1 = m i n ( f j , 0 , f j , 1 ) + g i , 1 f_{i,1}=min(f_{j,0},f_{j,1})+g_{i,1} fi,1=min(fj,0,fj,1)+gi,1

把它转换成矩阵乘法的形式:

[ i n f g u , 0 g u , 1 g u , 1 ] × [ f v , 0 f v , 1 ] = [ f u , 0 f u , 1 ] \begin{bmatrix} inf & g_{u,0}\\ g_{u,1} &g_{u,1} \end{bmatrix}\times \begin{bmatrix} f_{v,0} \\f_{v,1} \end{bmatrix}=\begin{bmatrix} f_{u,0}\\f_{u,1} \end{bmatrix} [infgu,1gu,0gu,1]×[fv,0fv,1]=[fu,0fu,1]

在这里,我们重定义了 × \times ×运算:

A × B = m i n ( A i , k + B k , j ) A\times B=min(A_{i,k}+B_{k,j}) A×B=min(Ai,k+Bk,j)

注意由于在树链剖分中叶子节点最后被访问到,
所以状态转移方程是从右向左更新的,
又因为矩阵具有结合律而没有交换率,
状态转移方程要写成这样的形式

在之前我们实现40pts的暴力时,我们通过变成 i n f inf inf来实现指定要求的状态,那么我们现在同样可以将 f i , 0   /   f i , 1 f_{i,0}\:/\:f_{i,1} fi,0/fi,1设为 i n f inf inf来标记一个点是否选择.


当u点强制选择时,

f u , 0 + = i n f f_{u,0}+=inf fu,0+=inf

f u , 0 = f v , 1 + g u , 0 f_{u,0}=f_{v,1}+g_{u,0} fu,0=fv,1+gu,0

其中 f j , 1 f_{j,1} fj,1显然不可改变(在动态dp中它是推出来的),所以我们不妨把 g i , 0 + = i n f g_{i,0}+=inf gi,0+=inf


同理当i点强制不选择时,

f u , 1 + = i n f f_{u,1}+=inf fu,1+=inf
f u , 1 = m i n ( f v , 1 , f v , 0 ) + g u , 1 f_{u,1}=min(f_{v,1},f_{v,0})+g_{u,1} fu,1=min(fv,1,fv,0)+gu,1

所以此时 g i , 1 + = i n f g_{i,1}+=inf gi,1+=inf


因为这道题的要求是“分别满足”的,所以在这里 + i n f +inf +inf的优越性就体现出来了,我们可以很容易通过 − i n f -inf inf来恢复到原来的状态.

总的时间复杂度为 O ( 2 n + m   l o g 2   n ) O(2n+m\:log^2\:n) O(2n+mlog2n).

程序实现

#include<iostream>
#include<cstdio>
#define ll long long
#define mid ((l+r)>>1)
#define inf 5e9
#define maxn 100010
using namespace std;
struct edge{
	int v,next;
}e[maxn<<1];
int head[maxn],tot;
inline void add(int u,int v){
	e[++tot].v =v;
	e[tot].next =head[u];
	head[u]=tot;
}
struct matrix{
	ll mat[2][2];
	matrix(){mat[0][0]=mat[0][1]=mat[1][0]=mat[1][1]=inf;}//调用时初始化
	matrix operator * (matrix x)const {
		matrix c;
		for(int i=0;i<2;i++)
			for(int j=0;j<2;j++)
				for(int k=0;k<2;k++)
				c.mat [i][j]=min(c.mat [i][j],mat[i][k]+x.mat [k][j]);
		return c;//重载运算符*
	}
};//矩阵乘法
int dfn[maxn];//dfn[线段树数列中的编号]=原编号
ll wt[maxn];
matrix ans[maxn<<2],val[maxn];
inline void push_up(int p){ans[p]=ans[p<<1]*ans[p<<1|1];}
inline void build(int p,int l,int r){
	if(l==r){
		ans[p]=val[dfn[l]];
		return ;
	}
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	push_up(p);
}
inline void update(int p,int l,int r,int k){
	if(l==r){
		ans[p]=val[dfn[k]];//修改时找到位置再赋值(听说减小常数?)
		return ;
	}
	if(k<=mid)update(p<<1,l,mid,k);
	if(mid<k)update(p<<1|1,mid+1,r,k);
	push_up(p);
}
matrix query(int p,int l,int r,int ql,int qr){
	if(ql<=l&&r<=qr)return ans[p];
	if(qr<=mid)return query(p<<1,l,mid,ql,qr);
	else if(ql>mid)return query(p<<1|1,mid+1,r,ql,qr);
	else return query(p<<1,l,mid,ql,qr)*query(p<<1|1,mid+1,r,ql,qr);
}//常规的线段树操作
int size[maxn],fa[maxn],son[maxn];
inline void dfs1(int u,int pre){
	size[u]=1;
	fa[u]=pre;
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs1(v,u);
		size[u]+=size[v];
		if(size[v]>size[son[u]])son[u]=v;
	}
}
int top[maxn],id[maxn],ed[maxn],cnt;//ed记录数列尾部(叶子节点),状态由叶子节点转移而来
ll f[maxn][2];
inline void dfs2(int u,int topu){
	top[u]=topu;
	id[u]=++cnt;
	dfn[cnt]=u;
	ed[topu]=cnt;
	f[u][0]=0;f[u][1]=wt[u];
	val[u].mat [0][1]=0;
	val[u].mat [1][0]=val[u].mat [1][1]=wt[u];
	if(!son[u])return ;
	dfs2(son[u],topu);
	f[u][0]+=f[son[u]][1];
	f[u][1]+=min(f[son[u]][1],f[son[u]][0]);
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==fa[u]||v==son[u])continue;
		dfs2(v,v);
		f[u][0]+=f[v][1];
		f[u][1]+=min(f[v][0],f[v][1]);
		val[u].mat [0][1]+=f[v][1];
		val[u].mat [1][0]+=min(f[v][0],f[v][1]);
		val[u].mat [1][1]=val[u].mat [1][0];//给对应矩阵的位置赋初值
	}
}
int n,m;
inline void update_path1(int u,ll w){
	val[u].mat [1][0]+=w;
	val[u].mat [1][1]=val[u].mat [1][0];//update_path1表示i点强制不选择
	matrix bef,aft;
	while(u){
		bef=query(1,1,n,id[top[u]],ed[top[u]]);
		update(1,1,n,id[u]);
		aft=query(1,1,n,id[top[u]],ed[top[u]]);
		u=fa[top[u]];
		val[u].mat [1][0]+=min(aft.mat [0][1],aft.mat [1][1])-min(bef.mat [0][1],bef.mat [1][1]);
		val[u].mat [1][1]=val[u].mat [1][0];
		val[u].mat [0][1]+=aft.mat [1][1]-bef.mat [1][1];
	}
}
inline void update_path2(int u,ll w){
	val[u].mat [0][1]+=w;//update_path2表示i点强制选择
	matrix bef,aft;
	while(u){
		bef=query(1,1,n,id[top[u]],ed[top[u]]);
		update(1,1,n,id[u]);
		aft=query(1,1,n,id[top[u]],ed[top[u]]);
		u=fa[top[u]];
		val[u].mat [1][0]+=min(aft.mat [0][1],aft.mat [1][1])-min(bef.mat [0][1],bef.mat [1][1]);
		val[u].mat [1][1]=val[u].mat [1][0];
		val[u].mat [0][1]+=aft.mat [1][1]-bef.mat [1][1];//矩阵修改操作,新值=原值+变化值
	}
}
int main(){
	string type;
	scanf("%d%d",&n,&m);cin>>type;
	for(register int i=1;i<=n;++i)scanf("%lld",&wt[i]);
	for(register int i=1,u,v;i<n;++i){
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	dfs1(1,0);
	dfs2(1,1);
	build(1,1,n);
	for(register int i=1,a,x,b,y;i<=m;++i){
		scanf("%d%d%d%d",&a,&x,&b,&y);
		if(x==0)update_path1(a,inf);
		else update_path2(a,inf);
		if(y==0)update_path1(b,inf);
		else update_path2(b,inf);//修改成指定值
		matrix Ans=query(1,1,n,id[1],ed[1]);
		if(min(Ans.mat [0][1],Ans.mat [1][1])>=inf)printf("-1\n");//如果出现了怎么选择都是inf的情况,说明没有可行方案
		else printf("%lld\n",min(Ans.mat [0][1],Ans.mat [1][1]));
		if(x==0)update_path1(a,-inf);
		else update_path2(a,-inf);
		if(y==0)update_path1(b,-inf);
		else update_path2(b,-inf);//修改回原值
	}
	return 0;
} 

后记

还是想说一下这种直接修改的做法为什么是正确的. 首先,它只影响了从当前节点到根的状态,所以满足了树形dp的要求. 此外,虽然有两个点要修改,但是考虑原来的动态dp模型,发现修改多个点/多次是不会影响其正确性的,我们相当于只是执行了4次动态dp的修改而已,所以也是正确的,而不需要考虑“两个点在同一条重链上怎么处理”之类的问题.

此外,非常感谢@10MN47 的帮助和差错. 包括但不限于发现了我状态转移矩阵的错误(打草稿时矩阵写着写着就笔误了)和我赋值时候的错误(直接把指定点改成了 i n f inf inf而不是 ± i n f \pm inf ±inf

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值