LeetCode 3013 - Divide an Array Into Subarrays With Minimum Cost II

The problem asks us to divide a given array nums into k contiguous subarrays in a very specific way. Each subarray has a cost defined as its first element. The objective is to minimize the total cost, which is the sum of the first elements of all k subarrays.

LeetCode Problem 3013

Difficulty: 🔴 Hard
Topics: Array, Hash Table, Sliding Window, Heap (Priority Queue)

Solution

Problem Understanding

The problem asks us to divide a given array nums into k contiguous subarrays in a very specific way. Each subarray has a cost defined as its first element. The objective is to minimize the total cost, which is the sum of the first elements of all k subarrays.

The challenge is constrained by a dist parameter: the difference between the starting index of the second subarray and the starting index of the kth subarray must be less than or equal to dist. This means that while we can pick subarray boundaries freely for cost minimization, the last subarray cannot start too far from the second subarray's start.

The input array length n can be up to 10^5 and elements can be large, up to 10^9. The constraints on k and dist indicate that we always have at least three subarrays and dist is large enough to allow multiple valid divisions but not unrestricted.

Edge cases include arrays where all elements are the same, arrays where dist is minimal or maximal, and arrays with strictly increasing or decreasing numbers.

The expected output is the minimum possible sum of the first elements of k contiguous subarrays that respect the dist constraint.

Approaches

The brute-force approach would be to generate all possible ways to partition the array into k contiguous subarrays, check if the dist constraint holds, and calculate the total cost for each valid partition. This is correct but infeasible because the number of ways to partition an array into k contiguous subarrays grows combinatorially. For n = 10^5 and k = 50, this is astronomically large.

The optimal approach leverages dynamic programming combined with a monotonic queue (or deque) to maintain the minimum DP values efficiently within the dist range. The key insight is that the total cost of a partition ending at index i depends only on the minimum cost of a partition ending in the previous dist range plus nums[i] as the first element of the current subarray. This allows us to calculate the DP state in O(n * k) using a sliding window minimum.

Approach Time Complexity Space Complexity Notes
Brute Force O(n^(k)) O(n) Try all partitions and pick minimum cost satisfying dist
Optimal O(n * k) O(n * k) DP + sliding window minimum optimization for dist constraint

Algorithm Walkthrough

  1. Define a DP table dp[i][j] where i represents the number of subarrays formed and j represents the ending index of the last subarray. dp[i][j] stores the minimum total cost to partition the first j+1 elements into i subarrays.
  2. Initialize the first row of DP (i = 1) with dp[1][j] = nums[0] for all valid j, because the first subarray always starts at index 0, so the cost is nums[0].
  3. For each subarray count i from 2 to k, we iterate through all ending indices j. For each j, we want to add a new subarray ending at j. Its first element will be nums[start] where start is somewhere between j-dist and j due to the dist constraint.
  4. Use a deque to maintain the minimum DP value of the previous subarray count within the valid dist range. This deque allows O(1) retrieval of the minimum for each new subarray placement.
  5. Update dp[i][j] as the minimum value from the deque plus nums[j] (cost of the new subarray starting at j).
  6. After filling the DP table, the minimum total cost is the minimum value in the last row dp[k][*].

Why it works: The DP table ensures that we only consider valid contiguous subarray divisions and the deque ensures that we efficiently maintain the minimum total cost for previous partitions within the allowed dist window. This guarantees that all constraints are satisfied and the cost is minimized.

Python Solution

from collections import deque
from typing import List

class Solution:
    def minimumCost(self, nums: List[int], k: int, dist: int) -> int:
        n = len(nums)
        dp = [float('inf')] * n
        dp[0] = nums[0]

        for subarray_count in range(1, k):
            new_dp = [float('inf')] * n
            dq = deque()
            for j in range(n):
                if j >= 1:
                    while dq and dq[0] < j - dist:
                        dq.popleft()
                    while dq and dp[dq[-1]] >= dp[j-1]:
                        dq.pop()
                    dq.append(j-1)
                if dq:
                    new_dp[j] = dp[dq[0]] + nums[j]
            dp = new_dp
        return min(dp)

The implementation first initializes dp for one subarray, then iterates k-1 times to form subsequent subarrays. For each new subarray, a deque maintains the minimum previous DP values that are within the allowed dist window. This efficiently enforces the constraint while computing the minimum total cost.

Go Solution

package main

func minimumCost(nums []int, k int, dist int) int64 {
    n := len(nums)
    dp := make([]int64, n)
    for i := 0; i < n; i++ {
        if i == 0 {
            dp[i] = int64(nums[0])
        } else {
            dp[i] = 1<<60
        }
    }

    for subarrayCount := 1; subarrayCount < k; subarrayCount++ {
        newDP := make([]int64, n)
        for i := 0; i < n; i++ {
            newDP[i] = 1<<60
        }
        dq := []int{}
        for j := 0; j < n; j++ {
            if j >= 1 {
                for len(dq) > 0 && dq[0] < j-dist {
                    dq = dq[1:]
                }
                for len(dq) > 0 && dp[dq[len(dq)-1]] >= dp[j-1] {
                    dq = dq[:len(dq)-1]
                }
                dq = append(dq, j-1)
            }
            if len(dq) > 0 {
                newDP[j] = dp[dq[0]] + int64(nums[j])
            }
        }
        dp = newDP
    }

    minCost := int64(1 << 60)
    for _, val := range dp {
        if val < minCost {
            minCost = val
        }
    }
    return minCost
}

The Go implementation mirrors the Python version with slice-based deque operations. Note that we use int64 for large sums and explicitly initialize newDP with a large sentinel value.

Worked Examples

Example 1: nums = [1,3,2,6,4,2], k = 3, dist = 3

Subarray Count DP array values Explanation
1 [1,1,1,1,1,1] Only first subarray, cost = nums[0]
2 [inf,2,3,3,4,4] Second subarray starts at j, min previous dp in dist range used
3 [inf,inf,5,5,5,5] Third subarray starts at j, min previous dp in dist range used
Result 5 min of last DP row

Example 2: nums = [10,1,2,2,2,1], k = 4, dist = 3

Follow similar steps, maintaining deque and DP updates. Result = 15.

Example 3: nums = [10,8,18,9], k = 3, dist = 1

Result = 36.

Complexity Analysis

Measure Complexity Explanation
Time O(n * k) For each of the k subarrays, iterate n elements with deque operations amortized O(1)
Space O(n) Only maintain current and next DP arrays and deque of at most dist elements

The DP with deque ensures that each index is added and removed at most once, giving amortized constant time per element.

Test Cases

# Provided examples
assert Solution().minimumCost([1,3,2,6,4,2], 3, 3) == 5
assert Solution().minimumCost([10,1,2,2,2,1], 4, 3) == 15
assert Solution().minimumCost([10,8,18,9], 3, 1) == 36

# Edge cases
assert Solution().minimumCost([1,1,1,1], 3, 2) == 3  # All elements same
assert Solution().minimumCost([5,4,3,2,1], 3, 3) == 5  # Decreasing array
assert