算法--动态规划

一:动态规划原理

1、基本思想

问题的 最优解如果可以由子问题的最优解推导而出,则可以先求解子问题的最优解,再构造原问题的最优解;若子问题有较多的重复出现,则可以自底向上的从最终子问题向原问题逐步求解。

2、使用条件

可分为多个相关子问题,子问题的解被重复使用

1)优化子结构

  • 一个问题的优化接包含了子问题的优化解
  • 缩小子问题集合,只需那些优化问题中包含的子问题,降低实现复杂性
  • 可以自下而上

2)重叠子问题

  • 在问题的求解过程中,很多子问题的解将被重复使用

3、动态规划五部曲

  1. 确定dp数组(dp table)以及下标的含义

  2. 确定递推公式

  3. dp数组如何初始化

  4. 确定遍历顺序

    ​ 确定遍历顺序可以通过递推公式,dp[i][j]由哪个方向的dp推出

  5. 举例推导dp数组

4、思考问题

这道题⽬我举例推导状态转移公式了么?

我打印dp数组的⽇志了么?

打印出来了dp数组和我想的⼀样么?

5、如何调试

(1)找问题的最好⽅式就是把dp数组打印出来

(2)状态转移在dp数组的上具体情况模拟⼀遍

二:背包问题

在这里插入图片描述

1、01背包详解

N件物品和⼀个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i]每件物品只能⽤⼀次,求解将哪些物品装⼊背包⾥物品价值总和最⼤。

1)二维数组

下标定义:

dp[i][j]表示从下标[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

202203220842194

​ 先遍历背包或者先遍历物品都可以,因为dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上⻆⽅向(包括正左和正上两个⽅向)

在这里插入图片描述

2)一维数组(滚动数组)

滚动数组:上一层可以重复利用,直接拷贝到当前层。

dp[i-1]那一层拷贝到dp[i]上,表达式变成:dp[i][j]=max(dp[i][j],dp[i][j-weight[i]]+value[i])

转换成一维数组:dp[j]=max(dp[j],dp[j-weight[i]]+value[i])

for(int i = 0; i < weight.size(); i++) { // 遍历物品
     for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
     	dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
     }
}

一维数组是倒叙遍历,每次取得状态不会和之前取得状态重合,这样每种物品就只取⼀次了。

2、完全背包详解

有N件物品和⼀个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有⽆限个(也就是可以放⼊背包多次),求解将哪些物品装⼊背包⾥物品价值总和最⼤。

完全背包和01背包问题唯⼀不同的地⽅就是,每种物品有⽆限件

1)遍历顺序解析

我们知道01背包内嵌的循环是从⼤到⼩遍历,为了保证每个物品仅被添加⼀次

⽽完全背包的物品是可以添加多次的,所以要从⼩到⼤去遍历,即:

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
     for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量
     	dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
     }
}

在完全背包中,对于⼀维dp数组来说,其实两个for循环嵌套顺序同样⽆所谓!

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了

2)完全背包分类

弄清什么是组合,什么是排列很重要。

组合不强调顺序,(1,5)和(5,1)是同⼀个组合。

排列强调顺序,(1,5)和(5,1)是两个不同的排列。

如果求组合数就是外层for循环遍历物品,内层for遍历背包。

如果求排列数就是外层for遍历背包,内层for循环遍历物品。

3、 解题思路

1)组合问题

组合问题公式

dp[i] += dp[i-num]

2)True、False问题

139. 单词拆分
416. 分割等和子集

True、False问题公式

dp[i] = dp[i] or dp[i-num]

3)最大最小问题

474. 一和零
322. 零钱兑换

最大最小问题公式

dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)

以上三组公式是解决对应问题的核心公式

4、解题步骤

0.动态规划三部曲

1.分析是否为背包问题。
2.是以上三种背包问题中的哪一种。
3.是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用。
4.如果是组合问题,是否需要考虑元素之间的顺序。需要考虑顺序有顺序的解法,不需要考虑顺序又有对应的解法。

5、背包问题判定

背包问题具备的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数字,也可能是字符串,问:能否使用nums中的元素做各种排列组合得到target。

6、背包问题技巧

1.如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;

for num in nums:
    for i in range(target, nums-1, -1):

