Docs
/
DSA Advanced
Chapter 3
03 — DP Advanced
DP on Trees
Maximum Path Sum (Binary Tree)
function maxPathSum(root: TreeNode | null): number {
let maxSum = -Infinity;
function dfs(node: TreeNode | null): number {
if (!node) return 0;
const left = Math.max(0, dfs(node.left)); // Ignore negative paths
const right = Math.max(0, dfs(node.right));
// Path through this node (potential answer)
maxSum = Math.max(maxSum, left + node.val + right);
// Return max single-side path (for parent to use)
return node.val + Math.max(left, right);
}
dfs(root);
return maxSum;
}
Tree Diameter
function treeDiameter(adj: number[][]): number {
const n = adj.length;
let diameter = 0;
function dfs(node: number, parent: number): number {
let max1 = 0, max2 = 0; // Two longest paths from node
for (const child of adj[node]) {
if (child === parent) continue;
const depth = dfs(child, node) + 1;
if (depth > max1) { max2 = max1; max1 = depth; }
else if (depth > max2) { max2 = depth; }
}
diameter = Math.max(diameter, max1 + max2);
return max1;
}
dfs(0, -1);
return diameter;
}
Rerooting Technique
Compute answer for all nodes as root efficiently.
// Sum of distances from each node to all other nodes
function sumOfDistances(n: number, edges: number[][]): number[] {
const adj: number[][] = Array.from({ length: n }, () => []);
for (const [u, v] of edges) { adj[u].push(v); adj[v].push(u); }
const count = new Array(n).fill(1); // Subtree size
const ans = new Array(n).fill(0);
// Pass 1: Root at 0, compute subtree sizes and ans[0]
function dfs1(node: number, parent: number, depth: number) {
ans[0] += depth;
for (const child of adj[node]) {
if (child === parent) continue;
dfs1(child, node, depth + 1);
count[node] += count[child];
}
}
// Pass 2: Reroot — when moving root from parent to child:
// ans[child] = ans[parent] - count[child] + (n - count[child])
function dfs2(node: number, parent: number) {
for (const child of adj[node]) {
if (child === parent) continue;
ans[child] = ans[node] - count[child] + (n - count[child]);
dfs2(child, node);
}
}
dfs1(0, -1, 0);
dfs2(0, -1);
return ans;
}
DP on Graphs
Shortest Path in DAG (DP approach)
// Topological order → relax edges
function shortestPathDAG(adj: [number, number][][], n: number, src: number): number[] {
const dist = new Array(n).fill(Infinity);
dist[src] = 0;
const order = topologicalSort(adj, n);
for (const u of order) {
if (dist[u] === Infinity) continue;
for (const [v, w] of adj[u]) {
dist[v] = Math.min(dist[v], dist[u] + w);
}
}
return dist;
}
Number of Paths in DAG
function countPaths(adj: number[][], n: number, src: number, dest: number): number {
const dp = new Array(n).fill(0);
dp[dest] = 1;
const order = topologicalSort(adj, n).reverse(); // Reverse topo order
for (const u of order) {
for (const v of adj[u]) {
dp[u] += dp[v];
}
}
return dp[src];
}
Digit DP
Count numbers in range [L, R] satisfying some digit property.
// Count numbers from 0 to N with digit sum = S
// State: position, currentSum, tight (is current prefix = N's prefix?)
function countWithDigitSum(N: string, S: number): number {
const n = N.length;
const memo = new Map<string, number>();
function dp(pos: number, sum: number, tight: boolean, started: boolean): number {
if (sum > S) return 0;
if (pos === n) return started && sum === S ? 1 : 0;
const key = `${pos},${sum},${tight},${started}`;
if (memo.has(key)) return memo.get(key)!;
const limit = tight ? parseInt(N[pos]) : 9;
let result = 0;
for (let d = 0; d <= limit; d++) {
result += dp(
pos + 1,
started || d > 0 ? sum + d : 0,
tight && d === limit,
started || d > 0
);
}
memo.set(key, result);
return result;
}
return dp(0, 0, true, false);
}
// Count in range [L, R]: f(R) - f(L-1)
Interval DP
Problems on contiguous intervals.
Matrix Chain Multiplication
// Minimum multiplications to multiply chain of matrices
// dims[i] = rows of matrix i, dims[n] = cols of last matrix
function matrixChain(dims: number[]): number {
const n = dims.length - 1;
const dp = Array.from({ length: n }, () => new Array(n).fill(0));
// len = chain length
for (let len = 2; len <= n; len++) {
for (let i = 0; i <= n - len; i++) {
const j = i + len - 1;
dp[i][j] = Infinity;
for (let k = i; k < j; k++) {
dp[i][j] = Math.min(
dp[i][j],
dp[i][k] + dp[k + 1][j] + dims[i] * dims[k + 1] * dims[j + 1]
);
}
}
}
return dp[0][n - 1];
}
Burst Balloons
function maxCoins(nums: number[]): number {
const arr = [1, ...nums, 1];
const n = arr.length;
const dp = Array.from({ length: n }, () => new Array(n).fill(0));
for (let len = 2; len < n; len++) {
for (let left = 0; left < n - len; left++) {
const right = left + len;
for (let k = left + 1; k < right; k++) {
dp[left][right] = Math.max(
dp[left][right],
dp[left][k] + dp[k][right] + arr[left] * arr[k] * arr[right]
);
}
}
}
return dp[0][n - 1];
}
Key Takeaways
- DP on Trees: DFS post-order; return subtree info to parent; rerooting for all-root answers
- DP on Graphs: Works on DAGs; use topological order as DP iteration order
- Digit DP: Count numbers with digit properties; state = (position, property, tight)
- Interval DP:
dp[i][j]= answer for subarray[i..j]; iterate by interval length - These patterns appear in hard interview and competitive programming problems