Problems
Master dynamic programming by solving these carefully selected problems. Each problem demonstrates different DP techniques and builds your problem-solving skills progressively.
Problem Categories
Easy Problems
1. Climbing Stairs
LeetCode 70 | Difficulty: Easy | Acceptance: 52.1%
Brief: Find how many distinct ways you can climb to the top of a staircase where you can take 1 or 2 steps at a time.
Why this pattern?: The number of ways to reach step n depends on the sum of ways to reach steps n-1 and n-2, creating overlapping subproblems.
Key Insight: This is essentially the Fibonacci sequence - each step’s solution builds on the previous two steps.
Visual State Transition:
graph LR
A[Step 0] --> B[Step 1]
A --> C[Step 2]
B --> D[Step 3]
C --> D
D --> E[Step 4]
C --> E
Code Solution:
function climbStairs(n) {
if (n <= 1) return 1;
let prev2 = 1, prev1 = 1;
for (let i = 2; i <= n; i++) {
const current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}def climb_stairs(n):
if n <= 1:
return 1
prev2, prev1 = 1, 1
for i in range(2, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current
return prev1public class Solution {
public int climbStairs(int n) {
if (n <= 1) return 1;
int prev2 = 1, prev1 = 1;
for (int i = 2; i <= n; i++) {
int current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
}class Solution {
public:
int climbStairs(int n) {
if (n <= 1) return 1;
int prev2 = 1, prev1 = 1;
for (int i = 2; i <= n; i++) {
int current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
};func climbStairs(n int) int {
if n <= 1 {
return 1
}
prev2, prev1 := 1, 1
for i := 2; i <= n; i++ {
current := prev1 + prev2
prev2, prev1 = prev1, current
}
return prev1
}def climb_stairs(n)
return 1 if n <= 1
prev2, prev1 = 1, 1
(2..n).each do |i|
current = prev1 + prev2
prev2, prev1 = prev1, current
end
prev1
end2. House Robber
LeetCode 198 | Difficulty: Medium | Acceptance: 47.6%
Brief: Maximize money robbed from houses arranged in a line without robbing adjacent houses.
Why this pattern?: At each house, you must decide whether to rob it (adding its value to the solution from two houses back) or skip it (keeping the best solution from the previous house).
Key Insight: The optimal solution at house i is the maximum of robbing house i plus the best solution from house i-2, or the best solution from house i-1.
Visual State Transition:
graph TD
A[House 1] --> B[House 2]
A --> C[House 3]
B --> D[House 4]
C --> D
C --> E[House 5]
D --> E
Code Solution:
function rob(nums) {
if (!nums.length) return 0;
let prev2 = 0, prev1 = nums[0];
for (let i = 1; i < nums.length; i++) {
const current = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = current;
}
return prev1;
}def rob(nums):
if not nums:
return 0
prev2, prev1 = 0, nums[0]
for i in range(1, len(nums)):
current = max(prev1, prev2 + nums[i])
prev2, prev1 = prev1, current
return prev1public class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
int prev2 = 0, prev1 = nums[0];
for (int i = 1; i < nums.length; i++) {
int current = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
}class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty()) return 0;
int prev2 = 0, prev1 = nums[0];
for (int i = 1; i < nums.size(); i++) {
int current = max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
};func rob(nums []int) int {
if len(nums) == 0 {
return 0
}
prev2, prev1 := 0, nums[0]
for i := 1; i < len(nums); i++ {
current := max(prev1, prev2+nums[i])
prev2, prev1 = prev1, current
}
return prev1
}
func max(a, b int) int {
if a > b {
return a
}
return b
}def rob(nums)
return 0 if nums.empty?
prev2, prev1 = 0, nums[0]
(1...nums.length).each do |i|
current = [prev1, prev2 + nums[i]].max
prev2, prev1 = prev1, current
end
prev1
endMedium Problems
3. Longest Increasing Subsequence
LeetCode 300 | Difficulty: Medium | Acceptance: 45.8%
Brief: Find the length of the longest strictly increasing subsequence in an integer array.
Why this pattern?: For each element, the longest increasing subsequence ending at that element depends on all previous elements that are smaller.
Key Insight: For each position i, check all previous positions j where nums[j] < nums[i] and extend the LIS ending at j.
Visual State Transition:
graph TD
A[Position 0] --> B[Position 1]
A --> C[Position 2]
B --> D[Position 3]
C --> D
C --> E[Position 4]
D --> E
Code Solution:
function lengthOfLIS(nums) {
if (!nums.length) return 0;
const dp = new Array(nums.length).fill(1);
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp);
}def length_of_lis(nums):
if not nums:
return 0
dp = [1] * len(nums)
for i in range(1, len(nums)):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)public class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) return 0;
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int max = 1;
for (int val : dp) {
max = Math.max(max, val);
}
return max;
}
}class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if (nums.empty()) return 0;
vector<int> dp(nums.size(), 1);
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};func lengthOfLIS(nums []int) int {
if len(nums) == 0 {
return 0
}
dp := make([]int, len(nums))
for i := range dp {
dp[i] = 1
}
for i := 1; i < len(nums); i++ {
for j := 0; j < i; j++ {
if nums[i] > nums[j] {
dp[i] = max(dp[i], dp[j]+1)
}
}
}
max := 1
for _, val := range dp {
if val > max {
max = val
}
}
return max
}
func max(a, b int) int {
if a > b {
return a
}
return b
}def length_of_lis(nums)
return 0 if nums.empty?
dp = Array.new(nums.length, 1)
(1...nums.length).each do |i|
(0...i).each do |j|
if nums[i] > nums[j]
dp[i] = [dp[i], dp[j] + 1].max
end
end
end
dp.max
end4. Coin Change
LeetCode 322 | Difficulty: Medium | Acceptance: 39.5%
Brief: Find the fewest number of coins needed to make up a given amount using unlimited supply of given coin denominations.
Why this pattern?: The minimum coins needed for amount i can be found by considering all coin denominations and taking the minimum of (1 + coins needed for amount i - coin_value).
Key Insight: This is an unbounded knapsack problem where each coin can be used multiple times.
Visual State Transition:
Amount: 0 1 2 3 4 5
Coins: [1, 2, 5]
DP: [0, 1, 1, 2, 2, 3]
Process:
1. For coin=1: dp[1]=1, dp[2]=2, dp[3]=3, dp[4]=4, dp[5]=5
2. For coin=2: dp[2]=min(2,1)=1, dp[3]=min(3,2)=2, dp[4]=min(4,3)=3, dp[5]=min(5,4)=4
3. For coin=5: dp[5]=min(4,1)=1Code Solution:
function coinChange(coins, amount) {
const dp = new Array(amount + 1).fill(amount + 1);
dp[0] = 0;
for (const coin of coins) {
for (let i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] === amount + 1 ? -1 : dp[amount];
}def coin_change(coins, amount):
dp = [amount + 1] * (amount + 1)
dp[0] = 0
for coin in coins:
for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)
return -1 if dp[amount] == amount + 1 else dp[amount]public class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
};func coinChange(coins []int, amount int) int {
dp := make([]int, amount+1)
for i := range dp {
dp[i] = amount + 1
}
dp[0] = 0
for _, coin := range coins {
for i := coin; i <= amount; i++ {
dp[i] = min(dp[i], dp[i-coin]+1)
}
}
if dp[amount] == amount+1 {
return -1
}
return dp[amount]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}def coin_change(coins, amount)
dp = Array.new(amount + 1, amount + 1)
dp[0] = 0
coins.each do |coin|
(coin..amount).each do |i|
dp[i] = [dp[i], dp[i - coin] + 1].min
end
end
dp[amount] == amount + 1 ? -1 : dp[amount]
end5. Edit Distance
LeetCode 72 | Difficulty: Hard | Acceptance: 51.3%
Brief: Find the minimum number of operations (insert, delete, replace) required to convert word1 to word2.
Why this pattern?: The edit distance between prefixes of the two words can be built up from smaller subproblems by considering the last characters.
Key Insight: If the last characters match, use the solution for the prefixes without these characters. If they don’t match, consider all three possible operations.
Visual State Transition:
flowchart TD
A[Start: Empty strings] --> B[Compare chars]
B --> C{Characters match?}
C -->|Yes| D[Use diagonal value]
C -->|No| E[Take minimum of three options]
D --> F[Fill current cell]
E --> F
F --> G{End of strings?}
G -->|No| B
G -->|Yes| H[Result: Edit distance]
style A fill:#e1f5fe
style H fill:#e8f5e8
style C fill:#fff3e0
Code Solution:
function minDistance(word1, word2) {
const m = word1.length, n = word2.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
// Initialize base cases
for (let i = 0; i <= m; i++) dp[i][0] = i;
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
dp[i - 1][j] + 1, // Delete
dp[i][j - 1] + 1, // Insert
dp[i - 1][j - 1] + 1 // Replace
);
}
}
}
return dp[m][n];
}def min_distance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Initialize base cases
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(
dp[i - 1][j] + 1, # Delete
dp[i][j - 1] + 1, # Insert
dp[i - 1][j - 1] + 1 # Replace
)
return dp[m][n]public class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// Initialize base cases
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
Math.min(dp[i - 1][j] + 1, // Delete
dp[i][j - 1] + 1), // Insert
dp[i - 1][j - 1] + 1 // Replace
);
}
}
}
return dp[m][n];
}
}class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// Initialize base cases
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min({
dp[i - 1][j] + 1, // Delete
dp[i][j - 1] + 1, // Insert
dp[i - 1][j - 1] + 1 // Replace
});
}
}
}
return dp[m][n];
}
};func minDistance(word1 string, word2 string) int {
m, n := len(word1), len(word2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
// Initialize base cases
for i := 0; i <= m; i++ {
dp[i][0] = i
}
for j := 0; j <= n; j++ {
dp[0][j] = j
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if word1[i-1] == word2[j-1] {
dp[i][j] = dp[i-1][j-1]
} else {
dp[i][j] = min(
min(dp[i-1][j]+1, // Delete
dp[i][j-1]+1), // Insert
dp[i-1][j-1]+1) // Replace
}
}
}
return dp[m][n]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}def min_distance(word1, word2)
m, n = word1.length, word2.length
dp = Array.new(m + 1) { Array.new(n + 1, 0) }
# Initialize base cases
(0..m).each { |i| dp[i][0] = i }
(0..n).each { |j| dp[0][j] = j }
(1..m).each do |i|
(1..n).each do |j|
if word1[i - 1] == word2[j - 1]
dp[i][j] = dp[i - 1][j - 1]
else
dp[i][j] = [
dp[i - 1][j] + 1, # Delete
dp[i][j - 1] + 1, # Insert
dp[i - 1][j - 1] + 1 # Replace
].min
end
end
end
dp[m][n]
end6. Jump Game II
LeetCode 45 | Difficulty: Medium | Acceptance: 39.7%
Brief: Find the minimum number of jumps needed to reach the last index in an array where each element represents maximum jump length.
Why this pattern?: The minimum jumps to reach position i can be found by considering all previous positions that can reach i.
Key Insight: For each position, update all reachable positions with the minimum jumps needed to reach them.
Visual State Transition:
graph TD
A[Position 0] --> B[Position 1]
A --> C[Position 2]
A --> D[Position 3]
B --> E[Position 4]
C --> E
C --> F[Position 5]
D --> F
Code Solution:
function jump(nums) {
if (nums.length <= 1) return 0;
const dp = new Array(nums.length).fill(Infinity);
dp[0] = 0;
for (let i = 0; i < nums.length; i++) {
for (let j = 1; j <= nums[i] && i + j < nums.length; j++) {
dp[i + j] = Math.min(dp[i + j], dp[i] + 1);
}
}
return dp[nums.length - 1];
}def jump(nums):
if len(nums) <= 1:
return 0
dp = [float('inf')] * len(nums)
dp[0] = 0
for i in range(len(nums)):
for j in range(1, nums[i] + 1):
if i + j < len(nums):
dp[i + j] = min(dp[i + j], dp[i] + 1)
return dp[-1]public class Solution {
public int jump(int[] nums) {
if (nums.length <= 1) return 0;
int[] dp = new int[nums.length];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 0; i < nums.length; i++) {
for (int j = 1; j <= nums[i] && i + j < nums.length; j++) {
dp[i + j] = Math.min(dp[i + j], dp[i] + 1);
}
}
return dp[nums.length - 1];
}
}class Solution {
public:
int jump(vector<int>& nums) {
if (nums.size() <= 1) return 0;
vector<int> dp(nums.size(), INT_MAX);
dp[0] = 0;
for (int i = 0; i < nums.size(); i++) {
for (int j = 1; j <= nums[i] && i + j < nums.size(); j++) {
dp[i + j] = min(dp[i + j], dp[i] + 1);
}
}
return dp[nums.size() - 1];
}
};func jump(nums []int) int {
if len(nums) <= 1 {
return 0
}
dp := make([]int, len(nums))
for i := range dp {
dp[i] = math.MaxInt32
}
dp[0] = 0
for i := 0; i < len(nums); i++ {
for j := 1; j <= nums[i] && i+j < len(nums); j++ {
dp[i+j] = min(dp[i+j], dp[i]+1)
}
}
return dp[len(nums)-1]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}def jump(nums)
return 0 if nums.length <= 1
dp = Array.new(nums.length, Float::INFINITY)
dp[0] = 0
(0...nums.length).each do |i|
(1..nums[i]).each do |j|
if i + j < nums.length
dp[i + j] = [dp[i + j], dp[i] + 1].min
end
end
end
dp[-1]
endHard Problems
7. Regular Expression Matching
LeetCode 10 | Difficulty: Hard | Acceptance: 27.9%
Brief: Implement regular expression matching with support for ‘.’ and ‘’ where ‘.’ matches any single character and ‘’ matches zero or more of the preceding element.
Why this pattern?: The matching of pattern p against string s can be broken down into subproblems based on the current characters and whether the next character in p is ‘*’.
Key Insight: When encountering ‘*’, we must consider both zero occurrences (skip the pattern) and one or more occurrences (match current character and continue).
Visual State Transition:
stateDiagram-v2
[*] --> Start: Begin matching
Start --> MatchChar: Single char match
Start --> ZeroOccur: * with zero occurrence
Start --> OneOrMore: * with one+ occurrence
MatchChar --> Start: Continue
ZeroOccur --> Start: Skip pattern
OneOrMore --> OneOrMore: Continue matching
OneOrMore --> Start: Stop matching
Start --> [*]: End of strings
Code Solution:
function isMatch(s, p) {
const m = s.length, n = p.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(false));
dp[0][0] = true; // Empty string matches empty pattern
// Handle patterns with *
for (let j = 1; j <= n; j++) {
if (p[j - 1] === '*') {
dp[0][j] = dp[0][j - 2]; // Zero occurrence
}
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (p[j - 1] === '.' || p[j - 1] === s[i - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p[j - 1] === '*') {
// Zero occurrence
dp[i][j] = dp[i][j - 2];
// One or more occurrence
if (p[j - 2] === '.' || p[j - 2] === s[i - 1]) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}def is_match(s, p):
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True # Empty string matches empty pattern
# Handle patterns with *
for j in range(1, n + 1):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2] # Zero occurrence
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == '.' or p[j - 1] == s[i - 1]:
dp[i][j] = dp[i - 1][j - 1]
elif p[j - 1] == '*':
# Zero occurrence
dp[i][j] = dp[i][j - 2]
# One or more occurrence
if p[j - 2] == '.' or p[j - 2] == s[i - 1]:
dp[i][j] = dp[i][j] or dp[i - 1][j]
return dp[m][n]public class Solution {
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true; // Empty string matches empty pattern
// Handle patterns with *
for (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2]; // Zero occurrence
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) == '.' || p.charAt(j - 1) == s.charAt(i - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '*') {
// Zero occurrence
dp[i][j] = dp[i][j - 2];
// One or more occurrence
if (p.charAt(j - 2) == '.' || p.charAt(j - 2) == s.charAt(i - 1)) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
}class Solution {
public:
bool isMatch(string s, string p) {
int m = s.length(), n = p.length();
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
dp[0][0] = true; // Empty string matches empty pattern
// Handle patterns with *
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '*') {
dp[0][j] = dp[0][j - 2]; // Zero occurrence
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '.' || p[j - 1] == s[i - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p[j - 1] == '*') {
// Zero occurrence
dp[i][j] = dp[i][j - 2];
// One or more occurrence
if (p[j - 2] == '.' || p[j - 2] == s[i - 1]) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
};func isMatch(s string, p string) bool {
m, n := len(s), len(p)
dp := make([][]bool, m+1)
for i := range dp {
dp[i] = make([]bool, n+1)
}
dp[0][0] = true // Empty string matches empty pattern
// Handle patterns with *
for j := 1; j <= n; j++ {
if p[j-1] == '*' {
dp[0][j] = dp[0][j-2] // Zero occurrence
}
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if p[j-1] == '.' || p[j-1] == s[i-1] {
dp[i][j] = dp[i-1][j-1]
} else if p[j-1] == '*' {
// Zero occurrence
dp[i][j] = dp[i][j-2]
// One or more occurrence
if p[j-2] == '.' || p[j-2] == s[i-1] {
dp[i][j] = dp[i][j] || dp[i-1][j]
}
}
}
}
return dp[m][n]
}def is_match(s, p)
m, n = s.length, p.length
dp = Array.new(m + 1) { Array.new(n + 1, false) }
dp[0][0] = true # Empty string matches empty pattern
# Handle patterns with *
(1..n).each do |j|
if p[j - 1] == '*'
dp[0][j] = dp[0][j - 2] # Zero occurrence
end
end
(1..m).each do |i|
(1..n).each do |j|
if p[j - 1] == '.' || p[j - 1] == s[i - 1]
dp[i][j] = dp[i - 1][j - 1]
elsif p[j - 1] == '*'
# Zero occurrence
dp[i][j] = dp[i][j - 2]
# One or more occurrence
if p[j - 2] == '.' || p[j - 2] == s[i - 1]
dp[i][j] = dp[i][j] || dp[i - 1][j]
end
end
end
end
dp[m][n]
endRecommended Study Order
- Start with Easy Problems: Climbing Stairs and House Robber to master the basic DP pattern
- Move to Medium Problems: Longest Increasing Subsequence, Coin Change, and Jump Game II for more complex state transitions
- Tackle Hard Problems: Edit Distance and Regular Expression Matching for advanced 2D DP patterns