Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Depth-First Search (DFS)

Builds on Bottom-Up DFS, which you haven’t finished yet.

P1HardBuilt on Bottom-Up DFS~5 min speedrun

Tree DP (Max Path Sum / Diameter)

Built on
Bottom-Up DFSThe function returns one downward leg, and a separate global_max records the best path that bends at each node.
Unlocks
Greedy Tree DP
The problem

Binary Tree Maximum Path Sum (LC 124)(opens leetcode.com)

Think of a path here as any walk through the tree that only ever moves along real edges, never revisiting a node, and never required to touch the root.

Add up the values of every node along a path and you get that path's sum. Given the root of a binary tree, find the largest such sum over any non-empty path you could trace.

Example 1
Input:root = [1,2,3]
Output:6
The optimal path is 2 -> 1 -> 3 with a path sum of 2 + 1 + 3 = 6.
213

Tree Diameter DP

Postorder ➔ Update Global Summit [L + Node + R] ➔ Return Single Leg [Node + max(L, R)]
Spot it when: The answer is the best path anywhere in the tree, not just from the root: maximum path sum, diameter, longest same-value path.
Tree Diameter DP
Python
1from typing import Optional
2 
3class TreeNode:
4 def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None):
5 self.val = val
6 self.left = left
7 self.right = right
8 
9def max_path_sum(root: Optional[TreeNode]) -> int:
10 """Binary Tree Maximum Path Sum: best path anywhere, bending at most once.
11 Time: O(N) | Space: O(H) recursion stack
12 """
13 global_max = float('-inf') # -inf, not 0: an all-negative tree still has an answer
14 
15 def dfs(node: Optional[TreeNode]) -> int:
16 nonlocal global_max
17 if not node:
18 return 0 # Base case: null node contributes 0 gain
19 
20 # 1. Postorder descent; clamp negative returns (decline harmful paths)
21 left_gain = max(0, dfs(node.left))
22 right_gain = max(0, dfs(node.right))
23 
24 # 2. THE SUMMIT BRIDGE: Arched path bending through node + both legs
25 global_max = max(global_max, node.val + left_gain + right_gain)
26 
27 # 3. THE EXTENDABLE LEG: Hand up only ONE best linear branch to parent
28 return node.val + max(left_gain, right_gain)
29 
30 dfs(root)
31 return global_max
Invariant: dfs(node) returns the best downward leg that starts at node, and global_max holds the best path that bends at any node already finished.

Every path in a tree has one highest node where it bends, so at each node compute two different things from the children's gains. The summit, node.val + left_gain + right_gain, is the best path bending here and only updates global_max; the leg, node.val + max(left_gain, right_gain), is the only thing returned, because a path continuing up through the parent can use just one side. Clamp gains with max(0, ...) so a negative branch is simply left out.

See it run

Tree DP: the bend and the leg, step by step

left_gain - · right_gain - · summit - · leg - · global_max -inf · answer not yet

left_gain 0 · right_gain 0 · summit 9 · leg - · global_max 9 · answer not yet

left_gain 0 · right_gain 0 · summit 9 · leg 9 · global_max 9 · answer not yet

left_gain 0 · right_gain 0 · summit 15 · leg - · global_max 15 · answer not yet

left_gain 0 · right_gain 0 · summit 15 · leg 15 · global_max 15 · answer not yet

left_gain 0 · right_gain 0 · summit 7 · leg - · global_max 15 · answer not yet

left_gain 0 · right_gain 0 · summit 7 · leg 7 · global_max 15 · answer not yet

left_gain 15 · right_gain 7 · summit 42 · leg - · global_max 42 · answer not yet

left_gain 15 · right_gain 7 · summit 42 · leg 35 · global_max 42 · answer not yet

left_gain 9 · right_gain 35 · summit 34 · leg - · global_max 42 · answer not yet

left_gain 9 · right_gain 35 · summit 34 · leg 25 · global_max 42 · answer not yet

