「APIO2014」连珠线 树形DP

「APIO2014」连珠线

Part0

O ( n 2 ) O(n^2) O(n2)的暴力

我们看一下这个这一个连珠线的产生过程。

确定了一个根后,我们定义折链为LCA不为这两个点的链。就像下图这样。

我们定义中心点为一对蓝线产生时加入的点,每个点只能为一对蓝线产生时加入的点。

如果我们确定一个根,一个个在原有的点上往下加点时我们是不会加出折链的。故如果我们确定一个点为两个蓝线的中点,只有可能是它的父亲与他的一个儿子断开红线形成的。故我们可以用树形DP。定义 d p [ i ] [ 0 ] dp[i][0] dp[i][0]为i不为中心点时它和它的子树产生的最优答案(也就是蓝线长度之和), d p [ i ] [ 1 ] dp[i][1] dp[i][1]为i是中心点时它和它的子树产生的最优答案。那么就有以下转移方程。(u为当前决策的点,v为u的儿子)
d p [ u ] [ 0 ] = ∑ max ⁡ ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] + d i s ( u , v ) ) d p [ u ] [ 1 ] = d p [ u ] [ 0 ] + max ⁡ ( − max ⁡ ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] + d i s ( u , v ) + d p [ v ] [ 0 ] + d i s ( u , v ) ) dp[u][0]=\sum \max(dp[v][0],dp[v][1]+dis(u,v))\\ dp[u][1]=dp[u][0]+\max(-\max(dp[v][0],dp[v][1]+dis(u,v)+dp[v][0]+dis(u,v)) dp[u][0]=max(dp[v][0],dp[v][1]+dis(u,v))dp[u][1]=dp[u][0]+max(max(dp[v][0],dp[v][1]+dis(u,v)+dp[v][0]+dis(u,v))
我们以每个点为根都DFS一遍即可。

关键代码:
void dfs(int now){
	dp[now][1]=-2e9;//初值
	dp[now][0]=0;
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(fa[now]==nxt)continue;
		fa[nxt]=now;
		dfs(nxt);
		dp[now][0]+=max(dp[nxt][0],dp[nxt][1]+edge[i].d);
	}
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(fa[now]==nxt)continue;
		dp[now][1]=max(dp[now][1],dp[now][0]-max(dp[nxt][0],dp[nxt][1]+edge[i].d)+dp[nxt][0]+edge[i].d);
	}
}
for(int i=1;i<=n;i++)fa[i]=0,dfs(i),ans=max(ans,dp[i][0]);//求答案

Part1

由于枚举根不行,又因为每一个节点的状态都是由它的子节点转移而来,故可以换根。

下面举个例子来解释算法流程。

假设有这样一颗树。

我们已经算出了以1为根的答案。现在我们我们要算以2为根的答案。那么我们就要减去2及其子树对 d p [ 1 ] [ 0 ] dp[1][0] dp[1][0] d p [ 1 ] [ 1 ] dp[1][1] dp[1][1]的贡献,那么得到新的 d p [ 1 ] [ 0 ] dp[1][0] dp[1][0] d p [ 1 ] [ 1 ] dp[1][1] dp[1][1]就是1,5,6三点的dp数组。再转移给2就好了,就可以求出以2为根的答案了。

减去贡献这个事,对于 d p [ 1 ] [ 0 ] dp[1][0] dp[1][0]来说只用减去 max ⁡ ( d p [ 2 ] [ 0 ] , d p [ 2 ] [ 1 ] + d i s ( 1 , 2 ) ) \max(dp[2][0],dp[2][1]+dis(1,2)) max(dp[2][0],dp[2][1]+dis(1,2))。但是对于 d p [ 1 ] [ 1 ] dp[1][1] dp[1][1]来说就不一样了。我们观察 d p [ 1 ] [ 1 ] dp[1][1] dp[1][1]的转移发现我们只用知道除2以外1的其他子节点的 max ⁡ ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] + d i s ( 1 , v ) ) \max(dp[v][0],dp[v][1]+dis(1,v)) max(dp[v][0],dp[v][1]+dis(1,v))就可以求出新的 d p [ 1 ] [ 1 ] dp[1][1] dp[1][1]。这个可以通过记一个最大值一个次大值解决,也可以用前后缀最大值。

