樹形揹包學習筆記
樹形揹包的一般形式
給定一棵有\(n\)個節點的點權樹,要求你從中選出\(m\)個節點,使得這些選出的節點的點權和最大,一個節點能被選當且僅當其父親節點被選中,根節點可以直接選。
\(n^3\)解法
原理
考慮設\(f[u][i]\)表示在\(u\)的子樹中選擇\(i\)個節點(包括它本身)的最大貢獻,則可列出以下轉移方程。
\[ f[u][i]=max(f[u][j]+f[v][i-j]+d[v])\ [j=1...i-1] \]
其中\(d[v]\)表示點\(v\)的點權,\(i-j\)表示在子樹\(v\)中選擇\(i-j\)個節點。
由於遍歷整棵樹是\(\Theta(n)\)的,而選取\(i\)
例題
這是一道樹形揹包的模板題,可以將題目轉化為在\(n+1\)個節點中選\(m+1\)個節點。於是最後的答案就是\(f[0][m+1]\)。
#include <cstdio> #include <algorithm> using std::max; const int N = 3e2 + 10, M = 3e2 + 10; int n, m, f[N][N], s[N], son[N][N]; void dfs (int u) { for (int i = 1; i <= son[u][0]; ++i) { int v = son[u][i]; dfs(v); for (int j = m + 1; j >= 1; --j) for (int k = 0; k < j; ++k) f[u][j] = max(f[u][j], f[u][j - k] + f[v][k]); } } int main () { scanf ("%d%d", &n, &m); for (int i = 1, fa; i <= n; ++i) { scanf ("%d%d", &fa, s + i); f[i][1] = s[i]; son[fa][++son[fa][0]] = i; } dfs(0); printf ("%d\n", f[0][m + 1]); return 0; }
\(n^2\)解法
警告:此演算法可能思維難度較大,而且一般聯賽不會考(但不排除作為壓軸題考出),視情況閱讀!
原理
顯然,\(n^3\)演算法的時間開銷是很\(Big\)的,比如這道題:洛谷 P4322 最佳團體。
此題在\(01\)分數規劃後採取樹形揹包\(check\),但是,\(n^3log\)的時間複雜度是不允許,考慮優化樹形揹包的\(check\)過程
首先,既然要優化,我們就得知道瓶頸在哪。瓶頸在於,我們是一邊\(dfs\)一邊更新的,由於要遍歷子樹,我們同時還要知道選擇多少個節點,那麼我們是否可以先跑一遍\(dfs\)處理出\(dfs\)序然後根據\(dfs\)
設\(f[i][j]\)為當前\(dp\)到\(dfs\)序為\(i\)的點,目前已經選了\(j\)個節點。則有轉移方程(\(d[i]\)表示點權):
1.選取當前節點:
\[ f[i+1][j+1]=f[i][j]+d[i] \]
如果選了這個點,則在\(dfs\)序後一個節點要麼是它的子節點,要麼下一棵子樹(則證明其沒有子節點)。
2.不選當前節點:
\[ f[nx[i]][j]=f[i][j] \]
其中\(nx[i]\)表示下一棵子樹,因為你沒選這個點,當然不能選擇其子節點。
由於\(dfs\)序為\(\Theta(n)\)的,然後列舉\(j\)為\(O(m)\)的,所以總複雜度為\(O(nm)\)。
例題
#include <cstdio>
#include <algorithm>
using std::min;
typedef long long ll;
const int N = 3e2 + 10, M = 3e2 + 10, Inf = 1e9 + 7;
int n, m, d[N], s[N], dfn[N], son[N][N], time, f[N][N], nx[N];
inline void upt (int &a, int b) { if(a < b) a = b; }
void Init_dfs(int u) {
dfn[u] = time++;
for (int i = 1; i <= son[u][0]; ++i)
Init_dfs(son[u][i]);
nx[dfn[u]] = time;
}
void Doit_dp() {
for (int i = 1; i <= n; ++i)
d[dfn[i]] = s[i];
for (int i = 1; i <= n + 1; ++i)
for (int j = 0; j <= m; ++j)
f[i][j] = -Inf;
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= min(i, m); ++j) {
upt(f[i + 1][j + 1], f[i][j] + d[i]);
upt(f[nx[i]][j], f[i][j]);
}
}
int main () {
scanf("%d%d", &n, &m); ++m;
for (int i = 1, fa; i <= n; ++i) {
scanf("%d%d", &fa, s + i);
son[fa][++son[fa][0]] = i;
}
Init_dfs(0);//預處理dfs
Doit_dp();//動態規劃
printf("%d\n", f[n + 1][m]);
return 0;
}
之前我們提到的洛谷 P4322 最佳團體,就是用\(01\)分數規劃&樹形揹包來解決的
// luogu-judger-enable-o2
#include <cstdio>
#include <algorithm>
using std::min;
using std::max;
const int N = 3e3 + 10, inf = 1e9 + 7;
const double eps = 1e-5;
int n, K, s[N], p[N], son[N][N], dfn[N], time, nx[N];
int from[N], to[N], nxt[N], cnt;//Edges
double f[N][N], d[N];
inline void addEdge (int u, int v) {
to[++cnt] = v, nxt[cnt] = from[u], from[u] = cnt;
}
inline void upt(double &a, double b) {
if (a < b) a = b;
}
void dfs (int u) {
dfn[u] = time++;
for (int i = from[u]; i; i = nxt[i]) dfs(to[i]);
nx[dfn[u]] = time;
}
inline bool check (double k) {
for (int i = 1; i <= n; ++i)
d[dfn[i]] = p[i] - k * s[i];
for (int i = 1; i <= n + 1; ++i)
for (int j = 0; j <= K; ++j)
f[i][j] = -inf;
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= min(i, K); ++j) {
upt(f[i + 1][j + 1], f[i][j] + d[i]);
upt(f[nx[i]][j], f[i][j]);
}
return f[n + 1][K] >= eps;
}
int main () {
scanf("%d%d", &K, &n); ++K;
for (int i = 1, fa; i <= n; ++i) {
scanf("%d%d%d", s + i, p + i, &fa);
addEdge(fa, i);
}
dfs(0);
double l = 0, r = 10000, ans;
while (r - l >= eps) {
double mid = (l + r) * 0.5;
if (check(mid)) ans = mid, l = mid + eps;
else r = mid - eps;
}
printf ("%.3lf\n", ans);
return 0;
}