Loading learning content...
You've identified recursive structure. You've decomposed the problem into subproblems. Now comes the crucial final step: assembling subproblem solutions into the complete answer.
This combination step is where many recursive solutions succeed or fail. A clean combination produces elegant code; a messy one creates bugs and confusion. The way you combine results determines not just correctness but also the clarity and efficiency of your solution.
This page explores the art and science of solution assembly—how to think about combining partial results and the patterns that make this process systematic and reliable.
By mastering this page, you will: (1) Understand the different types of combination operations, (2) Learn to design combination logic that's correct by construction, (3) Recognize common combination patterns across problem types, (4) Develop intuition for debugging combination-related bugs.
The way subproblem solutions combine varies dramatically across problem types. Understanding these categories helps you quickly identify the right approach.
Subproblem results combine through mathematical operations: addition, multiplication, max, min, AND, OR.
Examples:
total = node.value + sum(left) + sum(right)height = 1 + max(height(left), height(right))paths = paths(left) + paths(right)valid = check(node) && valid(left) && valid(right)Key Property: Order of combination usually doesn't matter (operations are associative).
Choose one subproblem result based on some criterion.
Examples:
Key Property: Not all subproblem results are used—one is selected.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
interface TreeNode { value: number; left?: TreeNode; right?: TreeNode;} // Type 1: Aggregation - combine all subproblem resultsfunction treeSum(node: TreeNode | undefined): number { if (!node) return 0; // Addition aggregates all values return node.value + treeSum(node.left) + treeSum(node.right);} function treeHeight(node: TreeNode | undefined): number { if (!node) return 0; // Max aggregates depths, +1 for current level return 1 + Math.max(treeHeight(node.left), treeHeight(node.right));} // Type 2: Selection - choose one resultfunction binarySearch(arr: number[], target: number, lo = 0, hi = arr.length - 1): number { if (lo > hi) return -1; const mid = Math.floor((lo + hi) / 2); if (arr[mid] === target) return mid; // Select which subproblem to explore (not both!) return arr[mid] < target ? binarySearch(arr, target, mid + 1, hi) : binarySearch(arr, target, lo, mid - 1);} // Type 3: Construction - build new structurefunction reverseList(arr: number[]): number[] { if (arr.length <= 1) return [...arr]; // Build result by placing first at end return [...reverseList(arr.slice(1)), arr[0]];} // Type 4: Merge - interleave maintaining orderfunction merge(left: number[], right: number[]): number[] { if (!left.length) return right; if (!right.length) return left; // Merge by comparing fronts and recursing if (left[0] <= right[0]) { return [left[0], ...merge(left.slice(1), right)]; } else { return [right[0], ...merge(left, right.slice(1))]; }}Build a new data structure from subproblem results.
Examples:
[...reverse(rest), first]Key Property: The combination creates new structure, not just values.
Interleave or systematically combine ordered subproblem results.
Examples:
Key Property: Order matters; elements from different sources must be correctly interleaved.
A powerful mental model is to think of your recursive solution as having an explicit combine function:
solve(problem) = combine(solve(subproblem1), solve(subproblem2), ..., local_contribution)
The combine function takes subproblem solutions and produces the full solution. Making this function explicit—even if just in your mind—clarifies your thinking.
Ask these questions:
What are the inputs? The results from each recursive call, plus any local contribution from the current level.
What's the output type? It must match what the enclosing recursive call expects.
Is it correct? If subproblem solutions are correct, does combining them correctly solve the original problem?
Is it efficient? Does the combination add minimal overhead?
Recursive correctness follows induction: if your base case is correct, and your combine function produces a correct solution from correct subproblem solutions, then your entire solution is correct. Focus on these two pieces, and correctness follows automatically.
Problem: Find the maximum sum path from root to any leaf in a binary tree.
Thinking through the combine function:
The combine function is: max(leftMax, rightMax) + node.value
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
interface TreeNode { value: number; left?: TreeNode; right?: TreeNode;} // Maximum path sum from root to any leaffunction maxPathToLeaf(node: TreeNode | undefined): number { // Base case: empty subtree contributes nothing // (We handle leaves specially to avoid -Infinity issues) if (!node) return -Infinity; // Leaf: just this node's value if (!node.left && !node.right) return node.value; // Get subproblem solutions const leftMax = maxPathToLeaf(node.left); const rightMax = maxPathToLeaf(node.right); // Combine: take better path, add current value // // The 'combine function' is explicitly: // combine(leftMax, rightMax, nodeValue) = max(leftMax, rightMax) + nodeValue // return Math.max(leftMax, rightMax) + node.value;} // Verify with example:// 10// / \// 5 15// / \// 3 7// // maxPathToLeaf(3) = 3 (leaf)// maxPathToLeaf(7) = 7 (leaf)// maxPathToLeaf(5) = max(3, 7) + 5 = 12// maxPathToLeaf(15) = 15 (leaf)// maxPathToLeaf(10) = max(12, 15) + 10 = 25 const tree: TreeNode = { value: 10, left: { value: 5, left: { value: 3 }, right: { value: 7 } }, right: { value: 15 },};console.log(maxPathToLeaf(tree)); // 25Certain combination patterns appear repeatedly. Internalizing them accelerates your solution design.
Combine subresult with local contribution using an associative operation.
combine(subresult, local) = subresult ⊕ local
where ⊕ is +, *, max, min, &&, ||, etc.
Examples: Sum of elements, product of elements, any/all checks.
Combine by collecting all results into a single collection.
combine(results1, results2) = results1 ++ results2
Examples: All paths in tree, all permutations, all valid solutions.
Transform subresults and include local contribution.
combine(subresults, local) = [transform(s) for s in subresults] + [local]
Examples: Building solutions that include current element, path collection.
| Pattern | Operation | Identity | Use Case |
|---|---|---|---|
| Sum | a + b | 0 | Counting, totals |
| Product | a * b | 1 | Combinatorics, probability |
| Maximum | max(a, b) | -∞ | Optimization, best path |
| Minimum | min(a, b) | +∞ | Optimization, shortest |
| All (AND) | a && b | true | Validation, constraints |
| Any (OR) | a || b | false | Existence, search |
| Concatenate | a ++ b | [] | Collection, generation |
Each combination operation has an identity value—the value that doesn't change the result. For sum it's 0, for product it's 1, for max it's -∞. Your base case should return the identity for your combination operation. This ensures correct behavior even with empty subproblems.
Some problems have combination logic that's less straightforward. Let's examine common tricky cases.
Sometimes the information you need to compute the result differs from the result itself.
Example: Tree diameter (longest path between any two nodes).
The diameter might pass through any node, but to compute it at each node, you need the heights of subtrees. The combination involves computing diameter locally (left_height + right_height) while returning height upward.
Solution: Return multiple values, or use a helper function that tracks different information.
Not all subproblems contribute equally.
Example: Expression evaluation where operator determines how to combine left and right values.
eval(a + b) = eval(a) + eval(b)
eval(a * b) = eval(a) * eval(b)
eval(a - b) = eval(a) - eval(b)
Solution: Case analysis based on the current node's operator.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
interface TreeNode { value: number; left?: TreeNode; right?: TreeNode;} // Tricky Scenario 1: Need to track height but compute diameterfunction treeDiameter(root: TreeNode | undefined): number { let maxDiameter = 0; // Helper returns HEIGHT but updates diameter as side effect function height(node: TreeNode | undefined): number { if (!node) return 0; const leftHeight = height(node.left); const rightHeight = height(node.right); // Diameter through this node = left + right heights const diameterThroughNode = leftHeight + rightHeight; maxDiameter = Math.max(maxDiameter, diameterThroughNode); // Return height for parent's calculation return 1 + Math.max(leftHeight, rightHeight); } height(root); return maxDiameter;} // Alternative: Return both values as tuplefunction heightAndDiameter(node: TreeNode | undefined): [number, number] { if (!node) return [0, 0]; const [leftH, leftD] = heightAndDiameter(node.left); const [rightH, rightD] = heightAndDiameter(node.right); const height = 1 + Math.max(leftH, rightH); const diameterHere = leftH + rightH; const diameter = Math.max(diameterHere, leftD, rightD); return [height, diameter];} // Tricky Scenario 2: Operator-dependent combinationtype ExprNode = | { type: 'number'; value: number } | { type: 'op'; op: '+' | '-' | '*' | '/'; left: ExprNode; right: ExprNode }; function evaluate(expr: ExprNode): number { if (expr.type === 'number') return expr.value; const left = evaluate(expr.left); const right = evaluate(expr.right); // Combination depends on operator switch (expr.op) { case '+': return left + right; case '-': return left - right; case '*': return left * right; case '/': return left / right; }}When your recursive solution gives wrong answers, the bug is often in the combination step. Here's how to diagnose and fix these issues.
If your base case returns the wrong identity value, all results will be off.
Symptoms: Off-by-one errors, wrong totals, missing elements.
Fix: Verify base case returns the identity for your combination operation.
Forgetting to include the current level's contribution.
Symptoms: Results seem partial, missing expected values.
Fix: Trace through a small example and verify each level contributes.
Using + when you need max, or concatenation when you need interleaving.
Symptoms: Structurally wrong results, not just numerically wrong.
Fix: Return to problem statement and verify what combination means.
For operations that aren't commutative (subtraction, division, concatenation), order matters.
Symptoms: Results are flipped, reversed, or permuted incorrectly.
Fix: Verify operand order matches problem requirements.
When debugging combination logic, trace through the smallest non-trivial example by hand. For trees, use a 3-node tree. For lists, use 2-3 elements. Watch exactly what each recursive call returns and how those values combine. This almost always reveals the bug.
You now understand how to design and implement the combination step of recursive solutions. This completes the core recursive problem-solving framework: identify structure, decompose, combine. The next page solidifies these skills with practice problems and patterns.