Minimum Cost to Divide Array Into Subarrays
HardPrompt
You are given two integer arrays, nums and cost, of the same size, and an integer k.
You can divide nums into subarrays. The cost of the ith subarray consisting of elements nums[l..r] is:
(nums[0] + nums[1] + ... + nums[r] + k * i) * (cost[l] + cost[l + 1] + ... + cost[r]).
Note that i represents the order of the subarray: 1 for the first subarray, 2 for the second, and so on.
Return the minimum total cost possible from any valid division.
Example 1:
Input: nums = [3,1,4], cost = [4,6,6], k = 1
Output: 110
Explanation:
The minimum total cost possible can be achieved by dividingnums into subarrays [3, 1] and [4].
- The cost of the first subarray
[3,1]is(3 + 1 + 1 * 1) * (4 + 6) = 50. - The cost of the second subarray
[4]is(3 + 1 + 4 + 1 * 2) * 6 = 60.
Example 2:
Input: nums = [4,8,5,1,14,2,2,12,1], cost = [7,2,8,4,2,2,1,1,2], k = 7
Output: 985
Explanation:
The minimum total cost possible can be achieved by dividingnums into subarrays [4, 8, 5, 1], [14, 2, 2], and [12, 1].
- The cost of the first subarray
[4, 8, 5, 1]is(4 + 8 + 5 + 1 + 7 * 1) * (7 + 2 + 8 + 4) = 525. - The cost of the second subarray
[14, 2, 2]is(4 + 8 + 5 + 1 + 14 + 2 + 2 + 7 * 2) * (2 + 2 + 1) = 250. - The cost of the third subarray
[12, 1]is(4 + 8 + 5 + 1 + 14 + 2 + 2 + 12 + 1 + 7 * 3) * (1 + 2) = 210.
Constraints:
1 <= nums.length <= 1000cost.length == nums.length1 <= nums[i], cost[i] <= 10001 <= k <= 1000
Approaches
2 approaches with complexity analysis and trade-offs.
A straightforward approach to this problem is using dynamic programming. We can define a DP state dp[i][m] as the minimum cost to partition the first i elements of the nums array into exactly m subarrays. To compute dp[i][m], we can try all possible split points j for the last subarray. If the last subarray is nums[j...i-1], then the first j elements must have been optimally partitioned into m-1 subarrays. This leads to a recurrence relation involving three nested loops, resulting in a cubic time complexity.
Algorithm
- Precompute prefix sums for
numsandcostarrays, let's call themprefixNumsandprefixCostrespectively. This allows for O(1) range sum queries. - Define a 2D DP table,
dp[i][m], to store the minimum cost to partition the prefixnums[0...i-1]into exactlymsubarrays. - Initialize the
dptable with a large value representing infinity. Set the base casedp[0][0] = 0. - Iterate through the number of subarrays
mfrom 1 ton. - For each
m, iterate through the end index of the prefixifrommton. - To compute
dp[i][m], iterate through all possible split pointsjfromm-1toi-1. The last subarray would benums[j...i-1]. - The cost of this partition is the optimal cost for
nums[0...j-1]withm-1subarrays (dp[j][m-1]) plus the cost of the newm-th subarray. - The cost of the
m-th subarraynums[j...i-1]is(prefixNums[i] + k * m) * (prefixCost[i] - prefixCost[j]). - The recurrence relation is:
dp[i][m] = min(dp[i][m], dp[j][m-1] + (prefixNums[i] + k * m) * (prefixCost[i] - prefixCost[j])). - After filling the
dptable, the minimum total cost is the minimum value in the last row of thedptable, i.e.,min(dp[n][m])formfrom 1 ton.
Walkthrough
Let's define dp[i][m] as the minimum cost to partition the prefix nums[0...i-1] into m subarrays. Our goal is to find the minimum of dp[n][m] over all possible numbers of subarrays m (from 1 to n).
To compute dp[i][m], we consider the last subarray in the partition. Suppose it starts at index j and ends at i-1. This means the prefix nums[0...j-1] was partitioned into m-1 subarrays. The minimum cost for that is dp[j][m-1]. The newly formed subarray nums[j...i-1] is the m-th subarray in the sequence.
The cost of this m-th subarray is given by the formula: (sum(nums[0...i-1]) + k * m) * sum(cost[j...i-1]).
To efficiently calculate the sums, we can precompute prefix sum arrays for both nums and cost. Let prefixNums[i] be the sum of the first i elements of nums, and prefixCost[i] be the sum of the first i elements of cost.
The recurrence relation becomes:
dp[i][m] = min_{m-1 <= j < i} { dp[j][m-1] + (prefixNums[i] + k * m) * (prefixCost[i] - prefixCost[j]) }
The base case is dp[0][0] = 0, representing a cost of 0 to partition an empty prefix into 0 subarrays.
We iterate m from 1 to n, i from m to n, and j from m-1 to i-1. This three-level nested loop structure gives the O(N^3) complexity. All cost calculations should use long to prevent integer overflow.
import java.util.Arrays; class Solution { public long minimumCost(int[] nums, int[] cost, int k) { int n = nums.length; long[] prefixNums = new long[n + 1]; long[] prefixCost = new long[n + 1]; for (int i = 0; i < n; i++) { prefixNums[i + 1] = prefixNums[i] + nums[i]; prefixCost[i + 1] = prefixCost[i] + cost[i]; } long[][] dp = new long[n + 1][n + 1]; for (long[] row : dp) { Arrays.fill(row, Long.MAX_VALUE / 2); } dp[0][0] = 0; for (int m = 1; m <= n; m++) { // Number of subarrays for (int i = m; i <= n; i++) { // End index of the prefix (exclusive) for (int j = m - 1; j < i; j++) { // Split point long lastSubarrayCost = (prefixNums[i] + (long)k * m) * (prefixCost[i] - prefixCost[j]); if (dp[j][m - 1] != Long.MAX_VALUE / 2) { dp[i][m] = Math.min(dp[i][m], dp[j][m - 1] + lastSubarrayCost); } } } } long minTotalCost = Long.MAX_VALUE; for (int m = 1; m <= n; m++) { minTotalCost = Math.min(minTotalCost, dp[n][m]); } return minTotalCost; }}Complexity
Time
O(N^3) due to three nested loops for `m`, `i`, and `j`.
Space
O(N^2) to store the DP table.
Trade-offs
Pros
Relatively simple to understand and derive.
Correctly models the problem's state transitions.
Cons
The time complexity of O(N^3) is too high for the given constraints (N <= 1000), leading to a Time Limit Exceeded (TLE) error on most platforms.
The O(N^3) DP solution can be optimized by observing the structure of its recurrence. After rearranging the terms, the inner loop that finds the minimum value can be seen as a set of line queries. Specifically, we are trying to find min(C_j - M_j * x_i), where C_j and M_j depend on the split point j, and x_i depends on the current index i. This structure is a perfect fit for the Convex Hull Trick (CHT) optimization. By maintaining the lower envelope of these lines using a deque, we can perform the minimization step in amortized constant time, reducing the overall complexity from O(N^3) to O(N^2).
Algorithm
- Use the same DP state
dp[i][m]and prefix sums as theO(N^3)approach. - Rearrange the recurrence relation to isolate the terms dependent on the inner loop variable
j:dp[i][m] = (prefixNums[i] + k*m) * prefixCost[i] + min_{m-1 <= j < i} { dp[j][m-1] - (prefixNums[i] + k*m) * prefixCost[j] } - Recognize that the minimization part is equivalent to finding the minimum value of a set of linear functions. For each
j, we have a lineL_j(x) = M_j * x + C_j, whereM_j = -prefixCost[j],C_j = dp[j][m-1], and we query it atx = prefixNums[i] + k*m. - This is a classic Convex Hull Trick (CHT) problem. Since the slopes
M_jare monotonically decreasing (asprefixCost[j]is increasing) and query pointsxare monotonically increasing (asprefixNums[i]is increasing), we can use a deque to maintain the lower envelope of the lines in amortized O(1) time per operation. - For each
mfrom 1 ton, we build the CHT structure for lines derived fromdp[...][m-1]and query it to computedp[...][m]. - The outer loop for
mand the inner loop foriremain, but the innermost loop forjis replaced by amortized O(1) CHT operations. - To optimize space, notice that computing
dp[...][m]only requiresdp[...][m-1]. We can use two 1D arrays (dp_prev,dp_curr) instead of a 2D table, reducing space toO(N).
Walkthrough
The key to optimization lies in the recurrence relation:
dp[i][m] = min_{m-1 <= j < i} { dp[j][m-1] + (prefixNums[i] + k*m) * (prefixCost[i] - prefixCost[j]) }
Let's expand and regroup the terms:
dp[i][m] = (prefixNums[i] + k*m) * prefixCost[i] + min_{m-1 <= j < i} { dp[j][m-1] - (prefixNums[i] + k*m) * prefixCost[j] }
For a fixed m, as we iterate i, the term (prefixNums[i] + k*m) * prefixCost[i] is constant with respect to j. The challenge is the min part. Let x_i = prefixNums[i] + k*m. We need to find min_{j} { dp[j][m-1] - x_i * prefixCost[j] }.
This is equivalent to finding the minimum value among a set of lines. Each potential split point j defines a line L_j(x) = (-prefixCost[j]) * x + dp[j][m-1]. We want to find min_{j} L_j(x_i).
We can apply the Convex Hull Trick. For a fixed m, we iterate i from m to n. We need to query for x_i = prefixNums[i] + k*m. Since nums contains positive integers, prefixNums[i] is strictly increasing with i, so our query points x_i are monotonic. The slopes of our lines are M_j = -prefixCost[j]. Since cost contains positive integers, prefixCost[j] is strictly increasing, making the slopes M_j strictly decreasing.
This specific scenario (monotonic queries on lines with monotonic slopes) allows for a highly efficient CHT implementation using a double-ended queue (deque). The deque will store the lines that form the lower envelope (the set of lines that can be optimal).
For each m:
- We use a 1D array
dp_prevto hold the values ofdp[...][m-1]anddp_currfordp[...][m]. - We initialize a deque to store
Lineobjects (or just their slopes and intercepts). - We iterate
ifrommton. In each iteration: a. We form a new lineL_{i-1}fromdp_prev[i-1]andprefixCost[i-1]. We add this line to our deque, maintaining the convex property by removing lines from the back of the deque that become redundant. b. We query the deque withx_ito find the optimal line. Since query points are increasing, we can remove lines from the front of the deque that are no longer optimal. c. We use the result of the query to computedp_curr[i]. - After the loop for
ifinishes, we find the minimum cost formpartitions, which isdp_curr[n], and update our overall answer. Then, we setdp_prev = dp_currfor the next iteration ofm.
This process reduces the innermost loop to an amortized O(1) operation, making the total time complexity O(N^2) and space complexity O(N) with the space optimization.
import java.util.ArrayDeque;import java.util.Arrays;import java.util.Deque; class Solution { // Represents a line y = mx + c static class Line { long m, c; Line(long m, long c) { this.m = m; this.c = c; } long eval(long x) { return m * x + c; } } // Using cross-product to avoid floating point issues private boolean isRedundant(Line l1, Line l2, Line l3) { // (c3 - c1) * (m1 - m2) <= (c2 - c1) * (m1 - m3) return (l3.c - l1.c) * (l1.m - l2.m) <= (l2.c - l1.c) * (l1.m - l3.m); } public long minimumCost(int[] nums, int[] cost, int k) { int n = nums.length; long[] prefixNums = new long[n + 1]; long[] prefixCost = new long[n + 1]; for (int i = 0; i < n; i++) { prefixNums[i + 1] = prefixNums[i] + nums[i]; prefixCost[i + 1] = prefixCost[i] + cost[i]; } long[] dpPrev = new long[n + 1]; Arrays.fill(dpPrev, Long.MAX_VALUE / 2); dpPrev[0] = 0; long minTotalCost = Long.MAX_VALUE; for (int m = 1; m <= n; m++) { // Number of subarrays long[] dpCurr = new long[n + 1]; Arrays.fill(dpCurr, Long.MAX_VALUE / 2); Deque<Line> deque = new ArrayDeque<>(); for (int i = m; i <= n; i++) { // Add line for split point j = i - 1 // Line is based on dpPrev, i.e., m-1 partitions if (dpPrev[i - 1] != Long.MAX_VALUE / 2) { Line newLine = new Line(-prefixCost[i - 1], dpPrev[i - 1]); while (deque.size() >= 2 && isRedundant(deque.get(deque.size() - 2), deque.getLast(), newLine)) { deque.removeLast(); } deque.addLast(newLine); } // Query for current i if (!deque.isEmpty()) { long x = prefixNums[i] + (long)k * m; while (deque.size() >= 2 && deque.getFirst().eval(x) >= deque.get(1).eval(x)) { deque.removeFirst(); } long minVal = deque.getFirst().eval(x); dpCurr[i] = x * prefixCost[i] + minVal; } } minTotalCost = Math.min(minTotalCost, dpCurr[n]); dpPrev = dpCurr; } return minTotalCost; }}Complexity
Time
O(N^2). The two outer loops run N times each. The inner CHT operations (deque manipulations) take amortized O(1) time.
Space
O(N^2) for the DP table, which can be optimized to O(N) by only storing the DP values for the previous number of partitions.
Trade-offs
Pros
Efficient enough to pass within the time limits.
Reduces time complexity from cubic to quadratic.
Can be space-optimized to O(N).
Cons
Significantly more complex to understand and implement correctly compared to the naive DP.
Requires knowledge of Convex Hull Trick optimization.
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.