Java
Completed
-
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
237: Dynamic Programming Patterns in Java
A few years ago, I was mentoring a developer who was building a route-optimization tool for a logistics client. He had written a beautiful, clean recursive function to find the most cost-effective way to combine shipments. It worked perfectly during his local tests with three or four stops. But the moment we pushed it to a staging environment with real-world data—say, twenty stops—the application didn't just slow down; it effectively froze. He spent two days hunting for a memory leak, convinced the JVM was choking on object allocation. The reality was simpler: he was recalculating the same sub-problems millions of times over. His code was essentially trying to solve the same puzzle from scratch every time it encountered a familiar state.
That's the "wall" you hit when you need Dynamic Programming (DP). At its core, DP isn't some mystical mathematical art; it's just recursion with a memory. If you've already spent the CPU cycles to find the answer to a specific sub-problem, why on earth would you do it again? In Java, we handle this by trading a bit of memory (usually in the form of an array or a Map) to buy back a massive amount of time.
Trading Space for Time with Memoization
The most intuitive way to dive into DP is the "top-down" approach, which we call memoization. You keep your recursive structure—which is often easier to reason about—but you wrap it in a cache. Before your method does any heavy lifting, it checks if the answer is already in the cache. If it is, it returns it immediately. If not, it calculates the result and stores it before returning.
Take the 0/1 Knapsack problem: you have a bag with a weight limit, and a set of items with specific weights and values. You want to maximize the value. A naive recursive approach branches exponentially. But notice that you'll often hit the same state: "What is the max value I can get with 5kg remaining and 3 items left to consider?"
public class KnapsackMemo {
private Integer[][] memo;
public int solve(int[] weights, int[] values, int capacity, int n) {
memo = new Integer[n + 1][capacity + 1];
return helper(weights, values, capacity, n);
}
private int helper(int[] weights, int[] values, int capacity, int n) {
if (n == 0 || capacity == 0) return 0;
// Check the cache first
if (memo[n][capacity] != null) return memo[n][capacity];
if (weights[n - 1] > capacity) {
return memo[n][capacity] = helper(weights, values, capacity, n - 1);
} else {
// Max of: excluding the item OR including the item
int include = values[n - 1] + helper(weights, values, capacity - weights[n - 1], n - 1);
int exclude = helper(weights, values, capacity, n - 1);
return memo[n][capacity] = Math.max(include, exclude);
}
}
}
I prefer starting with this method because it preserves the logic of the problem. However, you're still paying the overhead of the call stack. For very deep problems, you'll hit a StackOverflowError, which is where the second pattern comes in.
Building the Solution from the Ground Up
The "bottom-up" approach, or tabulation, flips the script. Instead of starting with the big problem and breaking it down, you solve the smallest possible sub-problems first and use those results to build up to the final answer. This is almost always done using an iterative loop and a 2D array (a table).
In the Knapsack example, we build a table where the rows represent the items we've considered and the columns represent the increasing capacity of the bag. By the time we reach the bottom-right cell of the table, we have the answer for the full capacity and all items, and we never once had to make a recursive call.
public int solveTabulation(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= capacity; w++) {
if (weights[i - 1] <= w) {
// Max of taking the item or leaving it
dp[i][w] = Math.max(values[i - 1] + dp[i - 1][w - weights[i - 1]], dp[i - 1][w]);
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
return dp[n][capacity];
}
You'll notice that the tabulation approach is generally faster and more memory-stable. One pro tip: if you look closely at the tabulation code, you'll see we only ever reference the previous row (i - 1). If you're really squeezed for memory, you can actually optimize this further by using a single-dimensional array and updating it backwards. It's a common trick in production code to reduce space complexity from O(N*W) to O(W).
📋 Practical Task
The Budget-Constrained Server Rack Optimizer
You are designing a system to maximize the "Compute Power" of a server rack. The rack has a maximum power draw limit (in Watts). You have a list of available server blades, each with a specific power consumption and a corresponding compute power rating. You cannot split a blade; you either install it or you don't.
Your Task: Implement a class RackOptimizer with a method getMaxComputePower(int maxPower, int[] powerDraws, int[] computeRatings). Use the bottom-up (tabulation) dynamic programming pattern to find the maximum compute power possible without exceeding the maxPower limit.
Requirements:
- The solution must run in O(N * W) time, where N is the number of blades and W is the maxPower.
- Do not use recursion to avoid potential stack overflow on large blade sets.
- Handle edge cases where the
powerDrawsarray might be empty or themaxPoweris zero.
There are no comments for now.