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).

12 Core Algorithmic Patterns & 168 Practice Problems

  • 1. Two Pointers (9 Paradigms, 32 Problems): Converging pointers, sorted pair sums, container with most water, trapping rain water, 3Sum, plus the Sliding Window and Fast & Slow Pointers paradigms (Floyd cycle detection, monotonic window invariants, longest substrings, minimum window).
  • 2. Binary Search (8 Paradigms, 12 Problems): Monotonic predicate partitioning, boundary searching, rotated arrays, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 8 Problems): Bitmasking, XOR tricks, counting set bits, subset enumeration via bitmasks.
  • 4. Math & Geometry (5 Paradigms, 10 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (8 Paradigms, 17 Problems): Path sums, lowest common ancestor, tree diameter, subtree serialization, validating BSTs.
  • 6. Tree/Graph Breadth-First Search (4 Paradigms, 11 Problems): Level-order traversals, shortest path, rotting oranges, word ladders.
  • 7. Graphs (6 Paradigms, 14 Problems): Topological sort, cycle detection, Dijkstra, bipartite validation, network delay.
  • 8. Backtracking (5 Paradigms, 13 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens.
  • 9. Dynamic Programming (7 Paradigms, 14 Problems): Memoization vs tabulation, knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (8 Paradigms, 10 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (5 Paradigms, 13 Problems): Trie, Union-Find, LRU Cache, LFU Cache, Monotonic Stacks.
  • 12. Intervals & Stack / Miscellaneous (7 Paradigms, 14 Problems): Merge intervals, daily temperatures, largest rectangle in histogram, trapping rain water via stack.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Multi-language Rosetta Stone contrasting abstract invariants with concrete solutions across Python, C#, Java, TypeScript, C++, Go, and Rust.
  2. Stage 2 (Active Recall): Reconstruct algorithmic template invariants from memory with real-time feedback before looking at solutions.
  3. Stage 3 (Senior SWE AI Mock Coach): Simulated senior mock interview evaluating Big-O space/time tradeoffs, edge cases, and code reviews in Monaco Editor.
  4. Stage 4 (Solve on Your Own): Timed sandbox challenges verified against automated test suites in Python, C#, Java, and TypeScript.

Equipped with SM-2 Spaced Repetition Review Hub, Studio Cockpit workspace layout, and interactive study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Compute Coins: 40 free coins upon signup, +20 daily login bonus, +25 referral bonus.
  • 24-Hour AI Coaching Pass: 5 compute coins unlocks unlimited senior SWE AI coaching for a full 24 hours.
  • BYOK (Bring Your Own Key): Completely free unlimited AI coaching if using your own Gemini/OpenAI API key.
  • Refund & Subscription Policy: No recurring charges, no subscription traps, and no paid paywalls. Free tier provides full learning path.
  • Platform Operator: Hi👋WebEnterprise Inc. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

180Items
Theory Context•Depth-First Search (DFS)
MediumLC 337

House Robber III (LeetCode 337)

You will see how returning two states per node, robbed and skipped, lets each parent apply the no-parent-and-child rule on its own.

Target Frequency:AmazonGoogleMicrosoft

The houses in a neighbourhood are joined like a binary tree. The only way in is the house at root, and every other house is reached from exactly one house above it, its parent. A node's value is the money inside that house.

A burglar may break into any set of houses on one night, with one rule: if two houses joined by an edge (a parent and its child) are both broken into, the alarm goes off. Return the most money the burglar can take without ever breaking into both ends of an edge.

Worked Examples

Example 1
Input:root = [3,2,3,null,3,null,1]
Output:7
23331
Explanation: Rob the root (3) and the two bottom houses (3 and 1). No two of them are parent and child, and 3 + 3 + 1 = 7.
Example 2
Input:root = [3,4,5,1,3,null,1]
Output:9
143351
Explanation: Skip the root and rob its two children, 4 + 5 = 9. Robbing the root instead leaves only the bottom row, 3 + 1 + 3 + 1 = 8.

⚖️Formal Constraints & Bounds

  • The number of nodes in the tree is in the range [1, 104].

  • 0 <= Node.val <= 104

Deep-Dive & Conceptual Insights

Why It Works & Core Invariant

One number per subtree hides whether the child was robbed, so each subtree returns two: the best total with its root robbed and with it skipped. A robbed parent adds its children's skipped totals, a skipped parent adds each child's better total, and the root takes the better of its own two.

Real-World Scenario & Production Applications

Picking people from an org chart so that nobody is picked together with their direct manager, while maximizing a total score, is the same problem: textbooks call it planning a company party. Any hierarchy with a parent-child exclusion rule, such as not scheduling a service and the one it directly depends on in the same maintenance window, has this shape.

Subproblems & Recurrence Decomposition3 Phases

🧩Subproblem 1: Circular Constraint Decoupling

Because House 1 and House N are adjacent, robbing both is illegal. Decouple the circular graph into two independent linear subproblems: House 0 to N-2 (exclude last) and House 1 to N-1 (exclude first).

Mathematical Recurrence / Code Invariant
# Decouple into two linear runs
loot_1 = rob_linear(nums[:-1])  # Exclude last house
loot_2 = rob_linear(nums[1:])   # Exclude first house
return max(loot_1, loot_2)

Step-by-Step Execution Trace Table

Example 1, root = [3,2,3,null,3,null,1], in postorder (each row is one dfs(node) returning):

Stepnodeleft (rob, skip)right (rob, skip)rob_this = node.val + left_skip + right_skipskip_this = max(left) + max(right)Returns
13, the right child of 2(0, 0)(0, 0)3 + 0 + 0 = 30 + 0 = 0(3, 0)
22(0, 0)(3, 0)2 + 0 + 0 = 20 + 3 = 3(2, 3)
31, the right child of the right 3(0, 0)(0, 0)1 + 0 + 0 = 10 + 0 = 0(1, 0)
43, the right child of the root(0, 0)(1, 0)3 + 0 + 0 = 30 + 1 = 1(3, 1)
53, the root(2, 3)(3, 1)3 + 3 + 1 = 73 + 3 = 6(7, 6)
Endmax(7, 6) = 7
Scroll horizontally to see all columns, or expand to full screen

At step 2, taking the right child's best, max(3, 0) = 3, in rob_this would give 2 + 0 + 3 = 5: house 2 robbed together with the 3 below it. Carried up, that mistake makes the root report 12.

Trace Inputroot = [3,2,3,null,3,null,1]
Expected7

Example 1, root = [3,2,3,null,3,null,1], in postorder (each row is one dfs(node) returning):

Stepnodeleft (rob, skip)right (rob, skip)rob_this = node.val + left_skip + right_skipskip_this = max(left) + max(right)Returns
13, the right child of 2(0, 0)(0, 0)3 + 0 + 0 = 30 + 0 = 0(3, 0)
22(0, 0)(3, 0)2 + 0 + 0 = 20 + 3 = 3(2, 3)
31, the right child of the right 3(0, 0)(0, 0)1 + 0 + 0 = 10 + 0 = 0(1, 0)
43, the right child of the root(0, 0)(1, 0)3 + 0 + 0 = 30 + 1 = 1(3, 1)
53, the root(2, 3)(3, 1)3 + 3 + 1 = 73 + 3 = 6(7, 6)
Endmax(7, 6) = 7
Scroll horizontally to see all columns, or expand to full screen

At step 2, taking the right child's best, max(3, 0) = 3, in rob_this would give 2 + 0 + 3 = 5: house 2 robbed together with the 3 below it. Carried up, that mistake makes the root report 12.

Core Invariant Specification & Code Shape
Archetype Code Shape
Python
1One number per subtree can't tell the parent whether the child was robbed, so `dfs(node)` returns two: `(rob_this, skip_this)`, the best total in `node`'s subtree when `node` is robbed and when it is skipped.
2The parent applies the rule: a robbed node needs both children skipped, a skipped node lets each child take its better state. Answer with `max(dfs(root))`, since the root may be either.
3The shape: `if not node: return (0, 0)`; `left_rob, left_skip = dfs(node.left)`; `right_rob, right_skip = dfs(node.right)`; build `rob_this` and `skip_this`; `return (rob_this, skip_this)`.
4The trap: `rob_this = node.val + left_skip + right_skip`, never `node.val + max(left_rob, left_skip) + ...`: on `[3,2,3,null,3,null,1]` taking the child's max returns 12 instead of 7.

Target: House Robber III (LeetCode 337). `(rob_this, skip_this)`: one number could not tell the parent whether the child was robbed.

Boundary Model: Depth-First Recursion / Call Stack Frame Lifecycle

Subtree bottom-up aggregation: a node combines child subtree solutions upon backtrack; top-down invariants pass accumulated state down the path.

Loop Invariant Termination

Base case: if not node: return base; recursive calls on left/right child nodes.

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

Bottom-Up DFS lets every subtree hand its parent one finished number. House Robber III breaks that: the best total for a subtree depends on whether its root is robbed, and the parent needs to know which, because a robbed parent can't sit on top of a robbed child. So each subtree hands up one best total per state of its root: (rob_this, skip_this). The parent picks from those states under the rule, and nothing below the children is ever looked at again. That is Tree State DP: the House Robber recurrence of Linear DP, moved onto a tree.

🏢 The Analogy: Two Numbers From Every Team

A company picks staff for a weekend shift, and no one may work it together with their direct manager. Each team lead sends up two numbers: the best total the team can give if the lead works, and if the lead stays home. The lead's own manager never asks for names. If she works, she adds each team's stays-home number; if she stays home, she adds each team's larger number. One number per team would not be enough: she couldn't tell whether it already counts the lead she may not work with.

🪄 The Mathematical Harmony / Magic Trick
Code / Blueprint
def dfs(node):
if not node:
return (0, 0)
left_rob, left_skip = dfs(node.left)
right_rob, right_skip = dfs(node.right)
rob_this = node.val + left_skip + right_skip
skip_this = max(left_rob, left_skip) + max(right_rob, right_skip)
return (rob_this, skip_this)
 
return max(dfs(root))
 

Each pair is final when it is returned: rob_this and skip_this both respect the rule everywhere below node. The rule only links a node to its children, so the parent enforces it with the children's states alone. The trap sits on the rob_this line: a child's better state, max(left_rob, left_skip), may be the one where the child is robbed, so a robbed node adds left_skip, never the child's max.

💡 Summary

Return (rob_this, skip_this) from every node in postorder: robbed means node.val plus both children's skip states; skipped means each child's better state. The root takes the max of its two. Every node is solved once: O(N)O(N)O(N) time and O(H)O(H)O(H) recursion stack.

  • Robbing on top of a child's best: rob_this = node.val + left_skip + right_skip. Adding max(left_rob, left_skip) instead lets a robbed child sit under a robbed parent; on [3,2,3,null,3,null,1] it returns 12 instead of 7.

  • Forcing the children of a skipped node: skip_this adds max(left_rob, left_skip) + max(right_rob, right_skip). Adding left_rob + right_rob misses two skipped houses in a row ([4,1,null,2,null,3] is 7, not 6).

  • Returning the root's rob state: return max(dfs(root)). The root has no parent, so skipping it is allowed ([3,4,5,1,3,null,1] is 9, not 8).

  • Recursing on grandchildren without saving results: max(node.val + rob(grandchildren), rob(children)) solves the same subtrees again and again, exponential in the height; returning both states solves each node once.

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

You will see how a senior engineer spots a per-node choice with a parent-child rule and answers it with two states per node.

Pattern Recognition Signals

The 10-second spot

"Binary tree" plus a rule between directly linked nodes ("never break into a parent and its child") plus "the most money": every node is a yes/no choice, and which choices are allowed depends on the children's choices. That is the signal for Tree State DP: each node returns one best total per state it can be in.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

dfs(node) returns (rob_this, skip_this), the best total in node's subtree when node is robbed and when it is skipped. A robbed node needs both children skipped: rob_this = node.val + left_skip + right_skip. A skipped node lets each child take its better state: skip_this = max(left_rob, left_skip) + max(right_rob, right_skip). The answer is max(dfs(root)).

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • rob_this adds left_skip + right_skip, never max(left_rob, left_skip): a child's better state may be its robbed state, and on [3,2,3,null,3,null,1] taking it returns 12 instead of 7.

  • skip_this adds max(left_rob, left_skip), not left_rob: two skipped houses in a row can be best, as in [4,1,null,2,null,3], where robbing 4 and 3 gives 7.

  • Return max(dfs(root)), not dfs(root)[0]: the root has no parent, and on [3,4,5,1,3,null,1] the best total, 9, skips it.

  • Don't recurse on grandchildren without saving results (max(node.val + rob(grandchildren), rob(children))): the same subtrees are solved again and again, exponential in the height. Returning both states solves each node once.

The 60-Second Interview Pitch

Say this out loud before you type a single line

I'd use Tree State DP. The rule links each house to its parent, so one number per subtree isn't enough: the parent needs to know whether its child was robbed. So my DFS returns two numbers for every node: the best total in its subtree if the node is robbed, and if it is skipped. I compute them in postorder. If I rob the node, both children must be skipped, so rob_this is node.val plus the two children's skip values. If I skip it, each child is free, so skip_this adds the larger of each child's two values. The answer is the larger of the root's two. The trap is robbing a node on top of a child's best total: that best may already rob the child, which breaks the rule. Each node is solved once, so it's O(N) time and O(H) recursion stack.

So: return (rob_this, skip_this); rob_this adds the children's skip states, skip_this each child's better state, and the root takes the max.

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N)

Look at the code: dfs is called once for every node and once for every empty child slot, and a tree with N nodes has N + 1 empty slots, so there are 2N + 1 calls. Each call does O(1) work: two tuple unpacks, one sum for rob_this, and two max calls and one sum for skip_this. No subtree is solved twice, because each call hands both of its states up and the parent reuses them instead of calling dfs on a grandchild. Total: O(N).

SPACE COMPLEXITY

O(H) recursion stack

The recursion goes down one level per call, so the call stack holds at most H + 1 frames, where H is the height of the tree: about log N when it is balanced, and N on a chain. Each frame keeps four integers. The answer is one integer.

Formal Recurrence Relation

T(N)=(2N+1)⋅O(1)=O(N)T(N) = (2N + 1) \cdot O(1) = O(N)T(N)=(2N+1)⋅O(1)=O(N)

Look at the code: dfs is called once for every node and once for every empty child slot, and a tree with N nodes has N + 1 empty slots, so there are 2N + 1 calls. Each call does O(1) work: two tuple unpacks, one sum for rob_this, and two max calls and one sum for skip_this. No subtree is solved twice, because each call hands both of its states up and the parent reuses them instead of calling dfs on a grandchild. Total: O(N).

Derivation Progression

Calls

N + (N + 1) = 2N + 1

dfs runs once per node and once per empty child slot, where if not node: return (0, 0) answers at once.

Work per call

O(1)

Unpack left_rob, left_skip and right_rob, right_skip, add up rob_this, take two max values for skip_this, return the pair.

No repeated subtrees

each subtree solved once

The parent reuses the pair its child returned; it never calls dfs on a grandchild, which is what makes the brute force exponential.

Total

O(N)

2N + 1 calls, O(1) each, then one max at the root.

Variable Definitions

NNN

Number of nodes (houses) in the tree

HHH

Height of the tree: about log N when balanced, up to N on a chain

Memory Architecture & Bounds

🟣 Call Stack

O(H): one frame per level of the current root-to-node path

🔵 Auxiliary Heap

O(1): each frame keeps four integers, no extra structure

🟢 Output Space

O(1): one integer

Boundary Best / Worst Cases

Best Case

O(N)O(N)O(N) time and an O(log⁡N)O(\log N)O(logN) stack on a balanced tree

Average Case

O(N)O(N)O(N) time; the stack depth is the tree height

Worst Case

O(N)O(N)O(N) time and an O(N)O(N)O(N) stack on a chain

Graph & Tree Traversal Frontier

Graph & Tree Traversal Frontier
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "binary tree", "never break into two houses joined by an edge", "the most money". A yes/no choice at every node plus a rule between a parent and its child: Tree State DP, where each node returns one best total per state it can be in.

CONSTRAINTS & BOUNDS

1≤N≤1041 \le N \le 10^41≤N≤104 nodes and values up to 10410^4104, so any total stays below 10810^8108. Trying every set of houses is 2N2^N2N; recursing on grandchildren without saving results is exponential in the height; returning both states is O(N)O(N)O(N) time and an O(H)O(H)O(H) recursion stack.

FAANG PRODUCTION TRAPS & EDGE CASES

A chain of 10410^4104 houses is 10410^4104 calls deep, past the default recursion limit of many runtimes (CPython's is 1000): raise the limit or run the same postorder with an explicit stack. Memoizing rob(node) in a hash map keyed by node also reaches O(N)O(N)O(N), but pays for an extra map of NNN entries; returning the pair needs no map at all.

Core Algorithmic State Invariants

1. Two States per Node

`dfs(node)` returns `(rob_this, skip_this)`: the best total in the subtree when `node` is robbed and when it is skipped. One number could not tell the parent which of the two it got.

2. Robbed Parent, Skipped Children

`rob_this = node.val + left_skip + right_skip`. Adding a child's better state, `max(left_rob, left_skip)`, instead would let a robbed child sit under a robbed parent.

3. Each Node Solved Once

Postorder hands both states up, so a parent never looks below its children: O(N) time and an O(H) recursion stack.

Theory Context•Depth-First Search (DFS)
MediumLC 337

House Robber III (LeetCode 337)

You will see how returning two states per node, robbed and skipped, lets each parent apply the no-parent-and-child rule on its own.

Target Frequency:AmazonGoogleMicrosoft

The houses in a neighbourhood are joined like a binary tree. The only way in is the house at root, and every other house is reached from exactly one house above it, its parent. A node's value is the money inside that house.

A burglar may break into any set of houses on one night, with one rule: if two houses joined by an edge (a parent and its child) are both broken into, the alarm goes off. Return the most money the burglar can take without ever breaking into both ends of an edge.

Worked Examples

Example 1
Input:root = [3,2,3,null,3,null,1]
Output:7
23331
Explanation: Rob the root (3) and the two bottom houses (3 and 1). No two of them are parent and child, and 3 + 3 + 1 = 7.
Example 2
Input:root = [3,4,5,1,3,null,1]
Output:9
143351
Explanation: Skip the root and rob its two children, 4 + 5 = 9. Robbing the root instead leaves only the bottom row, 3 + 1 + 3 + 1 = 8.

⚖️Formal Constraints & Bounds

  • The number of nodes in the tree is in the range [1, 104].

  • 0 <= Node.val <= 104

Deep-Dive & Conceptual Insights

Why It Works & Core Invariant

One number per subtree hides whether the child was robbed, so each subtree returns two: the best total with its root robbed and with it skipped. A robbed parent adds its children's skipped totals, a skipped parent adds each child's better total, and the root takes the better of its own two.

Real-World Scenario & Production Applications

Picking people from an org chart so that nobody is picked together with their direct manager, while maximizing a total score, is the same problem: textbooks call it planning a company party. Any hierarchy with a parent-child exclusion rule, such as not scheduling a service and the one it directly depends on in the same maintenance window, has this shape.

Subproblems & Recurrence Decomposition3 Phases

🧩Subproblem 1: Circular Constraint Decoupling

Because House 1 and House N are adjacent, robbing both is illegal. Decouple the circular graph into two independent linear subproblems: House 0 to N-2 (exclude last) and House 1 to N-1 (exclude first).

Mathematical Recurrence / Code Invariant
# Decouple into two linear runs
loot_1 = rob_linear(nums[:-1])  # Exclude last house
loot_2 = rob_linear(nums[1:])   # Exclude first house
return max(loot_1, loot_2)

Step-by-Step Execution Trace Table

Example 1, root = [3,2,3,null,3,null,1], in postorder (each row is one dfs(node) returning):

Stepnodeleft (rob, skip)right (rob, skip)rob_this = node.val + left_skip + right_skipskip_this = max(left) + max(right)Returns
13, the right child of 2(0, 0)(0, 0)3 + 0 + 0 = 30 + 0 = 0(3, 0)
22(0, 0)(3, 0)2 + 0 + 0 = 20 + 3 = 3(2, 3)
31, the right child of the right 3(0, 0)(0, 0)1 + 0 + 0 = 10 + 0 = 0(1, 0)
43, the right child of the root(0, 0)(1, 0)3 + 0 + 0 = 30 + 1 = 1(3, 1)
53, the root(2, 3)(3, 1)3 + 3 + 1 = 73 + 3 = 6(7, 6)
Endmax(7, 6) = 7
Scroll horizontally to see all columns, or expand to full screen

At step 2, taking the right child's best, max(3, 0) = 3, in rob_this would give 2 + 0 + 3 = 5: house 2 robbed together with the 3 below it. Carried up, that mistake makes the root report 12.

Trace Inputroot = [3,2,3,null,3,null,1]
Expected7

Example 1, root = [3,2,3,null,3,null,1], in postorder (each row is one dfs(node) returning):

Stepnodeleft (rob, skip)right (rob, skip)rob_this = node.val + left_skip + right_skipskip_this = max(left) + max(right)Returns
13, the right child of 2(0, 0)(0, 0)3 + 0 + 0 = 30 + 0 = 0(3, 0)
22(0, 0)(3, 0)2 + 0 + 0 = 20 + 3 = 3(2, 3)
31, the right child of the right 3(0, 0)(0, 0)1 + 0 + 0 = 10 + 0 = 0(1, 0)
43, the right child of the root(0, 0)(1, 0)3 + 0 + 0 = 30 + 1 = 1(3, 1)
53, the root(2, 3)(3, 1)3 + 3 + 1 = 73 + 3 = 6(7, 6)
Endmax(7, 6) = 7
Scroll horizontally to see all columns, or expand to full screen

At step 2, taking the right child's best, max(3, 0) = 3, in rob_this would give 2 + 0 + 3 = 5: house 2 robbed together with the 3 below it. Carried up, that mistake makes the root report 12.

Core Invariant Specification & Code Shape
Archetype Code Shape
Python
1One number per subtree can't tell the parent whether the child was robbed, so `dfs(node)` returns two: `(rob_this, skip_this)`, the best total in `node`'s subtree when `node` is robbed and when it is skipped.
2The parent applies the rule: a robbed node needs both children skipped, a skipped node lets each child take its better state. Answer with `max(dfs(root))`, since the root may be either.
3The shape: `if not node: return (0, 0)`; `left_rob, left_skip = dfs(node.left)`; `right_rob, right_skip = dfs(node.right)`; build `rob_this` and `skip_this`; `return (rob_this, skip_this)`.
4The trap: `rob_this = node.val + left_skip + right_skip`, never `node.val + max(left_rob, left_skip) + ...`: on `[3,2,3,null,3,null,1]` taking the child's max returns 12 instead of 7.

Target: House Robber III (LeetCode 337). `(rob_this, skip_this)`: one number could not tell the parent whether the child was robbed.

Boundary Model: Depth-First Recursion / Call Stack Frame Lifecycle

Subtree bottom-up aggregation: a node combines child subtree solutions upon backtrack; top-down invariants pass accumulated state down the path.

Loop Invariant Termination

Base case: if not node: return base; recursive calls on left/right child nodes.

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

Bottom-Up DFS lets every subtree hand its parent one finished number. House Robber III breaks that: the best total for a subtree depends on whether its root is robbed, and the parent needs to know which, because a robbed parent can't sit on top of a robbed child. So each subtree hands up one best total per state of its root: (rob_this, skip_this). The parent picks from those states under the rule, and nothing below the children is ever looked at again. That is Tree State DP: the House Robber recurrence of Linear DP, moved onto a tree.

🏢 The Analogy: Two Numbers From Every Team

A company picks staff for a weekend shift, and no one may work it together with their direct manager. Each team lead sends up two numbers: the best total the team can give if the lead works, and if the lead stays home. The lead's own manager never asks for names. If she works, she adds each team's stays-home number; if she stays home, she adds each team's larger number. One number per team would not be enough: she couldn't tell whether it already counts the lead she may not work with.

🪄 The Mathematical Harmony / Magic Trick
Code / Blueprint
def dfs(node):
if not node:
return (0, 0)
left_rob, left_skip = dfs(node.left)
right_rob, right_skip = dfs(node.right)
rob_this = node.val + left_skip + right_skip
skip_this = max(left_rob, left_skip) + max(right_rob, right_skip)
return (rob_this, skip_this)
 
return max(dfs(root))
 

Each pair is final when it is returned: rob_this and skip_this both respect the rule everywhere below node. The rule only links a node to its children, so the parent enforces it with the children's states alone. The trap sits on the rob_this line: a child's better state, max(left_rob, left_skip), may be the one where the child is robbed, so a robbed node adds left_skip, never the child's max.

💡 Summary

Return (rob_this, skip_this) from every node in postorder: robbed means node.val plus both children's skip states; skipped means each child's better state. The root takes the max of its two. Every node is solved once: O(N)O(N)O(N) time and O(H)O(H)O(H) recursion stack.

  • Robbing on top of a child's best: rob_this = node.val + left_skip + right_skip. Adding max(left_rob, left_skip) instead lets a robbed child sit under a robbed parent; on [3,2,3,null,3,null,1] it returns 12 instead of 7.

  • Forcing the children of a skipped node: skip_this adds max(left_rob, left_skip) + max(right_rob, right_skip). Adding left_rob + right_rob misses two skipped houses in a row ([4,1,null,2,null,3] is 7, not 6).

  • Returning the root's rob state: return max(dfs(root)). The root has no parent, so skipping it is allowed ([3,4,5,1,3,null,1] is 9, not 8).

  • Recursing on grandchildren without saving results: max(node.val + rob(grandchildren), rob(children)) solves the same subtrees again and again, exponential in the height; returning both states solves each node once.

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

You will see how a senior engineer spots a per-node choice with a parent-child rule and answers it with two states per node.

Pattern Recognition Signals

The 10-second spot

"Binary tree" plus a rule between directly linked nodes ("never break into a parent and its child") plus "the most money": every node is a yes/no choice, and which choices are allowed depends on the children's choices. That is the signal for Tree State DP: each node returns one best total per state it can be in.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

dfs(node) returns (rob_this, skip_this), the best total in node's subtree when node is robbed and when it is skipped. A robbed node needs both children skipped: rob_this = node.val + left_skip + right_skip. A skipped node lets each child take its better state: skip_this = max(left_rob, left_skip) + max(right_rob, right_skip). The answer is max(dfs(root)).

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • rob_this adds left_skip + right_skip, never max(left_rob, left_skip): a child's better state may be its robbed state, and on [3,2,3,null,3,null,1] taking it returns 12 instead of 7.

  • skip_this adds max(left_rob, left_skip), not left_rob: two skipped houses in a row can be best, as in [4,1,null,2,null,3], where robbing 4 and 3 gives 7.

  • Return max(dfs(root)), not dfs(root)[0]: the root has no parent, and on [3,4,5,1,3,null,1] the best total, 9, skips it.

  • Don't recurse on grandchildren without saving results (max(node.val + rob(grandchildren), rob(children))): the same subtrees are solved again and again, exponential in the height. Returning both states solves each node once.

The 60-Second Interview Pitch

Say this out loud before you type a single line

I'd use Tree State DP. The rule links each house to its parent, so one number per subtree isn't enough: the parent needs to know whether its child was robbed. So my DFS returns two numbers for every node: the best total in its subtree if the node is robbed, and if it is skipped. I compute them in postorder. If I rob the node, both children must be skipped, so rob_this is node.val plus the two children's skip values. If I skip it, each child is free, so skip_this adds the larger of each child's two values. The answer is the larger of the root's two. The trap is robbing a node on top of a child's best total: that best may already rob the child, which breaks the rule. Each node is solved once, so it's O(N) time and O(H) recursion stack.

So: return (rob_this, skip_this); rob_this adds the children's skip states, skip_this each child's better state, and the root takes the max.

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N)

Look at the code: dfs is called once for every node and once for every empty child slot, and a tree with N nodes has N + 1 empty slots, so there are 2N + 1 calls. Each call does O(1) work: two tuple unpacks, one sum for rob_this, and two max calls and one sum for skip_this. No subtree is solved twice, because each call hands both of its states up and the parent reuses them instead of calling dfs on a grandchild. Total: O(N).

SPACE COMPLEXITY

O(H) recursion stack

The recursion goes down one level per call, so the call stack holds at most H + 1 frames, where H is the height of the tree: about log N when it is balanced, and N on a chain. Each frame keeps four integers. The answer is one integer.

Formal Recurrence Relation

T(N)=(2N+1)⋅O(1)=O(N)T(N) = (2N + 1) \cdot O(1) = O(N)T(N)=(2N+1)⋅O(1)=O(N)

Look at the code: dfs is called once for every node and once for every empty child slot, and a tree with N nodes has N + 1 empty slots, so there are 2N + 1 calls. Each call does O(1) work: two tuple unpacks, one sum for rob_this, and two max calls and one sum for skip_this. No subtree is solved twice, because each call hands both of its states up and the parent reuses them instead of calling dfs on a grandchild. Total: O(N).

Derivation Progression

Calls

N + (N + 1) = 2N + 1

dfs runs once per node and once per empty child slot, where if not node: return (0, 0) answers at once.

Work per call

O(1)

Unpack left_rob, left_skip and right_rob, right_skip, add up rob_this, take two max values for skip_this, return the pair.

No repeated subtrees

each subtree solved once

The parent reuses the pair its child returned; it never calls dfs on a grandchild, which is what makes the brute force exponential.

Total

O(N)

2N + 1 calls, O(1) each, then one max at the root.

Variable Definitions

NNN

Number of nodes (houses) in the tree

HHH

Height of the tree: about log N when balanced, up to N on a chain

Memory Architecture & Bounds

🟣 Call Stack

O(H): one frame per level of the current root-to-node path

🔵 Auxiliary Heap

O(1): each frame keeps four integers, no extra structure

🟢 Output Space

O(1): one integer

Boundary Best / Worst Cases

Best Case

O(N)O(N)O(N) time and an O(log⁡N)O(\log N)O(logN) stack on a balanced tree

Average Case

O(N)O(N)O(N) time; the stack depth is the tree height

Worst Case

O(N)O(N)O(N) time and an O(N)O(N)O(N) stack on a chain

Graph & Tree Traversal Frontier

Graph & Tree Traversal Frontier
Synthesizing vector architecture diagram...
Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "binary tree", "never break into two houses joined by an edge", "the most money". A yes/no choice at every node plus a rule between a parent and its child: Tree State DP, where each node returns one best total per state it can be in.

CONSTRAINTS & BOUNDS

1≤N≤1041 \le N \le 10^41≤N≤104 nodes and values up to 10410^4104, so any total stays below 10810^8108. Trying every set of houses is 2N2^N2N; recursing on grandchildren without saving results is exponential in the height; returning both states is O(N)O(N)O(N) time and an O(H)O(H)O(H) recursion stack.

FAANG PRODUCTION TRAPS & EDGE CASES

A chain of 10410^4104 houses is 10410^4104 calls deep, past the default recursion limit of many runtimes (CPython's is 1000): raise the limit or run the same postorder with an explicit stack. Memoizing rob(node) in a hash map keyed by node also reaches O(N)O(N)O(N), but pays for an extra map of NNN entries; returning the pair needs no map at all.

Core Algorithmic State Invariants

1. Two States per Node

`dfs(node)` returns `(rob_this, skip_this)`: the best total in the subtree when `node` is robbed and when it is skipped. One number could not tell the parent which of the two it got.

2. Robbed Parent, Skipped Children

`rob_this = node.val + left_skip + right_skip`. Adding a child's better state, `max(left_rob, left_skip)`, instead would let a robbed child sit under a robbed parent.

3. Each Node Solved Once

Postorder hands both states up, so a parent never looks below its children: O(N) time and an O(H) recursion stack.

Rosetta Dual-Monaco Comparison
Python 3
CANONICAL INVARIANT TEMPLATE
Loading...
CONCRETE: HOUSE ROBBER III (LEETCODE 337)
T = O(N)S = O(H) recursion stack
Loading...
Pattern Implementation Mapping TableCanonical Invariant ⟷ Concrete Code ⟷ Engineering Rationale
Canonical InvariantConcrete CodeEngineering Rationale
One best total per state of the nodedef dfs(node: Optional[TreeNode]) -> tuple[int, int]:`(rob_this, skip_this)`: one number could not tell the parent whether the child was robbed.
Empty subtreeif not node: return (0, 0)No house, no money, whichever state its missing root is in.
Children report both states first (postorder)left_rob, left_skip = dfs(node.left) right_rob, right_skip = dfs(node.right)Each pair is already the best for that child's whole subtree; nothing below the children is looked at again.
Robbed node: children skipped (the trap)rob_this = node.val + left_skip + right_skipA child's better state may be its robbed state, so a robbed node must add the skip states, never `max(left_rob, left_skip)`.
Skipped node: each child takes its better stateskip_this = max(left_rob, left_skip) + max(right_rob, right_skip)Nothing forbids a child either way, so two skipped houses in a row are allowed.
Hand both states upreturn (rob_this, skip_this)The parent applies the rule; this node only reports.
The root picks its better statereturn max(dfs(root))The root has no parent, so both of its states are allowed.
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•