left_gain - · right_gain - · summit - · leg 25 · global_max 42 · answer 42

Each circle is a tree node. The bend is the best path that turns at a node, using both children. The leg is one straight path down one side, and it is the only value a node returns to its parent. The list under the tree shows each returned leg.

  • at node the node whose bend and leg are computed now
  • best bend the node where the best path so far turns
  • leg used the edge of a child leg that is added at this node
  • leg up the edge the returned leg travels along, to the parent

Start with global_max = -inf. Each node will compute a bend, which only updates global_max, and a leg, which is the only value it returns.

Node 9: left_gain = 0, right_gain = 0. The bend here is 9 + 0 + 0 = 9, which is above the old global_max, so global_max becomes 9.

The leg returned is 9 + max(0, 0) = 9. Only one side goes up, so the parent can extend one straight path and not a fork.

Node 15: left_gain = 0, right_gain = 0. The bend here is 15 + 0 + 0 = 15, which is above the old global_max, so global_max becomes 15.

The leg returned is 15 + max(0, 0) = 15. Only one side goes up, so the parent can extend one straight path and not a fork.

Node 7: left_gain = 0, right_gain = 0. The bend here is 7 + 0 + 0 = 7, which is not above global_max, so global_max stays 15.

The leg returned is 7 + max(0, 0) = 7. Only one side goes up, so the parent can extend one straight path and not a fork.

Node 20: left_gain = 15, right_gain = 7. The bend here is 20 + 15 + 7 = 42, which is above the old global_max, so global_max becomes 42.

The leg returned is 20 + max(15, 7) = 35. Only one side goes up, so the parent can extend one straight path and not a fork.

Node -10: left_gain = 9, right_gain = 35. The bend here is -10 + 9 + 35 = 34, which is not above global_max, so global_max stays 42.

The leg returned is -10 + max(9, 35) = 25. Only one side goes up, so the parent can extend one straight path and not a fork.

The recursion ended. dfs(root) returned the leg 25, but the answer is global_max = 42.

9-1015207
legs returned (node: leg)
empty
1 / 12
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Adding a negative leg without max(0, ...)

    Mistake
    left_gain = dfs(node.left)
    right_gain = dfs(node.right)
    Fix
    left_gain = max(0, dfs(node.left))
    right_gain = max(0, dfs(node.right))

    What happens: A path may stop at any node. So a subtree whose best leg is negative should be left out. Without max(0, ...), that negative leg is added to the summit and to the returned leg. Every path through that node then loses value.

    Example: root = [2,-1]
    Mistake: returns 1Fix: returns 2
  2. 2

    Gotcha 2: Starting global_max at 0 instead of -inf

    Mistake
    global_max = 0
    Fix
    global_max = float('-inf')

    What happens: A path must hold at least one node. When every value is negative, the best path is the single largest value. That value is below 0, so a start of 0 is never replaced. The function then returns 0, which is not the sum of any path.

    Example: root = [-3]
    Mistake: returns 0Fix: returns -3
  3. 3

    Gotcha 3: Returning the summit instead of the leg

    Mistake
    return node.val + left_gain + right_gain
    Fix
    return node.val + max(left_gain, right_gain)

    What happens: The parent adds the returned value to its own path. That path can go down only one side of node. The summit uses both sides of node. If node returns the summit, the parent builds a path that forks, which is not a path at all.

    Example: root = [1,2,3,null,null,4,5]
    Mistake: returns 15Fix: returns 12
  4. 4

    Gotcha 4: Returning dfs(root) instead of global_max

    Mistake
    return dfs(root)
    Fix
    dfs(root)
    return global_max

    What happens: dfs(root) returns the root's leg. The leg covers only paths that go down one side of the root. The best path can bend at the root or lie deeper in the tree. global_max holds the best summit of every node.

    Example: root = [1,2,3]
    Mistake: returns 4Fix: returns 6
  5. 5

    Gotcha 5: Extending a leg through a child with another value

    Mistake
    left_leg = left + 1 if node.left else 0
    right_leg = right + 1 if node.right else 0
    Fix
    left_leg = left + 1 if node.left and node.left.val == node.val else 0
    right_leg = right + 1 if node.right and node.right.val == node.val else 0

    What happens: In Longest Univalue Path (LC 687), every node on the path has the same value. A child's leg extends through node only when child.val == node.val. Otherwise that side adds 0 to both the summit and the leg. Without the value check, the code measures the longest path of any values.

    Example: Longest Univalue Path (LC 687): root = [5,4,5,1,1,null,5]
    Mistake: returns 4Fix: returns 2