注意如果这时从2转移到3,那么对于上面那个式子就还要考虑 max ⁡ ( d p [ 1 ] [ 0 ] , d p [ 1 ] [ 1 ] + d i s ( 1 , 2 ) ) \max(dp[1][0],dp[1][1]+dis(1,2)) max(dp[1][0],dp[1][1]+dis(1,2))(dp数组是新的),因为1是2的新的子节点,之前是没有考虑到的。

AC代码:(觉得看不懂代码就看看转移方程)
#include<cstdio>
#include<algorithm>
#include<vector>
#define M 200005
using namespace std;
struct E{
	int to,nx,d;
}edge[M<<1];
int tot,head[M];
void Addedge(int a,int b,int d){
	edge[++tot].to=b;
	edge[tot].d=d;
	edge[tot].nx=head[a];
	head[a]=tot;
}
int fa[M];
int dp[M][2];
vector<int>Pre[M],Suf[M];
void check_max(int &x,int y){if(x<y)x=y;}
void dfs(int now){
	dp[now][1]=-2e9;
	dp[now][0]=0;
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(fa[now]==nxt)continue;
		fa[nxt]=now;
		dfs(nxt);
		dp[now][0]+=max(dp[nxt][0],dp[nxt][1]+edge[i].d);
	}
	Pre[now].clear();
	Suf[now].clear();
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(fa[now]==nxt)continue;
		dp[now][1]=max(dp[now][1],dp[now][0]-max(dp[nxt][0],dp[nxt][1]+edge[i].d)+dp[nxt][0]+edge[i].d);
		Pre[now].push_back(-max(dp[nxt][0],dp[nxt][1]+edge[i].d)+dp[nxt][0]+edge[i].d);//预处理前后缀最大值 
		Suf[now].push_back(-max(dp[nxt][0],dp[nxt][1]+edge[i].d)+dp[nxt][0]+edge[i].d);
	}
	for(int i=1;i<(int)Pre[now].size();i++)check_max(Pre[now][i],Pre[now][i-1]);
	for(int i=(int)Suf[now].size()-2;i>=0;i--)check_max(Suf[now][i],Suf[now][i+1]);
}
int ans;
void redfs(int now,int lst0,int lst1,int dis){
	dp[now][0]+=max(lst0,lst1+dis);//加上一个根对它的答案 
	dp[now][1]+=max(lst0,lst1+dis);//这个不用更新也可 
	dp[now][1]=max(dp[now][1],dp[now][0]-max(lst0,lst1+dis)+lst0+dis);
	check_max(ans,dp[now][0]);//更新答案 
	int tot=-1;
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(fa[now]==nxt)continue;
		tot++;
		int now_dp0=dp[now][0]-max(dp[nxt][0],dp[nxt][1]+edge[i].d);//新的dp0 
		int now_dp1=-max(lst0,lst1+dis)+lst0+dis;//*** 
		if(tot!=0)check_max(now_dp1,Pre[now][tot-1]);//除了nxt外其他点的贡献 
		if(tot!=(int)Pre[now].size()-1)check_max(now_dp1,Suf[now][tot+1]);
		redfs(nxt,now_dp0,now_dp0+now_dp1,edge[i].d);//注意新的dp1还要加上dp0(不理解看转移方程) 
	}
}
int main(){
	//freopen("beads.in","r",stdin);
	//freopen("beads.out","w",stdout);
	int n;
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		int a,b,d;
		scanf("%d%d%d",&a,&b,&d);
		Addedge(a,b,d);
		Addedge(b,a,d);
	}
	dfs(1);
	redfs(1,0,-1e9,-1e9);//这里的初值不要太小,会爆int 
	printf("%d\n",ans);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值