LeetCode刷题笔记--动态规划

目录

方法介绍

练习题目

1、53.最大子序组和

2、124.二叉树的最大路径和

3、300.最长上升子序列

总结


方法介绍

动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划不是某一种具体的算法,而是一种算法思想:若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。与分治法不同,其分解得到的子问题往往是相互联系的。

动态规划常常适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果,因此动态规划方法所耗时间往往远少于朴素解法。

动态规划有自底向上自顶向下两种解决问题的方式。

①自顶向下即记忆化递归;

②自底向上就是递推。

使用动态规划解决的问题有个明显的特点,一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做无后效性,求解问题的过程形成了一张有向无环图。动态规划只解决每个子问题一次,具有天然剪枝的功能,从而减少计算量。

动态规划算法适用于解最优化问题,其求解三大步骤:

①定义数组dp[i]中元素含义;

②找出数组元素之间的关系式;

③找出初始值;


练习题目

1、53.最大子序组和

题目(中等):给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:输入:nums = [-2,1,-3,4,-1,2,1,-5,4]  输出:6

解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:输入:nums = [1]  输出:1
示例 3:输入:nums = [5,4,-1,7,8]  输出:23

求解:

关键理解:

我们 不知道和最大的连续子数组一定会选哪一个数,那么我们可以求出 所有 经过输入数组的某一个数的连续子数组的最大和。将问题分解为以下子问题:

子问题 1:以 -2 结尾的连续子数组的最大和是多少;
子问题 2:以 1 结尾的连续子数组的最大和是多少;
子问题 3:以 -3 结尾的连续子数组的最大和是多少;

...

其中子问题1以 -2 结尾的连续子数组是 [-2],因此最大和就是 −2。

子问题 2:以 11 结尾的连续子数组的最大和是多少;
以 11 结尾的连续子数组有 [-2,1] 和 [1] ,其中 [-2,1] 就是在「子问题 1」的后面加上 1 得到。-2 + 1 = -1 < 1−2+1=−1<1 ,因此「子问题 2」 的答案是 11。

如果编号为 i 的子问题的结果是负数或者 0 ,那么编号为 i + 1 的子问题就可以把编号为 i 的子问题的结果舍弃掉(这里 i 为整数,最小值为 1 ,最大值为 8),这是因为:

一个数 a 加上负数的结果比 a 更小;
一个数 a 加上 0 的结果不会比 a 更大;
而子问题的定义必须以一个数结尾,因此如果子问题 i 的结果是负数或者 00,那么子问题 i + 1 的答案就是以 nums[i] 结尾的那个数。

复杂度分析

时间复杂度:O(n),其中 n 为nums 数组的长度。我们只需要遍历一遍数组即可求得答案。
空间复杂度:O(1),我们只需要常数空间存放若干变量。

代码1:

class Solution {
    public int maxSubArray(int[] nums) {
        int len=nums.length;
        if(len==1){return nums[0];}
        int[] dp=new int[len];//表示以nums[i]结尾的连续子数组的最大和
        dp[0]=nums[0];

        for(int i=1;i<len;i++){
            if(dp[i-1]>0){
                //连续子数组
                dp[i]=dp[i-1]+nums[i];
            }else{
                //前一个结尾的最大连续数组和为负,则舍弃重新开始
                dp[i]=nums[i];
            }
        }

        int res=nums[0];
        for(int i=0;i<len;i++){
            //遍历dp数组,找到最大和
            res=Math.max(res,dp[i]);
        }

        return res;
    }
}

代码2:

class Solution {
    public int maxSubArray(int[] nums) {
        int len=nums.length;
        if(len==1){return nums[0];}
        int res=nums[0];
        int pre=0;

        for(int num:nums){
            pre=Math.max(num,pre+num);
            res=Math.max(pre,res);
        }
        return res;
    }
}

2、124.二叉树的最大路径和

题目(困难):路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

示例 1:


输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:


输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

 求解:

考虑实现一个简化的函数 maxGain(node),该函数计算二叉树中的一个节点的最大贡献值,具体而言,就是在以该节点为根节点的子树中寻找以该节点为起点的一条路径,使得该路径上的节点值之和最大。

