DP问题学习笔记整合

DP问题学习笔记

1. 相关概念

  • dp两大性质

    1. 最优子结构问题: 如果问题的解是最优的,那么子问题的解也是最优的
    2. 利用备忘录思想解决重叠子问题计算问题
  • dp问题coding的常见步骤

    1. 找出最优解的性质,利用树, 图等数据解构刻画其结构特征
    2. 递归的定义的最优值
    3. 自底向上分析,找出状态转移方程, 计算子问题的最优解
    4. 根据子问题的最优解构造最优解
  • dp解题步骤:

    第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?

    第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]…..dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。

    第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值

经典例题

一维dp

最大子段和

示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

  • 采用dp的思路, 我们把这个数组分成不同的段, dp[i] 表示以nums[i]结尾的那个段的最大字段和, 所以就存在nums[i]这个元素是加入前一段中, 还是自成一段这就是需要思考的问题, 只用求dp[i-1] + nums[i] 和 nums[i] 的最大值即可

  • 通过上面的分析, 很容易写出状态转移方程

// dp初始化
dp[0] = nums[0]
dp[i] = Max(dp[i-1] + nums[i], nums[i])
  • 这个题很有个很坑的地方就是, dp中最后一个元素并不是最终要求的结果, 这个我们平时做的题有很大的出入, dp[i]的含义是以nums[i]结尾的那个段的最大字段和, 那么dp中最后一个元素表示的是以nums中最后一个元素结尾的那个段的最大字段和, 最大的字段和不一定以nums中最后一个元素结尾,所以要最终要求的目标是dp数组中的最大值.

    public int maxSubArray(int[] nums) {
            if(nums == null || nums.length <= 0) throw new IllegalArgumentException();
            int[] dp = new int[nums.length];
            dp[0] = nums[0];
            for(int i = 1; i < nums.length; ++i){
                dp[i] = Math.max(nums[i], nums[i] + dp[i-1]);
            }
            // return dp[nums.length-1]; 神坑
            int maxValue = Integer.MIN_VALUE;
            for(int j = 0; j < dp.length; ++j){
                if(dp[j] > maxValue)maxValue = dp[j];
            }
            return maxValue;
    
  • 优化 观察dp方程里面, dp[i]依赖dp[i-1]和nums[i],所以可以用一个变量来表示dp[i-1], 同时用一个变量来表示最大子段和, 最后也就不用再遍历dp数组了

public int maxSubArray(int[] nums) {
        if (nums == null || nums.length == 0) {
            throw new IllegalArgumentException();
        }

        int numsLen = nums.length;
        int preMaxSum = 0; // 表示dp[i-1]
        int maxSums = Integer.MIN_VALUE; // 表示最大子段和

        for (int i = 1; i <= numsLen; i++) {
            preMaxSum = Math.max(preMaxSum + nums[i-1],nums[i-1]);
            maxSums = Math.max(maxSums, preMaxSum);
        }

        return maxSums;
    }
  • 继续想 如果想让你求出最大子段和是哪些序列? 如何做? 可以使用一个数组location记录以某个元素的最大子段和序列的个数, 看代码即可
public int[] maxSubArrayResult(int[] nums) {
        if (nums == null || nums.length == 0) {
            throw new IllegalArgumentException();
        }

        int numsLen = nums.length;
        int preMaxSum = 0;
        int maxSums = nums[0];
        int maxSumsIndex = 1;
        int[] location = new int[numsLen + 1];

        for (int i = 1; i <= numsLen; i++) {
            int tmp = preMaxSum + nums[i-1];
            if (tmp > nums[i-1]) {
                location[i] = location[i-1] + 1;
                preMaxSum = tmp;
            } else {
                location[i] = 1;
                preMaxSum = nums[i-1];
            }

            if (maxSums < preMaxSum) {
               maxSums = tmp;
               maxSumsIndex = i;
            }
        }

        int resultLen = location[maxSumsIndex];
        int start = maxSumsIndex - resultLen + 1;
        int[] result = new int[resultLen];
        System.arraycopy(nums, start - 1, result, 0, resultLen);

        return result;
    }
  • 采用分治的思路, 我们把数组从中间分开, 最大子序列的位置就存在以下三种情况

    • 最大子序列在左半边, 采用递归解决
    • 最大子序列在右半边, 采用递归解决
    • 最大子序列横跨左右半边, 左边的最大值加上右边的最大值
  • 时间复杂度分析

T(n) = 2 F(n/2) + n

时间复杂度O(nlgn)

public int maxSubArray(int[] nums) {
        if(nums == null || nums.length <= 0) throw new IllegalArgumentException();
        return  helper(nums, 0, nums.length-1);

    }

    private int helper(int []  nums, int start, int end){
        if(nums == null || nums.length <= 0) throw new IllegalArgumentException();

        if(start == end)return nums[start];

        
        int middle = start + (end - start) / 2;
        int leftSums = helper(nums,start, middle);
        int rightSums = helper(nums,middle+1, end);
        
        // 横跨左右两边
        int leftRightSums;

        // 左边的最大值
        int lsums = Integer.MIN_VALUE, temp = 0;
        for(int i = middle; i >= start; i--){
               temp += nums[i];
               if(temp > lsums)lsums = temp;
        }

        // 右边的最大值
        int rsums = Integer.MIN_VALUE;
        temp = 0;
        for(int j = middle+1; j <= end; j++){
            temp += nums[j];
            if(temp > rsums) rsums = temp;
        }
        leftRightSums = rsums + lsums;

        return Math.max(Math.max(leftSums, rightSums), leftRightSums);
    }

  • 打家劫舍

最大乘积和

  • 题目描述给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
    示例 1:

输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

一不注意就做成了最大子段和
[-2, 3, -4]

maxDp[-2, 3, 12]
当nums[i]为正数时, nums[i] * maxDp[i-1]有可能是最大
当nums[i]为负数时, nums[i] * maxDp[i-1]要变成最小的了, 要想变成最大的, 就需要维护一个minDp
这种题目的话, 一般要利用最大和最小两个特性来解决

代码

public int maxProduct(int[] nums) {
        if (nums == null || nums.length == 0) {
            throw new IllegalArgumentException();
        }

        int len = nums.length;
        int returnVal = nums[0];
        int preMaxMulti = 1; // 最大乘积
        int preMinMulti = 1; // 最小乘积
        int tmp;

        for (int i = 1; i <= len; i++) {
            if (nums[i-1] < 0) { // 当前元素为负数时, 则前一个元素的最小乘积乘以一个负数才可能会最大
                tmp = preMaxMulti;
                preMaxMulti = Math.max(nums[i-1], nums[i-1] * preMinMulti);
                preMinMulti = Math.min(nums[i-1], nums[i-1] * tmp);
            }

            if(nums[i-1] >= 0) { // // 当前元素为正数时, 则前一个元素的最大乘积乘以一个正数才可能会最大
                preMaxMulti = Math.max(nums[i-1], nums[i-1] * preMaxMulti);
                preMinMulti = Math.min(nums[i-1], nums[i-1] * preMinMulti);
            }

            returnVal = Math.max(preMaxMulti, returnVal);
        }

        return returnVal;
    }

二维dp

矩阵连乘

图片分割

0-1背包

posted @ 2021-05-21 22:13  FizzPu  阅读(73)  评论(0编辑  收藏  举报