P3177 [HAOI2015] 树上染色 题解+感悟

原题链接:色上染树

好题,强推

遇事不决,观察题目

呃呃呃呃,显然得这是一道树d题

呃呃呃呃接下来就不会做了???

别慌,想想树形dp题目照常是怎么做的

对于一道树形dp题,要么从下往上递推,要么从上往下递推

很明显,因为左右儿子之间的决策会相互影响,而且答案肯定是合并而来的,所以得出转移的过程:自下往上

说得好,那么该怎样转移呢?

很容易列出状态转移方程: f i , j f_{i,j} fi,j,表示以第i个点为根,总共染了j个点的答案

ez,秒啦!

容易证明这样的转移是错误的,即当前的最优不能推出往上全局的最优(而且转移也很难写)

怎么办呢?

羊毛产自羊身上,可是贡献就一定产自点身上吗?

我们思考一下,答案的贡献可以由哪几部分构成

  1. 每个选择与未选择的点

  2. 每个点之间的距离

既然点走不通,我们转换思路,尝试着用距离来统计贡献而距离又由哪几部分构成?答:每条边

我们发现对于每条边而言,它的一端形成的树上的点另一端的树的点两两之间之间的路径一定会经过这条边,换言之,这条边的贡献可以用以下式子表达:
v a l = ( ( c n t l _ b l a c k × c n t r _ b l a c k ) + ( c n t l _ w h i t e × c n t r _ w h i t e ) ) × w val = ((cnt_{l\_black} \times cnt_{r\_black} )+ (cnt_{l\_white} \times cnt_{r\_white} ))\times w val=((cntl_black×cntr_black)+(cntl_white×cntr_white))×w

其中, c n t l _ b l a c k cnt_{l\_black} cntl_black 表示这条边左半边的黑色点的数量, c n t r _ b l a c k cnt_{r\_black} cntr_black表示这条边右半边的黑色点的数量,白色同理,w表示这条边的边权(长度)

容易证明,所有边的贡献值之和就是所有点的贡献之和

接下来的的问题也就变成了“如何取黑色点才能使所有边的贡献之和最大”

显然边的贡献可以自下往上递推而来,保证染色点数相同的情况下贡献最大的肯定最优

再问一句:为什么

因为题目要求全树上黑色点的数量一定为k,所以它的黑色点要么在边的上方,要么在边的下方,而白色点的数量又取决于黑色点的数量,所以当下方黑色点数量相同时,对上方操作的影响也是相同的,所以贡献最大肯定最优

所以可以做出状态转移数组: d p i , j dp_{i,j} dpi,j 表示到第i个点时,下方有j个黑色点,下方边的总贡献值和的最大值

所以得出状态转移方程:

d p i , j = m a x ( d p i , j , d p i , k + d p t o , j − k + v a l ) dp_{i,j}=max(dp_{i,j},dp_{i,k}+dp_{to,j-k}+val) dpi,j=max(dpi,j,dpi,k+dpto,jk+val)

其中 t o to to 表示这条边的一个子节点

具体看代码

#include<bits/stdc++.h>
#define ll long long
#define size Size
using namespace std;
int n,k;
const int Max=2005;
ll inf=0x7ffffffffffffff;
struct edge{
	int to,next,w;
}p[Max*2];
int head[Max],last[Max],idx=0;
void add(int u,int v,int w)
{
	if(head[u])
		p[last[u]].next=++idx;
	else
		head[u]=++idx;
	last[u]=idx;
	p[idx].to=v;
	p[idx].w=w;
}
int size[Max];
ll dp[Max][Max];
int dfs(int now,int from)
{
	size[now]++;
	dp[now][0]=0;
	dp[now][1]=0;
	for(int i=head[now];p[i].to!=0;i=p[i].next)
	{
		int to=p[i].to;
		if(to==from)
			continue;
		size[now]+=dfs(to,now);
		for(int j=min(k,size[now]);j>=0;j--)
		{
			for(ll l=0;l<=min(j,size[to]);l++)
			{
				if(dp[now][j-l]==-inf)
					continue;
				ll val=l*p[i].w*(k-l)+(ll)(size[to]-l)*p[i].w*(n-size[to]-k+l);
				dp[now][j]=max(dp[now][j],dp[now][j-l]+dp[to][l]+val);
			}
		}
	}
	return size[now];
}
void init()
{
	for(int i=1;i<=n;i++)
		for(int j=0;j<=k;j++)
			dp[i][j]=-inf;
}
int main()
{
	scanf("%d%d",&n,&k);
	init();
	int x,y,w;
	for(int i=1;i<n;i++)
	{
		scanf("%d%d%d",&x,&y,&w);
		add(x,y,w);
		add(y,x,w);
	}
	dfs(1,1);
	printf("%lld",dp[1][k]);
}

注意这里的dp数组一定要初始化,因为我们的背包是倒着做的,一些情况可能并不存在,需要特判

感悟:对于树形dp,当点的思路走不通时,可以考虑转换成边的思路求解

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值