树形dp<1>——换根dp

请不要问我为什么不先讲树形dp和树上背包,问就是不知道QAQ

正片

树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。

通常需要两次 DFS,第一次 DFS 预处理诸如深度,点权和之类的信息,在第二次 DFS 开始运行换根动态规划。

        ——以上内容来自OI WIKI

怎么说呢,换根dp就是把一个不是树根的点提上去,让TA成为树根。这样的话,新的树根的原来的父亲就成了TA现在的儿子,而TA原来的儿子不变。听起来有亿点点绕,那么就来看一道题吧。

----------------------------------------------------不那么华丽的分割线--------------------------------------------------

洛谷P3478

其实我们想一下,每次换一个结点当做根,那么原来这个结点的孩子的深度全部减少1,而TA的"兄弟姐妹"们的深度都全部加1。总结一下,我们让size_{i}表示i的孩子的个数,f_{i}表示i为根时的结果,可以得出res_{y}=res_{x}+n-2\times size_{y}(x是y原来的父亲)。这里大家可以自行推一下。

OK,那接下来就是代码部分了。(我用了链式前向星存图,如果你们喜欢用二维数组也不是不行)

#include 
using namespace std;
struct edge{
	int to;
	int nxt;
}e[maxn<<1];
int head[maxn];
int cnt,n;
long long ans;
long long res[maxn],dep[maxn],size[maxn];
void add(int u,int v){
	e[++cnt].nxt=head[u];
	head[u]=cnt;
	e[cnt].to=v;
}
void dfs1(int x,int fa){
	size[x]=1;
	dep[x]=dep[fa]+1;
	for(int i=head[x];i;i=e[i].nxt){
		int y=e[i].to;
		if(y==fa)
			continue;
		dfs1(y,x);
		size[x]+=size[y];
	}
}
void dfs2(int x,int fa){
	for(int i=head[x];i;i=e[i].nxt){
		int y=e[i].to;
		if(y==fa)
			continue;
		res[y]=res[x]+n-2*size[y];
		dfs2(y,x);
	}
}
int main(){
	cin>>n;
	for(int i=1;i>u>>v;
		add(u,v);
		add(v,u);
	}
	dfs1(1,0);
	for(int i=1;i<=n;i++)
		res[1]+=dep[i];
	dfs2(1,0);
	int idx=0;
	for(int i=1;i<=n;i++){
		if(ans

这不再找几题练练?

CF1324F

嗯,需要翻译吗?

直接推公式,令dp_{v}为根是v时的答案。我们做一遍dfs,得出dp_{v}=a_{v}+\sum_{to\epsilon children_{v}}^{max(0,dp_{to})}。接下来,开始换根。我们把根v换成根to:①把to的孩子从v当中除掉dp_{v}=dp_{v}-max(0,dp_{to}),②把v变成to的孩子dp_{to}=dp_{to}+max(0,dp_{v})

OK,然后就没啥问题了,时间复杂度为\Theta (n)。贴个代码:

#include 
using namespace std;
vector a,ans,dp;
vector> tree;
void dfs(int v,int p=-1){
	dp[v]=a[v];
	for(int u:tree[v]){
		if(u==p)
			continue;
		dfs(u,v);
		dp[v]+=max(dp[u],0);
	}
}
void solve(int v,int p=-1){
	ans[v]=dp[v];
	for(int u:tree[v]){
		if(u==p)
			continue;
		dp[v]-=max(0,dp[u]);
		dp[u]+=max(0,dp[v]);
		solve(u,v);
		dp[u]-=max(0,dp[v]);
		dp[v]+=max(0,dp[u]);
	}
}
int main(){
	int n;
	cin>>n;
	for(int i=0;i>a[i];
		if(a[i]==0)
			a[i]=-1;
	}
	for(int i=0;i>u>>v;
		u--;
		v--;
		tree[u].push_back(v);
		tree[v].push_back(u);
	}
	dfs(0);
	solve(0);
	for(int i=0;i

最后一题~

我们应该锻炼一下自己读英文题的水平,所以——诶不是,你怎么打开洛谷了?好吧,我知道我是拦不住你的。

相信大家都已近康过翻译了,所以我就不翻了。不要给自己找理由,不会就是不会!

很不显然,这题比上题难(官方给的2100,上题官方评分1800)。那么怎么做呢?我们令dp_{v}为v是根的答案,则dp_{v}=sz_{v}+\sum_{to\epsilon children_{v}}^{dp_{to}}。(children_{v}是v的孩子) 但问题是,我们怎么才能把根从1换到n而且不超时呢?Good Question! 当我们把根从v换成to的时候,我们要把to的那坨子树给砍掉。所以,我们要用dp_{v}去减dp_{to}sz_{to},同时改变sz_{v},让TA减掉to的子树的大小。OK,到这儿,就成功一半了。接下来就是把v变成to的孩子。我们让sz_{to}加上sz_{v},然后把dp_{to}增加dp_{v}sz_{v}。也许有点晕,贴上这个片段的代码:

void dfs(int v,int p=-1){
	ans=max(ans,dp[v]);
	for(auto u:tree[v]){
		if(u==p)	
			continue;
		dp[v]-=dp[u];
		dp[v]-=sz[u];
		sz[v]-=sz[u];
		sz[u]+=sz[v];
		dp[u]+=sz[v];
		dp[u]+=dp[v];
		dfs(u,v);
        //回溯
		dp[u]-=dp[v];
		dp[u]-=sz[v];
		sz[u]-=sz[v];
		sz[v]+=sz[u];
		dp[v]+=sz[u];
		dp[v]+=dp[u];
	}
}

所以,我们先用一趟dfs算出所有的sz_{i},然后用一趟dfs算出一个点的dp值,最后换根就OK辣。贴上代码:

#include 
using namespace std;
long long ans;
vector sz;
vector dp;
vector> tree;
int getsize(int v,int p=-1){
	sz[v]=1;
	for(auto u:tree[v]){
		if(u==p)
			continue;
		sz[v]+=getsize(u,v);
	}
	return sz[v];
}
long long precalc(int v,int p=-1){
	dp[v]=sz[v];
	for(auto u:tree[v]){
		if(u==p)
			continue;
		dp[v]+=precalc(u,v);
	}
	return dp[v];
}
void dfs(int v,int p=-1){
	ans=max(ans,dp[v]);
	for(auto u:tree[v]){
		if(u==p)	
			continue;
		dp[v]-=dp[u];
		dp[v]-=sz[u];
		sz[v]-=sz[u];
		sz[u]+=sz[v];
		dp[u]+=sz[v];
		dp[u]+=dp[v];
		dfs(u,v);
		dp[u]-=dp[v];
		dp[u]-=sz[v];
		sz[u]-=sz[v];
		sz[v]+=sz[u];
		dp[v]+=sz[u];
		dp[v]+=dp[u];
	}
}
int main(){
	int n;
	cin>>n;
	for(int i=0;i>u>>v;
		tree[u].push_back(v);
		tree[v].push_back(u);
	}
	sz=vector(n);
	dp=vector(n);
	getsize(0);
	precalc(0);
	dfs(0);
	cout<

当然,这题好像还可以不换根。这里就不说了(其实是我不会,况且今天换根dp是主角),有兴趣的可以自行研究。

那么本期博客就到这里了。我们下期再见!

注意事项:本期的代码直接提交均无法AC,请各位不要无脑Ctrl C+Ctrl V,看懂之后自己写一遍。

你可能感兴趣的:(动态规划,动态规划)