一天一大 leet(分割数组的最大值)难度:困难-Day20200725

img

题目:

给定一个非负整数数组和一个整数 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,在所有情况中最小。

抛砖引玉

img

动态规则

思路

  • 声明dp记录长i的数组分割成j段,每段和最大值组成的list中最小的值
7,2,510,8
sum1sum2
dp[3][1]dp[5][2]
  • dp[i][j] -> 前i个数字分割成j段,每段和的最大值中的最小值

  • nums中增加一个元素时,这个元素一定是要追加到最后一个分段里面
  • 那此时dp[i][j]要存放的值是上一个位置的结果dp[i-x][j-1]和最后一个分段[x-i]和中较大的值
  • 其中x是最后一个分段的起点,例子中的4
  • x的取值:j-1到i,即最后一段最长是j-1,最短i
    • 最长时前面每段一个
    • 最短时只有最后一个元素
  • 每增加一个元素遍历m进行分割,得到每个分割段最大值
  • 再将所有分割组合得到的最大值存放到dp中,如果之前该位置出现过较小的结果则不替换

边界

  • dp[0][0],0个数分成0段默认0
  • dp[0][0]被默认占用则dp需要声明成[len+1][m+1]的数组
  • 在分割nums是逐个增加元素,存在m大于当前给定数组的情况,怎么遍历分割时,j的边界为m与i中较小的值
/**
 * @param {number[]} nums
 * @param {number} m
 * @return {number}
 */
var 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]
};

二分法

根据结果范围枚举可能的结果
再这个校验假设的结果是否成立

  • 不管怎么分段结果都应该在nums最大值max和nums元素和sum之间
  • 二分法查找max到sum之间的元素
  • 检查其是否满足,逐步缩小可能的结果范围,返回可能的最小值

校验逻辑

  • 假设结果val
  • 遍历nums,当累加和大于val,则更换起点从超过val的第一个数算起重新累加
  • 每次重置起点即形成一个片段
  • 最终形成的片段数没有超过m,则说明这个假设的结果val成立
/**
 * @param {number[]} nums
 * @param {number} m
 * @return {number}
 */
var splitArray = function(nums, m) {
  let max = 0
      sum = 0;
  for (let i = 0; i < nums.length; i++) {
    if (max < nums[i]) max = nums[i];
    sum += nums[i];
  }

  while (max < sum) {
    let mid = parseInt((sum - max) / 2, 10) + max;
    // 随机选择的值成立则这个值默认为最大的可能结果继续查找
    if (check(nums, mid, m)) {
      sum = mid;
    } else {
      // 不满足,重置最小可能结果
      max = mid + 1;
    }
  }

  function check(nums, val, m) {
    let sum = 0,
        index = 1;
    for (let i = 0; i < nums.length; i++) {
      // 如果分段和大于了假设的结果说明,i是该分段的终点,形成一个分段
      // index记录+1,i就成了下一个分段的起点(重置sum)开始校验下一个分段
      if (sum + nums[i] > val) {
        index++;
        sum = nums[i];
      } else {
        sum += nums[i];
      }
    }
    // 如果index即分段数量满足小于等于m则说明这个假设值成立
    return index <= m;
  }

// 返回最小可能结果
  return max;
};

博客: 小书童博客

每天的每日一题,写的题解会同步更新到公众号一天一大 lee 栏目
欢迎关注留言

公号: 坑人的小书童

坑人的小书童

探险家小扣的行动轨迹,都将保存在记录仪中。expeditions[i] 表示小扣第 i 次探险记录,用一个字符串数组表示。其中的每个「营地」由大小写字母组成,通过子串 -> 连接。例:"Leet->code->Campsite",表示到访了 "Leet"、"code"、"Campsite" 三个营地。expeditions[0] 包含了初始小扣已知的所有营地;对于之后的第 i 次探险(即 expeditions[i] 且 i > 0),如果记录中包含了之前均没出现的营地,则表示小扣 新发现 的营地。 请你找出小扣发现新营地最多且索引最小的那次探险,并返回对应的记录索引。如果所有探险记录都没有发现新的营地,返回 -1。注意: 大小写不同的营地视为不同的营地; 营地的名称长度均大于 0。用python实现。给你几个例子:示例 1: 输入:expeditions = ["leet->code","leet->code->Campsite->Leet","leet->code->leet->courier"] 输出:1 解释: 初始已知的所有营地为 "leet" 和 "code" 第 1 次,到访了 "leet"、"code"、"Campsite"、"Leet",新发现营地 2 处:"Campsite"、"Leet" 第 2 次,到访了 "leet"、"code"、"courier",新发现营地 1 处:"courier" 第 1 次探险发现的新营地数量最多,因此返回 1。示例 2: 输入:expeditions = ["Alice->Dex","","Dex"] 输出:-1 解释: 初始已知的所有营地为 "Alice" 和 "Dex" 第 1 次,未到访任何营地; 第 2 次,到访了 "Dex",未新发现营地; 因为两次探险均未发现新的营地,返回 -1
最新发布
04-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值