树上最经典的dp题目无疑是树上背包,是基于点的一种状态定义和转移方式,但是这样的思维方式有时候是不可行的,关于边的讨论方式也是一种非常优秀的转移方式,并且此时的状态中存储的往往不是此状态下的价值,而是对整体答案的贡献(应为如果能表示出价值往往可以对点dp来求解),我们通过一道例题来看这个问题。
P3177 [HAOI2015]树上染色
题目大意:有一棵点数为 N 的树,树边有边权。给你一个在 0~ N 之内的正整数 K ,你要在这棵树中选择 K个点,将其染成黑色,并将其他 的N-K个点染成白色 。 将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的受益。问受益最大值是多少。
n、k<=2000;
题解:很轻松可以发现如果关于每个点及其子树定义状态并转移,我们并不能求出此状态下的权值,如果暴力的讨论图又无疑会T,我们就可以发现如果对于边讨论,每条边对于答案的贡献是经过它路径的条数*边权,同时发现对于一条边,我们已经得知其一端子树的大小,如果枚举子树中的黑点个数,就可以得出其两端分别的黑、白点个数,然后就可以轻易的求出边的贡献,那么我们就有一个非常清晰的状态转移方程:
枚举i、j,分别表示对于本点所有子树和其单一子树的黑点个数,用val表示此边的贡献,就有
dp[u][i] = max( dp[u][i], dp[u][i-j] + dp[v][j] + val )
很轻松就能做出结果。
注:1、在进行树上dp时一定要注意能不能本状态的合法性和转移到本状态的合法性,例如本题转移时要注意一颗子树一颗子树地枚举,且i需要倒叙枚举,原因在于防止由(有本子树的状态)转移到(有本子树的状态),这样无疑是错的,另有例如,由不存在的状态转移到本状态,这样也同样是错的,在本题中体现为dp[u][i-j]状态不一定在以前讨论过,我们可以利用至负无穷或标记的方式来规避出错,写dp时一定要考虑到这些情况,加以解决。
2、注意开LL
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=2e3+10;
typedef long long LL;
struct Edge{
int to,nxt;LL val;
}e[maxn*2];
LL n,k,head[maxn],cnt,dp[maxn][maxn],size[maxn];
void add(int x,int y,int z)
{
cnt++;e[cnt].to=y,e[cnt].val=z;
e[cnt].nxt=head[x],head[x]=cnt;
}
void dfs(int now,int fa)
{
size[now]=1;
dp[now][0]=dp[now][1]=0;
for(int i=head[now];i;i=e[i].nxt){
int to=e[i].to;
if(to!=fa){
dfs(to,now);
size[now]+=size[to];
}
}
for(int i=head[now];i;i=e[i].nxt)
for(LL j=min(size[now],k);j>=0;j--)
if(e[i].to!=fa){
int to=e[i].to;LL val=e[i].val;
for(LL w=0;w<=min(size[to],j);w++){
if(dp[now][j-w]!=-1){
LL val_=w*(k-w)*val+(size[to]-w)*(n-size[to]-(k-w))*val;
dp[now][j]=max(dp[now][j],dp[now][j-w]+dp[to][w]+val_);
}
}
}
}
int main()
{
//freopen("3177.txt","r",stdin);
LL x,y,z;
scanf("%lld%lld",&n,&k);
for(int i=1;i<n;i++){
scanf("%lld%lld%lld",&x,&y,&z);
add(x,y,z),add(y,x,z);
}
memset(dp,-1,sizeof(dp));
dfs(1,0);
printf("%lld",dp[1][k]);
return 0;
}