Dynamic Programming/Dynamic Programming on Trees

Lesson 8.71,412 words

Dynamic Programming on Trees

When the subproblems of a dynamic program are rooted subtrees, a single post-order DFS solves the whole thing in O(n)O(n): each node combines the already-computed answers of its children. We meet the archetype — maximum-weight independent set on a tree — then the "path through a node" pattern behind tree diameter and maximum path sum, and finally rerooting, which computes a per-node answer for every node as root in O(n)O(n) with two passes.

╌╌╌╌

Dynamic programming works whenever a problem decomposes into overlapping subproblems ordered so that each can be solved from smaller ones already in hand. On a sequence the natural subproblems are prefixes; on an interval they are subintervals. On a tree the natural subproblems are rooted subtrees, and the ordering that makes them solvable is the post-order traversal, which visits every node only after all of its children. Root the tree anywhere, define an answer for the subtree hanging below each node , and a single depth-first sweep fills the entire table.

The defining feature of these problems is that the recurrence is local: the answer at depends only on the answers at 's children,

never on grandchildren directly and never on the rest of the tree. Because the DFS touches each node and each edge exactly once and does work per child, the whole computation runs in time for a tree on nodes.1 The work is in choosing the state: what must remember about the subtree so that a parent can combine children without re-descending into them. This is the usual optimal-substructure question, specialized to subtrees.

The archetype: maximum-weight independent set

Let each node of a tree carry a weight . An independent set is a set of nodes no two of which are adjacent; we want one of maximum total weight. On a general graph this is NP-hard, but on a tree dynamic programming solves it in linear time, the canonical illustration of the whole technique.2

The idea is to make the state record whether itself is used, because that is exactly the one fact a parent needs in order to decide about itself. Define, for the subtree rooted at , two values:

  • , the best independent set of 's subtree in which is not taken;
  • , the best one in which is taken.

If is not taken, each child is free to be taken or not, so we keep the better of its two options. If is taken, no child may be taken, so each child must contribute its :

The base case falls out for free: a leaf has no children, so and . The answer for the whole tree is . This is House Robber III, where weights are the money in each house and the adjacency constraint forbids robbing a parent and its child on the same night.

Post-order combine for max-weight independent set: each node caches (skip / take ); the chosen set (root + its grandchildren) is shaded
Algorithm:MaxIndepSet(v)\textsc{MaxIndepSet}(v) — returns the pair (dp[v][0],dp[v][1])(dp[v][0],\, dp[v][1])
  1. 1
    if v=nilv = \text{nil} then
  2. 2
    return (0,0)(0, 0)
  3. 3
    takewvtake \gets w_v
    vv taken
  4. 4
    skip0skip \gets 0
    vv excluded
  5. 5
    for each child cc of vv do
  6. 6
    (c0,c1)MaxIndepSet(c)(c_0, c_1) \gets \textsc{MaxIndepSet}(c)
  7. 7
    skipskip+max(c0,c1)skip \gets skip + \max(c_0, c_1)
    child free
  8. 8
    taketake+c0take \gets take + c_0
    child forbidden
  9. 9
    return (skip,take)(skip, take)

Correctness is an induction on subtree size: assuming the recursive calls return correct pairs for every child, the two formulas above exhaust the only two choices for and combine each child's correct sub-answer optimally, so the returned pair is correct for . The procedure visits each node once and spends per child, hence total. The state, taken vs. not taken, is the part worth remembering: a single extra bit per node turns an intractable graph problem into a linear-time tree sweep.3

Paths through a node: diameter and maximum path sum

A second pattern arises when the quantity we care about is a path, not a set. The diameter of a tree is the number of edges on its longest path; the maximum path sum (where nodes carry values, possibly negative) is the largest total along any path. Neither is a clean subtree quantity, because the optimal path may bend at some node, descending into two different children.

The resolution is the signature move of tree DP on paths. At each node , let be the best downward path that starts at and goes into a single subtree. A child extends to . The best path that bends at combines its two best children:

