Tree DP is DP performed on a tree. Because of the inherent recursive nature of trees, tree DP is generally carried out recursively.
Basics
The following problem illustrates the general process of tree DP.
Example: Luogu P1352 Party without Bosses
A university has employees, numbered from . There are subordinate relationships among them, so their relationships form a tree rooted at the principal, with each parent node being the direct supervisor of its child nodes. At an anniversary party, inviting each employee increases the happiness index by , but if an employee's direct supervisor attends the party, that employee will refuse to attend no matter what. Write a program to calculate which employees should be invited to maximize the happiness index, and find the maximum happiness index.
Let denote the optimal solution for the subtree rooted at (the second dimension being 0 when does not attend the party and 1 when attends).
There are two decisions for each state (where each below is a child of ):
- When the supervisor does not attend the party, a subordinate may attend or not attend, so .
- When the supervisor attends the party, none of the subordinates attends, so .
We can update the optimal solution of the current node through DFS when returning to the previous level.
#include <algorithm>
#include <iostream>
using namespace std;
struct edge {
int v, next;
} e[6005];
int head[6005], n, cnt, f[6005][2], ans, is_h[6005], vis[6005];
void addedge(int u, int v) { // Build the graph
e[++cnt].v = v;
e[cnt].next = head[u];
head[u] = cnt;
}
void calc(int k) {
vis[k] = 1;
for (int i = head[k]; i; i = e[i].next) { // Enumerate every child of this node
if (vis[e[i].v]) continue;
calc(e[i].v);
f[k][1] += f[e[i].v][0];
f[k][0] += max(f[e[i].v][0], f[e[i].v][1]); // Transition equation
}
return;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; i++) cin >> f[i][1];
for (int i = 1; i < n; i++) {
int l, k;
cin >> l >> k;
is_h[l] = 1;
addedge(k, l);
}
for (int i = 1; i <= n; i++)
if (!is_h[i]) { // Start DFS from the root
calc(i);
cout << max(f[i][1], f[i][0]);
return 0;
}
}
In general, a tree DP state is usually the optimal solution for the current node. First use DFS to traverse all optimal solutions of the subtrees, then pass them upward to the parent node for transitions; the value at the root is ultimately the desired optimal solution.
Exercises
-
[POI2014]FAR-FarmCraft](https://www.luogu.com.cn/problem/P3574)
Tree knapsack
The knapsack problem on a tree is, simply put, a combination of the knapsack problem and tree DP.
Example: Luogu P2014 CTSC1997 Course Selection
There are courses. The -th course has credits, and each course has zero or one prerequisite. A course with a prerequisite can be studied only after its prerequisite has been completed.
A student wants to study courses. Find the maximum number of credits they can obtain.
The fact that each course has at most one prerequisite is similar to the fact that each node in a rooted tree has at most one parent.
Therefore, we can build a tree based on this property, so that all courses form a forest. For convenience, add a course worth credits (numbered ) as the prerequisite of every course with no prerequisite; this turns the forest into a tree rooted at course .
Let denote the maximum credits obtained in the subtree rooted at node , after the first subtrees of node have been traversed and courses have been selected.
The transition combines the characteristics of tree DP and knapsack DP: enumerate every child of node , while enumerating the number of courses selected in the subtree rooted at , and merge the subtree result into .
Let the number of children of node be and the size of the subtree rooted at be . The following transition can be written:
Note the several constraints in the transition above. These constraints ensure that meaningless states are not visited.
The second dimension of can be omitted easily with a rolling array; note that must then be enumerated in reverse order.
It can be proved that the time complexity is 1.
Reference code
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int f[305][305], s[305], n, m;
vector<int> e[305];
int dfs(int u) {
int p = 1;
f[u][1] = s[u];
for (auto v : e[u]) {
int siz = dfs(v);
// Note the upper and lower bounds of the following two loops
// Only consider already merged subtrees; states selecting more than m+1 courses are meaningless
for (int i = min(p, m + 1); i; i--)
for (int j = 1; j <= siz && i + j <= m + 1; j++)
f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]); // Transition equation
p += siz;
}
return p;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int k;
cin >> k >> s[i];
e[k].push_back(i);
}
dfs(0);
cout << f[0][m + 1];
return 0;
}
Exercises
Rerooting DP
The rerooting DP problem in tree DP is also called a two-pass scan. It usually does not specify a root, and changing the root affects values such as the sum of node depths and the sum of node weights.
It usually requires two DFS traversals. The first DFS precomputes information such as depths and sums of node weights, and the second DFS begins the rerooting dynamic programming.
The following examples familiarize you with this topic.
Example: [POI2008]STA-Station
Given a tree with nodes, find a node such that the sum of the depths of all nodes is maximized when this node is the root.
Let be the current node and be a child of the current node. First, use to denote the number of nodes in the subtree rooted at ; then . Clearly, one DFS is needed to calculate every . This DFS is the preprocessing step, which obtains the total number of nodes in the subtree when a particular node is used as the root.
Consider the transition; this is where rerooting appears. Let be the sum of the depths of all nodes when is the root.
The transition represents rerooting, from as the root to as the root. During this transition, using or as the root changes the depths of nodes in their subtrees. Specifically:
-
The depth of every node in the subtree of decreases by 1, so the total depth sum decreases by .
-
The depth of every node outside the subtree of increases by 1, so the total depth sum increases by .
These two conditions give the transition .
Thus, perform a second DFS over the entire tree and use the transition ; this obtains the depth sum for every possible root. Finally, scan all root depth sums once to obtain the answer.
Reference code
#include <iostream>
using namespace std;
int head[1000010 << 1], tot;
long long n, sz[1000010], dep[1000010];
long long f[1000010];
struct node {
int to, next;
} e[1000010 << 1];
void add(int u, int v) { // Build the graph
e[++tot] = {v, head[u]};
head[u] = tot;
}
void dfs(int u, int fa) { // Preprocess dfs
sz[u] = 1;
dep[u] = dep[fa] + 1;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (v != fa) {
dfs(v, u);
sz[u] += sz[v];
}
}
}
void get_ans(int u, int fa) { // Second dfs for rerooting dp
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (v != fa) {
f[v] = f[u] - sz[v] * 2 + n;
get_ans(v, u);
}
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
int u, v;
for (int i = 1; i <= n - 1; i++) {
cin >> u >> v;
add(u, v);
add(v, u);
}
dfs(1, 1);
for (int i = 1; i <= n; i++) f[1] += dep[i];
get_ans(1, 1);
long long int ans = -1;
int id;
for (int i = 1; i <= n; i++) { // Compute the answer
if (f[i] > ans) {
ans = f[i];
id = i;
}
}
cout << id << '\n';
return 0;
}