LeetCode刷题系列-410. 分割数组的最大值

题目

LeetCode 传送门

给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。

注意:
数组长度 n 满足以下条件:

  • 1 ≤ n ≤ 1000
  • 1 ≤ m ≤ min(50, n)

示例:

输入:
nums = [7,2,5,10,8]
m = 2

输出:
18

解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。

解法一:二分法

解题思路

  • 各分组的最大值的最小者 val 肯定在数组 nums 的最大值 max,以及 nums 数组所有元素之和 sums 之间。所以可以在[max, sums)之间任取一个值 val 作为最大值中的最小值。
  • 遍历数组 nums,并且根据最小值 val 进行分组,如果值 nums[i]加入到当前分组中使得该分组的和 sum 大于 val,说明 nums[i]不属于该分组,则从 i 的位置重新分组,sum 的值重置为 nums[i]
  • 假如最后的分组个数小于等于 m 个,说明 val 的值取大了,还可以更小。否则,说明 val 值选小了,应该更大。

图解

在这里插入图片描述
此时 max = 10; sums = 32

  1. 选取 [10, 32)的中间值 21 作为各分组内部之和的最小值。遍历数组,以最小值 21 作为分组条件,此时可以分成如下:
    在这里插入图片描述

  2. 此时满足分组个数小于等于 m,所以 21 的值取大了,还可能更小,所以变成在[10, 21]中重新取最小值,此时选择 15。同理分组变成如下:
    在这里插入图片描述

  3. 此时分组个数为 3,大于 m,所以 15 选小了。重新变成(15, 21]之间选值,这一步选 18,又可以分成
    在这里插入图片描述

  4. 此时分组个数为 2,选值区间又变成(15, 18],选取 17
    在这里插入图片描述

  5. 选值区间变成[18, 18],此时退出循环。

  6. 输出 18,即各数组之和的最小值。

    扫描二维码关注公众号,回复: 12897375 查看本文章

代码实现

/**
 * @param {number[]} nums
 * @param {number} m
 * @return {number}
 */
const splitArray = function (nums, m) {
    
    
  let max = 0;
  let sums = 0;
  // 获取nums的最大元素max 和 数组元素之和sums
  for (let i = 0, len = nums.length; i < len; ++i) {
    
    
    if (max < nums[i]) {
    
    
      max = nums[i];
    }
    sums += nums[i];
  }

  // 二分法在[max, nums)的选取假设最大值中的最小值val
  while (max < sums) {
    
    
    let mid = Math.floor((max + sums) / 2);

    if (checkVal(nums, mid, m)) {
    
    
      sums = mid - 1;
    } else {
    
    
      max = mid + 1;
    }
  }
  return max;
};

// 判断选取的val值能否满足分组个数小于m个
const checkVal = (nums, val, m) => {
    
    
  let pieces = 1;
  let sum = 0;
  for (let i = 0, len = nums.length; i < len; ++i) {
    
    
    sum += nums[i];
    if (sum > val) {
    
    
      ++pieces;
      sum = nums[i];
    }
  }
  return pieces <= m;
};

复杂度分析

时间复杂度:O(nlog(sum−max)),其中 sum 表示数组 nums 中所有元素的和,max 表示数组所有元素的最大值。每次二分查找时,需要对数组进行一次遍历,时间复杂度为 O(n),因此总时间复杂度是 O(nlog(sum−max))

空间复杂度:O(1)

解法二:动态规划

解题思路(@坑人的小书童的思路,我没想到这种解法)

假设 dp[i][j]表示前 i 个数分成 j 个分组后,各分组之和的最小值。假设改变状态的位置为 k,即 k 属于最后一个分段。那么其状态转移 dp[i][j]要存放的值是上一个位置的结果 dp[i-k][j-1]与最后一个分段(k, i)之间元素和中的较大值,即转移方程如下:
在这里插入图片描述
k 的取值范围在 j-1(至少前面有 j-1 个分组,每个分组一个元素)到 i(最后一个元素就是和的最小值),即最后一段(k, i)的长度为 1 ~ i-j+1

  • 每增加一个元素遍历 m 进行分割,得到每个分割段最大值
  • 再将所有分割组合得到的最大值存放到 dp 中,如果之前该位置出现过较小的结果则不替换

边界

  • dp[0][0],0 个数分成 0 段默认 0
  • dp[0][0]被默认占用则 dp 需要声明成[len+1][m+1]的数组
  • 在分割 nums 是逐个增加元素,存在 m 大于当前给定数组的情况,此时遍历分割时,j 的边界为 mi 中较小的值

代码实现

/**
 * @param {number[]} nums
 * @param {number} m
 * @return {number}
 */
const splitArray = function (nums, m) {
    
    
  let len = nums.length,
    sumList = Array(len + 1).fill(0),
    dp = Array.from({
    
     length: len + 1 }, () =>
      Array(m + 1).fill(Number.MAX_VALUE)
    );

  // 逐位增加,反面后面根据区间求区间和
  for (let i = 0; i < len; i++) {
    
    
    sumList[i + 1] = sumList[i] + nums[i];
  }

  // 默认值
  dp[0][0] = 0;

  for (let i = 1; i <= len; i++) {
    
    
    for (let j = 1; j <= Math.min(m, i); j++) {
    
    
      // 前i个数分成j段
      for (let x = j - 1; x < i; x++) {
    
    
        // x最后一段的起点
        // perv本轮分割完成 分段中最大的和
        let prev = Math.max(dp[x][j - 1], sumList[i] - sumList[x]);
        // 该分割情况下最大分段和的最小值
        dp[i][j] = Math.min(prev, dp[i][j]);
      }
    }
  }

  return dp[len][m];
};

复杂度分析

时间复杂度:O(n^2m),其中 n 是数组的长度,m 是分成的非空的连续子数组的个数。总状态数为 O(nm),状态转移时间复杂度 O(n),所以总时间复杂度为 O(n^2m)

空间复杂度:O(nm),为动态规划数组的开销。

猜你喜欢

转载自blog.csdn.net/sinat_36521655/article/details/107588325
今日推荐