leetcode410.分割数组最大值

题目描述

给定一个非负整数数组 nums 和一个整数 m ,你需要将这个数组分成 m 个非空的连续子数组。

设计一个算法使得这 m 个子数组各自和的最大值最小。

题目难度:困难

示例1

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

示例2

输入:nums = [1,2,3,4,5], m = 2
输出:9

示例3

输入:nums = [1,4,4], m = 3
输出:4

解法1(动态规划)

总体思路

动态规划分治法的思想类似,都是将大问题划分成若干个小问题,但是与分治法不同的是动态规划往往会保存已解决子问题的结果,在需要这些结果时不需要重复计算,可以直接取出,更加地节省时间。就本题而言,可以用dp[i][j]表示将子数组nums[0…i]分成j份的最大值的最小值,根据这个表示方法我们很容易能够找出一些i和j无意义的取值和一些能够直接得到结果的取值。
无意义的取值如下:

  • i任意,j=0(将某个数组分成0份这显然无意义)
  • i+1<j(不可能把2个数分成3份或更多份)

可直接计算的取值如下:

  • i任意,j=1(将i+1个数字分成1份,就是累加和,直接计算子数组和即可)
  • i+1 ==j(将m个数分成m份,只有一种分法,直接求出子数组最大值即可)

当以上两种特殊情况处理好之后就需要考虑一般情况,当i>=2,j>=2时有
dp[i][j] = min{max{dp[k][j-1],sum{nums[k+1…i]}}}
(其中k从j-2到i-1)

代码实现

public static int splitArray(int[] nums, int m) {
        int[][] db = new int[nums.length][m+1];

        //初始化数组,对于没有意义的情况将数组值置为无穷大
        for (int i=0;i<nums.length;i++){
            db[i][0] = Integer.MAX_VALUE;
        }
        for (int i=0;i<nums.length;i++){
            for (int j=i+2;j<m+1;j++){
                db[i][j] = Integer.MAX_VALUE;
            }
        }

        //对于可以直接计算的情况直接进行计算
        for (int i=0;i<nums.length;i++){
            db[i][1] = getSum(nums,0,i);
        }
        for (int i=1;i<nums.length;i++){
            for (int j=i;j<m+1;j++){
                if (i+1 == j){
                    db[i][j] = getMax(nums,i);
                }
            }
        }

        //对于正常情况根据递推公式进行计算
        for (int i=2;i<nums.length;i++){
            for (int j=2;j<=Math.min(i,m);j++){
                int minMax = Integer.MAX_VALUE;
                for (int k=j-2;k<i;k++){
                     int tempMax = Math.max(db[k][j-1],getSum(nums,k+1,i));
                     if (tempMax < minMax){
                         minMax = tempMax;
                     }
                }
                db[i][j] = minMax;

            }
        }

        printDB(db,nums,m);
        return db[nums.length-1][m];
    }

    //从nums数组种在一定范围内获取和
    public static int getSum(int[] nums,int begin,int end){
        int sum = 0;
        for (int i=begin;i<=end;i++){
            sum+=nums[i];
        }
        return sum;
    }

    //获取[0,end]范围内的最大值
    public static int getMax(int[] nums,int end){
        int max=nums[0];
        for (int i=1;i<=end;i++){
            if (nums[i]>max){
                max = nums[i];
            }
        }
        return max;
    }

运行结果

示例1
示例1运行结果
示例2
示例2运行结果
示例3
示例3运行结果

结果分析

由以上运行结果可知该算法对于所给示例能够得到正确结果,但是很显然,该算法时间复杂度为O(n³),估计很难满足要求,提交到leetcode平台后发现果然如此。
提交结果
这leetcode是真的猛啊,还有这么长的测试用例。

解法2(二分法)

总体思路

二分法在前面的题目中已经多次使用,但是真的是没想到本题还是使用二分法,经过”参考“别人的解题思路,也使得我有了一些想法。使用二分法需要有一个单调的一种趋势,在本题中这个趋势很容易理解,但是却很难想到,因为这似乎是一种逆向思维,即

  • 如果数组和的最大值越大,则所要的划分数应该越少
  • 反之,如果数组和的最大值越小,则所要的划分数应该越多

