Maximize Total Cost of Alternating Subarrays
MedPrompt
You are given an integer array nums with length n.
The cost of a subarray nums[l..r], where 0 <= l <= r < n, is defined as:
cost(l, r) = nums[l] - nums[l + 1] + ... + nums[r] * (−1)r − l
Your task is to split nums into subarrays such that the total cost of the subarrays is maximized, ensuring each element belongs to exactly one subarray.
Formally, if nums is split into k subarrays, where k > 1, at indices i1, i2, ..., ik − 1, where 0 <= i1 < i2 < ... < ik - 1 < n - 1, then the total cost will be:
cost(0, i1) + cost(i1 + 1, i2) + ... + cost(ik − 1 + 1, n − 1)
Return an integer denoting the maximum total cost of the subarrays after splitting the array optimally.
Note: If nums is not split into subarrays, i.e. k = 1, the total cost is simply cost(0, n - 1).
Example 1:
Input: nums = [1,-2,3,4]
Output: 10
Explanation:
One way to maximize the total cost is by splitting [1, -2, 3, 4] into subarrays [1, -2, 3] and [4]. The total cost will be (1 + 2 + 3) + 4 = 10.
Example 2:
Input: nums = [1,-1,1,-1]
Output: 4
Explanation:
One way to maximize the total cost is by splitting [1, -1, 1, -1] into subarrays [1, -1] and [1, -1]. The total cost will be (1 + 1) + (1 + 1) = 4.
Example 3:
Input: nums = [0]
Output: 0
Explanation:
We cannot split the array further, so the answer is 0.
Example 4:
Input: nums = [1,-1]
Output: 2
Explanation:
Selecting the whole array gives a total cost of 1 + 1 = 2, which is the maximum.
Constraints:
1 <= nums.length <= 105-109 <= nums[i] <= 109
Approaches
3 approaches with complexity analysis and trade-offs.
A natural way to approach this problem is to think about the choices we can make at each step. When we are at an index i, we need to decide where the current subarray, starting at i, will end. Let's say it ends at index j. We would then calculate the cost of this subarray nums[i...j] and recursively solve the problem for the remaining part of the array, nums[j+1...]. This forms a recursive structure.
This plain recursive approach would be very slow due to re-computing solutions for the same subproblems. We can significantly improve it by using memoization, a technique where we store the results of expensive function calls and return the cached result when the same inputs occur again. This turns the exponential complexity into a polynomial one.
Algorithm
- Define a recursive function, say
solve(i), that computes the maximum possible cost for the suffix of the array starting at indexi, i.e.,nums[i:]. - The base case for the recursion is when
ireaches the end of the array (i >= n), in which case the cost is 0. - For a given
i, we can form the first subarray in multiple ways. It can benums[i:i],nums[i:i+1], ...,nums[i:n-1]. - If we choose the first subarray to be
nums[i:j], the total cost will becost(i, j)plus the maximum cost for the rest of the array, which issolve(j+1). - We iterate through all possible end points
jfromiton-1, calculate the total cost for each choice, and take the maximum. solve(i) = max_{i <= j < n} (cost(i, j) + solve(j+1))- This recursive solution has overlapping subproblems (e.g.,
solve(k)is computed multiple times). We can optimize this by using memoization. We use an array, saymemo, to store the results ofsolve(i)once computed. - Before computing
solve(i), we check if the result is already inmemo. If so, we return it directly.
Walkthrough
We define a function solve(i) that returns the maximum total cost we can obtain from the subarray nums[i...n-1]. The final answer to the problem will be solve(0).
To compute solve(i), we consider all possible split points. The first subarray must start at i. Let's say it ends at j, where i <= j < n. The cost for this split is cost(i, j) + solve(j+1). We need to find the j that maximizes this value.
The cost(i, j) is calculated as nums[i] - nums[i+1] + ... + (-1)^(j-i) * nums[j]. We can compute this cost inside the loop that iterates over j.
To avoid recomputing solve(k) for the same k multiple times, we use a memoization table (e.g., a long[] memo) to store the results. The state of our DP is just the starting index i, so we need a 1D array for memoization.
class Solution { private long[] memo; private int[] nums; private int n; public long maximumTotalCost(int[] nums) { this.nums = nums; this.n = nums.length; this.memo = new long[n]; Arrays.fill(memo, Long.MIN_VALUE); return solve(0); } private long solve(int i) { if (i >= n) { return 0; } if (memo[i] != Long.MIN_VALUE) { return memo[i]; } long maxCost = Long.MIN_VALUE; long currentSubarrayCost = 0; for (int j = i; j < n; j++) { if ((j - i) % 2 == 0) { currentSubarrayCost += nums[j]; } else { currentSubarrayCost -= nums[j]; } maxCost = Math.max(maxCost, currentSubarrayCost + solve(j + 1)); } return memo[i] = maxCost; }}Complexity
Time
O(N^2). There are `N` states (`i` from 0 to `n-1`). For each state `solve(i)`, we loop from `j = i` to `n-1`, which takes `O(N-i)` time. The total time complexity is the sum of `(N-i)` for `i` from 0 to `N-1`, which is `O(N^2)`.
Space
O(N) for the recursion stack depth in the worst case and for the memoization array.
Trade-offs
Pros
It's a direct translation of the problem's recursive nature.
It's conceptually easier to formulate than more optimized solutions.
Cons
The
O(N^2)time complexity is too slow for the given constraints (n <= 10^5) and will result in a Time Limit Exceeded (TLE) error on most platforms.
The O(N^2) solution can be improved by re-framing the decision at each step. Instead of deciding where a subarray ends, let's consider each element nums[i] and decide its role. For any element nums[i], it can either start a new subarray or continue the one ending at nums[i-1]. The sign of nums[i]'s contribution depends on its position within its subarray.
This leads to a linear time dynamic programming solution. The state of our DP at index i needs to capture two possibilities: the maximum cost ending with nums[i] being added, and the maximum cost ending with nums[i] being subtracted. This is because the choice for nums[i+1] will depend on whether nums[i] was added or subtracted.
Algorithm
- We define a 2D DP array,
dp[n][2], wherenis the length ofnums. dp[i][0]will store the maximum total cost for the prefixnums[0...i], assumingnums[i]contributes positively to the cost (i.e., it's at an even-indexed position within its subarray).dp[i][1]will store the maximum total cost for the prefixnums[0...i], assumingnums[i]contributes negatively.- Base Case (i=0):
nums[0]must start a new subarray, so it's always added.dp[0][0] = nums[0]dp[0][1]is impossible, so we initialize it to a very small number (negative infinity).
- Transitions (for i > 0):
- To compute
dp[i][0](nums[i]is added): This can happen in two ways:- Start a new subarray at
i. The cost ismax(dp[i-1][0], dp[i-1][1]) + nums[i]. - Continue the previous subarray.
nums[i]is added ifnums[i-1]was subtracted. The cost isdp[i-1][1] + nums[i]. The recurrence isdp[i][0] = max(max(dp[i-1][0], dp[i-1][1]), dp[i-1][1]) + nums[i], which simplifies todp[i][0] = max(dp[i-1][0], dp[i-1][1]) + nums[i].
- Start a new subarray at
- To compute
dp[i][1](nums[i]is subtracted): This can only happen by continuing a subarray wherenums[i-1]was added. The cost isdp[i-1][0] - nums[i].
- To compute
- Final Answer: After iterating through the whole array, the maximum possible cost is
max(dp[n-1][0], dp[n-1][1]).
Walkthrough
Let dp[i][0] be the maximum cost for a split of nums[0...i] where nums[i] has a positive sign, and dp[i][1] be the maximum cost where nums[i] has a negative sign.
For nums[i] to have a positive sign, it can either be the start of a new subarray (position 0, which is even) or be at an even distance from the start of its current subarray. If it starts a new subarray, the total cost is the maximum cost from the prefix nums[0...i-1] plus nums[i]. If it continues a subarray, it must follow an element with a negative sign. The recurrence relation captures both cases optimally.
For nums[i] to have a negative sign, it must continue a subarray and follow an element that had a positive sign.
This logic gives us the recurrence relations to build up the solution from i=0 to n-1.
class Solution { public long maximumTotalCost(int[] nums) { int n = nums.length; if (n == 0) { return 0; } long[][] dp = new long[n][2]; // Base case: i = 0 // dp[i][0]: max cost ending at i with nums[i] added // dp[i][1]: max cost ending at i with nums[i] subtracted dp[0][0] = nums[0]; dp[0][1] = Long.MIN_VALUE; // Cannot subtract the first element for (int i = 1; i < n; i++) { // To add nums[i], we can either start a new subarray or continue one // where the previous element was subtracted. // Start new: max(dp[i-1][0], dp[i-1][1]) + nums[i] // Continue: dp[i-1][1] + nums[i] // The max of these is max(dp[i-1][0], dp[i-1][1]) + nums[i] dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]) + nums[i]; // To subtract nums[i], we must continue a subarray where the // previous element was added. dp[i][1] = dp[i-1][0] - nums[i]; } return Math.max(dp[n - 1][0], dp[n - 1][1]); }}Complexity
Time
O(N) because we iterate through the input array once, performing constant time operations at each step.
Space
O(N) to store the 2D DP table of size `n x 2`.
Trade-offs
Pros
Highly efficient with O(N) time complexity, which passes the given constraints.
It's a systematic way to solve the problem by building up the solution.
Cons
Uses O(N) extra space, which can be optimized away since each state only depends on the previous one.
The linear time DP approach is efficient, but we can observe that the computation for dp[i] only depends on the values from dp[i-1]. This means we don't need to store the entire dp table of size n x 2. We only need to keep track of the two state values from the previous step, dp[i-1][0] and dp[i-1][1]. This allows us to optimize the space complexity from O(N) down to O(1).
Algorithm
- Initialize two variables,
addandsub, to represent the maximum cost for the prefix ending at the previous element (i-1), wherenums[i-1]was added and subtracted, respectively. - For the first element
nums[0], initializeadd = nums[0]andsubto a very small number (as it cannot be subtracted). - Iterate from
i = 1ton-1:- At each step
i, we want to compute the newaddandsubvalues for the prefix ending ati. - Let
prev_add = addandprev_sub = subfrom the previous step. - The new
addvalue (current_add) ismax(prev_add, prev_sub) + nums[i]. - The new
subvalue (current_sub) isprev_add - nums[i]. - Update
add = current_addandsub = current_subfor the next iteration. Note that the update forsubmust use theaddvalue from the previous step, so a temporary variable is needed.
- At each step
- After the loop finishes, the maximum total cost for the entire array is
max(add, sub).
Walkthrough
We can get rid of the dp array and use just two variables to maintain the state. Let's call them add and sub.
add: Representsdp[i-1][0], the max cost of a prefix ending ati-1wherenums[i-1]was added.sub: Representsdp[i-1][1], the max cost of a prefix ending ati-1wherenums[i-1]was subtracted.
We iterate through the array, and at each index i, we calculate the new values for add and sub based on their previous values and nums[i]. Since the new sub depends on the old add, we need to be careful with the order of updates or use a temporary variable.
class Solution { public long maximumTotalCost(int[] nums) { int n = nums.length; if (n == 0) { return 0; } // add: max cost ending at previous element, with it being added // sub: max cost ending at previous element, with it being subtracted long add = nums[0]; long sub = Long.MIN_VALUE; // Cannot subtract the first element for (int i = 1; i < n; i++) { long prev_add = add; // Calculate new 'add' for current index i // This is max(previous add, previous sub) + nums[i] add = Math.max(add, sub) + nums[i]; // Calculate new 'sub' for current index i // This is previous add - nums[i] sub = prev_add - nums[i]; } return Math.max(add, sub); }}Complexity
Time
O(N), as it involves a single pass through the array.
Space
O(1), as we only use a constant number of variables to store the DP states regardless of the input size.
Trade-offs
Pros
Optimal solution with O(N) time and O(1) space complexity.
Very efficient for large inputs.
Cons
The logic can be slightly less intuitive to grasp initially compared to the version with the full DP table.
Solutions
Solution
class Solution {private Long[][] f;private int[] nums;private int n;public long maximumTotalCost(int[] nums) { n = nums.length; this.nums = nums; f = new Long[n][2]; return dfs(0, 0); }private long dfs(int i, int j) { if (i >= n) { return 0; } if (f[i][j] != null) { return f[i][j]; } f[i][j] = nums[i] + dfs(i + 1, 1); if (j == 1) { f[i][j] = Math.max(f[i][j], -nums[i] + dfs(i + 1, 0)); } return f[i][j]; }}class Solution {public: long long maximumTotalCost(vector<int> &nums) { int n = nums.size(); long long f[n][2]; fill(f[0], f[n], LLONG_MIN); auto dfs = [&](auto &&dfs, int i, int j) -> long long { if (i >= n) { return 0; } if (f[i][j] != LLONG_MIN) { return f[i][j]; } f[i][j] = nums[i] + dfs(dfs, i + 1, 1); if (j) { f[i][j] = max(f[i][j], -nums[i] + dfs(dfs, i + 1, 0)); } return f[i][j]; }; return dfs(dfs, 0, 0); }};class Solution: def maximumTotalCost(self, nums: List[int]) -> int: @ cache def dfs(i: int, j: int) -> int: if i >= len(nums): return 0 ans = nums[i] + dfs(i + 1, 1) if j == 1: ans = max(ans, - nums[i] + dfs(i + 1, 0)) return ans 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.