比较容易想到的一种做法是:设dp[i][j] 表示 i 为根,选 j 个点的答案。在子树合并时,枚举子树选取的点的个数和当前选取的结点个数,加上统计当前这条边对答案的贡献更新。
转移式子为:dp[u][i+j] = min(dp[u][i+j],dp[u][i]+dp[v][j] + i * j * w)
一跑会发现样例都过不了,原因是这条边的贡献只在选的点超过一个时才会计算,答案必然会漏算某些贡献。
一个补救的方法是:另开一个数组tp[i][j] 记录dp[i][j] 取最小值时,选的点到当前i点的距离之和。
转移方程为:dp[u][i+j] =min(dp[u][i+j],dp[u][i] + dp[v][j] + tp[u][i] * j + tp[v][j] * i + i * j * w
要注意在 dp[u][i+j] 相同的情况下,tp[u][i+j] 要取最小值,因为dp[u][i+j]相同的情况下,显然tp[u][i]越小对后面的转移越有利。
一看样例过了,似乎是一种可行的方案。交上去发现只有30分(还不如暴力打满40)。
原因是dp转移式子中,转移值不只取决于 dp[i][j],还涉及到tp[i][j],而 tp[i][j] 只在 dp[i][j] 尽量小的前提下尽量小。这相当于是在忽略tp[i][j]在转移中带来的影响,数据足够强的话很容易卡掉。
正确的做法是:
还是从贡献的角度考虑,一个很明确的思路是,如果在子树中确定好选的点,产生的新贡献和当前这条边权有关。观察到题目一定有解,并且我们只关注最后的答案。考虑直接用树形dp维护对最终答案的贡献。
dp[i][j] 表示 i 为根选 j个结点 对最终答案的贡献。显然dp[1][k] 会是正确的答案。
转移方程为:dp[u][i+j] = min(dp[u][i+j],dp[u][i] + dp[v][j] + w * j * (k - j))
当决定在子树v中选 j 个点时,最终答案连向这颗子树的边权一定会经过 j * (k - j) 次
细算一下复杂度上界是5e8,需要在转移时加一些剪枝降低上界。
代码:
#include
using namespace std;
const int maxn = 1e5 + 10;
#define pii pair
#define fir first
#define sec second
typedef long long ll;
vector<pii> g[maxn];
int vis[maxn],sum[maxn],m,n,k;
ll dp[maxn][300],tmp[1000];
void dfs(int u,int fa) {
for(int i = 0; i <= k; i++) {
dp[u][i] = 1e15;
}
dp[u][0] = 0;
if(vis[u]) sum[u] = 1,dp[u][1] = 0;
for(auto it : g[u]) {
if(it.fir == fa) continue;
dfs(it.fir,u);
for(int i = 0; i <= k; i++)
tmp[i] = 1e15;
for(int i = 0; i <= min(sum[u],k); i++) {
for(int j = 0; j <= sum[it.fir] && i + j <= k; j++) {
ll w = dp[u][i] + dp[it.fir][j] + 1ll * (k - j) * j * it.sec;
tmp[i+j] = min(tmp[i+j],w);
}
}
sum[u] += sum[it.fir];
for(int i = 0; i <= min(k,sum[u]); i++) {
dp[u][i] = min(dp[u][i],tmp[i]);
}
}
}
int main() {
scanf("%d%d%d",&n,&m,&k);
for(int i = 1,x; i <= m; i++) {
scanf("%d",&x);
vis[x] = 1;
}
for(int i = 1,u,v,w; i < n; i++) {
scanf("%d%d%d",&u,&v,&w);
g[u].push_back(pii(v,w));
g[v].push_back(pii(u,w));
}
dfs(1,0);
printf("%lld\n",dp[1][k]);
return 0;
}
/*
5 3 2
1 3 5
1 2 4
1 3 5
1 4 3
4 5 1
11 6 2
2 5 7 8 9 10
1 2 2
1 3 4
2 4 3
2 5 6
3 6 8
3 7 11
4 8 9
6 9 2
6 10 1
7 11 4
*/