3562. Maximum Profit from Trading Stocks with Discounts
Problem Description
You are given an integer n, representing the number of employees in a company. Each employee has a unique ID from 1 to n, where employee 1 is the CEO and is the direct or indirect boss of every other employee.
You are given two 1-based integer arrays, present and future, each of length n, where:
present[i]is the current price at which theith employee can buy a stock today.future[i]is the expected price at which theith employee can sell the stock tomorrow.
The company's hierarchy is described by a 2D integer array hierarchy, where hierarchy[i] = [u_i, v_i] means employee u_i is the direct boss of employee v_i. This structure forms a tree rooted at employee 1.
You also have an integer budget, representing the total funds available for investment.
The company has a discount policy: if an employee's direct boss buys their own stock, then that employee can buy their stock at half the original price, i.e. floor(present[v] / 2).
Your task is to return the maximum profit that can be achieved without exceeding the given budget.
Notes:
- You may buy each stock at most once.
- You cannot use any profit earned from future stock prices to fund additional investments; all purchases must come only from
budget.
The profit gained from buying a stock at cost cost and selling it later is future[v] - cost, where cost is either present[v] (no discount) or floor(present[v] / 2) (if the direct boss bought their own stock). The goal is to choose which employees buy stocks so that the total spent stays within budget and the total profit is maximized.
How We Pick the Algorithm
Why DFS?
This problem maps to DFS through a short path in the full flowchart.
The problem is a tree DP with knapsack merging over children, where each node's buy/skip decision and discount state propagate through the tree.
Open in FlowchartShow step-by-step reasoning
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The employees and their boss-subordinate relationships form a graph. Each
hierarchy[i] = [u_i, v_i]defines an edge between a boss and a subordinate.
Is it a tree?
- Yes: Employee
1(the CEO) is the direct or indirect boss of everyone, and each employee has exactly one direct boss. This structure forms a tree rooted at employee1, with no cycles.
DFS
- Yes: Since the structure is a tree, we use Depth-First Search to traverse it. We recurse from the root down to the leaves, and as each subtree's results return, we combine them into the parent's state using a knapsack-style merge over the budget.
Conclusion: The flowchart guides us to a Depth-First Search pattern. We perform tree DFS from the CEO, computing for each node a profit array indexed by remaining budget and whether the boss bought their stock, then merge child results back up to the root to find the maximum profit within the budget.
Intuition
The first thing to notice is the structure of the problem. Every employee has exactly one direct boss, and employee 1 is the boss of everyone. This means the hierarchy forms a tree rooted at employee 1. Whenever we deal with a tree where each node's answer depends on its children, a natural idea is to solve it from the bottom up using tree DFS.
The second key observation is the discount rule: an employee's buying cost depends on whether their direct boss bought their own stock. The cost is present[v] normally, but drops to floor(present[v] / 2) if the boss bought. This tells us that when we process a node, its decision is influenced by what its parent decided. So for each node we should track two scenarios: one where the boss bought (so this node gets the discount) and one where the boss did not (full price). We capture this with an extra dimension pre, where pre = 1 means the boss bought and pre = 0 means the boss did not.
The third observation is the budget constraint. We have a limited amount of money, each stock can be bought at most once, and we want to maximize total profit. This is exactly the flavor of a 0/1 knapsack problem: each employee is an "item" that either gets bought (paying its cost, gaining future[v] - cost profit) or skipped. The twist is that the items are arranged in a tree, and the cost of children depends on whether the parent was bought.
Putting these ideas together, for each node u we define f[j][pre] as the maximum profit obtainable in the subtree rooted at u when:
- the available budget is at most
j, and - the parent's purchase state is
pre.
To compute this, we first need to combine all of u's children. Since the budget is shared among the children, we merge them one by one using a knapsack: for each child, we decide how much budget j_v to give it. The merge formula is
nxt[j][pre] = max(nxt[j][pre], nxt[j - j_v][pre] + fv[j_v][pre])
This builds up nxt, the best profit obtainable from u's children for every budget level and every parent state.
Finally, we decide whether u itself buys. Its cost is cost = present[u] // (pre + 1), which automatically applies the discount when pre = 1. If u buys, it consumes cost from the budget and earns future[u] - cost, and crucially its own children now see pre = 1. If u does not buy, the children see pre = 0. So:
f[j][pre] = max(nxt[j][0], nxt[j - cost][1] + (future[u] - cost))
The left side means u skips buying (children inherit state 0), and the right side means u buys (children inherit state 1), as long as the budget allows.
The CEO (employee 1) has no boss, so we read the answer from dfs(1)[budget][0], using pre = 0 since there is no one above to grant a discount.
Pattern Learn more about Tree, Depth-First Search and Dynamic Programming patterns.
Solution Approach
We solve this with Tree Dynamic Programming combined with a 0/1 knapsack merge over the budget.
Building the tree. We first construct an adjacency list g, where g[u] holds all the direct subordinates of employee u:
g = [[] for _ in range(n + 1)]
for u, v in hierarchy:
g[u].append(v)
The DFS state. For each node u, the function dfs(u) returns a (budget + 1) x 2 array f, where f[j][pre] is the maximum profit achievable in the subtree rooted at u when:
- the budget is at most
j, and preis the purchase state ofu's direct boss (pre = 1means the boss bought,pre = 0means not).
Merging children with a knapsack. Before deciding whether u buys, we combine the results of all children into an array nxt, initialized to all zeros. The budget must be split among the children, so we treat each child as an item in a 0/1 knapsack. For each child v, we get fv = dfs(v) and merge it:
for v in g[u]:
fv = dfs(v)
for j in range(budget, -1, -1):
for jv in range(j + 1):
for pre in (0, 1):
val = nxt[j - jv][pre] + fv[jv][pre]
if val > nxt[j][pre]:
nxt[j][pre] = val
Here j is the total budget given to all children processed so far, and jv is the portion allocated to the current child v. We iterate j from high to low so each child is counted only once. The merge follows the formula:
nxt[j][pre] = max(nxt[j][pre], nxt[j - jv][pre] + fv[jv][pre])
After all children are merged, nxt[j][pre] is the best profit from u's subtree excluding u's own decision, given budget j and boss state pre.
Deciding whether u buys. Now we compute f. The cost for u is cost = present[u - 1] // (pre + 1), which is the full price when pre = 0 and the half (discounted) price when pre = 1:
for j in range(budget + 1):
for pre in (0, 1):
cost = present[u - 1] // (pre + 1)
if j >= cost:
f[j][pre] = max(nxt[j][0], nxt[j - cost][1] + (price - cost))
else:
f[j][pre] = nxt[j][0]
Two cases arise:
-
If
j < cost,ucannot afford its stock, so it must skip. Its children then see boss state0, givingf[j][pre] = nxt[j][0]. -
If
j >= cost,umay either skip or buy:- Skip: profit is
nxt[j][0](children see boss state0). - Buy: profit is
nxt[j - cost][1] + (price - cost), where we spendcost, gainfuture[u] - cost, and children now see boss state1.
We take the maximum of the two:
f[j][pre] = max(nxt[j][0], nxt[j - cost][1] + (price - cost)) - Skip: profit is
Note that the pre index in f[j][pre] only changes the cost of u itself (through the discount), while the children always branch on u's own buy/skip decision.
The answer. The CEO (employee 1) has no boss, so no discount applies and we read the result with pre = 0:
return dfs(1)[budget][0]
Complexity. Let B be the budget. The knapsack merge at each node costs O(B^2) per child, and summed across the tree this gives an overall time complexity of roughly O(n * B^2). The space used by the DP arrays is O(n * B) across the recursion stack.
Example Walkthrough
Let's trace through a small example to see how the tree DP + knapsack works.
Input:
n = 3present = [4, 8, 6]future = [7, 10, 12]hierarchy = [[1, 2], [1, 3]]budget = 10
Step 1 — Build the tree.
Employee 1 is the CEO and the direct boss of both 2 and 3:
1 / \ 2 3
So the adjacency list is:
g[1] = [2, 3]g[2] = []g[3] = []
Step 2 — DFS the leaves first.
We call dfs(1), which recurses into its children before deciding for itself.
dfs(2) (leaf): Employee 2 has no children, so nxt stays all zeros. We compute f for every budget j and every boss-state pre.
- For
pre = 0(boss did not buy):cost = present[1] // 1 = 8 // 1 = 8, profit if bought =future[1] - 8 = 10 - 8 = 2. - For
pre = 1(boss bought):cost = present[1] // 2 = 8 // 2 = 4, profit if bought =10 - 4 = 6.
Filling f[j][pre] = max(nxt[j][0], nxt[j-cost][1] + (price - cost)) (both nxt terms are 0):
j | f[j][0] (cost 8) | f[j][1] (cost 4) |
|---|---|---|
| 0–3 | 0 | 0 |
| 4–7 | 0 | 6 |
| 8–10 | 2 | 6 |
So with the discount (pre=1), employee 2 is far more profitable: a profit of 6 for only 4 spent.
dfs(3) (leaf): Same idea for employee 3.
- For
pre = 0:cost = present[2] // 1 = 6, profit =future[2] - 6 = 12 - 6 = 6. - For
pre = 1:cost = present[2] // 2 = 3, profit =12 - 3 = 9.
j | f[j][0] (cost 6) | f[j][1] (cost 3) |
|---|---|---|
| 0–2 | 0 | 0 |
| 3–5 | 0 | 9 |
| 6–10 | 6 | 9 |
Again the discounted branch wins: profit 9 for just 3 spent.
Step 3 — Merge children into nxt at node 1.
We start with nxt all zeros, then knapsack-merge dfs(2), then dfs(3).
After merging both children, nxt[j][pre] holds the best combined profit when total budget j is split between employees 2 and 3, all under boss-state pre.
Look at the two states that matter for the CEO's decision:
-
pre = 0(CEO does not buy): children pay full price. To buy both we need8 + 6 = 14 > 10, too much. Best single buy is employee3(profit 6, cost 6) or employee2(profit 2, cost 8). Forj = 10:nxt[10][0] = 6(buy only3). -
pre = 1(CEO bought): children get the discount. Employee2costs 4 (profit 6), employee3costs 3 (profit 9). Buying both costs4 + 3 = 7 ≤ 10, total profit6 + 9 = 15. Sonxt[j][1] = 15forj ≥ 7.
Step 4 — Decide whether employee 1 buys.
The CEO has no boss, so we evaluate with pre = 0, where cost = present[0] // 1 = 4, and buying earns future[0] - 4 = 7 - 4 = 3.
At j = budget = 10:
f[10][0] = max(nxt[10][0], nxt[10 - 4][1] + 3) = max(6, nxt[6][1] + 3)
Since nxt[6][1] = 15 (we can afford both discounted children with budget 6: 4 + 3 = 7... actually 7 > 6, so check nxt[6][1]).
Let's be precise. With budget 6 and both children discounted: buying both needs 7, not affordable. Best is buy 2 (cost 4, profit 6) and leftover 2 — can't afford 3 (needs 3). Or buy 3 (cost 3, profit 9) plus leftover 3 — can't afford 2 (needs 4). So nxt[6][1] = 9.
f[10][0] = max(6, 9 + 3) = max(6, 12) = 12
Verifying the winning plan:
- CEO buys: spend 4, earn 3. Children now get the discount.
- Remaining budget = 6, children see
pre = 1. - With 6 left, buy employee
3(cost 3, earn 9). Remaining 3 — can't afford employee2(cost 4). - Total spent:
4 + 3 = 7 ≤ 10. Total profit:3 + 9 = 12.
Step 5 — Read the answer.
return dfs(1)[budget][0] # = 12
The maximum profit within the budget is 12. The key insight the DP captures: the CEO buying its own stock (a modest profit of 3) unlocks the discount for the subtree, turning employee 3's purchase from cost-6 into cost-3 and making the whole plan more profitable than skipping the CEO.
Solution Implementation
1from typing import List
2
3
4class Solution:
5 def maxProfit(
6 self,
7 n: int,
8 present: List[int],
9 future: List[int],
10 hierarchy: List[List[int]],
11 budget: int,
12 ) -> int:
13 # Build the adjacency list for the tree (1-indexed nodes).
14 # graph[u] holds the direct subordinates (children) of u.
15 graph: List[List[int]] = [[] for _ in range(n + 1)]
16 for parent, child in hierarchy:
17 graph[parent].append(child)
18
19 def dfs(node: int) -> List[List[int]]:
20 # combined[j][parent_bought] = best total profit from the children's
21 # subtrees using budget j, where parent_bought indicates whether
22 # the current `node` ends up buying its own stock (which gives its
23 # children the 50% discount).
24 combined = [[0, 0] for _ in range(budget + 1)]
25
26 for child in graph[node]:
27 child_dp = dfs(child)
28 # Group knapsack: merge each child's dp into combined.
29 # Iterate budget descending to keep a 0/1-style combine per child.
30 for j in range(budget, -1, -1):
31 for spent in range(j + 1):
32 for parent_bought in (0, 1):
33 value = (
34 combined[j - spent][parent_bought]
35 + child_dp[spent][parent_bought]
36 )
37 if value > combined[j][parent_bought]:
38 combined[j][parent_bought] = value
39
40 # dp[j][parent_bought] = best profit in this whole subtree with
41 # budget j, where parent_bought tells whether THIS node's parent
42 # bought its stock (so this node may get the discount).
43 dp = [[0, 0] for _ in range(budget + 1)]
44 sell_price = future[node - 1]
45
46 for j in range(budget + 1):
47 for parent_bought in (0, 1):
48 # Cost of buying this node: half price if the parent bought.
49 cost = present[node - 1] // (parent_bought + 1)
50 if j >= cost:
51 # Option 1: do not buy this node -> children see parent_bought = 0.
52 # Option 2: buy this node -> children see parent_bought = 1,
53 # and we add the profit (sell_price - cost).
54 dp[j][parent_bought] = max(
55 combined[j][0],
56 combined[j - cost][1] + (sell_price - cost),
57 )
58 else:
59 # Not enough budget to buy this node; must skip it.
60 dp[j][parent_bought] = combined[j][0]
61
62 return dp
63
64 # Root (node 1) has no parent, so parent_bought = 0; use full budget.
65 return dfs(1)[budget][0]
661class Solution {
2 // Adjacency list representing the company hierarchy (manager -> subordinates)
3 private List<Integer>[] tree;
4 // present[i]: current price to buy stock for employee i
5 private int[] present;
6 // future[i]: future price of the stock for employee i
7 private int[] future;
8 // Total budget available
9 private int budget;
10
11 public int maxProfit(int n, int[] present, int[] future, int[][] hierarchy, int budget) {
12 this.present = present;
13 this.future = future;
14 this.budget = budget;
15
16 // Initialize the tree with n + 1 nodes (1-indexed employees)
17 tree = new ArrayList[n + 1];
18 Arrays.setAll(tree, k -> new ArrayList<>());
19
20 // Build the hierarchy: e[0] is the manager, e[1] is the subordinate
21 for (int[] edge : hierarchy) {
22 tree[edge[0]].add(edge[1]);
23 }
24
25 // Start DFS from the root (employee 1), return the best profit
26 // using the full budget when the parent did NOT buy (index 0)
27 return dfs(1)[budget][0];
28 }
29
30 /**
31 * Performs a post-order DFS to compute the maximum profit for the subtree rooted at u.
32 *
33 * @param u the current employee node
34 * @return a 2D array dp[j][parentBought] where:
35 * j = budget spent on this subtree
36 * parentBought = 0 if the parent did not buy, 1 if the parent bought
37 * The value is the maximum profit achievable under these constraints.
38 */
39 private int[][] dfs(int u) {
40 // childrenDp[j][parentBought]: best combined profit from all children
41 // of u using budget j, given whether u (their parent) bought the stock
42 int[][] childrenDp = new int[budget + 1][2];
43
44 // Merge each child's results into childrenDp using a knapsack-style combination
45 for (int child : tree[u]) {
46 int[][] childDp = dfs(child);
47 // Iterate budget in descending order to avoid reusing the same child twice
48 for (int j = budget; j >= 0; j--) {
49 // Allocate jv of the current budget j to this child
50 for (int jv = 0; jv <= j; jv++) {
51 // Track both states of the parent (u): not bought (0), bought (1)
52 for (int parentBought = 0; parentBought < 2; parentBought++) {
53 int candidate = childrenDp[j - jv][parentBought] + childDp[jv][parentBought];
54 if (candidate > childrenDp[j][parentBought]) {
55 childrenDp[j][parentBought] = candidate;
56 }
57 }
58 }
59 }
60 }
61
62 // dp[j][parentBought]: best profit for the subtree rooted at u
63 int[][] dp = new int[budget + 1][2];
64 int sellPrice = future[u - 1];
65
66 for (int j = 0; j <= budget; j++) {
67 for (int parentBought = 0; parentBought < 2; parentBought++) {
68 // If the parent bought the stock, u gets a 50% discount
69 int cost = present[u - 1] / (parentBought + 1);
70 if (j >= cost) {
71 // Choice 1: u does NOT buy -> children see parentBought = 0
72 // Choice 2: u buys -> pay cost, gain (sellPrice - cost),
73 // and children see parentBought = 1
74 dp[j][parentBought] = Math.max(
75 childrenDp[j][0],
76 childrenDp[j - cost][1] + (sellPrice - cost)
77 );
78 } else {
79 // Not enough budget for u to buy, so u must skip
80 dp[j][parentBought] = childrenDp[j][0];
81 }
82 }
83 }
84
85 return dp;
86 }
87}
881class Solution {
2public:
3 int maxProfit(int n, vector<int>& present, vector<int>& future,
4 vector<vector<int>>& hierarchy, int budget) {
5 // Build the adjacency list representing the company hierarchy.
6 // children[u] holds all direct subordinates of employee u.
7 vector<vector<int>> children(n + 1);
8 for (auto& edge : hierarchy) {
9 int parent = edge[0];
10 int child = edge[1];
11 children[parent].push_back(child);
12 }
13
14 // dfs returns a DP table for the subtree rooted at node u.
15 // The returned table 'result' is indexed as result[j][prev]:
16 // j -> the amount of budget allowed for this subtree
17 // prev -> whether the parent of u was bought (0 = not bought, 1 = bought)
18 // The stored value is the maximum profit achievable under these constraints.
19 auto dfs = [&](const auto& dfs, int u) -> vector<array<int, 2>> {
20 // 'merged' aggregates the best profit obtainable by distributing
21 // budget among all children of u. merged[j][prev] is the max profit
22 // using budget j across processed children, given parent-bought state.
23 vector<array<int, 2>> merged(budget + 1);
24 for (int j = 0; j <= budget; j++) {
25 merged[j] = {0, 0};
26 }
27
28 // Combine each child's DP table into 'merged' via a knapsack merge.
29 for (int child : children[u]) {
30 auto childDp = dfs(dfs, child);
31
32 // Iterate budget from high to low to avoid reusing the same
33 // child's allocation more than once (standard 0/1 knapsack order).
34 for (int j = budget; j >= 0; j--) {
35 // Try giving 'spend' budget to the current child.
36 for (int spend = 0; spend <= j; spend++) {
37 for (int prev = 0; prev < 2; prev++) {
38 int candidate = merged[j - spend][prev] + childDp[spend][prev];
39 if (candidate > merged[j][prev]) {
40 merged[j][prev] = candidate;
41 }
42 }
43 }
44 }
45 }
46
47 // Build the DP table for node u using the merged children result.
48 vector<array<int, 2>> result(budget + 1);
49 int sellingPrice = future[u - 1];
50
51 for (int j = 0; j <= budget; j++) {
52 for (int prev = 0; prev < 2; prev++) {
53 // If the parent was bought (prev == 1), u gets a discount,
54 // so its purchase cost is halved (present / 2); otherwise full price.
55 int cost = present[u - 1] / (prev + 1);
56
57 if (j >= cost) {
58 // Two choices:
59 // 1. Skip buying u: children see prev = 0, full budget j.
60 // 2. Buy u: pay 'cost', children see prev = 1 with budget j - cost,
61 // and we gain profit (sellingPrice - cost).
62 result[j][prev] = max(
63 merged[j][0],
64 merged[j - cost][1] + (sellingPrice - cost));
65 } else {
66 // Not enough budget to buy u; only the skip option is valid.
67 result[j][prev] = merged[j][0];
68 }
69 }
70 }
71
72 return result;
73 };
74
75 // The root (employee 1) has no parent, so prev = 0 (parent not bought).
76 return dfs(dfs, 1)[budget][0];
77 }
78};
791/**
2 * Computes the maximum profit obtainable by purchasing stocks within a budget,
3 * where buying a parent's stock grants the child a 50% discount on its price.
4 *
5 * @param n - Number of employees (nodes), labeled 1..n.
6 * @param present - present[i] is the current price of node (i + 1).
7 * @param future - future[i] is the future price of node (i + 1).
8 * @param hierarchy - Edges [u, v] meaning u is the direct superior of v.
9 * @param budget - Total money available to spend.
10 * @returns The maximum achievable profit.
11 */
12function maxProfit(
13 n: number,
14 present: number[],
15 future: number[],
16 hierarchy: number[][],
17 budget: number,
18): number {
19 // Adjacency list: children[u] holds the direct subordinates of node u.
20 const children: number[][] = Array.from({ length: n + 1 }, () => []);
21
22 for (const [superior, subordinate] of hierarchy) {
23 children[superior].push(subordinate);
24 }
25
26 /**
27 * Depth-first search returning a DP table for the subtree rooted at `node`.
28 *
29 * The returned table `dp` is indexed as dp[spent][discounted], where:
30 * - spent: amount of budget allocated to this subtree (0..budget).
31 * - discounted: 0 if this node is bought at full price (parent not bought),
32 * 1 if this node is eligible for the 50% discount (parent bought).
33 *
34 * dp[spent][discounted] is the maximum profit for this subtree using at most
35 * `spent` budget under the given discount condition.
36 */
37 const dfs = (node: number): number[][] => {
38 // mergedChildren[j][discounted]: best combined profit from already-processed
39 // children using exactly up to budget j, with the current node's discount state.
40 const mergedChildren: number[][] = Array.from(
41 { length: budget + 1 },
42 () => [0, 0],
43 );
44
45 // Merge each child's DP table into mergedChildren via a knapsack combination.
46 for (const child of children[node]) {
47 const childDp = dfs(child);
48
49 // Iterate budget downward to avoid reusing the same child multiple times.
50 for (let j = budget; j >= 0; j--) {
51 for (let childSpent = 0; childSpent <= j; childSpent++) {
52 for (let discounted = 0; discounted < 2; discounted++) {
53 mergedChildren[j][discounted] = Math.max(
54 mergedChildren[j][discounted],
55 mergedChildren[j - childSpent][discounted] +
56 childDp[childSpent][discounted],
57 );
58 }
59 }
60 }
61 }
62
63 // Build this node's DP table by deciding whether to buy its stock.
64 const dp: number[][] = Array.from({ length: budget + 1 }, () => [0, 0]);
65 const sellPrice = future[node - 1];
66
67 for (let j = 0; j <= budget; j++) {
68 for (let discounted = 0; discounted < 2; discounted++) {
69 // Cost is halved (floored) when the parent has been bought (discounted = 1).
70 const cost = Math.floor(present[node - 1] / (discounted + 1));
71
72 if (j >= cost) {
73 // If we buy this node, children become eligible for the discount (index 1).
74 const profitIfBuy =
75 mergedChildren[j - cost][1] + (sellPrice - cost);
76 // Otherwise children stay at full price (index 0).
77 dp[j][discounted] = Math.max(mergedChildren[j][0], profitIfBuy);
78 } else {
79 // Not enough budget to buy this node; children stay at full price.
80 dp[j][discounted] = mergedChildren[j][0];
81 }
82 }
83 }
84
85 return dp;
86 };
87
88 // The root (node 1) has no parent, so it is at full price (discounted = 0).
89 return dfs(1)[budget][0];
90}
91Time and Space Complexity
Time Complexity: O(n × budget²)
The code performs a tree DP via the dfs function, which is called once for each of the n nodes (since the hierarchy forms a tree rooted at node 1).
-
For each node
u, the merging of children's DP results runs a nested loop:for j in range(budget, -1, -1): for jv in range(j + 1): for pre in (0, 1): ...The outer loop runs
budget + 1times and the inner loop runs up toj + 1times, givingO(budget²)work per child. A subtle but crucial point: when summing this knapsack-merge cost over all edges across the entire tree, the total work is bounded byO(n × budget²). This is because each node is merged into its parent exactly once, so the merging cost is distributed across then - 1edges, contributingO(n × budget²)overall. -
The second loop in each
dfscall computesfand runsO(budget)times per node, contributingO(n × budget)in total, which is dominated by the merging cost.
Therefore, the total time complexity is O(n × budget²).
Space Complexity: O(n × budget)
- Each call to
dfsallocates the arraysnxtandf, each of sizeO(budget). - Due to the recursion, multiple
dfsframes can be active simultaneously along a path from the root to a leaf. In the worst case (a degenerate chain-like tree), the recursion depth reachesO(n), and each frame holdsO(budget)space. - This yields a stack-related space usage of
O(n × budget).
Additionally, the adjacency list g requires O(n) space. The dominant term is the recursion stack, so the overall space complexity is O(n × budget).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Iterating the knapsack budget in ascending order (item reuse)
The most frequent mistake in the group-knapsack merge is iterating the total budget j in ascending order instead of descending order. Although the merge here reads from a separate child_dp array (rather than the same array being written, as in a classic 1D knapsack), the subtle danger appears when people try to "optimize" by collapsing combined into the merge in place, or when they reverse the loop directions out of habit.
The correct loop must process j from high to low so that the combined[j - spent] value used as a base never already includes the current child's contribution:
for j in range(budget, -1, -1): # CORRECT: descending
for spent in range(j + 1):
...
If you write for j in range(budget + 1): (ascending) and you ever change combined[j - spent] to refer to an updated value of the same child, you would let one child be allocated budget multiple times, inflating profit. Keep the descending order as a defensive habit, and never write back into a slot you will read again for the same child.
Pitfall 2: Forgetting that pre only affects node's own cost, not its children
A very common conceptual error is to propagate the parent_bought flag straight through to the children. People mistakenly write:
# WRONG
dp[j][parent_bought] = max(
combined[j][parent_bought], # wrong index
combined[j - cost][parent_bought] + (sell_price - cost),
)
The flag parent_bought describes whether this node's boss bought, which only changes this node's discounted cost. What the children care about is whether this node itself bought. Therefore:
- The skip branch must read
combined[j][0](this node did not buy → children get no discount). - The buy branch must read
combined[j - cost][1](this node bought → children get the discount).
Using parent_bought as the index into combined mixes up two different "who bought" questions and produces wrong answers on any tree deeper than two levels.
Pitfall 3: Computing the discounted cost incorrectly
The discount is floor(present[v] / 2), elegantly expressed as present[node - 1] // (parent_bought + 1):
parent_bought = 0→ divide by1→ full price.parent_bought = 1→ divide by2→ floored half price.
Pitfalls here include:
- Applying the discount with
present[node - 1] / 2(true division) instead of//(floor division), which yields a float and breaks integer budget comparisons. - Applying the discount to the selling price
futureas well — only the buying cost is discounted;sell_priceis alwaysfuture[node - 1]. - Off-by-one indexing: arrays are described as 1-based in the statement but stored 0-based, so the node's own data is at
present[node - 1]/future[node - 1].
Pitfall 4: Recursion depth and performance on large/deep trees
dfs is recursive, and a degenerate "chain" tree (each boss has exactly one subordinate) reaches depth n. For large n this can hit Python's default recursion limit. Mitigations:
import sys sys.setrecursionlimit(n + 10)
Additionally, the O(n · B²) knapsack can be slow for large budgets. A practical optimization is to cap the inner spent loop by the budget actually consumable in the child's subtree, or to track each subtree's maximum useful spend and avoid iterating past it — this prunes the quadratic merge significantly when subtrees are small.
Pitfall 5: Reading the final answer with the wrong flag
The CEO (node 1) has no boss, so no discount can ever apply to it. The answer must be read with parent_bought = 0:
return dfs(1)[budget][0] # CORRECT
Reading dfs(1)[budget][1] would incorrectly grant the CEO a phantom half-price discount that has no parent to justify it, overstating the profit.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapHow would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).
Recommended Readings
Tree Data Structure Explained A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!