#(树形动规)洛谷P3177 [HAOI2015]树上染色(省选/NOI-)

题目描述

有一棵点数为 N 的树,树边有边权。给你一个在 0~ N 之内的正整数 K ,你要在这棵树中选择 K个点,将其染成黑色,并将其他 的N-K个点染成白色 。 将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的受益。问受益最大值是多少。

输入格式

第一行包含两个整数 N, K 。接下来 N-1 行每行三个正整数 fr, to, dis , 表示该树中存在一条长度为 dis 的边 (fr, to) 。输入保证所有点之间是联通的。

输出格式

输出一个正整数,表示收益的最大值。

输入输出样例

输入 #1
3 1
1 2 1
1 3 2
输出 #1
3

说明/提示

对于 100% 的数据, 0<=K<=N <=2000

作者: __stdcall 更新时间: 2016-12-02 16:39  在Ta的博客查看  61 


HAOI的题,写的人就是少,不过这确实是道好题,来顶一发

应该很容易想到,dp是可做的

状态很容易想到,dp[u][i]表示以u为跟的子树中,选择i个黑节点,的最大值

然后我就不会做了, 去网上看了wmdcstdio神犇的题解

发现我这个状态定义是错误的,正确的状态应该是,dp[u][i]表示以u为跟的子树中,选择i个黑节点,对答案有多少贡献

为什么是说“对答案有多少贡献呢”?

主要是想到一点,即分别考虑每条边对答案的贡献

即,边一侧的黑节点数*另一侧的黑节点数*边权+一侧的白节点数*另一侧的白节点数*边权

这点很容易证明,但是不容易想到(原因是我太弱了)

然后情况就明了了,整个问题成了一个树形背包,考虑每个子节点分配多少个黑色节点(体积),然后算出这条边对答案的贡献(价值)

这里再一次强调“贡献”,是因为这个贡献不只是在当前子树内,而是对于整棵树来说的

转移方程为dp[u][i] = max( dp[u][i], dp[u][i-j] + dp[v][j] + val )

其中v为u的子节点,j为在这个子节点中选择的黑色点的个数,val为这条边的贡献

val = j*(k-j)*w + (sz[v]-j)*(n-k+j-sz[v])*w

其中w为这条边的边权,n为总的节点数,k为总的需要选择的黑色节点数,sz[v]为以v为根的子树的节点数量

//

这道题的题解已经相当多了,但是我认为都没有对本道题做一个足够清晰的剖析,尤其是对于DP的部分。所以我希望我的这篇题解能够在以前各位的题解的基础上更进一步,让大家有一个更清晰的理解。

先强调一下,这道题的细节非常重要,一定要注意!


题目要求将k个点染成黑色,求黑点两两距离及白点两两距离,使他们之和最大。

我们可以将距离转化为路径,然后再将路径路径拆分成边,就可以记录每条边被经过的次数,直接计算即可。

很简单对吧?那么问题来了,距离转化为路径好理解,路径拆为边也好说,可是每条边被经过的次数怎么计算呢?

我们可以这样想,我们任意取两个同色的点,对于每一条边,若不在这两个点的路径上,我们自然不考虑,若是在两个点的路径上,那么这条边的计数加一。我们可以转换一下,若是两个点在边的一侧,则不影响计数,若在边的两侧,则边的计数加一。那么我们推广一下,便可以得出,一条边的两侧每有一对同色点,这条边就要被经过一次。也就是说,一条边被经过的次数等于边的两侧同色点个数的乘积。那么我们便可以求出每条边被经过的次数

tot=k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k)tot=k(mk)+(sz[v]k)(nmsz[v]+k)

mm表示题目要求选的黑点数,sz[v]sz[v]表示当前子节点的子树大小,kk表示当前子节点的子树上已选择的黑点数

有了这个结论,我们就可以轻松地得出DP方程了。

f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]+tot*e[i].w)f[u][j]=max(f[u][j],f[u][jk]+f[v][k]+tote[i].w)

就是关于这个方程让我在做题的时候纠结了好久,为什么kk正序排列就是对的,倒序排列就是错的?已有的题解也没有做出很好的解释,我A了之后也没有继续研究。多亏了帮同学找树形DP入门题时我重新注意到了这道题,使我对这一奇怪的现象产生了疑惑。得到了DDOSvoid大佬的帮助并进行了多次试验后,我终于明白了其中的原因,也让我对这道题的理解加深了数层。

这道题kk前几篇题解必须正序枚举的原因并不是什么要用j-kjk更新答案,而是因为正序枚举kk是从00开始的,而这道题的状态转移必须要先将k=0k=0的状态转移过来才能成立。也就是说,这只是个巧合,jj的枚举要倒序没错,但kk的枚举必须正序简直就是无稽之谈。要想避免这一情况,只需提前转移一下k=0k=0的情况即可。

下面放代码(内有注解)

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
#define gc getchar
#define maxn 2005
using namespace std;

inline ll read(){//KD
ll a=0;int f=0;char p=gc();
while(!isdigit(p)){f|=p=='-';p=gc();}
while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
return f?-a:a;
}

struct ahaha{//定义邻接表
int w,to,next;
}e[maxn<<1];int tot,head[maxn];
inline void add(int u,int v,int w){
e[tot].w=w,e[tot].to=v,e[tot].next=head[u];head[u]=tot++;
}//加边操作

int n,m,sz[maxn];
ll f[maxn][maxn];//f[i][j]表示以i为根的子树内,涂抹j个黑点的贡献
void dfs(int u,int fa){//u节点及其父亲
sz[u]=1;f[u][0]=f[u][1]=0;//初始化u点节点数为1,以u节点为根,涂抹0个或者在u
//节点涂抹黑色,u-->v这条边一定不会有贡献!
for(int i=head[u];~i;i=e[i].next){//对于u的所有儿子连边
int v=e[i].to;if(v==fa)continue;//
dfs(v,u);sz[u]+=sz[v];//计算子节点节点数目
for(int j=min(m,sz[u]);j>=0;--j){ //等价于涂抹最多白点数目/
//在子树允许的最大范围内涂抹白点
if(f[u][j]!=-1) //如果u,j已经被定义了
f[u][j]+=f[v][0]+(ll)sz[v]*(n-m-sz[v])*e[i].w;//
//考虑子树全白的情况
for(int k=min(j,sz[v]);k;--k){//遍历所有的白点可能
if(f[u][j-k]==-1)continue;//u为根,j-k个涂黑是不可以的
//即没有办法从前面转移过来
ll val=(ll)(k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k))*e[i].w; //
//计算总的贡献值
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]+val);//更新
//u节点涂抹j个黑点的边对总价值的贡献
}//
}//
}
}

int main(){memset(head,-1,sizeof head);//初始化
n=read();m=read();//
if(n-m<m)m=n-m;//转化为白点
for(int i=1;i<n;++i){//
int u=read(),v=read(),w=read();//
add(u,v,w);add(v,u,w);//
}memset(f,-1,sizeof f);//
dfs(1,-1);//
printf("%lld",f[1][m]);//
return 0;
}

猜你喜欢

转载自www.cnblogs.com/little-cute-hjr/p/11441572.html