2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。

for num in nums:
    for i in range(nums, target+1):

三:力扣题目解析

——基础题目

746. 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

1)题目描述

题⽬描述理解为:

每当你爬上⼀个阶梯你都要花费对应的体⼒值,⼀旦⽀付了相应的体⼒值,你就可以选择向上爬⼀个阶梯或者爬两个阶梯。

2)思路解析

  1. 确定dp数组(dp table)以及下标的含义

    dp[i]表示从楼梯第i个台阶向上花费的最少体力

  2. 确定递推公式

    ​ 可选择向上爬一个或者两个台阶:dp[i-1] || dp[i-2]

    ​ 花费最少体力:选择min(dp[i-1],dp[i-2])

    ​ 楼梯到达i 个台阶需要支付的费用:cost[i]

  3. dp数组如何初始化

    ​ 每当你爬上⼀个阶梯你都要花费对应的体⼒值

    dp[0] = cost[0]

    dp[1] = cost[1]

  4. 确定遍历顺序

    从前到后遍历cost数组

  5. 举例推导dp数组

Math.min(dp[i-1],dp[i-2])+cost[i]

3)代码实现

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        if(cost.length==2){
            return Math.min(cost[0],cost[1]);
        }
        int[] dp = new int[cost.length];
        dp[0] = cost[0];
        dp[1] = cost[1];
        for(int i=2;i<cost.length;i++){
            dp[i] = Math.min(dp[i-1],dp[i-2])+cost[i];
        }
        return Math.min(dp[cost.length-1],dp[cost.length-2]);
    }
}

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

img
输入:m = 3, n = 7
输出:28

1)思路解析

  1. 确定dp数组(dp table)以及下标的含义

    dp[m][n]二维数组

    ​ 下标的含义:到达m-1,n-1位置的路径数

  2. 确定递推公式

    image-20220316173222185

    dp[i][j] = dp[i-1][j] + dp[i][j-1]

    dp 只能从两个方向过来

  3. dp数组如何初始化

    ​ 由于机器人每次只能向下或者向右走一格,所以dp[0][j] dp[i][0]即第一行第一列初始化为1

  4. 确定遍历顺序

    ​ 要确定dp[i-1][j] dp[i][j-1] 都有值,所以从左到右一层一层遍历

  5. 举例推导dp数组

    image-20220317090650066

2)代码实现

class Solution {
    public int uniquePaths(int m, int n) {
        if(m==1 || n==1) return 1;
        int[][] dp = new int[m][n];
        for(int i=0;i<m;i++){
            dp[i][0] = 1;
        }
        for(int j=0;j<n;j++){
            dp[0][j] = 1;
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j] = dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

343. 整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

1)思路解析

  1. 确定dp数组(dp table)以及下标的含义

    dp[j]表示拆分j得到的最大乘积dp[j]

  2. 确定递推公式

    推导:

    我们将原问题抽象为 f(n)
    ①那么 f(n) 等价于 max(1 * f(n - 1), 2 * f(n - 2), ..., (n - 1) * f(1))

    ​ ②数字i 可以拆分成 j + (i - j)。但 j * (i - j)不一定是最大乘积,因为i-j不一定大于dp[i - j](数字i-j拆分成整数之和的最大乘积),这 里要选择最大的值作为 dp[i] 的结果。

    ​ ③dp[j]=Math.max(Math.max(dp[j-i]*j,(j-i)*j),dp[j])

  3. dp数组如何初始化

    0,1无法拆分 所以从dp[2]开始初始化 dp[2]=1*1=1

  4. 确定遍历顺序

    dp[j]=Math.max(Math.max(dp[j-i]*j,(j-i)*j),dp[j])

    ​ 由推导公式可知dp必须从前往后遍历

    ​ 因为从dp[2]初始化,为了确保数据一定有值,所以j(背包大小)从3开始

    ​ 物品i从1,开始j-i>2

  5. 举例推导dp数组

image-20220319172526346

2)代码实现

class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n+1];
        if(n==2) return 1;
        if(n==3) return 2;
        dp[2] = 2;
        for(int i =3;i<=n;i++){
            for(int j=1;j<i-1;j++){
                dp[i] = Math.max(dp[i],Math.max(j *(i-j) , j*dp[i-j]));
            }
        }
        return dp[n];
    }
}