这成了我们解题的关键,我们可以使用二分法不断试探所需的划分数,直到满足题目要求为止,当然,需要注意的是题目要求的是”最小的最大值“,所以当找到符合条件的最大值后一定要将最大值最小化后才能符合条件。

代码实现

 public static int splitArray(int[] nums, int m) {
        int max=nums[0],sum=nums[0];
        //计算得到数组的最大值和数组各元素之和
        for (int i=1;i<nums.length;i++){
            if (nums[i]>max){
                max = nums[i];
            }
            sum += nums[i];
        }

        //定义数组子数组之和最大值的最小值的范围
        int left = max,right = sum;
        while (left < right){
            int mid = (left + right)/2;
            //计算最大值不大于mid需要划分的最少的数组个数
            int splits = 1,tempSum=0;
            for (int num : nums){
                if (tempSum + num > mid){//如果遍历和加上当前遍历的值大于mid,则”从头再来“
                    tempSum = 0;
                    splits++;
                }
                tempSum += num;
            }

            if (splits > m){//划分次数太多,说明当前所选最大值偏小
                left = mid + 1;
            }else{//划分次数太少,说明当前所选最大值偏大
                right = mid;
            }

        }

        return left;
    }

运行结果

1.示例1运行结果
示例1运行结果
2.示例2运行结果
示例2运行结果
3.示例3运行结果
示例3运行结果

结果分析

经过一番“折腾”,使用二分法也得出了正确的结果,由于二分法的效率是比较高的,所以一般都能够通过测试,提交到leetcode平台后也确实如此。
解法2提交结果
就本题而言,我是确实没有想到使用二分法(可能是自己经验不足,也有可能是自己太菜),但是二分法好像都和“非负整数数组”、“连续”等特点有关,而且实现起来好像比解法1的动态规划简单一点,没有那么多细节需要考虑,之后还要认真总结,看来二分法确实是很重要的一种思想,远没有想象中的那么简单。

总结

本题的难度为困难,开始我甚至不敢做,心想有必要在这中秋佳节找虐嘛,但是我还是不相信,最后还是选择了这道题目,结果表明我还是过于相信自己了,做的还是挺吃力的,当然,我也从中学习到了很多,也算是过了一个充实的节日吧。最后,祝大家节日快乐。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述:给定一个非负整数数组nums和一个整数m,你需要将这个数组分成m个非空的连续子数组。设计一个算法使得这m个子数组最大和最小。 解题思路: 这是一个典型的二分搜索题目,可以使用二分查找来解决。 1. 首先确定二分的左右边界。左边界为数组最大的值,右边界为数组所有元素之和。 2. 在二分搜索的过程,计算出分割数组的组数count,需要使用当前的间值来进行判断。若当前的间值不够分割成m个子数组,则说明mid值偏小,将左边界更新为mid+1;否则,说明mid值偏大,将右边界更新为mid。 3. 当左边界小于等于右边界时,循环终止,此时的左边界即为所求的结果。 具体步骤: 1. 遍历数组,找到数组最大值,并计算数组的总和。 2. 利用二分查找搜索左右边界,从左边界到右边界间的值为mid。 3. 判断当前的mid值是否满足题目要求,若满足则更新右边界为mid-1; 4. 否则,更新左边界为mid+1。 5. 当左边界大于右边界时,循环终止,返回左边界即为所求的结果。 代码实现: ```python class Solution: def splitArray(self, nums: List[int], m: int) -> int: left = max(nums) right = sum(nums) while left <= right: mid = (left + right) // 2 count = 1 total = 0 for num in nums: total += num if total > mid: total = num count += 1 if count > m: left = mid + 1 else: right = mid - 1 return left ``` 时间复杂度分析:二分搜索的时间复杂度为O(logN),其N为数组的总和,而遍历数组的时间复杂度为O(N),因此总的时间复杂度为O(NlogN)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值