Not this one if…
  • The path must start at the root and only go down, as in Maximum Depth of Binary Tree (LC 104), so the root's own return value is the answer and no path bends. See Bottom-Up DFS

Deep Dive

Why each node computes two different numbers

A path in a tree goes down from some node, turns at one highest node, and goes down again on the other side. Every path has exactly one highest node. So the best path in the tree is the best bend over all nodes.

At each node, two questions have two different answers. The first question is: what is the best path that turns at this node? That path can use both children, so its sum is node.val + left_gain + right_gain. Call it the bend. It is only recorded in global_max. The second question is: what can this node offer to its parent? The parent's path continues upward, so it can use only one side of this node. The best offer is node.val + max(left_gain, right_gain). Call it the leg. It is the only value dfs returns.

Take root = [-10, 9, 20, null, null, 15, 7]. Node 9 has a bend of 9 and a leg of 9. Node 15 has a bend of 15 and a leg of 15. Node 7 has a bend of 7 and a leg of 7. Node 20 has left_gain = 15 and right_gain = 7, so its bend is 20 + 15 + 7 = 42 and its leg is 20 + 15 = 35. The root -10 has left_gain = 9 and right_gain = 35, so its bend is 34 and its leg is 25. global_max ends at 42, which is the answer. The root's leg, 25, is not the answer.

Why the returned leg can use only one side

The parent adds the returned value to its own path. A path is a line. It can go down only one side of any node. If a node returned the bend, which uses both sides, the parent would build a path that forks into two branches below the node. That is not a path.

Take root = [1, 2, 3, null, null, 4, 5]. Node 3 has children 4 and 5. Its bend is 3 + 4 + 5 = 12, and 12 is a real path: 4, 3, 5. Its leg is 3 + 5 = 8. The root then has left_gain = 2 and right_gain = 8, so its bend is 1 + 2 + 8 = 11. The answer is 12.

If node 3 returned its bend 12, the root would compute 1 + 2 + 12 = 15. That sum counts 2, 1, 3, 4 and 5. Node 3 would have two neighbours below it (4 and 5) and one above it (1). No single path visits all five nodes, so 15 is wrong.

The two numbers have a simple rule. The bend answers "what is the best path with its turn here?" and goes to global_max. The leg answers "what is the best straight path that starts here and goes down?" and goes to the parent.

Two traps: a negative leg, and returning the wrong thing

A path can stop at any node, so a child with a negative leg should be left out. The code does this with max(0, dfs(node.left)). For root = [2, -1], the child returns -1. Without the clamp, the bend is 2 + (-1) = 1 and the code returns 1. With the clamp, the gain is 0 and the code returns 2.

The start value of global_max must be -inf. A path must contain at least one node. For root = [-3], the only path is the single node -3. With a start of 0, the bend -3 never replaces it, and the code returns 0. That value is not the sum of any path. With -inf, the code returns -3.

The last trap is the line return dfs(root). The root's call returns the root's leg, which covers only paths that go down one side of the root. For root = [1, 2, 3], the root's leg is 1 + 3 = 4, but the best path is 2, 1, 3 with a sum of 6. The code must call dfs(root) for its effect and then return global_max.

The cost, and how it differs from Bottom-Up DFS

Each node is visited once, and each visit does constant work. The time is O(N). The space is O(H) for the recursion stack, where H is the height.