96. 不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

img

输入:n = 3
输出:5

1)思路解析

  1. 确定dp数组(dp table)以及下标的含义

    n有dp[n]中不同的二叉搜索树

  2. 确定递推公式

    image-20220319173602596
    在这里插入图片描述

    有图可知推论:

    ​ 当头结点为1时,二叉搜索树的布局和n=2即dp[2]时的布局一样(不考虑节点的值)

    ​ 当头结点为2时,二叉搜索树的布局和n=1即dp[1]时的布局一样

    ​ 当头结点为3时,二叉搜索树的布局和n=2即dp[2]时的布局一样

    ​ 可知:dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

    ​ 进一步得出:dp[i] += dp[j - 1] * dp[i - j];

  3. dp数组如何初始化

    递推公式需要与dp[0]相乘,所以dp[0] = 1

  4. 确定遍历顺序

    头结点i需要从1开始遍历

    dp[i] += dp[j - 1] * dp[i - j]; 可知i-j必须大于0,所以j从1开始遍历到i

  5. 举例推导dp数组

    题目可知

2)代码实现

class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0]=1;
        dp[1]=1;
        for(int i =2; i<=n;i++){
            for(int j=1;j<=i;j++){
                dp[i]+=dp[j-1] * dp[i-j];
            }
        }
        return dp[n];
    }
}

——01背包问题

外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历

for num in nums:
    for i in range(target, nums-1, -1) :

416. 分割等和子集

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

1)题目解析

分割成等和子集,即分割成两份数值相等的集合,所以数组之和必须为偶数

2)思路解析

  1. 确定dp数组(dp table)以及下标的含义

    将数组一分为二,求平均数是否能够填满

  2. 确定递推公式

    j-nums[i]减去当前数值还能存放的数值

    dp[j-nums[i]]+nums[i]如果当前值存入

    dp[j]当前值存入比之前的小所以保留之前的数

  3. dp数组如何初始化

    dp[j]=0

  4. 确定遍历顺序

    ​ 由01背包问题可知

    for(int i = 0; i < nums.size(); i++) {
     for(int j = target; j >= nums[i]; j--)
    
  5. 举例推导dp数组

3)代码实现

class Solution {
    public boolean canPartition(int[] nums) {
        int sum=0;
        for(int i = 0;i<nums.length;i++){
            sum+=nums[i];
        }
        if(sum%2!=0) return false;
        int pack = sum/2;
        int[] dp = new int[pack+1];
        for(int i = 0;i<nums.length;i++){
            for(int j=pack;j>=nums[i];j--){
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        return dp[pack] == pack;
    }
}

1049. 最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

1)题目解析

该题目与上一题类似,都可以分成两块,填满平均值,将总数减去平均值乘以2即可得出剩下石头的最小可能重量

2)代码实现

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int n = stones.length;
        if(n==1) return stones[0];
        if(n==2) return Math.abs(stones[0]-stones[1]);
        int sum = 0;
        for(int num : stones){
            sum += num;
        }

        int mid = sum/2;
        int[] dp = new  int[mid+1];

        for(int i=0;i<n;i++){//遍历物品
            for(int j=mid;j>=stones[i];j--){//遍历背包
                dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return sum-dp[mid]-dp[mid];
    }
}

494. 目标和

给你一个整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

1)题目解析

每个数字都有两种状态:被进行“+”, 或者被进行“-”,因此可以将数组分成A和B两个部分:sumA sumB

在这里插入图片描述

由推论可知:sum、target固定,所有问题就是在集合nums中找到和为sumA的组合

转换成01背包:装满容量为sumA的背包

2)思路解析

  1. 确定dp数组以及下标的含义

    dp[j] 表示:填满j(包括j)这么⼤容积的包,有dp[i]种⽅法

  2. 组合问题递推公式

    dp[j] += dp[j - nums[i]]

  3. 数组初始化

    从递归公式可以看出,在初始化的时候dp[0] ⼀定要初始化为1,因为dp[0]是在公式中⼀切递推结果的起源,如果dp[0]是0的话,递归结果将都是0。

    dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1种⽅法,就是装0件物品。

  4. 组合问题遍历顺序

    01背包问题⼀维dp的遍历,nums放在外循环,target在内循环,且内循环倒序

