Advanced DP Patterns
TL;DR
Advanced DP extends fundamental techniques to complex problem domains. Interval DP: Solve problems on contiguous subarrays using dp[i][j] = optimal solution for subarray from i to j; matrix chain multiplication minimizes scalar multiplications by trying all split points. Bitmask DP: Represent subsets as integers where bit i indicates inclusion; Traveling Salesman Problem uses dp[mask][i] = minimum cost visiting exactly nodes in mask, ending at i. Digit DP: Solve counting problems with digit constraints by building numbers digit-by-digit; track leading zeros, tight constraint, and use memoization. DP Optimization: Monge property enables Knuth-Yao optimization reducing recurrence from O(n²) to O(n log n); convex hull trick optimizes linear recurrences; divide-and-conquer optimization useful when optimal transition point increases monotonically. Each pattern recognizes structure in problem constraints to enable efficient solutions.
Core Concepts
Interval DP Fundamentals
Interval DP solves problems where answer depends on contiguous subarrays. State represents an interval [i, j] and the DP combines solutions from smaller intervals.
Key Insight: Split intervals at all possible points and find optimal split.
Recurrence Pattern:
dp[i][j] = min/max(dp[i][k] + dp[k+1][j] + cost(i,j))
for all k from i to j-1
Complexity: O(n³) for most interval DP problems—three nested loops for i, j, k.
Bitmask DP for Subsets
Bitmask DP uses bit representation where each bit indicates subset membership. Bit i = 1 means element i included in subset.
Operations:
- Check bit:
(mask >> i) & 1 - Set bit:
mask | (1 << i) - Unset bit:
mask & ~(1 << i) - Count bits: Use __builtin_popcount()
State Variables: Usually include mask and current position.
Digit DP Structure
Digit DP solves counting problems with numeric constraints. Build valid numbers digit-by-digit from most significant digit.
State Tracking:
- Position: Which digit being considered
- Tight: Whether we're still bounded by original number limit
- Started: Whether we've placed non-zero digit (for leading zeros)
Memoization: Cache results by (position, tight, started) triple.
DP Optimization Techniques
Multiple optimization techniques reduce DP complexity when problem structure allows:
Monge Property: Problem satisfies quadrangle inequality, enabling Knuth-Yao optimization from O(n³) to O(n log n).
Convex Hull Trick: Optimizes linear recurrences by maintaining convex hull of lines.
Divide-and-Conquer: When optimal transition point monotonic, use divide-and-conquer to reduce complexity.
Key Algorithms
Matrix Chain Multiplication
Problem: Given matrix dimensions, find optimal parenthesization minimizing scalar multiplications.
State: dp[i][j] = minimum multiplications to compute product of matrices i to j
Recurrence:
dp[i][j] = min(dp[i][k] + dp[k+1][j] + rows[i] * cols[k] * cols[j])
for all k from i to j-1
Insight: Different parenthesizations have different costs based on intermediate dimensions.
Bitmask TSP
Problem: Find shortest path visiting all nodes exactly once (Traveling Salesman).
State: dp[mask][i] = minimum cost to visit exactly nodes in mask, ending at i
Recurrence:
dp[mask][i] = min(dp[mask without i][j] + cost(j, i))
for all j in mask where j != i
Complexity: O(2^n * n²) - exponential but feasible for n ≤ 20.
Digit DP Counting
Problem: Count numbers in range [0, N] satisfying digit constraint.
Approach:
- Solve for [0, N] and [0, N-1], subtract results
- Build numbers digit-by-digit tracking tight constraint
- Memoize by (position, tight, other_state)
Practical Example
- Python
- Java
- TypeScript
# Matrix Chain Multiplication
def matrix_chain_order(dimensions):
"""
dimensions[i] = rows of matrix i, dimensions[i+1] = cols of matrix i
Returns minimum scalar multiplications needed
"""
n = len(dimensions) - 1 # n matrices
dp = [[0] * n for _ in range(n)]
# l is the chain length
for l in range(2, n + 1):
for i in range(n - l + 1):
j = i + l - 1
dp[i][j] = float('inf')
for k in range(i, j):
# Cost of multiplying matrices i..k and k+1..j
# Plus cost of multiplying the two results
cost = dp[i][k] + dp[k+1][j] + \
dimensions[i] * dimensions[k+1] * dimensions[j+1]
dp[i][j] = min(dp[i][j], cost)
return dp[0][n-1]
# Traveling Salesman Problem with Bitmask DP
def traveling_salesman(dist):
"""
dist[i][j] = distance from city i to city j
Returns minimum tour cost starting from city 0
"""
n = len(dist)
# dp[mask][i] = min cost to visit cities in mask, ending at i
dp = [[float('inf')] * n for _ in range(1 << n)]
# Start from city 0
dp[1][0] = 0
for mask in range(1 << n):
for u in range(n):
if dp[mask][u] == float('inf'):
continue
# Try visiting unvisited city v
for v in range(n):
if not (mask & (1 << v)): # v not visited
new_mask = mask | (1 << v)
dp[new_mask][v] = min(dp[new_mask][v],
dp[mask][u] + dist[u][v])
# Find minimum cost to visit all cities and return to 0
full_mask = (1 << n) - 1
result = float('inf')
for i in range(1, n):
result = min(result, dp[full_mask][i] + dist[i][0])
return result
# Digit DP - Count numbers with digit constraint
def count_digit_dp(num_str, max_digit):
"""
Count numbers in [0, int(num_str)] where each digit <= max_digit
"""
n = len(num_str)
memo = {}
def dp(pos, tight, leading_zero):
"""
pos: current digit position
tight: whether still bounded by num_str
leading_zero: whether we haven't placed non-zero digit yet
"""
if pos == n:
return 0 if leading_zero else 1
if (pos, tight, leading_zero) in memo:
return memo[(pos, tight, leading_zero)]
limit = int(num_str[pos]) if tight else max_digit
result = 0
for digit in range(0, limit + 1):
new_tight = tight and (digit == limit)
new_leading = leading_zero and (digit == 0)
result += dp(pos + 1, new_tight, new_leading)
memo[(pos, tight, leading_zero)] = result
return result
return dp(0, True, True)
# Tests
print(matrix_chain_order([10, 20, 30, 40, 30])) # MCM example
print(traveling_salesman([[0,10,15,20], # TSP example
[10,0,35,25],
[15,35,0,30],
[20,25,30,0]]))
print(count_digit_dp("123", 2)) # Count numbers in [0,123] with digits <= 2
public class AdvancedDPPatterns {
// Matrix Chain Multiplication
public static int matrixChainOrder(int[] dimensions) {
int n = dimensions.length - 1;
int[][] dp = new int[n][n];
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
dp[i][j] = Integer.MAX_VALUE;
for (int k = i; k < j; k++) {
int cost = dp[i][k] + dp[k+1][j] +
dimensions[i] * dimensions[k+1] * dimensions[j+1];
dp[i][j] = Math.min(dp[i][j], cost);
}
}
}
return dp[0][n-1];
}
// Traveling Salesman Problem with Bitmask DP
public static int travelingSalesman(int[][] dist) {
int n = dist.length;
int[][] dp = new int[1 << n][n];
for (int i = 0; i < (1 << n); i++) {
for (int j = 0; j < n; j++) {
dp[i][j] = Integer.MAX_VALUE / 2;
}
}
dp[1][0] = 0;
for (int mask = 0; mask < (1 << n); mask++) {
for (int u = 0; u < n; u++) {
if (dp[mask][u] == Integer.MAX_VALUE / 2) continue;
for (int v = 0; v < n; v++) {
if ((mask & (1 << v)) == 0) {
int newMask = mask | (1 << v);
dp[newMask][v] = Math.min(dp[newMask][v],
dp[mask][u] + dist[u][v]);
}
}
}
}
int fullMask = (1 << n) - 1;
int result = Integer.MAX_VALUE;
for (int i = 1; i < n; i++) {
result = Math.min(result, dp[fullMask][i] + dist[i][0]);
}
return result;
}
// Digit DP - Count numbers with digit constraint
public static int countDigitDP(String numStr, int maxDigit) {
int n = numStr.length();
int[][][] memo = new int[n][2][2];
for (int i = 0; i < n; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
memo[i][j][k] = -1;
}
}
}
return dp(0, 1, 1, numStr, maxDigit, memo);
}
private static int dp(int pos, int tight, int leadingZero,
String numStr, int maxDigit, int[][][] memo) {
if (pos == numStr.length()) {
return leadingZero == 0 ? 1 : 0;
}
if (memo[pos][tight][leadingZero] != -1) {
return memo[pos][tight][leadingZero];
}
int limit = tight == 1 ? (numStr.charAt(pos) - '0') : maxDigit;
int result = 0;
for (int digit = 0; digit <= limit; digit++) {
int newTight = (tight == 1 && digit == limit) ? 1 : 0;
int newLeading = (leadingZero == 1 && digit == 0) ? 1 : 0;
result += dp(pos + 1, newTight, newLeading, numStr, maxDigit, memo);
}
return memo[pos][tight][leadingZero] = result;
}
public static void main(String[] args) {
System.out.println(matrixChainOrder(new int[]{10,20,30,40,30}));
System.out.println(travelingSalesman(new int[][]{{0,10,15,20},
{10,0,35,25},
{15,35,0,30},
{20,25,30,0}}));
System.out.println(countDigitDP("123", 2));
}
}
// Matrix Chain Multiplication
function matrixChainOrder(dimensions: number[]): number {
const n = dimensions.length - 1;
const dp = Array(n).fill(0).map(() => Array(n).fill(0));
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++) {
const cost = dp[i][k] + dp[k+1][j] +
dimensions[i] * dimensions[k+1] * dimensions[j+1];
dp[i][j] = Math.min(dp[i][j], cost);
}
}
}
return dp[0][n-1];
}
// Traveling Salesman Problem with Bitmask DP
function travelingSalesman(dist: number[][]): number {
const n = dist.length;
const dp = Array(1 << n).fill(0).map(() => Array(n).fill(Infinity));
dp[1][0] = 0;
for (let mask = 0; mask < (1 << n); mask++) {
for (let u = 0; u < n; u++) {
if (dp[mask][u] === Infinity) continue;
for (let v = 0; v < n; v++) {
if (!(mask & (1 << v))) {
const newMask = mask | (1 << v);
dp[newMask][v] = Math.min(dp[newMask][v],
dp[mask][u] + dist[u][v]);
}
}
}
}
const fullMask = (1 << n) - 1;
let result = Infinity;
for (let i = 1; i < n; i++) {
result = Math.min(result, dp[fullMask][i] + dist[i][0]);
}
return result;
}
// Digit DP - Count numbers with digit constraint
function countDigitDP(numStr: string, maxDigit: number): number {
const n = numStr.length;
const memo = new Map<string, number>();
function dp(pos: number, tight: boolean, leadingZero: boolean): number {
if (pos === n) {
return leadingZero ? 0 : 1;
}
const key = `${pos},${tight},${leadingZero}`;
if (memo.has(key)) return memo.get(key)!;
const limit = tight ? parseInt(numStr[pos]) : maxDigit;
let result = 0;
for (let digit = 0; digit <= limit; digit++) {
const newTight = tight && (digit === limit);
const newLeading = leadingZero && (digit === 0);
result += dp(pos + 1, newTight, newLeading);
}
memo.set(key, result);
return result;
}
return dp(0, true, true);
}
console.log(matrixChainOrder([10,20,30,40,30]));
console.log(travelingSalesman([[0,10,15,20],
[10,0,35,25],
[15,35,0,30],
[20,25,30,0]]));
console.log(countDigitDP("123", 2));
Common Pitfalls
Incorrect bitmask operations: Bit manipulations are error-prone. Test mask operations with concrete examples before using in DP.
Forgetting tight constraint in digit DP: Digit DP requires carefully tracking whether still bounded. Forgetting leads to counting invalid numbers.
Off-by-one in interval DP: Interval endpoints often cause errors. Explicitly test boundary cases like single elements and full range.
Memory explosion with bitmask DP: 2^20 is manageable; 2^30 is not. Check problem constraints before attempting bitmask DP on large n.
Missing state in memoization: Memoization key must include all parameters affecting result. Forgetting one parameter causes incorrect memoization.
Not initializing base cases properly: For TSP and similar problems, base cases determine entire solution. Verify base cases carefully.
Self-Check
- In matrix chain multiplication, why is the order of parenthesization important but the order of matrices is not?
- How does bitmask DP scale with n? What's the practical limit?
- In digit DP, what does the "tight" constraint track and why is it necessary?
One Takeaway
Advanced DP patterns recognize mathematical structure in problems: interval decomposition suggests interval DP, subset enumeration suggests bitmask DP, digit constraints suggest digit DP. Optimization techniques like Knuth-Yao apply when problem satisfies specific mathematical properties (monge inequality, convex hull structure). The breakthrough in advanced DP comes from recognizing which pattern applies—once identified, the DP formulation usually follows naturally. Practice identifying problem structure before attempting implementation.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- Halim, S., & Halim, F. (2013). Competitive Programming (3rd ed.). Lulu Independent Publish.