Hard

题目描述

给定一个整数数组 nums 和一个整数 k

你的任务是将 nums 精确地分割成 k 个子数组,并返回一个整数,表示所有有效分割中的最小可能得分。

分割的得分是其所有子数组值的总和。

子数组的值定义为 sumArr * (sumArr + 1) / 2,其中 sumArr 是其元素的总和。

示例 1:

输入:nums = [5,1,2,1], k = 2
输出:25
解释:
我们必须将数组分割成 k = 2 个子数组。一个最优分割是 [5] 和 [1, 2, 1]。
第一个子数组的 sumArr = 5,值 = 5 × 6 / 2 = 15。
第二个子数组的 sumArr = 1 + 2 + 1 = 4,值 = 4 × 5 / 2 = 10。
此分割的得分是 15 + 10 = 25,这是最小可能得分。

示例 2:

输入:nums = [1,2,3,4], k = 1
输出:55
解释:
由于我们必须将数组分割成 k = 1 个子数组,所有元素都属于同一个子数组:[1, 2, 3, 4]。
此子数组的 sumArr = 1 + 2 + 3 + 4 = 10,值 = 10 × 11 / 2 = 55。
此分割的得分是 55,这是最小可能得分。

示例 3:

输入:nums = [1,1,1], k = 3
输出:3
解释:
我们必须将数组分割成 k = 3 个子数组。唯一有效的分割是 [1], [1], [1]。
每个子数组的 sumArr = 1,值 = 1 × 2 / 2 = 1。
此分割的得分是 1 + 1 + 1 = 3,这是最小可能得分。

约束条件:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

解题思路

这是一道经典的分割优化问题。我们需要将数组分成恰好 k 个子数组,使得总代价最小。

基本动态规划思路:dp[i][j] 表示将前 j 个元素分成 i 个子数组的最小代价。状态转移方程为: dp[i][j] = min(dp[i-1][t] + cost(t+1, j)) for all valid t

其中 cost(l, r) 表示子数组 nums[l...r] 的代价,即 sum * (sum + 1) / 2

朴素的 DP 时间复杂度为 O(k*n²),对于本题的数据范围可能会超时。

分治优化(Divide and Conquer Optimization): 由于代价函数 sum * (sum + 1) / 2 是凸函数,满足四边形不等式,我们可以使用分治优化将复杂度降低到 O(knlog n)。

分治优化的核心思想是:对于状态 dp[i][j],如果我们知道最优分割点在某个区间 [optL, optR] 内,那么可以通过分治的方式缩小搜索范围。

实现细节:

  1. 预处理前缀和数组,快速计算子数组和
  2. 使用分治函数递归求解最优分割点
  3. 在分治过程中,利用最优分割点的单调性进行剪枝

这种优化特别适用于分割类 DP 问题,当代价函数满足凸性质时能显著提升效率。

代码实现

class Solution {
public:
    long long minPartitionScore(vector<int>& nums, int k) {
        int n = nums.size();
        vector<long long> prefix(n + 1, 0);
        for (int i = 0; i < n; i++) {
            prefix[i + 1] = prefix[i] + nums[i];
        }
        
        auto cost = [&](int l, int r) -> long long {
            long long sum = prefix[r] - prefix[l];
            return sum * (sum + 1) / 2;
        };
        
        vector<vector<long long>> dp(k + 1, vector<long long>(n + 1, 1e18));
        dp[0][0] = 0;
        
        function<void(int, int, int, int, int)> solve = [&](int level, int l, int r, int optL, int optR) {
            if (l > r) return;
            
            int mid = (l + r) / 2;
            long long best = 1e18;
            int bestK = -1;
            
            for (int k = max(level - 1, optL); k <= min(mid - 1, optR); k++) {
                long long val = dp[level - 1][k] + cost(k, mid);
                if (val < best) {
                    best = val;
                    bestK = k;
                }
            }
            
            dp[level][mid] = best;
            
            if (l < r) {
                solve(level, l, mid - 1, optL, bestK);
                solve(level, mid + 1, r, bestK, optR);
            }
        };
        
        for (int i = 1; i <= k; i++) {
            solve(i, i, n, 0, n);
        }
        
        return dp[k][n];
    }
};
class Solution:
    def minPartitionScore(self, nums: List[int], k: int) -> int:
        n = len(nums)
        prefix = [0] * (n + 1)
        for i in range(n):
            prefix[i + 1] = prefix[i] + nums[i]
        
        def cost(l, r):
            s = prefix[r] - prefix[l]
            return s * (s + 1) // 2
        
        dp = [[float('inf')] * (n + 1) for _ in range(k + 1)]
        dp[0][0] = 0
        
        def solve(level, l, r, opt_l, opt_r):
            if l > r:
                return
            
            mid = (l + r) // 2
            best = float('inf')
            best_k = -1
            
            for k_val in range(max(level - 1, opt_l), min(mid, opt_r + 1)):
                val = dp[level - 1][k_val] + cost(k_val, mid)
                if val < best:
                    best = val
                    best_k = k_val
            
            dp[level][mid] = best
            
            if l < r:
                solve(level, l, mid - 1, opt_l, best_k)
                solve(level, mid + 1, r, best_k, opt_r)
        
        for i in range(1, k + 1):
            solve(i, i, n, 0, n)
        
        return dp[k][n]