The shape appears in other problems. Diameter of Binary Tree (LC 543) uses the same two numbers with lengths. The leg is the depth of the node. The bend is left_depth + right_depth, the longest path that turns at this node. Longest Univalue Path (LC 687) adds a condition: a child's leg continues through the node only when the child has the same value.

Compare with Bottom-Up DFS. In Maximum Depth of Binary Tree (LC 104), the path must start at the root, so the root's return value is the answer, and no path bends. Here, the best path can lie anywhere, so the answer is stored in global_max. Bottom-Up DFS returns one value that is also the answer. This card returns one value that is only a building block, and the answer is stored on the side.

60-Second Pitch

"Every path has one highest node where it turns, so I compute two numbers at every node. The bend is node.val + left_gain + right_gain. It is the best path that turns here, and I only compare it with global_max. The leg is node.val + max(left_gain, right_gain). It is the only value I return, because my parent can extend only one side. I clamp each child gain with max(0, ...) so a negative branch is left out. I start global_max at -inf, because the path must contain at least one node. I return global_max, not the root's leg. Each node is visited once, so the time is O(N). The space is O(H) for the recursion stack."

Common Questions

Why do you return the leg and not the bend?

The parent extends the returned value into a longer path. A path can go down only one side of a node. The bend uses both sides, so the parent would get a fork. The leg uses one side, so it is a straight line that the parent can extend.

Why max(0, ...) on the child gains?

A path may stop at any node. If a child's best leg is negative, the path is better without it. The clamp makes that child add 0. For [2, -1] the answer is 2 with the clamp and 1 without it.

What are the time and space complexity?

O(N) time, because each node is visited once with constant work. O(H) space for the recursion stack, where H is the height. global_max is one number.

How does Diameter of Binary Tree (LC 543) fit this card?

The leg is the depth of the node: 1 + max(left_depth, right_depth). The bend is left_depth + right_depth, the number of edges of the longest path that turns at this node. global_max keeps the largest bend. The answer is global_max, not the root's depth.

What changes for Longest Univalue Path (LC 687)?

A child's leg continues through the node only when the child has the same value as the node. Otherwise that side adds 0 to both the bend and the leg. The structure is the same: return the leg, record the bend.

SWE Reasoning

Spot it

  • The answer is the best path anywhere in the tree, not only from the root: maximum path sum, diameter, longest path with a condition.
  • A path may start and end at any node, and it turns at most once.
  • The wording says "any path", "at least one node", "longest" or "may or may not pass through the root".
  • A node needs a value to offer its parent that is different from the best answer at the node itself.
  • The tree can hold negative values, so a branch may be better left out.

The invariant, step by step

  1. Every path has one highest node, where it turns. So the answer is the best bend over all nodes.
  2. dfs(node) returns the leg: the best straight path that starts at node and goes down one side.
  3. Each child's gain is max(0, dfs(child)). A negative leg is left out, because a path can stop at node.
  4. The bend at node is node.val + left_gain + right_gain. It is compared with global_max and never returned.
  5. The leg is node.val + max(left_gain, right_gain). It is returned, because the parent's path can use only one side of node.
  6. When the recursion ends, global_max is the best bend over all nodes. That is the best path.

Traps

  • Returning the bend. The parent would build a path that forks. Return the leg.
  • Returning dfs(root). It is the root's leg, not the best path. Return global_max.
  • No max(0, ...) on the gains. A negative leg lowers every path through the node.
  • global_max = 0 at the start. If all values are negative, the answer must be the largest single value. Use -inf.
  • Forgetting the condition in problems like Longest Univalue Path (LC 687). A leg may continue through a node only when the values match.

Practice

Canonical LeetCode:Binary Tree Maximum Path Sum (LC 124)Diameter of Binary Tree (LC 543)Longest Univalue Path (LC 687)Longest Path With Different Adjacent Characters (LC 2246)Tree Diameter (LC 1245)Binary Tree Longest Consecutive Sequence II (LC 549)
Theory Context•Depth-First Search (DFS)

