P3177 [HAOI2015]树上染色
题目描述
有一棵点数为(N)的树,树边有边权。给你一个在(0) ~ (N)之内的正整数(K),你要在这棵树中选择(K)个点,将其染成黑色,并将其他的(N-K)个点染成白色 。 将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的受益。问受益最大值是多少。
输入输出格式
输入格式:
第一行包含两个整数 (N, K) 。接下来 (N-1) 行每行三个正整数 (fr, to, dis) , 表示该树中存在一条长度为(dis)的边((fr,to)) 。输入保证所有点之间是联通的。
输出格式:
输出一个正整数,表示收益的最大值。
说明
对于(100%)的数据,(0<=K<=N <=2000)
这里阐述一下我的思考方式。
首先,真的一下子想到贪心去了。前几天做了消防【SDOI2011】,觉得在树的直径上会有一些奇妙优美的性质,遂开始想办法证明。
然后考试完了还在证贪心(emmmm)。
解法很容易懂,但是很难想(我太蒻了)
正解是树形(DP)
令(dp[i][j])为以(i)为根节点的子树中 在染了(j)个黑点时 所有的边再加上(i)头上的那个边 对整颗树 的答案贡献
有灵性就有灵性在这个整颗树上
在已经处理好子树(v)时,我们加入边(E_k(u,v))((u)为(v)的父亲)
则(E_k)的贡献即为边权(w*)子树(u)中白点个数(*)子树(u)外面的数白点个数(+w*)子树(u)中黑点个数(*)子树(u)外面的数黑点个数,这点不难想明白
当然,不做头顶上的边也是可以的,但很多树形(dp)管一下自己的脑门是比较方便的
转移时,我们对每棵子树做树上分组背包,最后在判断一下她自己头上的边
code
#include <cstdio>
#include <cstring>
#define ll long long
ll max(ll x,ll y) {return x>y?x:y;}
ll min(ll x,ll y) {return x<y?x:y;}
const int N=2001;
ll dp[N][N];
//根节点为i的子树j个点染黑所有边的最大答案
ll n,k;
struct Edge
{
ll to,next,w;
}edge[N*2];
ll head[N],cnt=0;
void add(ll u,ll v,ll w)
{
edge[++cnt].to=v;
edge[cnt].next=head[u];
edge[cnt].w=w;
head[u]=cnt;
}
int used[N];
ll dfs(ll u,ll c)
{
used[u]=1;
ll size=0;
memset(dp[u],-1,sizeof(dp[u]));
dp[u][0]=0;
for(ll i=head[u];i;i=edge[i].next)
{
ll v=edge[i].to,w=edge[i].w;
if(!used[v])
{
ll si=dfs(v,w);size+=si;
for(ll j=min(k,size);j>=0;j--)
{
ll r=min(j,min(si,k));
for(ll k0=0;k0<=r;k0++)
if(dp[u][j-k0]!=-1)
dp[u][j]=max(dp[u][j],dp[u][j-k0]+dp[v][k0]);
}
}
}
size++;
ll l=max(size+k-n,1);
for(ll i=min(k,size);i>=l;i--)
{
ll cc=c*(k-i)*i+c*(n-size-k+i)*(size-i);
dp[u][i]=max(dp[u][i],dp[u][i-1])+cc;
}
l=size+k-n;
dp[u][0]+=c*(n-size-k)*size;
return size;
}
int main()
{
scanf("%lld%lld",&n,&k);
ll u,v,w;
for(ll i=1;i<n;i++)
{
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
dfs(1,0);
printf("%lld
",dp[1][k]);
return 0;
}
细节问题:
1.分组背包转移方程为:(dp[u][j]=max(dp[u][j],dp[u][j-k0]+dp[v][k0]))
我们一定要判断(dp[u][j-k0])的合法性
2.开(long long)不要吝啬空间(除非卡的太死,反正也就两倍),能看尽量就开,只开一部分在计算时真的不注意就可能丢失了或者爆了
3.背包的上下界,最好加上,不然常数一大可能会(t)。有的不加甚至会出现一些问题。
2018.5.15