public class Solution {
    public long MinPartitionScore(int[] nums, int k) {
        int n = nums.Length;
        long[] prefix = new long[n + 1];
        for (int i = 0; i < n; i++) {
            prefix[i + 1] = prefix[i] + nums[i];
        }
        
        long Cost(int l, int r) {
            long sum = prefix[r] - prefix[l];
            return sum * (sum + 1) / 2;
        }
        
        long[,] dp = new long[k + 1, n + 1];
        for (int i = 0; i <= k; i++) {
            for (int j = 0; j <= n; j++) {
                dp[i, j] = long.MaxValue / 2;
            }
        }
        dp[0, 0] = 0;
        
        void Solve(int level, int l, int r, int optL, int optR) {
            if (l > r) return;
            
            int mid = (l + r) / 2;
            long best = long.MaxValue / 2;
            int bestK = -1;
            
            for (int kVal = Math.Max(level - 1, optL); kVal <= Math.Min(mid - 1, optR); kVal++) {
                long val = dp[level - 1, kVal] + Cost(kVal, mid);
                if (val < best) {
                    best = val;
                    bestK = kVal;
                }
            }
            
            dp[level, mid] = best;
            
            if (l < r) {
                Solve(level, l, mid - 1, optL, bestK);
                Solve(level, mid + 1, r, bestK, optR);
            }
        }
        
        for (int i = 1; i <= k; i++) {
            Solve(i, i, n, 0, n);
        }
        
        return dp[k, n];
    }
}
var minPartitionScore = function(nums, k) {
    const n = nums.length;
    const prefix = new Array(n + 1).fill(0);
    for (let i = 0; i < n; i++) {
        prefix[i + 1] = prefix[i] + nums[i];
    }
    
    const cost = (l, r) => {
        const sum = prefix[r] - prefix[l];
        return sum * (sum + 1) / 2;
    };
    
    const dp = Array(k + 1).fill().map(() => Array(n + 1).fill(Infinity));
    dp[0][0] = 0;
    
    const solve = (level, l, r, optL, optR) => {
        if (l > r) return;
        
        const mid = Math.floor((l + r) / 2);
        let best = Infinity;
        let bestK = -1;
        
        for (let kVal = Math.max(level - 1, optL); kVal <= Math.min(mid - 1, optR); kVal++) {
            const val = dp[level - 1][kVal] + cost(kVal, mid);
            if (val < best) {
                best = val;
                bestK = kVal;
            }
        }
        
        dp[level][mid] = best;
        
        if (l < r) {
            solve(level, l, mid - 1, optL, bestK);
            solve(level, mid + 1, r, bestK, optR);
        }
    };
    
    for (let i = 1; i <= k; i++) {
        solve(i, i, n, 0, n);
    }
    
    return dp[k][n];
};

复杂度分析

复杂度时间复杂度空间复杂度
分治优化DPO(k × n × log n)O(k × n)
朴素DPO(k × n²)O(k × n)

推荐解法: 分治优化DP,在满足数据约束的前提下具有更好的时间复杂度。