for the two children with the largest downward values. Here is the trap that catches everyone the first time:

Algorithm:MaxPathSum(v)\textsc{MaxPathSum}(v) — returns best downward path; updates global ansans
  1. 1
    if v=nilv = \text{nil} then
  2. 2
    return 00
  3. 3
    Lmax(0,MaxPathSum(left(v)))L \gets \max(0, \textsc{MaxPathSum}(left(v)))
    drop negative branches
  4. 4
    Rmax(0,MaxPathSum(right(v)))R \gets \max(0, \textsc{MaxPathSum}(right(v)))
  5. 5
    ansmax(ans,  wv+L+R)ans \gets \max(ans,\; w_v + L + R)
    bend here: both sides
  6. 6
    return wv+max(L,R)w_v + \max(L, R)
    extendable: one side
Max path sum: node bends, combining both children for (global max), but returns only upward

For Binary Tree Maximum Path Sum the prunes branches that would only hurt the total; the global records the best bend seen anywhere. For Diameter of Binary Tree the same skeleton applies with edge counts in place of values: and the diameter is the largest over all nodes . Both run in : one post-order pass, per node.

Rerooting: an answer for every root in

The hardest variant asks for a quantity computed with each node in turn as the root: for every node , say, the sum of distances from to all other nodes. Re-running an DFS from each of the roots costs . Rerooting (also called the all-roots or re-root technique) computes all answers in total, with two DFS passes: one down, one up.4

Take Sum of Distances in Tree. Fix an arbitrary root and let be the number of nodes in 's subtree and the sum of distances from to every node inside its own subtree. A post-order pass computes both, since a child at distance contributes (every node under is one edge farther from than from ):

Down-pass at root : each node caches (subtree size) and (in-subtree distance sum); is the true answer only at the root

That gives the true global answer only at the root, where the subtree is the whole tree: . The second pass pushes the answer from a parent to each child in . Moving the root from to an adjacent child , the nodes on 's side each get one closer (distance drops by ) and the remaining nodes each get one farther:

Subtract the subtree's contribution, add the rest: the entire adjustment is a single formula, so the down-pass plus the up-pass together are .

Rerooting from parent to child : subtract 's subtree, add the other nodes

A related linear-time tree DP is Distribute Coins in Binary Tree: each node returns to its parent the net coins it must send up or pull down, the signed excess summed over its subtree, and the total number of moves is the sum of absolute flows along every edge, accumulated in one post-order pass. Same shape: a local return value, a global accumulator, time.

Takeaways

  • On a tree, the natural DP subproblems are rooted subtrees, solved by a single post-order DFS that combines each node's children in , hence overall, since every node and edge is processed once.
  • The state must capture exactly what a parent needs. For maximum-weight independent set (House Robber III) that is one bit, taken vs. not taken: and .
  • The path-through-a-node pattern (diameter, max path sum) returns one thing and updates another: return the single best downward extension to the parent, but update a global max with the two-child bent path, never returning the bent path.
  • Rerooting computes a per-root answer for all nodes in via two passes: a down-pass fixes the root's answer from subtree aggregates, an up-pass transfers it edge-by-edge with a subtract my subtree, add the rest adjustment.
  • The recurring design questions are always the same: what does a node return to its parent, and what aggregate must the subtree cache so the combine stays .

Footnotes

  1. Erickson, Ch. — Dynamic Programming (trees): subtree subproblems solved bottom-up by post-order traversal in .
  2. Skiena, § — Dynamic Programming on Trees: maximum independent set on trees as the linear-time archetype of tree DP.
  3. CLRS, Ch. 15 — Dynamic Programming: optimal substructure and the combination of subproblem solutions, instantiated here on rooted subtrees.
  4. Skiena, § — Dynamic Programming on Trees: the all-roots / rerooting technique computing every node's answer in with two DFS passes.
Practice

╌╌ END ╌╌