leetcode_410_分割数组的最大值

题目:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/split-array-largest-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


思路:

这个题目很难,不过是典型的动态规划问题。

首先定义:

dp[i][j]:前i个连续数划分为j段时满足题目要求----最大数组和的最小值。

subSum[i,j]:从下标 i 到 j 的子数组的和。

很明显,可以使用dp数组来记忆化,使得将前i个数分割为j段时的状态,可以由将前i个数分割为(j-1)段时的状态转移而来

在前i个数中,我们遍历0-i之间,找到一个下标k,使得原先分割为j-1段的状态,转换为分割为j段的状态,即

分割为0-k(分割为j-1段),以及k-i(单独一段),而由于前i个数分割为j-1段时的状态以及保存记忆下来,所以,当将前i个数划分为j段时,其状态转移方程为:

dp[i][j] = min(dp[i][j],max(dp[k][j-1],subSum[k+1,i])

外面一个min用来保持:总体划分为 j 段时的段和保持最小状态。

里面一个max用来保持:使用下标k来将前 i 个数划分为 j 段时段和最大。


实现:

JAVA版:

package leetcode;

import java.util.Arrays;

/*
USER:LQY
DATE:2020/7/25
TIME:9:07
*/
public class leetcode_410_important {

    public static void main(String []args){

    }
    public int splitArray_DP(int[] nums, int m) {
        int lens = nums.length;
        // if(lens == m){
        //     return Math.max(nums);
        // }
        int [][]dp = new int[lens+1][m+1];
        for(int i = 0;i <= lens;i++)
            for(int j = 0;j <= m;j++)
                dp[i][j] = Integer.MAX_VALUE;

        dp[0][0] = 0;
        // int []subSum = new int[lens+1];
        // subSum[1] = nums[0];
        // for(int i = 2;i <= lens;i++)
        //     subSum[i] = subSum[i-1] + nums[i-1];
        for(int i = 1;i < lens;i++)
            nums[i] += nums[i-1];

        for(int i = 1;i <= lens;i++){
            for(int j = 1;j<=m && j<=i;j++){  //j<=m保持前i个连续数最多划分为m段。j<=i保证没有空的段
                for(int k = 0;k < i;k++){   //在前i个连续数已经被符合题意的划分为j-1段时,再在其基础上,找到一个k下标,使得前i个连续数划分为符合题意的j段。这里利用记忆化,从而会产生叠加效应,这也是动态规划的特点。
                    if(k == 0){
                        //此时,直接是将前i个连续数划分为一段,即完整的子数组。
                        dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j-1], nums[i-1]));
                    }else
                        dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j-1], nums[i-1] - nums[k-1]));
                }
            }
        }
        return dp[lens][m];
    }
    //二分查找。
//    根据题意可以知道,每一段的和的范围为[maxNums, sumNums]。
//    即不小于nums数组中的最大值,不大于nums数组的和。
//    前者为划分为单个元素的情况,后者为不划分的情况。
//    统计sunNums时如果使用的是int类型可能会越界,最好使用long类型,在返回时强制转换即可。
    public int splitArray_SEA(int[] nums, int m) {
        int maxNums = 0;
        int sumNums = 0;
        for(int i : nums){
            maxNums = maxNums>i? maxNums : i;
            sumNums += i;
        }
        int lowMax = maxNums;
        int highMax = sumNums;

        while(lowMax < highMax){
            int midMax = (lowMax+highMax) >> 1;

            int temp = 0;
            int div = 1;  //初始划分组数为1.
            for(int i : nums){
                temp += i;
                if(temp > midMax){  //再加上第i个数时就超过了
                    //此时前i-1个数可以划分为一组
                    div++;
                    temp = i;  //再初始化,相当于开始新的一组的划分。
                }
            }

            //再重新定位low以及high
            if(div > m){
                //划分多了。说明此时的midMax还是太小了。
                lowMax = midMax + 1;
            }else{
                //划分组数太少了,说明midMax太大了。
                highMax = midMax;
            }
        }
        return lowMax;
    }
}

C++版:

class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
        int n = nums.size();

        long dp[n+1][m+1];
        for(int i = 0;i <= n;i++)
            for(int j = 0;j <= m;j++)
                dp[i][j] = INT_MAX;
        dp[0][0] = 0;
        long subSum[n+1];
        subSum[0] = 0;
        subSum[1] = nums[0];
        for(int i = 2;i <= n;i++){
            subSum[i] = subSum[i-1] + nums[i-1];
        }

        for(int i = 1;i <= n;i++){
            for(int j = 1;j<=m && j<=i;j++){
                for(int k = 0;k < i;k++){
                    dp[i][j] = min(dp[i][j], max(dp[k][j-1], subSum[i]-subSum[k]));
                }
            }
        }
        return (int)dp[n][m];
    }
};

 

题目描述:给定一个非负整数数组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、付费专栏及课程。

余额充值