410. 分割数组的最大值 Krains 2020-08-29 20:21:39 动态规划二分查找

时间:2022-07-24
本文章向大家介绍410. 分割数组的最大值 Krains 2020-08-29 20:21:39 动态规划二分查找,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目链接

二分查找

对答案进行二分,得到mid,如果mid可以将数组切割成m组,并且每组之和小于mid,由于我们要找的是满足要求的最小值,所以可以排除区间(mid, right],去[left, mid]找,如果不满足,那么区间[left, mid]中任一数必然也不满足条件,应到[mid+1, right]找。

切割时贪心地去尽可能将让一组的和尽可能大,只有这一组放不下的时候才新开一组。

class Solution {
    public int splitArray(int[] nums, int m) {
        int right = Integer.MAX_VALUE;
        int left = 0;
        while(left < right){
            int mid = left + (right - left) / 2;
            if(canSplit(nums, m, mid)){
                right = mid;
            }else{
                left = mid + 1;
            }
        }

        return left;
    }

    public boolean canSplit(int[] nums, int m, int sum){
        int s = 0;
        int count = 1;
        for(int num : nums){
            // 如果数组中某个数大于sum,无法完成分组
            if(num > sum)
                return false;
            // 如果num能够加入分组,则加入,否者新开一个分组
            if(s + num <= sum){
                s += num;
            }else{
                s = num;
                count++;
            }
        }

        return count <= m;
    }
}

复杂度分析

  • 时间复杂度:O(Nlog2I)O(Nlog_2I)O(Nlog2​I),I其实等于sum(nums)-min(nums),代码没有体现这一点,主要是由于lc没有给出nums元素的范围。
  • 空间复杂度:O(1)O(1)O(1)

动态规划

状态表示

  • 集合:使用(i,j)(i, j)(i,j)表示前j个数字分i组的所有分组的集合。
  • 属性:一个集合就是一个分组,取各个小组之和有一个最大值,每个集合都有这么一个最大值,f(i,j)f(i,j)f(i,j)表示的就是这些集合中最大值的最小值。

状态计算/集合划分

  • 将元素[0, j)划分为第i组,f(i,j)=max(f(i−1,0),sum(nums[0,j)))f(i, j) = max(f(i-1, 0), sum(nums[0, j)))f(i,j)=max(f(i−1,0),sum(nums[0,j)))
  • 将元素[1, j)划分为第i组,f(i,j)=max(f(i−1,1),sum(nums[1,j)))f(i, j) = max(f(i-1, 1), sum(nums[1, j)))f(i,j)=max(f(i−1,1),sum(nums[1,j)))
  • ...
  • 将元素[k, j)划分为第i组,f(i,j)=max(f(i−1,k),sum(nums[k,j)))f(i, j) = max(f(i-1, k), sum(nums[k, j)))f(i,j)=max(f(i−1,k),sum(nums[k,j)))

取这些集合的最小值就是f(i,j)f(i, j)f(i,j),写出方程

f(i,j)=min(max(f(i−1,k),preSum[j]−preSum[k])),k∈[0,j),j∈[i,n]f(i, j) = min(max(f(i-1,k),preSum[j]-preSum[k])) ,kin[0,j),jin[i,n] f(i,j)=min(max(f(i−1,k),preSum[j]−preSum[k])),k∈[0,j),j∈[i,n]

preSum[j]nums[0, j)的前缀和,n是nums的长度。

边界处理:

初始化二维dp数组为INF,dp[0][0]=0

  • 当i=1时,只有将所有的数划分在第i组才有效
  • 当i=2时,状态dp[i-1][j]j>=1是有效的

当不符合划分规则时,dp[i-1][j]=INF,i-1>j,也就无法选取这些不符合规则的划分。

class Solution {
    public int splitArray(int[] nums, int m) {
        int n = nums.length;
        int[] preSum = new int[n+1];
        int[][] dp = new int[m+1][n+1];
        for(int i = 1; i <= n; i++){
            preSum[i] += preSum[i-1] + nums[i-1];
        }

        for(int i = 0; i <= m; i++){
           Arrays.fill(dp[i], Integer.MAX_VALUE);
        }
        
        dp[0][0] = 0;
        for(int i = 1; i <= m; i++){
            for(int j = i; j <= n; j++){
                for(int k = 0; k < j; k++){
                    dp[i][j] = Math.min(dp[i][j], Math.max(dp[i-1][k], preSum[j] - preSum[k]));
                }
            }
        }

        return dp[m][n];
    }
}

复杂度分析

  • 时间复杂度:O(mn2)O(mn^2)O(mn2)
  • 空间复杂度:O(mn)O(mn)O(mn),m是组数,n是nums的长度。

DFS记忆化

用dfs去搜索任何可能划分,使用记忆化剪枝。

class Solution {
    // 前缀和数组,因为数组和会超出int范围,因此用long
    long[] s;
    int n;

    public int splitArray(int[] nums, int m) {
        n = nums.length;
        s = new long[n+1];
        for(int i = 1; i <= n; i++){
            s[i] = s[i-1] + nums[i-1];
        }

        return (int)helper(0, 0, m, new Long[n+1][m+1]);
    }

    // [i, n)是待划分的区域,j表示当前是第几个划分点,j个划分点能够划出j+1个区域
    public long helper(int i, int j, int m, Long[][] memo){
        if(memo[i][j] != null)
            return memo[i][j];
		
        // 当j+1等于m时,划分完毕,直接返回当前区域的和
        if(j+1 == m)
            return s[n] - s[i];

        long min = Integer.MAX_VALUE;
        // 穷尽所有划分点
        for(int k = i; k < n; k++){
            // t是当前划分的组内和的最大值
            long t = Math.max(s[k+1]-s[i], helper(k+1, j+1, m, memo));
            // min是所有可能划分的组内和的最大值的最小值
            min = Math.min(t, min);
        }

        // 记忆
        return memo[i][j] = min;
    }
}