leetcode410:分割数组的最大值_填表法求解动态规划

leetcode410

视频讲解B站地址: https://www.bilibili.com/video/BV16T4y1j7w9/

动态规划算法

动态规划算法有两个难点:

  1. 寻找状态转移方程: 这个需要一点直觉.
  2. 状态边界的确定: 如果状态边界确定不好就会带来各种bug,这个视频中我通过填表法解决这个问题.

动态规划数组的定义

定义动态规划数组dp[i][j]表示将nums[0, ..., i]区间内的数分为j组得到的符合题意的结果.

下面以数组nums={7,3,5,10,8}为例,演示对应dp数组某几个位上的取值.

在这里插入图片描述

状态转移:状态之间的变换关系

dp[i][j]表示将数组区间nums[0...i]分为j组,可以由dp[k][j-1]得到:

也就是说先将数组区间的一部分nums[0..k]分为j-1组,再将剩下的nums[k+1, .., i]自成一组,再去求各组和的最大值.即:
d p [ i ] [ j ] = min ⁡ k 在 某 范 围 内 取 值 { max ⁡ ( d p [ k ] [ j − 1 ] , s u m ( n u m s [ k + 1 , . . . j ] ) ) } dp[i][j] = \min_{k在某范围内取值} \{ \max( dp[k][j-1], sum(nums[k+1,...j]) ) \} dp[i][j]=kmin{max(dp[k][j1],sum(nums[k+1,...j]))}

(这里的k是可以遍历的有一定的范围,再后文我们再讨论k具体的取值范围).

下面例子展示了如何由dp[1][2],dp[2][2],dp[3][2]得到dp[4][3]:

在这里插入图片描述

状态的边界: 填表法

分析状态的边界是动态规划问题最容易出现bug的地方,我在这里演示填表法,一种考虑边界的普适性的方法:

dp数组画成一张表格,确定i轴和j轴的范围,在本题中,i轴的取值范围从0len(nums)-1,j轴的取值范围从0最大组数.(当然不可能将元素分为0组,在这里j轴从0开始是为了照顾编程语言索引从0开始,简化编程,因此j=0这一列其实是无意义的).

在这里插入图片描述

下面分别考虑上表中的三个区域: dp数组取值不存在的区域,dp数组取值的边界区域,正常动态规划的区域.

dp数组取值不存在的区域

dp数组取值不存在有以下两种情况:

  1. 组数j0时,dp取值不存在,因为没办法将一些元素分为0组.
  2. 组数j大于元素个数i+1时,dp取值不存在,好比两颗糖没法分给三个孩子.

两个区域以红色标识出来,因为我们的动规取目标的是最小值,所以在编程时将dp数组这些区域的值设为无穷大即可.

在这里插入图片描述

dp数组取值的边界区域

dp数组取值的边界,也就是说动规从这些位置开始,需要特殊考虑的情况,对于本题来说,有两个边界区域:

  1. j==1,即把所有元素仅分为一组时,dp数组对应的值即为该组元素之和.
  2. j==(i+1),即组数和元素个数相同时,dp数组对应的值即为区间内最大元素值.

在这里插入图片描述

正常状态转移的区域

回顾正常转移的状态转移公式,在该公式中我们要确定k的取值范围:
d p [ i ] [ j ] = min ⁡ k 在 某 范 围 内 取 值 { max ⁡ ( d p [ k ] [ j − 1 ] , s u m ( n u m s [ k + 1 , . . . j ] ) ) } dp[i][j] = \min_{k在某范围内取值} \{\max(dp[k][j-1],sum(nums[k+1,...j]))\} dp[i][j]=kmin{max(dp[k][j1],sum(nums[k+1,...j]))}

在这里插入图片描述

下图的红色箭头和蓝色箭头分别表示求取dp[3][2]dp[4][3]时的转移情况:

  • i=3,j=2时,k取到了{0,1,2}.
  • i=4,j=3时,k取到了{1,2,3}

在这里插入图片描述

根据上图分析,当求取dp[i][j]时,k的取值范围应为: k<i(毕竟要留下一个给最后一组)且k+1>=j-1(元素数必须大于等于组数)

代码实现

伪代码

class Solution {