Builds on Bottom-Up DFS, which you haven’t finished yet.

P1HardBuilt on Bottom-Up DFS~5 min speedrun

Tree DP (Max Path Sum / Diameter)

Built on
Bottom-Up DFSThe function returns one downward leg, and a separate global_max records the best path that bends at each node.
Unlocks
Greedy Tree DP
The problem

Binary Tree Maximum Path Sum (LC 124)(opens leetcode.com)

Think of a path here as any walk through the tree that only ever moves along real edges, never revisiting a node, and never required to touch the root.

Add up the values of every node along a path and you get that path's sum. Given the root of a binary tree, find the largest such sum over any non-empty path you could trace.

Example 1
Input:root = [1,2,3]
Output:6
The optimal path is 2 -> 1 -> 3 with a path sum of 2 + 1 + 3 = 6.
213

Tree Diameter DP

Postorder ➔ Update Global Summit [L + Node + R] ➔ Return Single Leg [Node + max(L, R)]
Spot it when: The answer is the best path anywhere in the tree, not just from the root: maximum path sum, diameter, longest same-value path.
Tree Diameter DP
Python
1from typing import Optional
2 
3class TreeNode:
4 def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None):
5 self.val = val
6 self.left = left
7 self.right = right
8 
9def max_path_sum(root: Optional[TreeNode]) -> int:
10 """Binary Tree Maximum Path Sum: best path anywhere, bending at most once.
11 Time: O(N) | Space: O(H) recursion stack
12 """
13 global_max = float('-inf') # -inf, not 0: an all-negative tree still has an answer
14 
15 def dfs(node: Optional[TreeNode]) -> int:
16 nonlocal global_max
17 if not node:
18 return 0 # Base case: null node contributes 0 gain
19 
20 # 1. Postorder descent; clamp negative returns (decline harmful paths)
21 left_gain = max(0, dfs(node.left))
22 right_gain = max(0, dfs(node.right))
23 
24 # 2. THE SUMMIT BRIDGE: Arched path bending through node + both legs
25 global_max = max(global_max, node.val + left_gain + right_gain)
26 
27 # 3. THE EXTENDABLE LEG: Hand up only ONE best linear branch to parent
28 return node.val + max(left_gain, right_gain)
29 
30 dfs(root)
31 return global_max
Invariant: dfs(node) returns the best downward leg that starts at node, and global_max holds the best path that bends at any node already finished.

Every path in a tree has one highest node where it bends, so at each node compute two different things from the children's gains. The summit, node.val + left_gain + right_gain, is the best path bending here and only updates global_max; the leg, node.val + max(left_gain, right_gain), is the only thing returned, because a path continuing up through the parent can use just one side. Clamp gains with max(0, ...) so a negative branch is simply left out.

See it run

Tree DP: the bend and the leg, step by step

left_gain - · right_gain - · summit - · leg - · global_max -inf · answer not yet

left_gain 0 · right_gain 0 · summit 9 · leg - · global_max 9 · answer not yet

left_gain 0 · right_gain 0 · summit 9 · leg 9 · global_max 9 · answer not yet

left_gain 0 · right_gain 0 · summit 15 · leg - · global_max 15 · answer not yet

left_gain 0 · right_gain 0 · summit 15 · leg 15 · global_max 15 · answer not yet

left_gain 0 · right_gain 0 · summit 7 · leg - · global_max 15 · answer not yet

left_gain 0 · right_gain 0 · summit 7 · leg 7 · global_max 15 · answer not yet

left_gain 15 · right_gain 7 · summit 42 · leg - · global_max 42 · answer not yet

left_gain 15 · right_gain 7 · summit 42 · leg 35 · global_max 42 · answer not yet

left_gain 9 · right_gain 35 · summit 34 · leg - · global_max 42 · answer not yet

left_gain 9 · right_gain 35 · summit 34 · leg 25 · global_max 42 · answer not yet