3)代码实现

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum= 0;
        for(int i:nums){
            sum+=i;
        }

        if((sum+target)%2!=0) return 0;
        if(target>sum) return 0;
        int left = (sum+target)/2;
        if(left<0) left = -left;
        int[] dp = new int[left+1];
        dp[0] = 1;
        for(int i = 0;i<nums.length;i++){
            for(int j=left;j>=nums[i];j--){
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[left];
    }
}

474. 一和零

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5031 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"}{"10","1","0"}{"111001"} 不满足题意,因为它含 41 ,大于 n 的值 3

1)题目解析

最多m0n1 :说明需要存储两个数, 0 1的值,设置一个二维数组。strs 数组⾥的元素就是物品,每个物品都是⼀个!

⽽m n相当于是⼀个背包,两个维度的背包。

2)思路解析

最多i0j1strs的最大子集的大小为dp[i][j]

②包含当前:dp[i][j] = dp[i-zero][j-one]+1,其中zero,one为当前strs[i]0和1的个数

不包含当前:dp[i][j] =dp[i][j],从后往前遍历等于原来的

dp[0][0]0个0 和 0个1的最大子集的大小为0

④此题物品就是strs⾥的字符串,背包容量就是题⽬描述中的m和n。

for (string str : strs) { // 遍历物品
     int oneNum = 0, zeroNum = 0;
     for (char c : str) {
         if (c == '0') zeroNum++;
         else oneNum++;
     }
     for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
         for (int j = n; j >= oneNum; j--) {
         	dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
         }
     }
}

3)代码实现

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m+1][n+1];
        for( String str : strs){
            int zero =0;
            int one=0;
            char[] arr = str.toCharArray();
            for(char c:arr){
                if( c=='0') zero++;
                else one++;
            }
            for(int i=m;i>=zero;i--){
                for(int j=n;j>=one;j--){
                    dp[i][j] = Math.max(dp[i][j],dp[i-zero][j-one]+1);
                }
            }
        }
        return dp[m][n];
    }
}

——完全背包问题

完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。

for num in nums:
    for i in range(nums, target+1):

如果求组合数就是外层for循环遍历物品,内层for遍历背包。

如果求排列数就是外层for遍历背包,内层for循环遍历物品。

518. 零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

1)思路解析

dp[i]表示金额为i的硬币有dp[i]种方法

dp[i] = Math.max(dp[i-coins[i]],dp[i-1])

​ ①注意点:要确保icoins[i]

dp[0]=1:金额为0有一种方法,不放。

先遍历物品在遍历背包:从小到大遍历

for (int i = 0; i < coins.size(); i++) { // 遍历物品
     for (int j = coins[i]; j <= amount; j++) { // 遍历背包
     	dp[j] += dp[j - coins[i]];
     }
}

2)代码实现

class Solution {
    public int change(int amount, int[] coins) {
        int n = coins.length;
        int[] dp = new int[amount+1];
        dp[0] = 1;
        for(int i=0;i<n;i++){ //先遍历物品
            for(int j=coins[i];j<=amount;j++){ //在遍历背包
                    dp[j]+=dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
}

377. 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

1)思路解析

dp[i]表示总和为i的元素组合的个数

dp[i] += dp[i-nums[i]]

dp[0] = 1; 确保j-nums[i]=0的时候有值

**排列问题:背包放在外循环,将物品放在内循环

在这里插入图片描述

2)代码实现

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int n = nums.length;
        int[] dp = new int[target+1];
        dp[0] = 1;
        for(int j=0;j<=target;j++){
            for(int i=0;i<n;i++){
                if(j-nums[i]>=0)
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[target];
    }
}

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

1)思路解析

dp[i]表示s0i是否可以由wordDict拼接而成。

判断dp[i]是否为true

202203252037720

2)代码实现

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
       int n = s.length();
       boolean[] dp = new boolean[n+1];
       dp[0]=true;
       for(int i=1;i<=n;i++){
           for(int j=0;j<i;j++){
               if(dp[j] && wordDict.contains(s.substring(j,i))){
                   dp[i]=true;
                   break;
               }
           }
       }
       return dp[n];
    }
}