    public int splitArray(int[] nums, int m) {

        // dp[i][j]表示将数组nums[0, ..., i]这(i+1)个数分为j组的情况下,得到的符合题意的最小组和
        int[][] dp = new int[nums.length][m + 1];
        
        // 1. 处理dp值不存在的状态
        for (int i = 0; i < nums.length; i++) {
            dp[i][0] = Integer.MAX_VALUE;
        }
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 2; j <= m; j++) {
                dp[i][j] = Integer.MAX_VALUE;
            }
        }
        // 2. 处理边界状态
        for (int i = 0; i < nums.length && i + 1 <= m; i++) {
            dp[i][i + 1] = intervalMax(nums, 0, i);
        }
        for (int i = 0; i < nums.length; i++) {
            dp[i][1] = intervalSum(cumsum, 0, i);
        }
        // 3. 正常动态规划的状态转移
        for (int j = 2; j <= m; j++) {
            for (int i = j; i < nums.length; i++) {
                // 求取dp[i][j], k的取值范围从(j-2)到(i-1)
                dp[i][j] = Integer.MAX_VALUE;
                for (int k = j - 2; k < i; k++) {
                    int candidate = Math.max(dp[k][j - 1], intervalSum(cumsum, k + 1, i))
                    dp[i][j] = Math.min(dp[i][j], candidate);
                }
            }
        }

		// 返回dp[nums.length - 1][m]
        return dp[nums.length - 1][m];
    }

    private int intervalSum(int[] nums, int start, int end) {
		// 求数组nums从第start位到第end位的区间和
    }

    private int intervalMax(int[] nums, int start, int end) {
        // 求数组nums从第start位到第end位的区间最大值
    }

    public static void main(String[] args) {
        new Solution().splitArray(new int[]{7, 2, 5, 10, 8}, 2);
    }
}

Java代码

在实际编程中要注意的是,为了简化求取区间和的intervalSum函数,我们先对sum数组进行积分得到cumsum数组,通过在积分数组上做差得到数组区间和:

// 积分数组的计算
int[] cumsum = new int[nums.length];
cumsum[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
    cumsum[i] = cumsum[i - 1] + nums[i];
}

private int intervalSum(int[] cumsum, int start, int end) {
    if (start == 0)
        return cumsum[end];
    return cumsum[end] - cumsum[start - 1];
}

通过测试的Java代码如下:

class Solution {

    public int splitArray(int[] nums, int m) {

        // 计算积分数组cumsum,以简化数组区间和的计算
        int[] cumsum = new int[nums.length];
        cumsum[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            cumsum[i] = cumsum[i - 1] + nums[i];
        }

        // dp[i][j]表示将从nums[0]到nums[i]这(i+1)个数分为j组的情况下,得到的符合题意的最小组和
        int[][] dp = new int[nums.length][m + 1];
        // 1. 处理不可能到达的值
        for (int i = 0; i < nums.length; i++) {
            dp[i][0] = Integer.MAX_VALUE;
        }
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 2; j <= m; j++) {
                dp[i][j] = Integer.MAX_VALUE;
            }
        }
        // 2. 处理边界值
        for (int i = 0; i < nums.length && i + 1 <= m; i++) {
            dp[i][i + 1] = intervalMax(nums, 0, i);
        }
        for (int i = 0; i < nums.length; i++) {
            dp[i][1] = intervalSum(cumsum, 0, i);
        }
        // 3. 正常dp
        for (int j = 2; j <= m; j++) {
            for (int i = j; i < nums.length; i++) {
                // 求取dp[i][j]
                dp[i][j] = Integer.MAX_VALUE;
                for (int k = j - 2; k < i; k++) {
                    dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j - 1], intervalSum(cumsum, k + 1, i)));
                }
            }
        }

        return dp[nums.length - 1][m];
    }

    private int intervalSum(int[] cumsum, int start, int end) {
        if (start == 0)
            return cumsum[end];
        return cumsum[end] - cumsum[start - 1];
    }

    private int intervalMax(int[] nums, int start, int end) {
        int maxValue = Integer.MIN_VALUE;
        for (int i = start; i <= end; i++) {
            maxValue = Math.max(maxValue, nums[i]);
        }
        return maxValue;
    }

    public static void main(String[] args) {
        new Solution().splitArray(new int[]{7, 2, 5, 10, 8}, 2);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值