Skip to main content

Using Advanced DP with Other Patterns

Dynamic Programming (DP) combined with Binary Search is a powerful technique for optimizing problems where we can use binary search to reduce the search space while maintaining optimal substructure.

Problem Description Approach Time Complexity
Maximize Profit with Limited Transactions Given a list of stock prices and maximum transactions allowed, find the maximum profit. Use DP to compute max profit for up to k transactions, and binary search on the profit value. O(nk log(max_profit))
Minimize Cost with Constraints Find minimum cost to satisfy a constraint using DP and binary search on the solution space. Binary search on cost, use DP to validate feasibility. O(n log(C))

Example: Maximize Profit with Limited Transactions

function maxProfit(k, prices) {
    if (prices.length <= 1) return 0;

    const n = prices.length;
    const dp = Array(k + 1).fill().map(() => Array(n).fill(0));

    for (let i = 1; i <= k; i++) {
        let maxDiff = -prices[0];
        for (let j = 1; j < n; j++) {
            dp[i][j] = Math.max(dp[i][j - 1], prices[j] + maxDiff);
            maxDiff = Math.max(maxDiff, dp[i - 1][j] - prices[j]);
        }
    }

    return dp[k][n - 1];
}

// Binary search on profit
function maxProfitWithBinarySearch(k, prices) {
    let low = 0;
    let high = 1e9;
    let result = 0;

    while (low <= high) {
        const mid = Math.floor((low + high) / 2);
        if (canAchieveProfit(k, prices, mid)) {
            result = mid;
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }

    return result;
}

function canAchieveProfit(k, prices, target) {
    // Simplified check for feasibility
    return maxProfit(k, prices) >= target;
}

Example: Minimize Cost with Constraints

function minimizeCost(costs, constraints) {
    let low = 0;
    let high = 1e9;
    let result = 0;

    while (low <= high) {
        const mid = Math.floor((low + high) / 2);
        if (canSatisfyConstraints(costs, constraints, mid)) {
            result = mid;
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }

    return result;
}

function canSatisfyConstraints(costs, constraints, maxCost) {
    // DP to compute min cost for each constraint
    const dp = new Array(constraints.length + 1).fill(0);
    for (let i = 1; i <= constraints.length; i++) {
        dp[i] = dp[i - 1] + costs[i - 1];
        if (dp[i] > maxCost) return false;
    }
    return true;
}

The Longest Increasing Subsequence (LIS) problem can be efficiently solved using binary search in combination with dynamic programming. This approach reduces the time complexity from O(n²) to O(n log n).

Approach Description Time Complexity Space Complexity
Binary Search + DP Uses binary search to maintain an array of the smallest tail elements for increasing subsequences of different lengths. O(n log n) O(n)
function lengthOfLIS(nums) {
    if (nums.length === 0) return 0;

    const tails = [];

    for (let num of nums) {
        let left = 0, right = tails.length;
        while (left < right) {
            let mid = Math.floor((left + right) / 2);
            if (tails[mid] < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        if (left === tails.length) {
            tails.push(num);
        } else {
            tails[left] = num;
        }
    }

    return tails.length;
}

// Example usage:
const nums = [10, 9, 2, 5, 3, 7, 101, 18];
console.log(lengthOfLIS(nums)); // Output: 4 (subsequence: [2, 3, 7, 18])
Note: The tails array maintains the smallest tail element for each possible length of increasing subsequence. This allows binary search to efficiently find where to place or update elements.

3. Implementing DP + Trie

Dynamic Programming (DP) combined with Trie data structures is a powerful pattern for solving problems involving string matching, prefix-based operations, and optimization over sequences. This combination allows efficient handling of problems where:

  • Strings are processed with prefix constraints
  • Optimal substructure exists in string operations
  • Memory and time efficiency is critical due to large input sizes

Example: Word Break with Trie and DP

Given a string s and a dictionary of words, determine if s can be segmented into a sequence of dictionary words. Using DP with a Trie for efficient prefix lookups.

class TrieNode {
  constructor() {
    this.children = {};
    this.isEndOfWord = false;
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  insert(word) {
    let node = this.root;
    for (let char of word) {
      if (!node.children[char]) {
        node.children[char] = new TrieNode();
      }
      node = node.children[char];
    }
    node.isEndOfWord = true;
  }

  search(word) {
    let node = this.root;
    for (let char of word) {
      if (!node.children[char]) return false;
      node = node.children[char];
    }
    return node.isEndOfWord;
  }

  startsWith(prefix) {
    let node = this.root;
    for (let char of prefix) {
      if (!node.children[char]) return false;
      node = node.children[char];
    }
    return true;
  }
}

function wordBreak(s, wordDict) {
  const trie = new Trie();
  for (let word of wordDict) {
    trie.insert(word);
  }

  const n = s.length;
  const dp = new Array(n + 1).fill(false);
  dp[0] = true;

  for (let i = 1; i <= n; i++) {
    for (let j = 0; j < i; j++) {
      if (dp[j] && trie.search(s.substring(j, i))) {
        dp[i] = true;
        break;
      }
    }
  }

  return dp[n];
}

// Example usage:
const s = "leetcode";
const wordDict = ["leet", "code"];
console.log(wordBreak(s, wordDict)); // true
Component Description Time Complexity Space Complexity
Trie Construction Insert all words from dictionary into the Trie O(M) where M is total length of all words O(ALPHABET_SIZE * M)
DP Table Boolean array to track valid segmentations up to index i O(N) where N is length of string O(N)
Word Search in Trie Check if substring exists as a word in the dictionary O(K) where K is length of substring O(1)
Note: The overall time complexity becomes O(N^2 + M), which is better than naive string matching approaches when the dictionary size is large.

Key Advantages of DP + Trie Combination

  • Efficient Prefix Lookups: Trie allows fast prefix-based checks during DP transitions.
  • Optimized Memory Usage: Shared prefixes in the Trie reduce memory overhead compared to storing all words separately.
  • Scalability: Especially useful for large dictionaries or frequent queries.

4. Solving Dictionary Problems

Problem Type Description Solution Pattern Example Use Case
Word Frequency Counting Count occurrences of words in a text Hash Map / Dictionary Text analysis, keyword extraction
Anagram Detection Determine if two strings are anagrams Sorted character count map Language processing, puzzle solving
Grouping by Property Organize items based on a shared attribute Dictionary with lists as values Classification systems, data categorization

Example: Anagram Detection Using Dictionary

function areAnagrams(str1, str2) {
  const charCount = {};
  
  // Count characters in first string
  for (let char of str1) {
    charCount[char] = (charCount[char] || 0) + 1;
  }
  
  // Subtract character counts from second string
  for (let char of str2) {
    if (!charCount[char]) return false;
    charCount[char]--;
  }
  
  // Check all counts are zero
  return Object.values(charCount).every(count => count === 0);
}

// Example usage
console.log(areAnagrams("listen", "silent")); // true
console.log(areAnagrams("hello", "bello"));   // false

Example: Grouping Words by Anagram Class

function groupAnagrams(words) {
  const anagramMap = {};
  
  for (let word of words) {
    // Sort characters to form a key
    const sorted = word.split('').sort().join('');
    
    if (!anagramMap[sorted]) {
      anagramMap[sorted] = [];
    }
    
    anagramMap[sorted].push(word);
  }
  
  return Object.values(anagramMap);
}

// Example usage
const words = ["eat", "tea", "tan", "ate", "nat", "bat"];
console.log(groupAnagrams(words));
// Output: [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]
Note: The key for grouping anagrams is the sorted version of the word's characters. This ensures that all anagrams map to the same key.

5. Implementing DP + Graph

Dynamic Programming (DP) combined with graph algorithms is a powerful approach for solving optimization problems that involve both sequential decisions and graph traversal. This combination allows leveraging the optimal substructure property of DP with the connectivity and path-finding capabilities of graphs.

Pattern Description Use Case Example
Shortest Path with State Use DP to compute shortest paths in graphs where each node's state affects future decisions. Routing with resource constraints
const dp = new Array(n).fill(Infinity);
dp[0] = 0;
for (let i = 0; i < n; i++) {
  for (const [to, cost] of graph[i]) {
    dp[to] = Math.min(dp[to], dp[i] + cost);
  }
}
Maximum Flow with DP Combine graph flow algorithms with DP to optimize resource allocation over time or across states. Resource scheduling in networks
const maxFlow = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
  maxFlow[i] = Math.max(maxFlow[i], maxFlow[i-1] + flow[i]);
}

Example: Longest Path in DAG with DP

const longestPath = (graph, start, end) => {
  const dp = new Array(graph.length).fill(-Infinity);
  dp[start] = 0;

  for (let u = 0; u < graph.length; u++) {
    if (dp[u] !== -Infinity) {
      for (const [v, weight] of graph[u]) {
        dp[v] = Math.max(dp[v], dp[u] + weight);
      }
    }
  }

  return dp[end];
};
Note: This pattern is particularly effective in Directed Acyclic Graphs (DAGs) where topological sorting ensures that dependencies are resolved before computing values.

6. Solving Shortest Path with Restrictions

Restriction Type Description Implementation Approach Example Use Case
Edge Weight Constraints Edges have weights that must satisfy certain conditions (e.g., non-negative, bounded) Dynamic Programming with state tracking for weight limits Routing in networks where bandwidth or cost is constrained
Capacity Constraints Each edge or node has a capacity limit (e.g., maximum flow) Modified Dijkstra or Bellman-Ford with capacity tracking Transportation systems where vehicle or resource capacity is limited
Time-Based Restrictions Paths must respect time windows or delays (e.g., earliest/latest arrival) Time-expanded graph representation with state-based DP Scheduling problems or vehicle routing with time windows
Resource Consumption Each step consumes a resource (e.g., fuel, energy) with limits State-space DP where state includes remaining resource Robotics or game AI pathfinding with energy constraints

Example: Shortest Path with Resource Limitation

const graph = {
  'A': [{ to: 'B', weight: 2, cost: 1 }, { to: 'C', weight: 4, cost: 2 }],
  'B': [{ to: 'D', weight: 3, cost: 1 }],
  'C': [{ to: 'D', weight: 1, cost: 2 }],
  'D': []
};

function shortestPathWithResource(graph, start, end, maxCost) {
  const distances = {};
  const costs = {};
  const queue = [[start, 0, 0]]; // [node, distance, cost]

  while (queue.length > 0) {
    const [node, dist, cost] = queue.shift();
    if (cost > maxCost) continue;
    if (distances[node] !== undefined && distances[node] <= dist) continue;
    distances[node] = dist;
    costs[node] = cost;

    for (const edge of graph[node]) {
      const newDist = dist + edge.weight;
      const newCost = cost + edge.cost;
      if (newDist < (distances[edge.to] || Infinity)) {
        queue.push([edge.to, newDist, newCost]);
      }
    }
  }

  return distances[end] !== undefined ? distances[end] : -1;
}

console.log(shortestPathWithResource(graph, 'A', 'D', 3)); // Output: 6
Note: The state in DP includes both distance and resource consumption to ensure constraints are respected.

Key Considerations

  • State expansion must include constraint variables (e.g., cost, time)
  • Use of memoization to avoid recomputation in constrained state space
  • Efficient graph representation (e.g., adjacency list) for fast traversal
  • Time complexity increases with additional constraints (e.g., O(V * E * C) where C is constraint space)

7. Implementing DP + Segment Tree

Example: Range Sum Query with DP and Segment Tree

class SegmentTree {
  constructor(arr) {
    this.n = arr.length;
    this.tree = new Array(4 * this.n).fill(0);
    this.build(arr, 0, 0, this.n - 1);
  }

  build(arr, node, start, end) {
    if (start === end) {
      this.tree[node] = arr[start];
    } else {
      const mid = Math.floor((start + end) / 2);
      this.build(arr, 2 * node + 1, start, mid);
      this.build(arr, 2 * node + 2, mid + 1, end);
      this.tree[node] = this.tree[2 * node + 1] + this.tree[2 * node + 2];
    }
  }

  query(node, start, end, l, r) {
    if (r < start || end < l) return 0;
    if (l <= start && end <= r) return this.tree[node];
    const mid = Math.floor((start + end) / 2);
    return this.query(2 * node + 1, start, mid, l, r) +
           this.query(2 * node + 2, mid + 1, end, l, r);
  }

  update(node, start, end, idx, val) {
    if (start === end) {
      this.tree[node] = val;
    } else {
      const mid = Math.floor((start + end) / 2);
      if (idx <= mid) {
        this.update(2 * node + 1, start, mid, idx, val);
      } else {
        this.update(2 * node + 2, mid + 1, end, idx, val);
      }
      this.tree[node] = this.tree[2 * node + 1] + this.tree[2 * node + 2];
    }
  }
}

// Example usage:
const arr = [1, 3, 5, 7, 9, 11];
const segTree = new SegmentTree(arr);
console.log(segTree.query(0, 0, 5, 1, 3)); // Output: 15 (3 + 5 + 7)
Operation Description Time Complexity Space Complexity
Build Constructs the segment tree from an array O(n) O(4n)
Query Computes sum over a range [l, r] O(log n) O(log n) (recursion stack)
Update Updates value at a specific index O(log n) O(log n) (recursion stack)
Note: Segment trees are particularly useful when you have frequent range queries and updates.

Example: Maximum Subarray with Segment Tree

class MaxSegmentTree {
  constructor(arr) {
    this.n = arr.length;
    this.tree = new Array(4 * this.n).fill(-Infinity);
    this.build(arr, 0, 0, this.n - 1);
  }

  build(arr, node, start, end) {
    if (start === end) {
      this.tree[node] = arr[start];
    } else {
      const mid = Math.floor((start + end) / 2);
      this.build(arr, 2 * node + 1, start, mid);
      this.build(arr, 2 * node + 2, mid + 1, end);
      this.tree[node] = Math.max(this.tree[2 * node + 1], this.tree[2 * node + 2]);
    }
  }

  query(node, start, end, l, r) {
    if (r < start || end < l) return -Infinity;
    if (l <= start && end <= r) return this.tree[node];
    const mid = Math.floor((start + end) / 2);
    return Math.max(this.query(2 * node + 1, start, mid, l, r),
                    this.query(2 * node + 2, mid + 1, end, l, r));
  }
}

// Example usage:
const arr = [-2, 1, -3, 4, -1, 2, 1, -5, 4];
const maxSegTree = new MaxSegmentTree(arr);
console.log(maxSegTree.query(0, 0, 8, 3, 7)); // Output: 4 (maximum in subarray [4, -1, 2, 1])

8. Optimizing DP Transitions

Transition Type Description Optimization Techniques Example
State Space Reduction Minimizing the number of states in DP by merging equivalent states or using abstraction. Use equivalence classes, state compression, or domain-specific reductions. const states = new Set(); // Compress equivalent states
Memoization Caching intermediate results to avoid recomputation of subproblems. Use hash maps or arrays for memoization tables. Example: Memoizing Fibonacci
const memo = {};
function fib(n) {
  if (n in memo) return memo[n];
  if (n <= 1) return n;
  memo[n] = fib(n - 1) + fib(n - 2);
  return memo[n];
}
Space Optimization Reducing space complexity by using only necessary previous states. Use rolling arrays or iterative DP instead of full 2D tables. Example: Space Optimized LCS
function lcs(a, b) {
  let prev = new Array(b.length + 1).fill(0);
  let curr = new Array(b.length + 1).fill(0);
  for (let i = 1; i <= a.length; i++) {
    for (let j = 1; j <= b.length; j++) {
      if (a[i - 1] === b[j - 1]) {
        curr[j] = prev[j - 1] + 1;
      } else {
        curr[j] = Math.max(curr[j - 1], prev[j]);
      }
    }
    [prev, curr] = [curr, prev];
  }
  return prev[b.length];
}
Transition Pruning Eliminating transitions that do not contribute to the optimal solution. Use heuristics or constraints to filter out unnecessary transitions. Example: Pruning in Knapsack
function knapsack(weights, values, capacity) {
  const dp = new Array(capacity + 1).fill(0);
  for (let i = 0; i < weights.length; i++) {
    for (let w = capacity; w >= weights[i]; w--) {
      dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
    }
  }
  return dp[capacity];
}
Note: Transition optimization is crucial for performance in large-scale DP problems.

9. Implementing DP + Bitmask + Graph

Dynamic Programming (DP) combined with bitmasking and graph traversal is a powerful technique for solving combinatorial optimization problems where state dependencies involve subsets of elements and graph structures.

Component Description Use Case
Bitmask Represents subsets of elements using binary flags. Each bit corresponds to an element's inclusion in the subset. Efficiently encode and manipulate subsets for state tracking in DP.
Dynamic Programming Breaks down problems into overlapping subproblems and stores results to avoid recomputation. Solve optimization problems with optimal substructure.
Graph Traversal Traverses nodes in a graph to explore paths or compute properties like shortest distances. Handle dependencies between states in DP that form a graph structure.

Example: Traveling Salesman Problem (TSP) with Bitmask

Solves TSP using bitmask DP to track visited cities and graph traversal for edge weights.

const INF = 1e9;
const n = 4; // Number of cities
const dist = [
  [0, 10, 15, 20],
  [10, 0, 35, 25],
  [15, 35, 0, 30],
  [20, 25, 30, 0]
];

// dp[mask][i] = min cost to reach city i with visited cities in mask
const dp = Array(1 << n).fill().map(() => Array(n).fill(INF));
dp[1][0] = 0;

for (let mask = 1; mask < (1 << n); mask++) {
  for (let u = 0; u < n; u++) {
    if ((mask & (1 << u)) === 0) continue;
    for (let v = 0; v < n; v++) {
      if ((mask & (1 << v)) !== 0) 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 = INF;
for (let i = 0; i < n; i++) {
  minCost = Math.min(minCost, dp[(1 << n) - 1][i] + dist[i][0]);
}
console.log(minCost); // Output: 80
Note: The bitmask approach allows efficient state representation for small sets (up to 20-25 elements). For larger graphs, consider using more advanced data structures or optimizations like bitset libraries.

Key Concepts

  • Bitmasking is used to represent subsets of cities or nodes efficiently.
  • DP computes optimal costs for each state (visited subset, current node).
  • Graph structure defines the cost matrix or adjacency relationships.

10. Comparing Hybrid Approaches

Approach Description Use Case Pros Cons
Dynamic Programming + Greedy Uses greedy choices at each step, with DP to optimize subproblems. Optimization problems where local greedy choices lead to global optimum. Efficient for problems with optimal substructure and greedy property. May not work if greedy choice does not lead to global optimum.
DP + Divide & Conquer Divide the problem into subproblems, solve with DP to avoid recomputation. Problems that can be broken into overlapping subproblems with recursive structure. Reduces time complexity by avoiding redundant computations. Can increase space complexity due to memoization or tabulation.
DP + Backtracking Uses backtracking to explore all possibilities, with DP to memoize results. Problems requiring exhaustive search but with overlapping subproblems. Ensures all solutions are considered, avoids recomputation. Higher memory usage due to storing intermediate states.
DP + Graph Traversal Applies DP to graph problems, optimizing paths or weights using dynamic programming. Shortest path, weighted graph problems where subproblems can be optimized. Effective for problems with overlapping subproblems in graph structures. Complexity increases due to graph traversal overhead.

Example: DP + Greedy in Coin Change

function coinChangeGreedy(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: While greedy algorithms can be efficient, they do not always yield the optimal solution for coin change problems. DP ensures correctness by considering all possibilities.

Was this article helpful?