-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
243: Solving Common Dynamic Programming Problems in Java
Dynamic Programming (DP) often feels like a magic trick when you first see it in a textbook, but in practice, it's just an optimization technique. At its heart, DP is just recursion where we stop doing the same work twice by remembering the results of sub-problems. I've found that the best way to master this isn't by memorizing formulas, but by following a specific evolution: starting with a "naive" recursive solution and incrementally refining it.
Tackling the Coin Change Problem
Let's look at a classic: given a set of coin denominations and a target amount, what is the minimum number of coins needed to make that amount? If we have coins of [1, 2, 5] and we want to make 11, the answer is 3 (5 + 5 + 1).
My first instinct is always to think recursively. To find the minimum for 11, I can try taking one coin of each denomination and then ask, "What's the minimum needed for the remaining amount?" It looks like this:
public int minCoins(int[] coins, int amount) {
if (amount == 0) return 0;
if (amount < 0) return -1;
int min = Integer.MAX_VALUE;
for (int coin : coins) {
int res = minCoins(coins, amount - coin);
if (res >= 0 && res < min) {
min = 1 + res;
}
}
return (min == Integer.MAX_VALUE) ? -1 : min;
}
This works for small numbers, but it's a nightmare for performance. If the amount is 100, we're recalculating the minimum for "amount = 50" thousands of times. We're essentially building a massive tree of redundant calls.
Adding a Cache to Stop the Bleeding
To fix this, we use "Top-Down" DP, also known as memoization. I'll just throw an array into the mix to store the results we've already calculated. If we hit the same amount twice, we return the cached value immediately.
public int minCoinsMemo(int[] coins, int amount) {
return helper(coins, amount, new int[amount + 1]);
}
private int helper(int[] coins, int amount, int[] memo) {
if (amount == 0) return 0;
if (amount < 0) return -1;
if (memo[amount] != 0) return memo[amount];
int min = Integer.MAX_VALUE;
for (int coin : coins) {
int res = helper(coins, amount - coin, memo);
if (res >= 0 && res < min) {
min = 1 + res;
}
}
memo[amount] = (min == Integer.MAX_VALUE) ? -1 : min;
return memo[amount];
}
The "Integer Overflow" Trap
Now, here is where I usually trip up when I'm rushing through a coding interview or a tight deadline. I often try to simplify the logic by initializing my tracking variable to Integer.MAX_VALUE and then doing something like 1 + helper(...).
I once wrote a version of this where I didn't check if the recursive call returned -1 before adding 1 to it. In Java, Integer.MAX_VALUE + 1 wraps around to -2147483648. Suddenly, my "minimum" number of coins was a massive negative number, and the program told me I could make change for a million dollars using negative two billion coins. Always be careful when adding to "infinity" constants in Java.
Switching to a Bottom-Up Table
While memoization is great, recursion still has overhead (and can lead to a StackOverflowError if the amount is huge). The "professional" way to handle this is Tabulation. Instead of going from the target amount down to zero, we build the solution from zero up to the target.
I like this approach because it's iterative and usually faster. We create a dp array where dp[i] represents the minimum coins needed for amount i.
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
// Initialize with amount + 1 because that's a value impossible to reach
java.util.Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i - coin >= 0) {
dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
Notice how I avoided Integer.MAX_VALUE here. By using amount + 1 as my "infinity," I completely eliminate the risk of integer overflow while still having a value that is guaranteed to be larger than any possible real solution.
📋 Practical Task
Exercise: Solving the House Robber Problem
You are tasked with writing a program for a thief who wants to rob houses along a street. Each house has a certain amount of money stored in it. However, the thief cannot rob two adjacent houses because the security systems are connected; if two neighboring houses are broken into on the same night, the police are alerted.
Your Goal: Implement a method public int rob(int[] nums) that calculates the maximum amount of money the thief can rob without alerting the police.
Requirements:
- Use a Bottom-Up Dynamic Programming approach (Tabulation).
- Your solution should have a time complexity of O(n) and a space complexity of O(n).
- Handle the edge case where the input array is empty or contains only one house.
Input:
[2, 7, 9, 3, 1]
Output:
12 (Rob house 1 (2), house 3 (9), and house 5 (1) -> 2 + 9 + 1 = 12)
There are no comments for now.