具体而言,该函数的计算如下。

·空节点的最大贡献值等于 0。

·非空节点的最大贡献值等于节点值与其子节点中的最大贡献值之和(对于叶节点而言,最大贡献值等于节点值)。

例如:

叶节点 9、15、7 的最大贡献值分别为 9、15、7。

得到叶节点的最大贡献值之后,再计算非叶节点的最大贡献值。节点 20 的最大贡献值等于 20+max(15,7)=35,节点−10 的最大贡献值等于 -10+max(9,35)=25。

复杂度分析

时间复杂度:O(N),其中 N 是二叉树中的节点个数。对每个节点访问不超过 2 次。

空间复杂度:O(N),其中 N 是二叉树中的节点个数。空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。

代码:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int res=Integer.MIN_VALUE;//不能定义为静态,负责会保存上一次计算结果
    public int maxPathSum(TreeNode root) {
        getPath(root);
        return res;
    }
    public int getPath(TreeNode node){
        if(node==null){
            return 0;
        }

        //递归遍历左节点,大于0返回
        int left=Math.max(getPath(node.left),0);
         //递归遍历有节点,大于0返回
        int right=Math.max(getPath(node.right),0);

        //更新路径
        int path=left+node.val+right;
        if(res<path){
            res=path;
        }

        //返回当前节点最大路径
        return node.val+Math.max(left,right);
    }
}

3、300.最长上升子序列

题目(中等):给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

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

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

求解:

本题与53.最大子序组和的思想类似,将问题分解为求以各数组元素结尾的最大子序和。具体如下:

状态定义:dp[i] 的值代表 nums 以 nums[i] 结尾的最长子序列长度。
转移方程: 设 j∈[0,i),考虑每轮计算新 dp[i] 时,遍历 [0,i)列表区间,做以下判断:

1、当 nums[i] > nums[j] 时: nums[i] 可以接在nums[j] 之后(此题要求严格递增),此情况下最长上升子序列长度为 dp[j] + 1 ;
2、当 nums[i]<=nums[j] 时: nums[i] 无法接在nums[j] 之后,此情况上升子序列不成立,跳过。

上述所有 1. 情况 下计算出的 dp[j]+1 的最大值,为直到 i 的最长上升子序列长度(即 dp[i] )。实现方式为遍历 j 时,每轮执行 dp[i] = max(dp[i], dp[j] + 1)。
转移方程: dp[i] = max(dp[i], dp[j] + 1)   for  j  in  [0, i)。
初始状态:dp[i] 所有元素置 1,含义是每个元素都至少可以单独成为子序列,此时长度都为 1。
返回值:返回 dp 列表最大值,即可得到全局最长上升子序列长度。
 

复杂度分析:
时间复杂度 O(N^2) : 遍历计算 dp列表需 O(N),计算每个dp[i] 需 O(N)。
空间复杂度 O(N) : dp列表占用线性大小额外空间。

代码:

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len=nums.length;
        if(len==0) return 0;
        //以nums[i]结尾的最长递增子序列长度
        int[] dp=new int[len];
        //数组填充1
        Arrays.fill(dp,1);
        int res=0;

        for(int i=1;i<len;i++){
            //保证递增,遍历 i 前数组元素
            for(int j=0;j<i;j++){
                //找到小于自身数组元素nums[j],更新dp[i]
                if(nums[i]>nums[j]){
                    //比较当前递增序列和经过nums[j]的递增序列长度
                    //若新序列更长则进行更新
                    dp[i]=Math.max(dp[i],dp[j]+1);
                }
            }
            //记录最长递增子序列长度
            res=Math.max(res,dp[i]);
        }

        return res;
    }
}

总结

利用动态规划解决相关最优化问题,其核心也是难点就是状态转移方程(描述子问题之间的联系)的建立,要在成分理解题意的基础上,将复杂问题分解成相互联系的子问题,找到子问题之间的联系并由此建立状态转移方程,最后根据条件确定初始值。

参考《力扣 动态规划》

https://leetcode.cn/tag/dynamic-programming/problemset/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值