Painting the Walls
HardPrompt
You are given two 0-indexed integer arrays, cost and time, of size n representing the costs and the time taken to paint n different walls respectively. There are two painters available:
- A paid painter that paints the
ithwall intime[i]units of time and takescost[i]units of money. - A free painter that paints any wall in
1unit of time at a cost of0. But the free painter can only be used if the paid painter is already occupied.
Return the minimum amount of money required to paint the n walls.
Example 1:
Input: cost = [1,2,3,2], time = [1,2,3,2]
Output: 3
Explanation: The walls at index 0 and 1 will be painted by the paid painter, and it will take 3 units of time; meanwhile, the free painter will paint the walls at index 2 and 3, free of cost in 2 units of time. Thus, the total cost is 1 + 2 = 3.Example 2:
Input: cost = [2,3,4,2], time = [1,1,1,1]
Output: 4
Explanation: The walls at index 0 and 3 will be painted by the paid painter, and it will take 2 units of time; meanwhile, the free painter will paint the walls at index 1 and 2, free of cost in 2 units of time. Thus, the total cost is 2 + 2 = 4.
Constraints:
1 <= cost.length <= 500cost.length == time.length1 <= cost[i] <= 1061 <= time[i] <= 500
Approaches
3 approaches with complexity analysis and trade-offs.
This problem can be modeled as a variation of the classic 0/1 knapsack problem. For each wall, we have a choice: either pay to paint it or not. If we pay cost[i] for wall i, we get 1 + time[i] walls painted (one by the paid painter, time[i] by the free painter). Our goal is to select a subset of walls to pay for such that the total number of walls painted is at least n, while the total cost is minimized.
A top-down dynamic programming approach using recursion with memoization is a natural way to solve this. We define a function that explores these choices recursively, and we store the results of subproblems to avoid re-computation.
Algorithm
- Define a recursive function, let's call it
solve(i, remain), which calculates the minimum cost to paintremainwalls using the walls from indexiton-1. - Base Cases:
- If
remain <= 0, it means we have successfully painted at leastnwalls. The cost for this is 0, so we return 0. - If
i == n(we've considered all walls) butremain > 0, it's impossible to paint the remaining walls. We return a very large value (infinity) to indicate that this path is not a valid solution.
- If
- Recursive Step: For the wall at index
i, we have two choices:- Paint wall
i: We paycost[i]. This action paints walliand allows the free painter to work fortime[i]units, paintingtime[i]additional walls. In total,1 + time[i]walls are covered. The cost for this choice iscost[i] + solve(i + 1, remain - 1 - time[i]). - Skip wall
i: We don't pay for walli. The cost is determined by the choices for the remaining walls, which issolve(i + 1, remain).
- Paint wall
- The function
solve(i, remain)returns the minimum of the costs from these two choices. - Memoization: To avoid redundant calculations for the same state
(i, remain), we use a 2D arraymemo(e.g., of sizen x (n+1)). Before computing, we check ifmemo[i][remain]has already been calculated. If so, we return the stored value. Otherwise, we compute the result, store it in the memo table, and then return it. - The initial call to start the process is
solve(0, n).
Walkthrough
We define a recursive function solve(i, remain) that computes the minimum cost to paint remain more walls, given that we can choose from walls i through n-1. The state of our recursion is defined by the current wall index i and the number of walls remain that still need to be painted.
For each wall i, we explore two possibilities: we either pay for it or we don't.
- If we pay for wall
i, we incurcost[i], and the number of walls we still need to paint decreases by1 + time[i]. The total cost for this branch iscost[i]plus the result of the recursive call for the next wall:solve(i + 1, remain - 1 - time[i]). - If we don't pay for wall
i, the cost is simply the result of the recursive call for the next wall with the same remaining walls:solve(i + 1, remain).
The minimum of these two outcomes is the answer for the state (i, remain). The base cases for the recursion are when remain <= 0 (we're done, cost is 0) or when we run out of walls (i == n) but still have walls to paint (an invalid path, cost is infinity).
To make this efficient, we store the results for each state (i, remain) in a 2D memoization table. This ensures that each subproblem is solved only once.
import java.util.Arrays; class Solution { private int[][] memo; private int[] cost; private int[] time; private int n; public int paintWalls(int[] cost, int[] time) { this.n = cost.length; this.cost = cost; this.time = time; this.memo = new int[n][n + 1]; for (int[] row : memo) { Arrays.fill(row, -1); } return solve(0, n); } private int solve(int i, int remain) { if (remain <= 0) { return 0; } if (i == n) { return 1_000_000_007; // A large value representing infinity } if (memo[i][remain] != -1) { return memo[i][remain]; } // Option 1: Paint wall i int paintOption = cost[i] + solve(i + 1, remain - 1 - time[i]); // Option 2: Skip wall i int skipOption = solve(i + 1, remain); return memo[i][remain] = Math.min(paintOption, skipOption); }}Complexity
Time
O(n^2) - The number of states is `n * (n+1)`. Each state `(i, remain)` is computed once due to memoization, and each computation takes constant time.
Space
O(n^2) - The space is dominated by the memoization table `memo` of size `n x (n+1)`. The recursion depth also contributes up to `O(n)` to the call stack space.
Trade-offs
Pros
The logic directly follows the problem's recursive structure, making it relatively easy to understand and implement.
Guaranteed to find the optimal solution by exploring all valid choices.
Cons
The space complexity is
O(n^2), which might be high for very largen.Recursive solutions can lead to a
StackOverflowErrorfor deep recursion chains, althoughn=500is generally safe in most environments.Typically has higher constant factor overhead compared to iterative solutions due to function call stacks.
An alternative to the top-down recursive approach is a bottom-up iterative approach. This method systematically builds up the solution from the smallest subproblems to the final problem. We use a 2D array to store the results of subproblems, effectively turning the recursion into iteration. This avoids recursion overhead and the risk of stack overflow.
Algorithm
- Create a 2D DP table,
dp, of size(n+1) x (n+1). dp[i][j]will store the minimum cost to paint at leastjwalls using the firstiwalls.- Initialization:
- Set
dp[0][0] = 0, as it costs nothing to paint 0 walls with no walls available. - Initialize all
dp[0][j]forj > 0to a large value (infinity), since it's impossible to paint walls without any to choose from. dp[i][0]for alliwill be 0, as painting 0 walls always costs 0.
- Set
- Iteration:
- Loop through each wall
ifrom 1 ton. - Inside this loop, loop through the number of walls to paint,
j, from 1 ton. - For each
dp[i][j], calculate the cost based on two choices for walli-1:- Skip wall
i-1: The cost is inherited from the previous state,dp[i-1][j]. - Paint wall
i-1: The cost iscost[i-1]plus the cost to paint the remaining walls. Since painting walli-1covers1 + time[i-1]walls, we look up the cost fromdp[i-1][max(0, j - 1 - time[i-1])].
- Skip wall
- The state transition is:
dp[i][j] = min(dp[i-1][j], cost[i-1] + dp[i-1][max(0, j - 1 - time[i-1])]).
- Loop through each wall
- Result: The final answer is
dp[n][n], which represents the minimum cost to paint at leastnwalls using allnavailable walls.
Walkthrough
We create a 2D DP table, dp, where dp[i][j] represents the minimum cost to paint at least j walls using the first i walls (from index 0 to i-1). The table size will be (n+1) x (n+1).
We initialize dp[0][0] to 0 and the rest of the first row dp[0][j] to infinity. Then, we iterate from i = 1 to n. For each wall i-1, we iterate through the target number of painted walls j from 1 to n. At each cell dp[i][j], we decide whether to include wall i-1 in our set of paid walls.
- The cost if we don't pay for wall
i-1is simplydp[i-1][j]. - The cost if we do pay for wall
i-1iscost[i-1]plus the cost to cover the remaining walls. Since walli-1covers1 + time[i-1]walls, we need to find the cost of coveringj - (1 + time[i-1])walls using the firsti-1walls. This is given bydp[i-1][max(0, j - 1 - time[i-1])].
We take the minimum of these two options to populate dp[i][j]. After filling the entire table, the value dp[n][n] gives the minimum cost to paint at least n walls using all available walls.
import java.util.Arrays; class Solution { public int paintWalls(int[] cost, int[] time) { int n = cost.length; long[][] dp = new long[n + 1][n + 1]; long infinity = Long.MAX_VALUE / 2; // Use a large value that doesn't overflow on addition for (int j = 1; j <= n; j++) { dp[0][j] = infinity; } // dp[i][0] is 0 for all i, which is the default for (int i = 1; i <= n; i++) { int c = cost[i - 1]; int t = time[i - 1]; for (int j = 1; j <= n; j++) { long paintOption = c + dp[i - 1][Math.max(0, j - 1 - t)]; long skipOption = dp[i - 1][j]; dp[i][j] = Math.min(paintOption, skipOption); } } return (int) dp[n][n]; }}Complexity
Time
O(n^2) - We iterate through the `dp` table using two nested loops of size `n`.
Space
O(n^2) - Requires a 2D DP table of size `(n+1) x (n+1)`.
Trade-offs
Pros
Avoids recursion, eliminating the risk of stack overflow and reducing function call overhead.
The logic is systematic and can be easier to debug than a recursive solution.
Cons
The
O(n^2)space complexity can be a concern for memory-constrained environments.It is less space-efficient than the optimized 1D DP approach.
The most efficient approach in terms of memory is a space-optimized version of the bottom-up DP. Observing the recurrence relation dp[i][j] = min(dp[i-1][j], ...), we can see that computing the values for the current row i only requires values from the previous row i-1. This dependency allows us to reduce the space complexity from O(n^2) to O(n) by using only a single 1D array.
Algorithm
- Create a 1D DP array,
dp, of sizen+1. dp[j]will store the minimum cost to paint at leastjwalls.- Initialization:
- Set
dp[0] = 0. - Initialize all other elements
dp[j]forj > 0to a large value (infinity).
- Set
- Iteration:
- Loop through each wall
ifrom 0 ton-1. - Inside this loop, have a second loop for
jthat iterates backwards, fromndown to 1. - The update rule is
dp[j] = min(dp[j], cost[i] + dp[previous_j]), whereprevious_j = max(0, j - 1 - time[i]). - Iterating
jbackwards is crucial. It ensures that when we calculate the newdp[j]usingdp[previous_j], the valuedp[previous_j]is from the state before considering walli(equivalent todp[i-1]in the 2D version).
- Loop through each wall
- Result: After iterating through all the walls, the answer is
dp[n].
Walkthrough
This approach refines the 2D DP solution by using a single 1D array, dp, of size n+1. Here, dp[j] stores the minimum cost to paint at least j walls, considering the walls processed so far.
We initialize dp[0] to 0 and the rest of the array to infinity. We then iterate through each wall. For each wall i with cost[i] and time[i], we update the dp array. The key insight is to iterate the inner loop (for j) from n down to 1.
When we calculate the new value for dp[j], we consider two options:
- Don't paint wall
i: The cost to paintjwalls remainsdp[j](its value from before this iteration). - Paint wall
i: The cost iscost[i]plus the cost to paint the remainingj - (1 + time[i])walls. This previous cost is found atdp[max(0, j - 1 - time[i])].
By iterating j backwards, we ensure that when we access dp[k] where k < j, we are accessing the value from the previous outer loop iteration (i.e., without considering wall i). This correctly mimics the behavior of using dp[i-1] from the 2D table. After iterating through all walls, dp[n] will hold the minimum cost to paint at least n walls.
import java.util.Arrays; class Solution { public int paintWalls(int[] cost, int[] time) { int n = cost.length; int[] dp = new int[n + 1]; int infinity = 1_000_000_007; Arrays.fill(dp, infinity); dp[0] = 0; for (int i = 0; i < n; i++) { int c = cost[i]; int t = time[i]; for (int j = n; j > 0; j--) { int prevIndex = Math.max(0, j - 1 - t); if (dp[prevIndex] != infinity) { dp[j] = Math.min(dp[j], dp[prevIndex] + c); } } } return dp[n]; }}Complexity
Time
O(n^2) - The algorithm uses two nested loops. The outer loop runs `n` times (for each wall), and the inner loop runs `n` times (for each possible number of walls to paint).
Space
O(n) - We only need a 1D array of size `n+1` to store the DP states.
Trade-offs
Pros
Highly space-efficient, with
O(n)space complexity.Maintains the
O(n^2)time efficiency of the other DP approaches.Iterative nature avoids recursion overhead, making it very fast in practice.
Cons
The logic of iterating the inner loop backwards can be less intuitive at first glance compared to the 2D DP approach.
Solutions
Solution
class Solution {private int n;private int[] cost;private int[] time;private Integer[][] f;public int paintWalls(int[] cost, int[] time) { n = cost.length; this.cost = cost; this.time = time; f = new Integer[n][n << 1 | 1]; return dfs(0, n); }private int dfs(int i, int j) { if (n - i <= j - n) { return 0; } if (i >= n) { return 1 << 30; } if (f[i][j] == null) { f[i][j] = Math.min(dfs(i + 1, j + time[i]) + cost[i], dfs(i + 1, j - 1)); } return f[i][j]; }}class Solution {public: int paintWalls(vector<int> &cost, vector<int> &time) { int n = cost.size(); int f[n][n << 1 | 1]; memset(f, -1, sizeof(f)); function<int(int, int)> dfs = [&](int i, int j) -> int { if (n - i <= j - n) { return 0; } if (i >= n) { return 1 << 30; } if (f[i][j] == -1) { f[i][j] = min(dfs(i + 1, j + time[i]) + cost[i], dfs(i + 1, j - 1)); } return f[i][j]; }; return dfs(0, n); }};class Solution: def paintWalls(self, cost: List[int], time: List[int]) -> int: @ cache def dfs(i: int, j: int) -> int: if n - i <= j: return 0 if i >= n: return inf return min(dfs(i + 1, j + time[i]) + cost[i], dfs(i + 1, j - 1)) n = len(cost) return dfs(0, 0)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.