Purpose
Dynamic programming breaks a problem into overlapping sub-problems and builds up solutions to progressively larger subproblems until the original answer is obtained. Memoizing the answers to sub-problems is what makes it fast, often turning an exponential recursion into a polynomial time algorithm.
You can design dynamic programming algorithms by induction, with the added construct of caching previously solved subproblems. The work is in finding a valid recurrence relation that relates a given instance of the problem to its composite subproblems. This note collects worked problems, each with a recurrence, a correctness argument, and an implementation. Every code sample here has been checked against a brute force solution on small random inputs.
Two properties a problem needs before DP applies
Optimal substructure: an optimal solution is built from optimal solutions to subproblems, which is what lets each recurrence below take a max or min over smaller cases. Overlapping subproblems: the recursion revisits the same subproblems many times, which is what makes memoization pay off. Weighted interval scheduling has both once the jobs are sorted; the unsorted formulation has optimal substructure but distinct subproblems, so caching buys nothing.
Weighted Interval Scheduling
Problem: Given a set of jobs with start times , finish times , and weights , find the maximum weight subset of jobs that are compatible with each other.
A naive approach would be to use the following induction:
Given jobs , suppose we can compute the optimum job scheduling for jobs.
Then, for any jobs we can compute as:
- Case 1: :
- Then just return
- Case 2: :
- Delete jobs not compatible with and recurse on that new subset of jobs
However, this approach is unfortunately still exponential, since there are potentially possible subsets of jobs to consider. Note that this problem is equivalent to maximum independent set, which is NP-complete. To differentiate our solution from the general (supposed) unsolvability of this problem, we can instead rely on an extra property of our inputs, namely that they are partially order-able.
So we sort by finishing time of each job, which reduces the number of subproblems from , i.e. the possible prefix subsets of our sorted jobs.
Algorithm: Given weighted jobs sorted by finish time, suppose we can compute the for jobs.
- Case 1: :
- So all jobs that are not compatible with are not in
- We can find this efficiently, i.e. largest index such that is compatible with (binary search based on )
- Then, we just need to find
- Case 2: :
- Then we can return
- Our actual is then just the maximum of these two cases
#J[i] = (s_i, f_i, c_i)
def max_weighted_interval_subset(J: tuple[int, int, int]) -> int:
J = sorted(J, key=lambda x: x[1])
n = len(J)
memo = [0] * n
def p(n: int) -> int:
for i in range(n - 1, -1, -1):
if J[i][1] <= J[n][0]:
return i
return -1
def dp(n: int) -> int:
if n < 0:
return 0
if memo[n] != 0:
return memo[n]
memo[n] = max(J[n][2] + dp(p(n)), dp(n - 1))
return memo[n]
return dp(n - 1)Each subproblem dp(n) depends on only two smaller subproblems, dp(n - 1) and dp(p(n)). Uncached, the recursion tree has exponentially many nodes because both branches keep re-deriving the same prefixes. Memoization collapses that tree into a DAG with one node per prefix. For a 5-job instance with , , , :
flowchart TD d5["dp(5)"] --> d4["dp(4)"] d5 -->|"p(5) = 3"| d3["dp(3)"] d4 --> d3 d4 -->|"p(4) = 2"| d2["dp(2)"] d3 --> d2 d3 -->|"p(3) = 1"| d1["dp(1)"] d2 --> d1 d2 -->|"p(2) = 0"| d0["dp(0) = 0"] d1 --> d0
dp(3) is reachable along two paths, and deeper nodes along many more, yet each is computed once. The tree becomes table entries.
Knapsack Problem
Problem: Given items with weights and values , and a knapsack of capacity , find the maximum value subset of items that fit in the knapsack.
In other words, let be our set of items, and be the items we select. We want to find the maximum value of such that
What are we inducting on?
Assume holds , and then show holds . This is a bottom-up approach, where we start from the base case and build up to the final solution.
Algorithm: Define to be the solution for items with a knapsack of capacity . Then, we have the following induction:
#recursive
def knapsack_rec(W: int, w: list[int], v: list[int]) -> int:
n = len(w)
M = [[-1] * (W + 1) for _ in range(n + 1)]
def dp(i: int, cap: int) -> int:
if i == 0 or cap == 0:
return 0
if M[i][cap] != -1:
return M[i][cap]
if w[i - 1] > cap:
M[i][cap] = dp(i - 1, cap)
else:
M[i][cap] = max(v[i - 1] + dp(i - 1, cap - w[i - 1]), dp(i - 1, cap))
return M[i][cap]
return dp(n, W)
#iterative
def knapsack_it(W: int, w: list[int], v: list[int]) -> int:
n = len(w)
M = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, W + 1):
if w[i - 1] > j:
M[i][j] = M[i - 1][j]
else:
M[i][j] = max(v[i - 1] + M[i - 1][j - w[i - 1]], M[i - 1][j])
return M[n][W]The running time is pseudo-polynomial
The table has entries, and is a numeric value, not an input length. Encoding takes bits, so is exponential in the input size, and knapsack remains NP-complete despite this algorithm. P1 below asks for time polynomial in and , which is why it settles for a 2-approximation instead of the exact optimum.
String Building
Given 3 integers and , design an algorithm that runs in time polynomial in and outputs the number of length strings composed of copies of such that no more than copies of are placed consecutively and no more than copies of are placed consecutively.
Algorithm:
A, B = 0, 1
def num_strings(n, ka, kb):
dp = [[None, None] for _ in range(n + 1)]
def f(i, c):
if i == 1:
return 1
if dp[i][c] is not None:
return dp[i][c]
kc = ka if c == A else kb
notc = A if c == B else B
if i <= kc:
dp[i][c] = f(i - 1, notc) + f(i - 1, c)
else:
dp[i][c] = sum(f(i - j, notc) for j in range(1, kc + 1))
return dp[i][c]
return f(n, A) + f(n, B)Correctness: A string is valid if it has consecutive and consecutive .
Define for to be the number of valid strings ending in character . Our base case is , since there is only one length-1 string ending in .
Assuming we can compute for , we can compute as follows:
where is the other character, and is the maximum number of consecutive allowed.
For the case where , we can sum over all possible lengths of the last run of , and then recurse on the remaining string. For a run of of length in a string of length , we must have the first characters be some other valid string ending in , since otherwise our string would end in a run of of length . Therefore, we only need to count the valid strings of length for ending in , since this is exactly the number of valid strings ending in a run of between and copies of .
In the case where , we know that there is no way we’d have a run of of more than , so we can simply count the number of valid strings of size that either end in a or a . Then, we can add a to the end of all those strings to get our valid strings of length .
Therefore, we cover all cases and computes the correct value .
To compute the total number of valid strings of length , we simply return , since all valid strings either end in or .
Running Time
We compute total values of , corresponding to strings ending in , and ending in . When , we can compute our answer in constant time using the values previously computed. When , we need to sum over previously computed values, which is . Therefore, our total running time is at most .
Post Office
Problem: Interstate highway 5 is a straight highway from Washington all the way to California. There are villages alongside this highway. Think about the highway as an integer axis, and the position of village is an integer along this axis. Assume that there are no two villages in the same position, i.e., for . The distance between two villages is simply .
USPS is interested in building k post offices in some, but not necessarily all of the villages along highway 5, for some . A village and the post office in it have the same position. We want to choose the positions of these post offices so that the sum of the distances from each village to its nearest post office is minimized. Design an algorithm that runs in time polynomial in and outputs the minimum possible sum of distances to the optimal location for post offices.
Algorithm:
def min_dist_td(X, K):
X = sorted(X)
N = len(X)
if K >= N:
return 0
memo = {}
def f(n, k):
# min total distance for villages 1..n with k offices,
# given the k-th office sits at village n
if n <= k:
return 0
if (n, k) in memo:
return memo[(n, k)]
if k == 1:
memo[(n, k)] = sum(abs(X[i - 1] - X[n - 1]) for i in range(1, n))
else:
memo[(n, k)] = min(
f(i, k - 1) + sum(
min(
abs(X[j - 1] - X[n - 1]),
abs(X[j - 1] - X[i - 1])
) for j in range(i + 1, n))
for i in range(k - 1, n)
)
return memo[(n, k)]
def c(i):
return f(i, K) + sum(abs(X[i - 1] - X[j - 1]) for j in range(i + 1, N + 1))
return min(c(i) for i in range(K, N + 1))Correctness
Define to be the minimum sum of distances from the nearest post office of the first villages to post offices given the post office is placed in village .
When , we have a sum of , since every village has a post office. Then, assuming is correct , we can calculate by considering every possible placement of the post office, and choose the one that minimizes the sum of distances for villages between the and post office. However, if , then the only post office we can place is the one in , so we only need to find the distances of towns from .
In the case where , we must consider placing a post office in towns , since it doesn’t make sense to place the post office before we’ve seen villages, and we can only place up to the village before , since by construction it is the last of our post offices places in the towns. Therefore, we have the following recurrence:
Since holds for , and we always choose the placement of the post office that minimizes the total sum of distances, is correctly computed.
Now define for to be the total sum of distances if we place the post office in village (i.e. the last post office), and all other post offices optimally. Since correctly computes the sum of distances in this case up to the post office, and we are guaranteed that no other post office comes after the one placed in , all villages for will be closest to the post office in .
Therefore, we can compute as follows:
Finally, in order to compute our final answer, we just need to check every placement of the last post office and return the minimum sum of distances returned. Thus, we have a minimal sum of distances of…
And so our overall optimum is as follows:
Running Time: Computing runs in time, and since we need to run this calculation over at most values, to fully memoize all inputs it takes .
Then, each takes constant time to retrieve each , and an additional to compute the sum of the remaining distances, for a total of .
Finally, we call times, for a total of .
Thus, our running time is
RNA Secondary Structure
Problem: Given an RNA molecule , find a secondary structure that maximizes the number of base pairs.
Note that maximizing the number of base pairs is a practical problem, since RNA molecules fold into a secondary structure that minimizes free energy, and the number of base pairs is a good proxy for free energy.
Rules:
- Watson-Crick base pairs:
- No sharp turns: the end pair are separated by at least 4 intervening base pairs, i.e.
- No crossing: If
Algorithm:
#Bottom up over increasing substring length
WC = { 'A': 'U', 'U': 'A', 'C': 'G', 'G': 'C' }
def ssi(B):
N = len(B)
dp = [[0] * N for _ in range(N)]
for l in range(6, N + 1):
for i in range(N - l + 1):
j = i + l - 1
dp[i][j] = dp[i][j - 1]
for t in range(i, j - 4):
if WC[B[t]] == B[j]:
left = dp[i][t - 1] if t > i else 0
dp[i][j] = max(dp[i][j], 1 + left + dp[t + 1][j - 1])
return dp[0][N - 1] if N else 0Correctness:
Let be the maximum number of base pairs in a secondary structure of the substring .
Base Case: , we have
Suppose that for some , we have computed all . Then to compute for , we do the following:
- Case 1: Base is not involved in a pair in our solution
- , since we have solved for
- Case 2: Base pairs with for some
Sequence Alignment
Problem: Given two strings and , find an alignment with the minimum number of mismatches and gaps.
An alignment is a set of ordered pairs such that and .
- Correctness: Let be the min cost of aligning and .
Our base case is when either or are , we just need to do or deletes respectively.
- Case 1: matches
- Then, if , add one, and otherwise add zero to .
- Case 2: leaves unmatched
- Then, pay the gap cost for
- Case 3: leaves unmatched
- Then, pay gap cost for
#Bottom up, non memory optimized - T = O(mn), S = O(mn)
def seq_alignment(x, y):
m, n = len(x), len(y)
dp = [[None] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = min(
(0 if x[i - 1] == y[j - 1] else 1) + dp[i - 1][j - 1],
1 + dp[i - 1][j],
1 + dp[i][j - 1]
)
return dp[m][n]Note that in computational biology, you’ll be running this on strings with thousands or even millions of characters. Therefore, the space starts to become a problem. For instance, if , we do operations (which isn’t terrible), but we end up with a GB dp matrix.
You can optimize the space by only tracking the previous row of dp.
#Bottom up DP, optimized for space
def seq_alignment_linear_space(x, y):
m, n = len(x), len(y)
# base cases covered
dp_prev = list(range(n + 1))
dp_curr = [None] * (n + 1)
for i in range(1, m + 1):
dp_curr[0] = i
for j in range(1, n + 1):
dp_curr[j] = min(
(0 if x[i - 1] == y[j - 1] else 1) + dp_prev[j - 1],
1 + dp_prev[j],
1 + dp_curr[j - 1]
)
dp_prev, dp_curr = dp_curr, dp_prev
return dp_prev[n]Longest Path in a DAG
Problem Given a DAG , find the longest path.
Note: This problem is NP-hard for general directed graphs , as it has the Hamiltonian Path as a special case. However, with a DAG you can solve this in polynomial time.
Approach: Since is a DAG, it has a topological sort. Start by sorting vertices in their topological order.
Let be the length of the longest path that ends at vertex in the topological sort. We just need to guess the last edge of the longest path that ends in .
Then, to get our answer we can output…
Longest Increasing Subsequence
Given a sequence of numbers , find the longest increasing (not necessarily contiguous) subsequence.
Define as LIS that ends at . Our approach is to guess the previous element . Our base case is , and for any such that , , .
def LIS(X):
n = len(X)
dp = [None] * (n + 1)
dp[0] = 0
def f(i):
if dp[i] is None:
dp[i] = 1 + max([0] + [f(k) for k in range(1, i) if X[k - 1] < X[i - 1]])
return dp[i]
return max(f(i) for i in range(1, n + 1)) if n else 0Shortest Paths with Negative Edge Weights (Bellman-Ford)
Given a weighted directed graph and a source vertex , where the weight of edge is , find the shortest path from to all other vertices.
Note: if has a negative cycle, there is no solution, so suppose has no negative cycles.
Approach: Define as the length of the shortest path from among all paths that use at most edges. We induct on .
- Case 1: Shortest path from has edges:
- Case 2: Shortest path from has exactly edges
So we have…
Since any path in any graph has at most edges, the shortest path to has edges. Thus, is the length of the shortest path from .
Running Time: We solve subproblems, each taking time to solve, for a total running time of .
def bellman_ford(G, C, s):
# C[u][v] = edge cost, float('inf') where no edge exists
n = len(G)
dp = [[float('inf')] * n for _ in range(n)] # dp[i][v] = OPT(v, i)
dp[0][s] = 0
for i in range(1, n):
for v in range(n):
dp[i][v] = dp[i - 1][v]
for u in range(n):
dp[i][v] = min(dp[i][v], dp[i - 1][u] + C[u][v])
return dp[n - 1]P1 - Knapsack Approximation
Given items with integer weights and values and knapsack of weight such that for all . Design an algorithm that runs in time polynomial in and and outputs a 2-approximation for the knapsack problem.
def A(weights, values, W):
n = len(weights)
greed = [(values[i] / weights[i], i) for i in range(n)]
greed.sort(reverse=True)
w, v = 0, 0
for i in range(n):
if w + weights[greed[i][1]] <= W:
w += weights[greed[i][1]]
v += values[greed[i][1]]
else:
break
return vCorrectness: Let be my algorithm above, be an algorithm that makes the same greedy choice of selecting items in increasing order of , but with fractional selection allowed, and be the optimal algorithm for selecting items of only whole quantities. Let be the set of items chosen by , be the set of items chosen by , and be the set of items chosen by . Define and as the sum of weights and values respectively of items in the set of items .
Lemma 1: With fractional choices allowed, greedy (choosing highest ratio) upper bounds the optimal for non-fractional choices, i.e. . This holds because every non-fractional solution is also a feasible fractional solution, and greedy by value density is optimal for the fractional problem.
Lemma 2: chooses items with a total sum of weights
Proof: We know that each , so is guaranteed to choose at least two items before running out of room in the knapsack. Suppose for the sake of contradiction that the set of items chosen by had a total weight . Let be the weight of our knapsack right before terminates. Since we are guaranteed to be able to choose at least items, and each item has an integer weight , we have that . In order for our premise to be true, we need to have , but since each , we must have then been able to select the next item, so our algorithm wouldn’t have terminated at this point, which is a contradiction.
Lemma 3: will choose the whole item up until the last item it adds to its knapsack
Proof: By the design of the algorithm this must be true. Consider an arbitrary round of in which it considers item and has a current remaining weight of . We have two cases:
- Case 1:
- Then we take the entire item since this is the best value/weight item that we haven’t already visited
- Case 2:
- Then we take as much of the item as possible (weight of it), after which our knapsack is full and we terminate
No matter what, if we select a fractional amount of an item then it means we’ve filled the remaining weight of our knapsack and so the algorithm terminates immediately after.
Lemma 4: For any integers , if , then
Proof:
From (1), we have that
From (2) we have that , and by definition , so we have that . Both and choose values in the same order, and by (3) we have that picks only full items until the very last item added. Therefore, the items chosen by are exactly the same as the items chosen by up until the very last item. Let be the remaining weight in both and ‘s knapsack at this point, and be the last item added by . We have the following cases:
- Case 1:
- Then chooses all of , and will also choose , so and pick the exact same set and amounts of items, and we have , and so
- Case 2:
- Then chooses of , and will see the item that it can’t fit and terminate, in which case we have
In case 1, we clearly have a 2-approximation of , since is optimal.
In case 2 however, the key insight is that the value that the last item contributes to is no more than half of the total value, which I will show below.
We have that , since otherwise we would be able to choose a non-fractional amount of (since ). Considering the sum of values of the first items chosen by (as well as ), we have…
Since chooses items in increasing order of , we also have that for all , so we can put a lower bound on the sum of the first values chosen by
And since , we have…
Similarly, we can lower bound , since
So we have…
And since , by (4) we have that .
Therefore, knowing that is optimal with fractional weights, and that the only way and differ is in the last item added in case 2, we have that…
And so . Additionally, since is optimal for non-fractional weights, by definition we have , and is therefore a 2-approximation of .
Running Time:
Since , and division by is , calculating greed, which has elements, takes , and sorting it takes .
Then, I iterate through items in the loop, and once again since , and each iteration is only performing addition, we have work in each iteration, for a total of
Therefore, the overall running time of the algorithm is , which is polynomial in and
P2 - Maximum Sub-Rectangle
Problem: You are given an array where for all , is an integer that may be negative. For a rectangle where and , the value is the sum of all numbers in this rectangle, i.e.,
Design an algorithm that runs in time and outputs the value of the rectangle of largest value. Note that the value of the empty rectangle is zero.
def max_rectangle(A):
n = len(A)
memo = {}
pf_row = [[0] * (n + 1) for _ in range(n + 1)]
for x in range(1, n + 1):
for y in range(1, n + 1):
pf_row[x][y] = pf_row[x][y - 1] + A[x - 1][y - 1]
def row_sum(x, y1, y2):
return pf_row[x][y2] - pf_row[x][y1 - 1]
def g(y1, y2):
if y1 > y2:
return 0
if (y1, y2) in memo:
return memo[(y1, y2)]
best = max(g(y1 + 1, y2), g(y1, y2 - 1))
curr = 0
for x in range(1, n + 1):
curr = max(curr + row_sum(x, y1, y2), 0)
best = max(best, curr)
memo[(y1, y2)] = best
return best
return max(0, g(1, n))Correctness: Define as the maximum sum rectangle with the upper left corner being , and lower right corner being for all and . Additionally, define as the sum of the rectangle with the upper left corner being and lower right corner being , i.e.
Our base case is when , in which case we return because this is an empty/negative size rectangle.
Assuming we’ve calculated for all , we can calculate by considering all rectangles that either (1) don’t contain any of the row , (2) don’t contain any of the row , or (3) contain both rows and .
Since and , we can find (1) and (2) with and respectively. Then, to find (3) we need to consider each possible , choosing the values that maximum our sum for being fixed. This can be calculated as the largest sum of contiguous subsequences of rows between and . To do this, I reduce this problem to the largest contiguous subsequence of a list of numbers ,
Defining to be the LCS that can be made with which either includes or is empty, we induct over . My base case is when , in which case . Assuming is correct, we can calculate by taking maximum of either choosing to include in our subsequence, or to reset our subsequence to zero. Therefore, we have…
Then, to find the maximum sum of a contiguous subsequence of , we just need to take the maximum of for all . Let . My code calculates over the sum of rows between and in a bottom up fashion, but instead of storing all previous for , I only store the previous in a variable curr. Additionally, I keep track of the maximum I’ve seen thus far in another variable dp[y1][y2], but this is equivalent. Let denote the same problem, but ranging over values of the form , with the induction being over (the number of elements) still. Note that this is an equivalently solved problem, and the extra parameters are just to define bounds on the rows in which the instance of the problem exists.
Then we have the following recurrence for :
Since checks all possible and finds the best for each of them, we must find the largest sum rectangle, and can thus return as the maximum sub-rectangle in .
Running Time:
I start by finding the prefix sum of all rows in .
Then, I solve many subproblems, one for each . Each subproblem takes time to solve, since I need to loop through many sub-sub problems to find the maximum sum contiguous subsequence of row sums. Note that to calculate row sums, I use my precomputed prefix sum, which allows me to calculate the sum of a row in .
Therefore, the overall runtime is .
P3 - Count connected subsets of size k
Given a tree with vertices and an integer such that every vertex of has degree , we want to choose a set of vertices of the tree which are connected, i.e., for any pair of vertices the unique path between in is also in . Design a polynomial time algorithm that outputs the number of such sets .
from collections import deque
def num_sets_deg_3(T, k):
root = next(iter(T))
L = {root: 0}
q = deque([root])
while q:
curr = q.popleft()
for nxt in T[curr]:
if nxt not in L:
L[nxt] = L[curr] + 1
q.append(nxt)
def children(v):
return [u for u in T[v] if L[u] == L[v] + 1]
dp = { v: { 1: 1, 0: 1 } for v in T }
def f(v, k):
if k in dp[v]:
return dp[v][k]
c = children(v)
ans = 0
if len(c) == 1:
ans = f(c[0], k - 1)
elif len(c) == 2:
for c1 in range(k):
c2 = k - c1 - 1
ans += f(c[0], c1) * f(c[1], c2)
elif len(c) == 3:
for c1 in range(k):
for c2 in range(k - c1):
c3 = k - c1 - c2 - 1
ans += f(c[0], c1) * f(c[1], c2) * f(c[2], c3)
dp[v][k] = ans
return ans
return sum(f(u, k) for u in T)Correctness:
Start by choosing an arbitrary vertex as root, and run , returning the level of each vertex in the BFS-tree (with L[r] = 0).
Define as the number of connected subsets of size that must contain , and are otherwise composed of vertices with , i.e. only containing descendants of in the BFS tree. We prove correctness of by inducting on .
Our base cases are as follows:
- if , since there is only one empty set, and there can only be one set of size one containing , namely
- if and , since there is no way to make a set of size with less than vertices, and in this case, is our only potential element.
Note that is defined at
And so to calculate , we just need to sum over all possibilities that involve in terms of children of . Let (since ), and be ‘s children. Any given subproblem contains vertices in any subset of of size , so we need to iterate through all possible ways to choose such a subset of children for a given subproblem of size , accounting for the fact that any subset counted must contain .
- Case 0:
- This is a base case, so if we return , and otherwise return
- Case 1:
- We have only one child, so we just need to calculate , since we must choose as an element of any subset counted, and so we have remaining vertices to choose
- Case 2:
- We have two children, so we need to iterate through all possible ways to choose vertices from the first child and vertices from the second child, such that , keeping a sum of all possibilities. Then, via the product rule for counting, we just multiply for the total number of possibilities for each value of .
- Case 3:
- We have three children, so we need to iterate through all possible ways to choose vertices from the children, such that , keeping a sum of all possibilities. Then, via the product rule for counting, we just multiply for the total number of possibilities for each value of .
Note that these are disjoint cases, but the case where we choose only vertices from a subset of subtrees is handled by letting for the subtrees not chosen.
This gives rise to the following recurrence:
And since each of these problems has at most size in the recursive case, our inductive hypothesis holds and we can calculate .
To arrive at our final answer, we partition our possible answer space by sets of subsets that contain a given vertex , none of ‘s ancestors, and some of ‘s descendants. Our final answer is then the sum of all for . Note that each of these cases is disjoint, since each set of subsets must contain , and no ancestors of , so any previously calculated for being an ancestor of will not be included in answer for .
Therefore, we have a final solution of
Running Time:
Let be the number of vertexes in . We start by calculating the levels of each node in a BFS tree rooted at an arbitrary , which takes (since is a tree and therefore has edges) time.
For each call to , we solve subproblems, each of which take a constant time to compute given that subproblems with a have already been solved. We only have this constant time computation since the number of children is at most , since otherwise we would need to solve a number of subproblems exponential in the to check every subset of children to include. Therefore, each call takes time.
Since we call on all vertices in the tree, and each call takes at most time (but often performs better due to memoized answers from calls on ancestors of ), we have an upper bound of on computing the overall answer, which is polynomial