树上染色|P3177
https://www.luogu.com.cn/problem/P3177
首先:
%%%%%WVR CCCCCCCCCCCCCCCCCCCCCCCCCCCCCOrz
%%%%%WVR CCCCCCCCCCCCCCCCCCCCCCCCCCCOrz
%%%%%WVR CCCCCCCCCCCCCCCCCCCCCCOrz
%%%%%WVR CCCCCCCCCCCCCCCOrz
%%%%%WVR CCCCCCCCCCOrz
%%%%%WVR CCCCCOrz
快去膜他!!!
想了一天(一下午+一晚自习+一早上)不会
(准确来说先开始想了一堆玄学假做法)
WVR一眼秒掉后$15minAC$掉了
给我讲了后,代码思路清新简洁,通俗易懂,从里到外流露出一股清雅之气
与各路tj不同,这转移可谓一股清流
导致我想清楚后$50min$写完代码样例一边过,交上去一遍$AC$,完全没任何调试空间好不好
才怪你tmd明明炸了一次long long
(那确实怪我)
所以为啥我这么菜?
正题
简要题意:
给一棵有n个点的无根树,你可以将$k$个点染为黑色,$n-k$个点染成白色。
记总收益为 所有黑点两两之间距离和 加上 所有白点两两之间距离和
问最大收益
首先我陷入了树上背包
但发现不会做
不管怎么转移,都转移的有问题
(你怎么那么会说废话呢?)
快see see这个图!
我遇到了什么问题呢?
我当时想把4的dp值求出来
于是访问其每个儿子
我当时就想,这个儿子的子树内有$n$个黑点,我要是此节点选黑,那么这条边就被贡献了$w \times k$,若选白,就是$w \times (sz[son]-k)$
但手动模拟时就发现了问题
如我现在要在4节点访问5
当总黑点数为$3$时,我用原来子树的黑点为$2$的$dp$值加上5节点子树黑点为$1$的$dp$值,再加上4,5这条边做的贡献,因为有此边左边$2$个黑点,右边$1$个黑点,所以被贡献了$2 \times 1$次
so 我算出来的dp值为$2 + 2 \times 2 \times 1 = 4$
但这不对啊,应该是$6$啊!
显然我6,4的边少贡献了一次
而且以后再也贡献不了了
什么?你说你没听懂!?!
你听懂了才说明你和我一样不正常呢
好了不需要懂上面的咱看vr给的做法
直接一段代码就无需多言了
//大抵是这样的
//我们现在已经合并了一部分(可以没有)子树
//现在要合并这个子树(儿子nxt的子树)
//其中,在原树中选x个黑点,要合并的的子树选y个黑点
//然后他就把我的问题解决了
//啊我还是放一张图(要是vr手绘就好了)
//绿圈表示已经合并好的集合(对贡献没啥用),红圈表示要合并的子树集
//蓝圈表示红圈的补集
//那么
//我们一旦确定了红圈内黑点的个数,这条边对全局的贡献就确定了!
inb=y,outb=m-inb;//红圈里的黑点inb,剩下的黑点都在蓝圈里
inw=sz[nxt]-y,outw=(n-m)-inw;//白点反之亦然。
val=edge[i].w*(inb*outb+inw*outw);//算一下此情此景的贡献
dp[r][x+y]=max(dp[r][x+y],dp[r][x]+dp[nxt][y]+val);//这个顺推转移够清楚吧!!!
//当然,x就转移这块有点用
这几行核心代码让我幡然醒悟
尤其是
其根本没有调试空间!
去AC吧:
//vr讲了必须打出来
#include<iostream>
#include<cstring>
#define int long long
using namespace std;
int n,m;
const int MAXN=2e3+10;
struct tree{
int to;
int w;
int next;
}edge[MAXN*2];
int head[MAXN];
int _cnt=0;
void add(int u,int v,int w){
edge[++_cnt].to=v;
edge[_cnt].w=w;
edge[_cnt].next=head[u];
head[u]=_cnt;
}
int dp[MAXN][MAXN];
int sz[MAXN];
int tp[MAXN];
//vr讲的DP
void dfs(int r){
sz[r]=1;
dp[r][0]=0;//最少的价值是0罢了
dp[r][1]=0;//这是一个黑点的情况
int inb,outb,inw,outw;
for(int i=head[r];i!=0;i=edge[i].next){
int nxt=edge[i].to;
//一个一个子树合并
//与朴素的背包不同
//vr用更好理解的顺推写的
if(sz[nxt])continue;
dfs(nxt);
for(int x=0;x<=min(m,sz[r]);x++){//目前合并好的
for(int y=0;y<=min(m,sz[nxt]);y++){//要合并的
//对于这条边的贡献咋算捏
inb=y;
outb=m-inb;
inw=sz[nxt]-y;
outw=n-m-inw;
int val=edge[i].w*(inb*outb+inw*outw);
tp[x+y]=max(tp[x+y],dp[r][x]+dp[nxt][y]+val);
//vr教的小trick,如果不想仔细想dp转移滚动压维的细节,就开个临时数组防止覆盖
}
}
memcpy(dp[r],tp,sizeof(tp));//赋给它
memset(tp,-0x3f,sizeof(tp));//清空它
sz[r]+=sz[nxt];//这个更新sz罢了
}
return;//真就毫无细节
}
signed main(){
cin>>n>>m;
int u,v,w;
memset(dp,-0x3f,sizeof(dp));//这个细节vr是真滴严谨啊!题目没说w范围,他连负权边都考虑了 %%%wvr orz
for(int i=1;i<n;i++){
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
dfs(1);
cout<<dp[1][m];
return 0;
}