left_gain - · right_gain - · summit - · leg 25 · global_max 42 · answer 42

Each circle is a tree node. The bend is the best path that turns at a node, using both children. The leg is one straight path down one side, and it is the only value a node returns to its parent. The list under the tree shows each returned leg.

  • at node the node whose bend and leg are computed now
  • best bend the node where the best path so far turns
  • leg used the edge of a child leg that is added at this node
  • leg up the edge the returned leg travels along, to the parent

Start with global_max = -inf. Each node will compute a bend, which only updates global_max, and a leg, which is the only value it returns.

Node 9: left_gain = 0, right_gain = 0. The bend here is 9 + 0 + 0 = 9, which is above the old global_max, so global_max becomes 9.

The leg returned is 9 + max(0, 0) = 9. Only one side goes up, so the parent can extend one straight path and not a fork.

Node 15: left_gain = 0, right_gain = 0. The bend here is 15 + 0 + 0 = 15, which is above the old global_max, so global_max becomes 15.

The leg returned is 15 + max(0, 0) = 15. Only one side goes up, so the parent can extend one straight path and not a fork.

Node 7: left_gain = 0, right_gain = 0. The bend here is 7 + 0 + 0 = 7, which is not above global_max, so global_max stays 15.

The leg returned is 7 + max(0, 0) = 7. Only one side goes up, so the parent can extend one straight path and not a fork.

Node 20: left_gain = 15, right_gain = 7. The bend here is 20 + 15 + 7 = 42, which is above the old global_max, so global_max becomes 42.

The leg returned is 20 + max(15, 7) = 35. Only one side goes up, so the parent can extend one straight path and not a fork.

Node -10: left_gain = 9, right_gain = 35. The bend here is -10 + 9 + 35 = 34, which is not above global_max, so global_max stays 42.

The leg returned is -10 + max(9, 35) = 25. Only one side goes up, so the parent can extend one straight path and not a fork.

The recursion ended. dfs(root) returned the leg 25, but the answer is global_max = 42.

9-1015207
legs returned (node: leg)
empty
1 / 12
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Adding a negative leg without max(0, ...)

    Mistake
    left_gain = dfs(node.left)
    right_gain = dfs(node.right)
    Fix
    left_gain = max(0, dfs(node.left))
    right_gain = max(0, dfs(node.right))

    What happens: A path may stop at any node. So a subtree whose best leg is negative should be left out. Without max(0, ...), that negative leg is added to the summit and to the returned leg. Every path through that node then loses value.

    Example: root = [2,-1]
    Mistake: returns 1Fix: returns 2
  2. 2

    Gotcha 2: Starting global_max at 0 instead of -inf

    Mistake
    global_max = 0
    Fix
    global_max = float('-inf')

    What happens: A path must hold at least one node. When every value is negative, the best path is the single largest value. That value is below 0, so a start of 0 is never replaced. The function then returns 0, which is not the sum of any path.

    Example: root = [-3]
    Mistake: returns 0Fix: returns -3
  3. 3

    Gotcha 3: Returning the summit instead of the leg

    Mistake
    return node.val + left_gain + right_gain
    Fix
    return node.val + max(left_gain, right_gain)

    What happens: The parent adds the returned value to its own path. That path can go down only one side of node. The summit uses both sides of node. If node returns the summit, the parent builds a path that forks, which is not a path at all.

    Example: root = [1,2,3,null,null,4,5]
    Mistake: returns 15Fix: returns 12
  4. 4

    Gotcha 4: Returning dfs(root) instead of global_max

    Mistake
    return dfs(root)
    Fix
    dfs(root)
    return global_max

    What happens: dfs(root) returns the root's leg. The leg covers only paths that go down one side of the root. The best path can bend at the root or lie deeper in the tree. global_max holds the best summit of every node.

    Example: root = [1,2,3]
    Mistake: returns 4Fix: returns 6
  5. 5

    Gotcha 5: Extending a leg through a child with another value

    Mistake
    left_leg = left + 1 if node.left else 0
    right_leg = right + 1 if node.right else 0
    Fix
    left_leg = left + 1 if node.left and node.left.val == node.val else 0
    right_leg = right + 1 if node.right and node.right.val == node.val else 0

    What happens: In Longest Univalue Path (LC 687), every node on the path has the same value. A child's leg extends through node only when child.val == node.val. Otherwise that side adds 0 to both the summit and the leg. Without the value check, the code measures the longest path of any values.

    Example: Longest Univalue Path (LC 687): root = [5,4,5,1,1,null,5]
    Mistake: returns 4Fix: returns 2
