樹形背包學習筆記


樹形背包的一般形式

給定一棵有$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$和$j$是$O(m2)$的,所以整個程序的復雜度就是$O(nm2)$的。

例題

Luogu P2014 選課

這是一道樹形背包的模板題,可以將題目轉化為在$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$,但是,$nm^2log$的時間復雜度是不允許,考慮優化樹形背包的$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(nmlog)$。

例題

同樣是Luogu P2014 選課

#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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM