Skip to main content

Implementing Advanced DP Patterns

1. Applying DP on Trees

Pattern Description Use Case Example
Tree DP Dynamic Programming on tree structures where each node's state depends on its children. Computing subtree properties like sum, max, or min values.
function dfs(node) {
  if (!node) return 0;
  let sum = node.val;
  for (let child of node.children) {
    sum += dfs(child);
  }
  return sum;
}
Binary Tree Path Sum Compute path sums from root to leaves using DP. Find maximum sum path in a binary tree.
function maxPathSum(node) {
  if (!node) return 0;
  let left = Math.max(maxPathSum(node.left), 0);
  let right = Math.max(maxPathSum(node.right), 0);
  return node.val + left + right;
}
Subtree Aggregation Aggregate values from subtrees to compute global properties. Count nodes with specific property in subtree.
function countNodes(node) {
  if (!node) return 0;
  let left = countNodes(node.left);
  let right = countNodes(node.right);
  return left + right + (node.val > 0 ? 1 : 0);
}

Example: Tree Diameter Calculation

function diameterOfBinaryTree(root) {
  let maxDiameter = 0;

  function height(node) {
    if (!node) return 0;
    let left = height(node.left);
    let right = height(node.right);
    maxDiameter = Math.max(maxDiameter, left + right);
    return 1 + Math.max(left, right);
  }

  height(root);
  return maxDiameter;
}
Note: Tree DP problems often involve traversing the tree once and computing values bottom-up or top-down.

2. Implementing DP with Bitmasking

Dynamic Programming (DP) with bitmasking is a powerful technique used to solve problems where:

  • States can be represented using bits
  • There are a fixed number of elements to consider (e.g., 10 or fewer)
  • Each element can either be included or excluded from a state

This approach is particularly useful in problems like:

  • Travelling Salesman Problem (TSP)
  • Subset generation or enumeration
  • State space problems with limited choices
Bitmask Representation Description Usage Example
bitmask Integer where each bit represents inclusion/exclusion of an element 1010 means elements 1 and 3 are included, 0 and 2 are excluded
n Number of elements to consider (max 20 for efficiency) For n=4, bitmask can range from 0 to 15
mask Current bitmask state in DP transition 0101 for elements 0 and 2 included

Example: Subset Generation Using Bitmasking

const generateSubsets = (nums) => {
  const n = nums.length;
  const result = [];
  
  for (let mask = 0; mask < (1 << n); mask++) {
    const subset = [];
    for (let i = 0; i < n; i++) {
      if (mask & (1 << i)) {
        subset.push(nums[i]);
      }
    }
    result.push(subset);
  }
  
  return result;
};

// Example usage:
console.log(generateSubsets([1, 2, 3]));
// Output: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

Example: Travelling Salesman Problem (TSP) with Bitmasking

const tsp = (dist) => {
  const n = dist.length;
  const dp = Array(1 << n).fill().map(() => Array(n).fill(Infinity));
  dp[1][0] = 0; // Start at city 0

  for (let mask = 1; mask < (1 << n); mask++) {
    for (let u = 0; u < n; u++) {
      if (!(mask & (1 << u))) continue;
      
      for (let v = 0; v < n; v++) {
        if (mask & (1 << v)) continue;
        
        const newMask = mask | (1 << v);
        dp[newMask][v] = Math.min(dp[newMask][v], dp[mask][u] + dist[u][v]);
      }
    }
  }

  // Find minimum cost to return to start
  let minCost = Infinity;
  for (let i = 0; i < n; i++) {
    minCost = Math.min(minCost, dp[(1 << n) - 1][i] + dist[i][0]);
  }

  return minCost;
};

// Example usage:
const distances = [
  [0, 10, 15, 20],
  [10, 0, 35, 25],
  [15, 35, 0, 30],
  [20, 25, 30, 0]
];
console.log(tsp(distances)); // Output: 80
Note: Bitmasking is efficient for up to 20 elements due to exponential state space growth. For larger sets, consider other optimization techniques or approximation methods.

3. Solving Convex Hull Trick

The Convex Hull Trick (CHT) is a technique used to optimize dynamic programming problems where the transition function has the form:

y = mx + b

where m is the state variable and b is a constant. The key idea is to maintain a set of lines (linear functions) and efficiently query the minimum or maximum value at any given point.

Example: Minimize cost function

import bisect

class ConvexHullTrick:
    def __init__(self, is_min=True):
        self.lines = []
        self.is_min = is_min

    def add_line(self, m, b):
        # Remove lines that are dominated
        while len(self.lines) >= 2:
            # Check if the last line is dominated
            if self.is_dominated(self.lines[-2], self.lines[-1], (m, b)):
                self.lines.pop()
            else:
                break
        self.lines.append((m, b))

    def query(self, x):
        # Binary search for the best line
        idx = bisect.bisect_left(self.lines, (x, float('inf')))
        if self.is_min:
            return min(m * x + b for m, b in self.lines)
        else:
            return max(m * x + b for m, b in self.lines)

    def is_dominated(self, line1, line2, line3):
        # Check if line2 is dominated by line1 and line3
        m1, b1 = line1
        m2, b2 = line2
        m3, b3 = line3

        # For min case: line2 is dominated if it's never better than the other two
        return (b2 - b1) * (m3 - m2) <= (b3 - b2) * (m2 - m1)
Property Description Notes
Lines Set of linear functions y = mx + b Each line represents a possible transition in DP
Query Find minimum/maximum value at point x Efficiently computed using binary search
Dominance Check Remove lines that are never optimal Ensures linear complexity for queries
Note: The Convex Hull Trick is particularly useful in problems where the slope of transitions is increasing or decreasing, allowing for efficient maintenance of a convex hull of lines.

4. Implementing Divide and Conquer DP

Pattern Description Key Characteristics Use Cases
Divide and Conquer DP Breaks problem into smaller subproblems, solves recursively, and combines solutions Divide, conquer, combine phases; optimal substructure; overlapping subproblems Optimal binary search trees, matrix chain multiplication, closest pair of points

Example: Matrix Chain Multiplication

function matrixChainOrder(p) {
  const n = p.length - 1;
  const m = Array(n).fill().map(() => Array(n).fill(0));
  const s = Array(n).fill().map(() => Array(n).fill(0));

  for (let l = 2; l <= n; l++) {
    for (let i = 1; i <= n - l + 1; i++) {
      const j = i + l - 1;
      m[i][j] = Infinity;
      for (let k = i; k < j; k++) {
        const q = m[i][k] + m[k+1][j] + p[i-1] * p[k] * p[j];
        if (q < m[i][j]) {
          m[i][j] = q;
          s[i][j] = k;
        }
      }
    }
  }

  return m[1][n];
}

// Example usage:
const dimensions = [10, 20, 30, 40];
console.log(matrixChainOrder(dimensions)); // Output: 9000
Note: The divide and conquer approach for DP problems involves breaking down the problem into smaller subproblems, solving each recursively, and combining their solutions to form the overall solution. This is particularly effective in problems like matrix chain multiplication where optimal substructure and overlapping subproblems exist.

Key Takeaways

  • Divide and conquer DP is useful for problems with overlapping subproblems
  • It requires identifying the optimal substructure and combining solutions
  • Efficient implementation often involves memoization or bottom-up tabulation

5. Solving Knuth Optimization

Property Description Notes
Optimization Goal Minimize computation in dynamic programming problems with specific structure Applies to problems where cost function has monotonicity property
Monotonicity Condition For any i ≤ i' ≤ j ≤ j', C[i][j] ≥ C[i'][j] Ensures optimal substructure and allows optimization
Knuth's Criterion For any i ≤ i' ≤ j ≤ j', m[i][j'] ≤ m[i'][j] Where m[i][j] is the optimal split point for range [i,j]

Example: Matrix Chain Multiplication

function knuthOptimization(p) {
  const n = p.length - 1;
  const dp = Array(n + 1).fill().map(() => Array(n + 1).fill(0));
  const m = Array(n + 1).fill().map(() => Array(n + 1).fill(0));

  for (let l = 2; l <= n; l++) {
    for (let i = 1; i <= n - l + 1; i++) {
      const j = i + l - 1;
      dp[i][j] = Infinity;
      for (let k = m[i][j-1]; k <= m[i+1][j]; k++) {
        const cost = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j];
        if (cost < dp[i][j]) {
          dp[i][j] = cost;
          m[i][j] = k;
        }
      }
    }
  }

  return dp[1][n];
}
Note: Knuth Optimization reduces time complexity from O(n³) to O(n²) by leveraging the monotonic property of optimal split points.

Key Properties

  • Applicable when cost function satisfies monotonicity condition
  • Reduces search space for optimal split points
  • Improves DP runtime from O(n³) to O(n²)

6. Implementing Monotone Queue Optimization

Monotone queue optimization is a technique used to optimize dynamic programming (DP) problems by maintaining a monotonic queue to efficiently track the optimal states.

Property Description Usage
Queue Type Monotonic queue maintains either increasing or decreasing order of elements. Used to optimize DP transitions by reducing redundant computations.
State Representation Each element in the queue represents a state in the DP array. State is typically defined by index and value of the DP state.
Optimization Goal Reduce time complexity from O(n²) to O(n) in some DP problems. Applicable when transitions depend on previous optimal states in a specific order.

Example: Sliding Window Maximum Using Monotone Queue

function maxSlidingWindow(nums, k) {
    const dq = [];
    const result = [];

    for (let i = 0; i < nums.length; i++) {
        // Remove indices that are out of the current window
        while (dq.length > 0 && dq[0] <= i - k) {
            dq.shift();
        }

        // Remove indices whose values are less than current element
        while (dq.length > 0 && nums[dq[dq.length - 1]] < nums[i]) {
            dq.pop();
        }

        dq.push(i);

        // Add the maximum to result once window is formed
        if (i >= k - 1) {
            result.push(nums[dq[0]]);
        }
    }

    return result;
}

// Example usage:
const nums = [1, 3, -1, -3, 5, 3, 6, 7];
const k = 3;
console.log(maxSlidingWindow(nums, k)); // [3, 3, 5, 5, 6, 7]
Note: The monotone queue ensures that the front of the queue always holds the index of the maximum element in the current window.

Key Points

  • Monotone queue maintains elements in increasing or decreasing order.
  • Used to optimize DP problems with specific transition patterns.
  • Reduces time complexity in sliding window and similar problems.

7. Solving Slope Trick

Slope Trick is a powerful technique used to solve dynamic programming problems where the state transitions involve piecewise linear functions. It is particularly useful when the cost function can be represented as a set of linear segments, and we want to efficiently maintain or update these segments during transitions.

Property Description Notes
Function Representation Represents a piecewise linear function with breakpoints and slopes. Each segment has a constant slope, and the function is defined by its breakpoints and corresponding slopes.
Update Operation Efficiently updates the function when new data is added. Typically involves adding a new linear segment and merging overlapping segments to maintain the function's piecewise nature.
Query Operation Computes the minimum value of the function or evaluates it at a specific point. Can be done in O(log n) time using binary search or similar techniques.

Example: Minimum Cost to Make a String Valid

Given a string of parentheses, find the minimum number of insertions to make it valid using slope trick.

def min_insertions_slope_trick(s):
    # Initialize the slope function with zero cost
    slope_func = []
    cost = 0

    for char in s:
        if char == '(':
            # Add a new segment with slope 1
            slope_func.append((0, 1))
        else:
            # Add a new segment with slope -1
            slope_func.append((0, -1))
            # Merge segments to maintain piecewise linearity
            cost += merge_segments(slope_func)

    return cost

def merge_segments(segments):
    # Simplified logic for demonstration
    # In practice, this involves merging overlapping segments and updating costs
    return 0

# Example usage:
# print(min_insertions_slope_trick("(()))"))
Note: The slope trick is particularly effective in problems where the cost function can be modeled as a set of linear segments, and updates to these segments can be handled efficiently using data structures like heaps or balanced trees.

8. Implementing Alien's Optimization

Pattern Description Use Case Complexity
Alien's Optimization Optimization technique where we use a greedy approach to reduce the search space in dynamic programming problems. When subproblems can be solved greedily without affecting the global optimum. O(n log n) average

Example: Alien's Optimization in Coin Change

function coinChange(coins, amount) {
    const dp = new Array(amount + 1).fill(Infinity);
    dp[0] = 0;

    for (let i = 1; i <= amount; i++) {
        for (const coin of coins) {
            if (coin <= i && dp[i - coin] !== Infinity) {
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }

    return dp[amount] === Infinity ? -1 : dp[amount];
}
Note: This pattern is particularly useful in problems where greedy choices can be made without violating the optimal substructure property.

9. Solving Profile DP

Profile Description Use Case Example
State Represents the current state of the problem, often including indices or accumulated values. Used to track progress in dynamic programming problems where state transitions are required. state = [i, j, k]
Transition Function Defines how to move from one state to another, often involving recurrence relations. Essential for defining the recursive structure of DP problems. f(i, j) = f(i-1, j) + f(i, j-1)
Base Case Initial conditions or starting values for the DP recurrence. Ensures that the recursion has a stopping point and avoids infinite loops. f(0, 0) = 0
Optimal Substructure Optimal solutions contain optimal solutions to subproblems. Ensures that the problem can be broken down into smaller, manageable parts. f(i) = max(f(i-1), f(i-2) + cost[i])

Example: Climbing Stairs

function climbStairs(n) {
    if (n <= 2) return n;
    let prev2 = 1;
    let prev1 = 2;
    for (let i = 3; i <= n; i++) {
        let current = prev1 + prev2;
        prev2 = prev1;
        prev1 = current;
    }
    return prev1;
}
Note: The profile DP approach is particularly useful for problems where the state space can be reduced to a manageable size, allowing efficient computation.

10. Implementing Broken Profile DP

The Broken Profile DP pattern is a variation of dynamic programming where the state transitions are not strictly linear, and the problem involves multiple constraints or conditions that break the typical profile structure. This pattern is particularly useful in problems where:

  • Multiple states can be active simultaneously.
  • The transition between states is not deterministic based on a single previous state.
  • Some constraints or conditions affect the validity of transitions.

This pattern is often used in problems involving:

  • Resource allocation with constraints.
  • State machines with complex transitions.
  • Optimization problems where the solution space is not a simple sequence.
Aspect Description Notes
State Representation Each state is represented by a tuple or structure that includes multiple variables or constraints. Can include resource counts, time, or other relevant parameters.
Transition Logic Transitions depend on multiple conditions or constraints, not just a single previous state. May involve checking for valid combinations of parameters.
Base Case Initial state(s) must be defined, often involving a set of constraints or resource availability. Could involve multiple initial states if the problem allows for various starting conditions.
Optimization Optimization is performed over all valid states, considering the constraints. May require tracking multiple optimal paths or values.

Example: Resource Allocation with Constraints

Consider a problem where you have to allocate resources among tasks, but each task has specific constraints (e.g., time windows, resource limits). The state can be represented as a tuple (time, resources_used).

const tasks = [
  { id: 'task1', duration: 2, resources: 3 },
  { id: 'task2', duration: 1, resources: 2 },
  { id: 'task3', duration: 3, resources: 1 }
];

const maxResources = 5;
const dp = new Map();

// Initialize base case
dp.set('0_0', 0); // time 0, resources used 0

// For each task, update states
for (let i = 0; i < tasks.length; i++) {
  const task = tasks[i];
  const newStates = new Map();

  for (const [state, value] of dp.entries()) {
    const [time, resources] = state.split('_').map(Number);
    
    // If task can be scheduled
    if (resources + task.resources <= maxResources) {
      const newTime = time + task.duration;
      const newResources = resources + task.resources;
      const newState = `${newTime}_${newResources}`;
      
      if (!newStates.has(newState) || newStates.get(newState) < value + 1) {
        newStates.set(newState, value + 1);
      }
    }
  }

  // Merge new states into dp
  for (const [state, value] of newStates.entries()) {
    dp.set(state, value);
  }
}

console.log(dp); // Shows all valid states and their optimal values
Note: The Broken Profile DP pattern is particularly useful when the problem space is not a simple linear sequence, and transitions depend on multiple constraints or conditions. It allows for more complex state management and optimization strategies.

11. Solving SOS DP

Property Description Example
State Definition Represents the sum of elements in a subset up to index i sos[i] = sum of elements from 0 to i
Transition sos[i] = sos[i-1] + arr[i] sos[3] = sos[2] + arr[3]
Base Case sos[0] = arr[0] sos[0] = 5

Example: Computing SOS DP

const arr = [5, 3, 8, 2];
let sos = new Array(arr.length);
sos[0] = arr[0]; // 5
for (let i = 1; i < arr.length; i++) {
    sos[i] = sos[i - 1] + arr[i];
}
console.log(sos); // [5, 8, 16, 18]
Note: SOS DP is useful for efficiently computing prefix sums and answering range sum queries.

12. Implementing Digit DP Optimization

Digit Dynamic Programming (Digit DP) is a technique used to solve problems involving counting or summing numbers within a range that satisfy specific digit-based properties. It's particularly useful for problems where:

  • The constraints are based on digits (e.g., sum of digits, presence of a digit)
  • The solution space is too large to brute-force

Digit DP typically involves:

  1. Breaking down the problem into digit-by-digit transitions
  2. Using memoization to avoid recomputation
  3. Handling constraints like digit limits or specific properties

Example: Count Numbers with Sum of Digits Equal to K

function countNumbersWithDigitSum(n, k) {
    const memo = new Map();
    
    function dp(pos, sum, tight, started) {
        if (pos === n.length) {
            return sum === k ? 1 : 0;
        }
        
        const key = `${pos}-${sum}-${tight}-${started}`;
        if (memo.has(key)) return memo.get(key);
        
        let limit = tight ? parseInt(n[pos]) : 9;
        let result = 0;
        
        for (let digit = 0; digit <= limit; digit++) {
            if (!started && digit === 0) {
                result += dp(pos + 1, sum, tight && (digit === limit), false);
            } else {
                result += dp(pos + 1, sum + digit, tight && (digit === limit), true);
            }
        }
        
        memo.set(key, result);
        return result;
    }
    
    return dp(0, 0, true, false);
}

// Example usage:
console.log(countNumbersWithDigitSum("123", 6)); // Count numbers from 0 to 123 with digit sum = 6
Parameter Description Values
pos Current digit position in the number 0 to length of number
sum Sum of digits processed so far 0 to max possible sum
tight Whether the current digit is bounded by the input number true/false
started Whether we have started placing non-zero digits true/false
Note: The tight parameter ensures we don't exceed the upper bound of the input number. The started parameter handles leading zeros correctly.

Key Concepts in Digit DP

  • State Definition: Define a state that represents the progress of building a valid number.
  • Memoization: Use memoization to store computed states and avoid recomputation.
  • Digit-by-Digit Processing: Process each digit position one at a time, considering valid digits.
  • Boundary Conditions: Handle constraints like maximum value or specific digit properties.

Steps to Implement Digit DP

  1. Define the state variables (e.g., position, sum, tight, started)
  2. Identify base cases (e.g., end of number string)
  3. Define transitions for each digit (0 to 9, respecting tight constraint)
  4. Use memoization to cache results
  5. Handle edge cases like leading zeros and bounds

Digit DP vs Brute Force

  • Brute Force: Iterate through all numbers in range and check each one. Time complexity: O(N * D) where N is the range size, D is average number of digits.
  • Digit DP: Uses memoized recursion to avoid redundant computations. Time complexity: O(D * K * 2 * 2) where D is number of digits, K is max sum.

Was this article helpful?