『树形背包』「BZOJ4987」tree

tech2024-06-26  65

P r o b l e m \mathrm{Problem} Problem

从前有棵树。

找出 K K K 个点 A 1 , A 2 , … , A k A_1,A_2,…,A_k A1A2Ak

使得 ∑ d i s ( A i , A i + 1 ) , ( 1 ≤ i < K ) ∑dis(Ai,Ai+1),(1\le i<K) dis(Ai,Ai+1),(1i<K)最小。


S o l u t i o n \mathrm{Solution} Solution

题目可以转化为,从 a 1 a_1 a1出发需要依次遍历这些点,求行走路径步数的最小值。

显然,这 k k k 个点一定是一个连通块,否则一定能找到一个比答案更优的方案。如果我们选定了这连通块,我们会发现最短路径长度一定是 2 k − m a x l e n 2k-\mathrm{maxlen} 2kmaxlen m a x l e n \mathrm{maxlen} maxlen表示树的直径的长度。那么直径上的点被计算了两遍,而其他点则被计算了一遍。

现在我们树形DP,设 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示以 i i i 为根的子树中,选择了 j j j 个点的方案,其中有 k k k 个直径端点在该连通块内。

当前子树 y y y 以及原来统计的子树都不存在直径端点时,这条边贡献为 2 v 2v 2v。当前子树 y y y 存在直径的一个端点,贡献为 v v v。当前子树 y y y 存在直径的两个端点,则这条边贡献为 2 v 2v 2v。当前子树 y y y 不存在直径端点时,贡献为 2 v 2v 2v

然后就暴力转移即可。


C o d e \mathrm{Code} Code

#include <bits/stdc++.h> using namespace std; const int N = 4000; int n, m, res(1e9); int f[N][N][4], size[N]; vector < pair<int,int> > a[N]; int read(void) { int s = 0, w = 0; char c = getchar(); while (c < '0' || c > '9') w |= c == '-', c = getchar(); while (c >= '0' && c <= '9') s = s*10+c-48, c = getchar(); return w ? -s : s; } #define f(a,b,c) f[a][b][c] #define upd(x,y) x = min(x,y); void Dfs(int x,int fa) { size[x] = 1; f[x][1][0] = f[x][1][1] = f[x][1][2] = 0; for (int i=0;i<a[x].size();++i) { int y = a[x][i].first; int v = a[x][i].second; if (y == fa) continue; Dfs(y,x); for (int j=min(size[x],m);j>=0;--j) { for (int k=min(size[y],m);k>=0;--k) { upd(f(x,j+k,0),f(x,j,0)+f(y,k,0)+2*v); upd(f(x,j+k,1),f(x,j,0)+f(y,k,1)+v); upd(f(x,j+k,1),f(x,j,1)+f(y,k,0)+2*v); upd(f(x,j+k,2),f(x,j,1)+f(y,k,1)+v); upd(f(x,j+k,2),f(x,j,2)+f(y,k,0)+2*v); upd(f(x,j+k,2),f(x,j,0)+f(y,k,2)+2*v); } } size[x] += size[y]; } res = min(res,f[x][m][2]); return; } signed main(void) { n = read(), m = read(); memset(f,30,sizeof f); for (int i=1;i<n;++i) { int x = read(), y = read(), v = read(); a[x].push_back({y,v}); a[y].push_back({x,v}); } Dfs(1,0); cout << res << endl; return 0; }
最新回复(0)