Not this one if…
  • The path must start at the root and only go down, as in Maximum Depth of Binary Tree (LC 104), so the root's own return value is the answer and no path bends. See Bottom-Up DFS

Deep Dive

Why each node computes two different numbers

A path in a tree goes down from some node, turns at one highest node, and goes down again on the other side. Every path has exactly one highest node. So the best path in the tree is the best bend over all nodes.

At each node, two questions have two different answers. The first question is: what is the best path that turns at this node? That path can use both children, so its sum is node.val + left_gain + right_gain. Call it the bend. It is only recorded in global_max. The second question is: what can this node offer to its parent? The parent's path continues upward, so it can use only one side of this node. The best offer is node.val + max(left_gain, right_gain). Call it the leg. It is the only value dfs returns.

Take root = [-10, 9, 20, null, null, 15, 7]. Node 9 has a bend of 9 and a leg of 9. Node 15 has a bend of 15 and a leg of 15. Node 7 has a bend of 7 and a leg of 7. Node 20 has left_gain = 15 and right_gain = 7, so its bend is 20 + 15 + 7 = 42 and its leg is 20 + 15 = 35. The root -10 has left_gain = 9 and right_gain = 35, so its bend is 34 and its leg is 25. global_max ends at 42, which is the answer. The root's leg, 25, is not the answer.

Why the returned leg can use only one side

The parent adds the returned value to its own path. A path is a line. It can go down only one side of any node. If a node returned the bend, which uses both sides, the parent would build a path that forks into two branches below the node. That is not a path.

Take root = [1, 2, 3, null, null, 4, 5]. Node 3 has children 4 and 5. Its bend is 3 + 4 + 5 = 12, and 12 is a real path: 4, 3, 5. Its leg is 3 + 5 = 8. The root then has left_gain = 2 and right_gain = 8, so its bend is 1 + 2 + 8 = 11. The answer is 12.

If node 3 returned its bend 12, the root would compute 1 + 2 + 12 = 15. That sum counts 2, 1, 3, 4 and 5. Node 3 would have two neighbours below it (4 and 5) and one above it (1). No single path visits all five nodes, so 15 is wrong.

The two numbers have a simple rule. The bend answers "what is the best path with its turn here?" and goes to global_max. The leg answers "what is the best straight path that starts here and goes down?" and goes to the parent.

Two traps: a negative leg, and returning the wrong thing

A path can stop at any node, so a child with a negative leg should be left out. The code does this with max(0, dfs(node.left)). For root = [2, -1], the child returns -1. Without the clamp, the bend is 2 + (-1) = 1 and the code returns 1. With the clamp, the gain is 0 and the code returns 2.

The start value of global_max must be -inf. A path must contain at least one node. For root = [-3], the only path is the single node -3. With a start of 0, the bend -3 never replaces it, and the code returns 0. That value is not the sum of any path. With -inf, the code returns -3.

The last trap is the line return dfs(root). The root's call returns the root's leg, which covers only paths that go down one side of the root. For root = [1, 2, 3], the root's leg is 1 + 3 = 4, but the best path is 2, 1, 3 with a sum of 6. The code must call dfs(root) for its effect and then return global_max.

The cost, and how it differs from Bottom-Up DFS

Each node is visited once, and each visit does constant work. The time is O(N). The space is O(H) for the recursion stack, where H is the height.

