洛谷P3177 [HAOI2015] 树上染色 题解
题意:
有一棵点数为 n n n 的树,树边有边权。给你一个在 0 ∼ n 0 \sim n 0∼n 之内的正整数 m m m ,你要在这棵树中选择 m m m 个点,将其染成黑色,并将其他 的 n − m n-m n−m 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的受益。问受益最大值是多少。
upd.20220531 由于本人对树上背包掌握不熟,导致复杂度写假了
其实是题解区普遍写错罢了。
不过原文只是没用刷表法
于是在原文基础上做了修改,不影响观感,请放心食用(逃
显然树上背包
设 d p [ u ] [ j ] dp[u][j] dp[u][j] 表示 u u u 所在子树染了 j j j 个黑点的最大价值
容易推出一个大概的方程
d
p
[
u
]
[
j
]
=
max
(
d
p
[
u
]
[
j
]
,
d
p
[
u
]
[
j
−
k
]
+
d
p
[
v
]
[
k
]
+
val
)
dp[u][j]=\max(dp[u][j],dp[u][j-k]+dp[v][k]+\text{val})
dp[u][j]=max(dp[u][j],dp[u][j−k]+dp[v][k]+val)
注:下文会提到这个转移方程是有点问题的
黑点两两距离+白点两两距离
直接去算就是 O ( n 2 ) O(n^2) O(n2) 的了
考虑更好的计算方法
一条路径会包括若干条边
因为是树所以有很多的边会被重复走过
考虑将距离计算转化为边重复经过次数
根据乘法原理,可知边
(
u
,
v
)
(u,v)
(u,v) (
u
u
u 为父结点)的贡献为
val
=
w
(
u
,
v
)
×
(
k
(
m
−
k
)
+
(
sz
[
v
]
−
k
)
(
n
−
m
−
sz
[
v
]
+
k
)
)
\text{val}=w(u,v)\times(k(m-k)+(\text{sz}[v]-k)(n-m-\text{sz}[v]+k))
val=w(u,v)×(k(m−k)+(sz[v]−k)(n−m−sz[v]+k))
upd.20220531 然后填表法写出来就是这样的(原文代码)
for(int i=head[u]; i; i=e[i].next)
{
int v=e[i].v;
if(v==f)continue;
dfs(v,u);
sz[u]+=sz[v];
for(int j=min(sz[u],m); j>=0; j--)
{
if(dp[u][j]>=0) // k正着枚举的时候这个就不用了
dp[u][j]+=dp[v][0]+sz[v]*(n-m-sz[v])*e[i].w;
for(int k=min(sz[v],j); k>0; k--) // 这里正着枚举也可以
{
if(dp[u][j-k]<0)continue;
int val=(k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k))*e[i].w;
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]+val);
}
}
}
不难发现这个其实复杂度是可以被卡到 O ( n m 2 ) O(nm^2) O(nm2) 的
所以考虑刷表法,即
d
p
[
u
]
[
j
+
k
]
=
max
(
d
p
[
u
]
[
j
+
k
]
,
tmp
[
j
]
+
d
p
[
v
]
[
k
]
+
val
)
dp[u][j+k]=\max(dp[u][j+k],\text{tmp}[j]+dp[v][k]+\text{val})
dp[u][j+k]=max(dp[u][j+k],tmp[j]+dp[v][k]+val)
这里的
tmp
[
j
]
\text{tmp}[j]
tmp[j] 是上一轮的
d
p
[
u
]
[
j
]
dp[u][j]
dp[u][j] ,刷表的过程中会被刷坏,所以要先存一下
这样时间复杂度才是严格的 O ( n m ) O(nm) O(nm)
代码:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
#define N (int)(2e3+15)
int n,m;
struct Edge
{
int u,v,w,next;
}e[N<<1];
int pos=1,head[N],sz[N],tmp[N],dp[N][N];
void addEdge(int u,int v,int w)
{
e[++pos]={u,v,w,head[u]};
head[u]=pos;
}
void dfs(int u,int f)
{
sz[u]=1;
dp[u][0]=dp[u][1]=0;
for(int i=head[u]; i; i=e[i].next)
{
int v=e[i].v;
if(v==f)continue;
dfs(v,u);
for(int j=0; j<=min(sz[u],m); j++)
tmp[j]=dp[u][j];
for(int j=0; j<=min(sz[u],m); j++)
for(int k=0; k<=sz[v]&&j+k<=m; k++)
{
if(tmp[j]<0)continue;
int val=(k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k))*e[i].w;
dp[u][j+k]=max(dp[u][j+k],tmp[j]+dp[v][k]+val);
}
sz[u]+=sz[v];
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
// freopen("check.in","r",stdin);
// freopen("check.out","w",stdout);
cin >> n >> m;
for(int i=1,u,v,w; i<n; i++)
{
cin >> u >> v >> w;
addEdge(u,v,w);addEdge(v,u,w);
}
memset(dp,0xc0,sizeof(dp));
dfs(1,1);
cout << dp[1][m] << endl;
return 0;
}
这一段可以跳过,只是详细解释了link的东西,
而且ta的代码复杂度也是假的,因为都是填表法
注意到有些人 k k k 倒序枚举,要先转移 k = 0 k=0 k=0 的情况,这里解释一下
关于为什么要先将 k = 0 k=0 k=0 的转移
观察方程,因为如果直接倒序枚举,最后一次 k = 0 k=0 k=0 的枚举
会出现 d p [ u ] [ j ] = max ( d p [ u ] [ j ] , d p [ u ] [ j ] + val ) dp[u][j]=\max(dp[u][j],dp[u][j]+\text{val}) dp[u][j]=max(dp[u][j],dp[u][j]+val) 的情况
显然此时的 d p [ u ] [ j ] dp[u][j] dp[u][j] 已经被更新过了
因此会导致答案有误(偏大)
除了 k = 0 k=0 k=0 的特殊情况,其他时候 k k k 随便啥顺序枚举都是可以的
转载请说明出处