——打家劫舍

337. 打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例 1:

img

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

1)思路解析

dp[i]表示i个房子能盗取的最大金额

先确定树的遍历顺序:根左右遍历 money[] left[] right[]

当前节点有两种状态:

​ ①+左右节点不能偷 money[0] = root.val +left[1]+right[1]

​ ②不偷+左右节点可以偷 [1]

money[1] = Math.max(left[1],left[0])+Math.max(right[1],right[0]);

通过递归一直获取左右节点

2)代码实现

/**
 * 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 {
    public int rob(TreeNode root) {
        int[] result = tree(root);
        return Math.max(result[0],result[1]);
    }

    public int[] tree(TreeNode root){
        if(root==null) return new int[]{0,0};
        int[] left = tree(root.left);
        int[] right = tree(root.right);
        //偷cur
        int money1 = root.val+left[0]+right[0];
        //不偷
        int money0 = Math.max(left[0],left[1])+ Math.max(right[0],right[1]);
        return new int[]{money0,money1};

    }
}

——买卖股票

121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

1)思路解析

思路还是挺清晰的,还是DP思想:

  1. 记录【今天之前买入的最小值】
  2. 计算【今天之前最小值买入,今天卖出的获利】,也即【今天卖出的最大获利】
  3. 比较【每天的最大获利】,取最大值即可

2)代码实现

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length <= 1)
            return 0;
        int min = prices[0], max = 0;
        for(int i = 1; i < prices.length; i++) {
            max = Math.max(max, prices[i] - min);
            min = Math.min(min, prices[i]);
        }
        return max;
    }
}

122. 买卖股票的最佳时机 II

给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。

在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。
返回 你能获得的 最大 利润

示例 1:

输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

1)思路解析

每一天都有两种状态:①手上有股票 [0] ②手上没有股票 [1]

初始化:①第一天有股票 -price[0]

​ ②第一天无股票 0

遍历顺序:一次遍历 有两种状态

​ 手上有股票 :前一天的股票/之前没有股票今天买的股票

dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-price[i])

​ 手上没有股票 :前一天没有股票/今天卖了

dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]+price[i])

2)代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if(n==1) return 0;
        int[][] dp = new int[n][2];
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for(int i = 1 ;i < n;i++){
            dp[i][0] = Math.max(dp[i-1][0],-prices[i]+dp[i-1][1]);
            //手上有股票 前一天的股票/之前没有股票今天买的股票
            dp[i][1] = Math.max(dp[i-1][1],prices[i]+dp[i-1][0]);
            //手上没有股票 前一天没有股票/之前有股票今天卖了
        }
        return Math.max(dp[n-1][0],dp[n-1][1]);
    }
}

188. 买卖股票的最佳时机 IV

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

1)思路解析

每一天有2k+1个状态:

dp[i][0]:无股票 dp[0][0]=0

dp[i][1]:第一天买入

dp[i][2]:第一天卖出

dp[i][3]:第二天买入

dp[i][4]:第二天卖出

·······

dp[i][2k-1]:第k天买入

dp[i][2k]:第k天卖出

当前日没有操作,则延续之前的。

2)代码实现

class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        if(n<=1 ){
            return 0;
        }
        int[][] dp = new int[n][2*k+1];
        dp[0][0] = 0;
        for(int i=1;i<2*k+1;i++){
            if(i%2!=0){
                dp[0][i] = -prices[0];
            }
        }

        for(int i=1;i<n;i++){
            for(int j=0;j<2*k+1;j++){
                if(j==0){
                    dp[i][j] = dp[i-1][j];
                }else if(j%2==1){
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
                }else{
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
                }
            }
        }
        return dp[n-1][2*k];
    }
}

——子序列问题

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 。

1)思路解析

dp[i]:表示从0i的最长递增子序列

if(nums[i] > nums[j]) dp[i] = Math.max(dp[j]+1,dp[i]);

注意这⾥不是要dp[i]与 dp[j] + 1进⾏⽐较,⽽是我们要取dp[j] + 1]的最⼤值:即求j之前的最大递增子序列的大小

每一个单独数字都是一个递增子序列:Arrays.fill(dp,1);

2)代码解析

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        if(n==1){
            return 1;
        }
        int[] dp = new int[n];
        //从0到i的最长子序列
        int max=0;
       Arrays.fill(dp,1);
       //每一个单独数字都是一个递增子序列
        
       for(int i = 1;i<n;i++){
            for(int j=0;j<i;j++){
                //从i到j
                if(nums[i]>nums[j]){
                    dp[i]=Math.max(dp[i],dp[j]+1);
                }
            }
            max=Math.max(dp[i],max);
        }
        return max;
    }

}

674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:

输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

1)思路解析

//贪心算法

①只要递增 数值加一 取最大值

②不递增将数值变为1

2)代码解析

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int n = nums.length;
        if(n == 1){
            return 1;
        }
        int max = 1;
        int count=1;
        for(int i=0;i<n-1;i++){
            if(nums[i+1]>nums[i]){
                count++;
            }else{
                count=1;
            }            
            max=Math.max(count,max);
        }
        return max;
    }
}

718. 最长重复子数组

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

示例 1:

输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。

1)思路解析

子数组就是子序列

求连续子序列dp[i][j]dp[i-1][j-1]得出

A[i-1]==B[j-1] dp[i][j]=dp[i-1][j-1]+1

在遍历的时候将dp最大值记录

2)代码解析

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int n1 = nums1.length;
        int n2 = nums2.length;
        int[][] dp= new int[n1+1][n2+1];
        int max =0;
        for(int i=1;i<=n1;i++){
            for(int j=1;j<=n2;j++){
                if(nums1[i-1] == nums2[j-1]){
                    dp[i][j]=dp[i-1][j-1]+1;
                }
                max=Math.max(dp[i][j],max);
            }
        }
        return max;
    }
}

1143. 最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

1)思路解析

主要就是两⼤情况:

text1[i - 1] 与 text2[j - 1]相同,text1[i - 1]text2[j - 1]不相同

①如果text1[i - 1]text2[j - 1]相同,那么找到了⼀个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;

②如果text1[i - 1]text2[j - 1]不相同,那就看看text1[0, i - 2]text2[0, j - 1]的最⻓公共⼦序列 和text1[0, i - 1]text2[0, j - 2]的最⻓公共⼦序列,取最⼤的。

即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

代码如下:
if (text1[i - 1] == text2[j - 1]) {

 	dp[i][j] = dp[i - 1][j - 1] + 1;

} else {

 	dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

}

1143. 最长公共子序列

2)代码解析

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int n1= text1.length();
        int n2=text2.length();
        int max=0;
        char[] text1Arr = text1.toCharArray();
        char[] texr2Arr = text2.toCharArray();
        int[][] dp = new int[n1+1][n2+1];
        for(int i=1;i<=n1;i++){
            for(int j=1;j<=n2;j++){
                if(text1Arr[i-1]==texr2Arr[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j]);
                }
                max = Math.max(dp[i][j],max);
            }
        }
        return max;
    }
}

有的子序列。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

1)思路解析

主要就是两⼤情况:

text1[i - 1] 与 text2[j - 1]相同,text1[i - 1]text2[j - 1]不相同

①如果text1[i - 1]text2[j - 1]相同,那么找到了⼀个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;

②如果text1[i - 1]text2[j - 1]不相同,那就看看text1[0, i - 2]text2[0, j - 1]的最⻓公共⼦序列 和text1[0, i - 1]text2[0, j - 2]的最⻓公共⼦序列,取最⼤的。

即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

代码如下:
if (text1[i - 1] == text2[j - 1]) {

 	dp[i][j] = dp[i - 1][j - 1] + 1;

} else {

 	dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

}

2)代码解析

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int n1= text1.length();
        int n2=text2.length();
        int max=0;
        char[] text1Arr = text1.toCharArray();
        char[] texr2Arr = text2.toCharArray();
        int[][] dp = new int[n1+1][n2+1];
        for(int i=1;i<=n1;i++){
            for(int j=1;j<=n2;j++){
                if(text1Arr[i-1]==texr2Arr[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                }else{
                    dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j]);
                }
                max = Math.max(dp[i][j],max);
            }
        }
        return max;
    }
}
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

噜啦啦412

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值