The shape appears in other problems. Diameter of Binary Tree (LC 543) uses the same two numbers with lengths. The leg is the depth of the node. The bend is left_depth + right_depth, the longest path that turns at this node. Longest Univalue Path (LC 687) adds a condition: a child's leg continues through the node only when the child has the same value.

Compare with Bottom-Up DFS. In Maximum Depth of Binary Tree (LC 104), the path must start at the root, so the root's return value is the answer, and no path bends. Here, the best path can lie anywhere, so the answer is stored in global_max. Bottom-Up DFS returns one value that is also the answer. This card returns one value that is only a building block, and the answer is stored on the side.

60-Second Pitch

"Every path has one highest node where it turns, so I compute two numbers at every node. The bend is node.val + left_gain + right_gain. It is the best path that turns here, and I only compare it with global_max. The leg is node.val + max(left_gain, right_gain). It is the only value I return, because my parent can extend only one side. I clamp each child gain with max(0, ...) so a negative branch is left out. I start global_max at -inf, because the path must contain at least one node. I return global_max, not the root's leg. Each node is visited once, so the time is O(N). The space is O(H) for the recursion stack."

Common Questions

Why do you return the leg and not the bend?

The parent extends the returned value into a longer path. A path can go down only one side of a node. The bend uses both sides, so the parent would get a fork. The leg uses one side, so it is a straight line that the parent can extend.

Why max(0, ...) on the child gains?

A path may stop at any node. If a child's best leg is negative, the path is better without it. The clamp makes that child add 0. For [2, -1] the answer is 2 with the clamp and 1 without it.

What are the time and space complexity?

O(N) time, because each node is visited once with constant work. O(H) space for the recursion stack, where H is the height. global_max is one number.

How does Diameter of Binary Tree (LC 543) fit this card?

The leg is the depth of the node: 1 + max(left_depth, right_depth). The bend is left_depth + right_depth, the number of edges of the longest path that turns at this node. global_max keeps the largest bend. The answer is global_max, not the root's depth.

What changes for Longest Univalue Path (LC 687)?

A child's leg continues through the node only when the child has the same value as the node. Otherwise that side adds 0 to both the bend and the leg. The structure is the same: return the leg, record the bend.

SWE Reasoning

Spot it

  • The answer is the best path anywhere in the tree, not only from the root: maximum path sum, diameter, longest path with a condition.
  • A path may start and end at any node, and it turns at most once.
  • The wording says "any path", "at least one node", "longest" or "may or may not pass through the root".
  • A node needs a value to offer its parent that is different from the best answer at the node itself.
  • The tree can hold negative values, so a branch may be better left out.

The invariant, step by step

  1. Every path has one highest node, where it turns. So the answer is the best bend over all nodes.
  2. dfs(node) returns the leg: the best straight path that starts at node and goes down one side.
  3. Each child's gain is max(0, dfs(child)). A negative leg is left out, because a path can stop at node.
  4. The bend at node is node.val + left_gain + right_gain. It is compared with global_max and never returned.
  5. The leg is node.val + max(left_gain, right_gain). It is returned, because the parent's path can use only one side of node.
  6. When the recursion ends, global_max is the best bend over all nodes. That is the best path.

Traps

  • Returning the bend. The parent would build a path that forks. Return the leg.
  • Returning dfs(root). It is the root's leg, not the best path. Return global_max.
  • No max(0, ...) on the gains. A negative leg lowers every path through the node.
  • global_max = 0 at the start. If all values are negative, the answer must be the largest single value. Use -inf.
  • Forgetting the condition in problems like Longest Univalue Path (LC 687). A leg may continue through a node only when the values match.

Practice

Canonical LeetCode:Binary Tree Maximum Path Sum (LC 124)Diameter of Binary Tree (LC 543)Longest Univalue Path (LC 687)Longest Path With Different Adjacent Characters (LC 2246)Tree Diameter (LC 1245)Binary Tree Longest Consecutive Sequence II (LC 549)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•