Using Advanced DP with Other Patterns
1. Implementing DP + Binary Search
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;
}
2. Solving LIS with Binary Search
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) |
Example: Finding LIS using Binary Search
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])
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) |
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"]]
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 | |
| Maximum Flow with DP | Combine graph flow algorithms with DP to optimize resource allocation over time or across states. | Resource scheduling in networks | |
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];
};
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
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) |
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 |
| 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 |
| 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 |
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
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];
}