动态规划汇总(未完待续)

前言:刚开始学或做动态规划的题目的时候,能用dp数组做出来的,就用dp数组,别理那些用几个临时变量的,你dp数组用多了,理解了,自然就会去改进,所以不要一上来就要求那么高,这样容易费时间又打击自己,想想一开始我们都是暴力法慢慢走过来的。

基础

70. 爬楼梯

https://leetcode.cn/problems/climbing-stairs/
dp[n] = dp[n-1] + dp[n-2]

class Solution {
    public int climbStairs(int n) {
        int n2 = 0;
        int n1 = 1;
        int n3 = 0;
        while(n-- > 0) {
            n3 = n1 + n2;
            n2 = n1;
            n1 = n3;
        }        
        return n3;
    }
}
62. 不同路径

力扣传送门

思路:动态规划
跟下一题基本一样,由于机器人只能向下或向右,说明到达某个点的路径之和等于上面+左边。
dp[i][j]代表从起点到该点的路径条数。从而可得动态方程为dp[i][j] = dp[i-1][j] + dp[i][j-1]
💡特殊处理,第一行和第一列的每个节点路径都是1。这里直接在for循环里面使用if判断。

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] sf = new int[m][n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0 || j == 0) {
                    sf[i][j] = 1;
                } else {
                    sf[i][j] = sf[i - 1][j] + sf[i][j - 1];
                }
            }
        }
        return sf[m - 1][n - 1];
    }
}

同类题型:63. 不同路径 II

64. 最小路径和

力扣传送门

思路:动态规划
由于每个点只能向右或向下,说明到达某个点的最小路径只有两种可能,要么是来自左边,要么来自上面。
dp[i][j]代表起点到(i, j)的最小路径。从而可得出动态方程为dp[i][j] = Max(dp[i-1][j], dp[i][j-1]) + grid[i][j],按正常顺序遍历矩阵,就能获得每个点的最小路径。
💡特殊处理,对首行的点,方向只能由左往右,最小路径的计算为dp[0][j] = dp[0][j-1] + grid[0][j]。首列同理。
🤔这里的dp数组由grid代替,因为矩阵的值并不需要保留,每次用完就没用了。

class Solution {
    public int minPathSum(int[][] grid) {
        int l1 = grid.length;
        int l2 = grid[0].length;

        int k = 1; 
        //第一行
        while (k < l2) {
            grid[0][k] += grid[0][k - 1];
            k++;
        }
        k = 1;
        //第一列
        while (k < l1) {
            grid[k][0] += grid[k - 1][0];
            k++;
        }

        for (int i = 1; i < l1; i++) {
            for (int j = 1; j < l2; j++) {
                grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);
            }
        }
        return grid[l1 - 1][l2 - 1];
    }
}

🤔可以使用搜索,例如DFS,但是搜索会出现局部路径重合,也就是走过的路再走一次,这会浪费不必要的时间,而动态规划只需要遍历整个矩阵一次。

*303. 区域和检索 - 数组不可变

力扣传送门

题意:求数组某个区间的元素之和。(要求调用次数很多)
思路:动态规划
最简单的思路当然是累计该区间,然后返回。但如果对该方法重复调用,就会m每次都计算一遍,可理解为没有缓存。
所以可通过保存每个区间的和,调用时,再返回即可,用dp[i][j]表示区间为[i,j]的数组元素和。如果用暴力法的话,会出现重复计算的情况,例如求[0,3]的和,我们并不需要从第一个元素加到第四个元素,而是借助[0,2]的和,其实就是动态规划,动态规划会帮我们记录中间的值,从而为下一个值的计算省不少事。这里也不难得出动态方程为dp[i][j] = dp[i][j-1] + nums[j]
但是这样还是比较慢的,因为需要用到两层for循环,其实我们并不需要计算所有区间,我们只需要计算0-1 0-2 0-3……0-n这些区间,对于其它区间可通过减法获得,例如[2,3] = [0,3] - [0,1], 其中dp[i]代表0-i的区间和,于是可得新的动态方程为dp[i] = dp[i-1] + nums[i]
💡由于这里nums数组元素用过就不用了,所以直接用一个dp数组指向nums数组,而不是通过new。

class NumArray {

    private int[] dp; //保存[0-1] [0-2]......[0-n]的数组区间和

    public NumArray(int[] nums) {
        int len = nums.length;
        //指向nums
        dp = nums;       
        for (int i = 1; i < len ;i++) {
            dp[i] = dp[i-1] + nums[i]; 
        }
    }
    
    public int sumRange(int left, int right) {      
        //特判left为0的情况  
        return  (left == 0) ? dp[right] : dp[right]-dp[left-1];
    }
}
198. 打家劫舍

偷钱,但不能偷相邻的。

力扣传送门

思路:动态规划
nums[i]表示第i家存放的金额,dp[i]表示偷第i家时能获得的最高金额
其中dp[0]=nums[0],dp[1]=nums[1],dp[2]=dp[0]+arr[2],dp[3]=Max(dp[0],dp[1]) + nums[3],dp[3]之后都是如此。
也就是说当i>2时,偷第i家时能获得的最高金额取决于在i-2家或i-3家中所能偷到的最高金额,即dp[i]=Max(dp[i-2],dp[i-3]) + nums[i]
由于计算dp[i]数组时,我们需要用到nums[i]数组,但和那些小于i的nums数据没关系,也就是可以不保留,所以可用nums数组代替dp数组。
代码一开始不理解也正常,都是从简单到复杂,不断优化。可能我这里呈现了很简练的代码,但并不代表这是我一开始就能写出来的。

class Solution {
    public int rob(int[] nums) {
        int len=nums.length;
        int max=0;
        for(int i=0;i<len;i++){
            if(i==2){
                nums[2]+=nums[0];//i=2的情况
            }else if(i>2){
                nums[i]+= Math.max(nums[i-2],nums[i-3]);
            }
            if(max<nums[i]) max=nums[i];//记录最大值,包括了i=0和1的情况
        }
        return max;
    }
}
213. 打家劫舍 II

偷钱,且不能偷相邻的,附加条件:第一家和最后一家是相邻的。

力扣传送门

思路:动态规划
附加条件,导致动态规划偷最后一家时,不知道前面的是否偷了第一家。我们可以调用两次动态规划避免该问题,即[0, len-2]和[1,len-1],这样就保证第一家和最后一家不会同时被偷。
注意调用两次,如果把nums作为dp数组第一次会修改nums,所以不能,但可以用临时变量代替,观察dp[i]=Max(dp[i-2],dp[i-3]) + nums[i],每次计算dp[i],需要dp[i-2] (dp_2),dp[i-3] (dp_3),也需要dp[i-1] (dp_1)==>因为使用临时变量并没有下标,只有保存dp[i-1],才能在下次更新dp_2和dp_3,不然根本没法移动。
⚡️ 动态规划的公式不一定唯一,例如官方解答代码更简洁

class Solution {
    public int rob(int[] nums) {        
        int len =nums.length;
        return Math.max(solve(0,len-2,nums),solve(1,len-1,nums));
    }
    
    private int solve(int idx, int l, int[] nums){
        int max = nums[0]; //防止只有一个 例如[1]       
        int dp_2=0, dp_3=0, dp_1=0; //补零,特殊值处理(这样每个数都符合dp[i]=Max(dp[i-2],dp[i-3]) + nums[i]),省去很多特殊判断。
        for(int i=idx; i<=l; i++) {
            int tmp = Math.max(dp_2, dp_3) + nums[i];
            dp_3 = dp_2;
            dp_2 = dp_1;            
            dp_1 = tmp;
            if(max < tmp) max = tmp;
        }
        return max;
    }
}
120. 三角形最小路径和

https://leetcode.cn/problems/triangle/
从下往上 dp[i][j] = value[i][j] + Min(dp[i][j], dp[i][j+1]) i代表行 j代表列
同理如果该题求最大路径,则只需要将Min改为Max即可

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {        
        int size = triangle.size() - 1;
        for(int i = size - 1; i >= 0; i--) {   
            //下一行
            List<Integer> next = triangle.get(i + 1);        
            List<Integer> row = triangle.get(i);            
            int l = row.size();
            for(int j = 0; j < l; j++)  {
                row.set(j, row.get(j) + Math.min(next.get(j), next.get(j+1)));
            }
        }
        //最小值就在顶点
        return triangle.get(0).get(0);
    }
}

经典

53. 最大子数组和

求最大连续子序列和,要求序列连续

力扣传送门

思路:动态规划
连续子序列必定是以某个元素结尾或开头,所以只要求出以每个元素为结尾的最大连续子序列和,然后记录那个最大。
⚡️以某个元素结尾或开头都行,只是数组遍历方向不同而已
用dp[i]表示以nums[i]结尾的最大连续子序列和,其中,该序列必须包含nums[i]。至于最大连续子序列和,要么是其本身,要么就是本身加上前一个的最优解dp[i-1]
动态方程:dp[i]=max(nums[i],dp[i-1]+nums[i])

class Solution {
    public int maxSubArray(int[] nums) {
        int l = nums.length;
        int[] dp = new int[l];
        dp[0]=nums[0];
        int max=dp[0];
        //从前往后遍历
        for(int i=1;i<l;i++){
            dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
            //每个dp[i]表示以nums[i]结尾的最大连续子序列和,也就是说每个dp[i]都可作为结果
            if(dp[i]>max){
                max=dp[i];
            }
        }
        return max;
    }
}

优化:从动态方程中,求解dp[i],只需要依赖到上一个元素的dp值,即dp[i-1],所以可使用一个变量保存上一个元素的dp值,省去创建dp数组,且这里重新换了种思路,也就是以某个元素作为开头的。

class Solution {
    public int maxSubArray(int[] nums) {       
        int l = nums.length - 1;
        int dp = nums[l];
        int max = dp;
        //从后往前遍历
        for(int i=l-1;i>=0;i--){
            dp=Math.max(dp+nums[i],nums[i]);
            if(dp>max){
                max=dp;
            }
        }
        return max;
    }
}
300. 最长递增子序列

也叫最长不下降子序列(LIS),不要求序列连续,严格递增

力扣传送门

思路:动态规划

以某个元素作为结尾,dp[i]表示以i为结尾的LIS,如果前面存在比该元素小的,则用该元素的LIS来更新当前的LIS,这样最后的LIS才为最优。由于序列不要求连续,所以当我们想获取前面的LIS时,就不是只比较前一个元素,而是从0到前一个元素都需要比较。也就是比较那些比当前小的元素,从而更新LIS.
动态方程:dp[i]=max{dp[i], dp[j]+1} (j<i && nums[i]>=nums[j])
测试用例:[6,7,4,5] [5,6,3,7]

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len=nums.length;
        int[] dp = new int[len];
        
        int max = 0;
        for(int i=0;i<len;i++){
	        //本身初始化
            dp[i]=1; 
            for(int j=0;j<i;j++){            
            	//在[0-i)中找比i小的元素,更新dp[i]
                if (nums[i] > nums[j] && (dp[j]+1) > dp[i]){
                     dp[i]=dp[j]+1;
                }
            }
            // 记录最大值
            if (max < dp[i]) {
                max = dp[i];
            }
        }
        return max;
    }
}
1143. 最长公共子序列

https://leetcode.cn/problems/longest-common-subsequence/

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int l1 = text1.length();
        int l2 = text2.length();
        int[][] dp = new int[l1+1][l2+1];

        for(int i=1;i<=l1;i++){
            for(int j=1;j<=l2;j++){
                if(text1.charAt(i-1)==text2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1]+1;
                }else{
                    dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }

        return dp[l1][l2];
    }
}

类似题目:583. 两个字符串的删除操作

动态规划填表法

下面的题目基本就是利用二维数组,实现动态规划,整个过程就是在填这张二维数组表。其实在最长公共子序列哪里就有体现了。

💡下面的题目在空间复杂度上都能优化,从二维到一维(滚动数组),但暂不更,所以下面都是用二维数组实现。

NC145 01背包

有n个物品,求体积为V的背包能装物品的最大重量。

牛客

看完必会,不用看代码,代码有些繁琐
只不过文章例子求的是背包最大价值,其实都一样。

import java.util.*;
public class Solution {
    /*
    定义一个二维数组 dp 存储最大重量,其中 dp[i][j] 表示前 i 件物品体积为j 的情况下能达到的最大重量。
    设第 i 件物品体积为v,重量为w,
    根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
    --第 i 件物品不可添加到背包,dp[i][j] = dp[i-1][j]
    --第 i 件物品可添加到背包中,max(dp[i][j] = dp[i-1][j-v] + w, dp[i-1][j])
    */  
    public int knapsack (int V, int n, int[][] vw) {
        int[][] dp = new int[n+1][V+1];
        for(int i = 1; i <= n; i++) {
            //某物品体积和重量
            int v = vw[i-1][0];
            int w = vw[i-1][1];
            for (int j = 1; j <= V; j++) {                
                if(j < v) {
                    //装不下
                    dp[i][j] = dp[i-1][j];
                } else {
                    //装的下
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-v] + w);
                }                                
            }
        }        
        return dp[n][V];
    }
}

⚡️这里有个非常重要的问题需要讨论,因为这关乎到下面的题目,如果你不搞懂,下面基本就是看一道晕一道:看似一样的代码,却不能理解它的含义。
问题:一定要十分明确dp[i][j]的含义dp边界问题(第一行和第一列)
首先含义:dp[i][j]代表从前i个元素中选择一些元素,使得它们的体积之和为j时所能获得的最大重量,根据动态方程我们知道它的值由上一行的某个数组决定,也就是dp[i-1][?],问号具体多少得看装不装的下,那问题来了,我们二维数组是从i=1,j=1开始遍历的,也就是省去了第一行和第一列,那为什么不用计算第一行和第一列呢?因为它们都是0,而数组默认初始值就是0,所以不用计算,这个时候就有跳出骂道了,你说个pi,这不很简单的问题嘛,我只想说你先把后面的题给做出来好吧!言归正传,注意0-1背包问题刚好是边界都为0,所以我们根本就不需要考虑,但是下面的题就不一样了,因为下面的初始边界值需要你确定,那如何解决?就是根据dp数组的含义确定初始边界值,从而才能够计算后面的值,像这里第一行dp[0][j]代表前0个元素的体积之和为j时所能获得的最大重量,哪都0个了,所以肯定都为0,同理对于dp[i][0],代表前i个元素的体积之和为0时所能获得的最大重量,由于物品的体积不可能为0(其实也是题目限定的),所以第一列也都为0。后面的题我也会再次强调含义和边界值问题。

416. 分割等和子集

将数组划分为两个子数组,要求两数组的元素和相等。

https://leetcode.cn/problems/partition-equal-subset-sum/

转化:找到一个子集的和为数组总和的一半(sum/2),也就是选择某些元素,使得这些元素的和为sum/2。

首先是数组类型问题,一开始你可能会把dp数组用int类型来使用,也就是
dp[i][j]表示从前i个元素中选择一些元素,使得它们的和为j时所能达到的最大值。其中,当dp[i][j]等于sum(数组)的一半时,就证明存在,这样是可以的,一开始我也这么做,但你会发现很多题解用的是boolean类型,也就是它们的类型是根据题目的求解结果确定的,但一开始又很难看懂,这个时候就需要用到我在0-1背包强调过的问题。也就是dp数组含义和边界问题
首先dp[i][j]代表从前i个元素中选择一些元素,使得它们的和为j时能否分割为等和子集,它的值由dp[i-1][?]决定,而这个时候就需要考虑边界值问题,如果像0-1背包那样,说明第一行和第一列都为false,那dp数组最总必为false,这显然不对,所以我们需要分析边界值,从而使得后面的值能够计算。首先是第一行dp[0][j],代表前0个元素的和为j时能否分割为等和子集,其中j的值从0到sum/2,其中前0个元素的和只能是0了,这个时候也是能分割的,也就是空数组分割为两个空数组,虽然不符合题意,但边界值就是只能怎么算,对于其它dp[0][j]则为false。接着是第一列dp[i][0],代表从前i个元素中选择一些元素,使得它们的和为0时能否分割为等和子集,由于是前i个中选,说明i肯定包含0,所以dp[i][0]都为true,也就是第一列为true,这个时候就能根据动态方程继续往后计算了

class Solution {
    public boolean canPartition(int[] nums) {        
        //计算数组总和
        int sum=0;
        for(int s :nums){
            sum+=s;
        }
        //奇数直接返回
        if((sum&1)==1){
            return false;
        }
        //数组和的一半
        sum/=2;
        int l=nums.length;
        boolean[][] dp = new boolean[l+1][sum+1];        
         //边界
        dp[0][0] = true;
        for(int i=1; i<=l; i++){
            //边界
            dp[i][0] = true;
            //某个数
            int num = nums[i-1];
            for(int j=1; j<=sum; j++){                 
                if(j >= num){
                    //可以加入时,由前面的dp决定,只要存在true,则当前dp肯定为true
                    dp[i][j] = dp[i-1][j-num] || dp[i-1][j];
                }else{
                    //不可以加入,由上一个决定
                    dp[i][j] = dp[i-1][j];
                }                
            }
        }
        return dp[l][sum];
    }
}
494. 目标和

为数组的所有元素添加正号和负号,使得它们的和为target,求添加正负号有多少种方法

https://leetcode.cn/problems/target-sum/

法一:动态规划:数学推理转为上一题
正号的数组A,负号的数组B,A-B=target
A+A-B = target + A
2*A = target+A+B
A=(target+A+B)/2
也就是找到和为target+A+B一半的集合即可,类似上一题,只不过这道题是求方法数总数,上一道题是判断是否存在。
从这个转换中我们只需找到和为target+A+B一半的集合即可,而不用考虑减的情况。
不过这个规律可能想不到,我觉得没关系,这就跟求完全平方数一样,如果你不知道完全平方数能用加法解决,那你就只会用乘法,所以问题不大,最关键的是dp的求解。

首先明确dp数组的含义:dp[i][j]表示从前i个元素中选择一些元素,使得它们的和为j时的方法数,从而可知dp的类型为int。接着是边界问题:第一行dp[0][i]代表从前0个元素中选择一些元素,使得它们的和为j时的方法数,那就只有dp[0][0]为1了,对于第一列dp[i][0]代表从前i个元素中选择一些元素,使得它们的和为0时的方法数,前i个,说明i肯定包含0,但此时dp[i][0]就不一定是等于1了,因为nums数组中的元素可以为0,这就导致第一列的初始值不一定是0,所以第一列也需要代入动态方程中取计算,也就是j的取值要从0开始取.例如
[0,0,0,0,0,0,0,0,1] 1 很明显dp[i][0]的值肯定>=1,例如dp[1][0]==2,dp[2][0] = 4

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        //这里的sum代表A
        int sum = target;
        for(int num : nums) {
            sum+=num;
        }
        //奇数或者小于0直接返回
        if((sum & 1) == 1  || sum < 0) {
            return 0; 
        }
        //一半
        sum/=2;
        int len = nums.length;
        int[][] dp = new int[len+1][sum+1];
        //边界
        dp[0][0] = 1;
        for(int i=1; i<=len; i++) {            
            int num = nums[i-1];         
            //注意j从0开始   
            for(int j=0; j<=sum; j++) {
                if(j >= num) {
                    //可以加入时,由前面的dp方法数决定,注意是加法,而不是二选一
                    dp[i][j] = dp[i-1][j-num] + dp[i-1][j];
                } else {
                    //不可以加入时
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[len][sum];        
    }
}

法二:dfs 贼容易理解,但慢

class Solution {
    int count=0;

    public int findTargetSumWays(int[] nums, int S) {
        dfs(0,0,nums,S);
        return count;
    }

    public void dfs(int sum, int i, int[] arr, int s){
        if(i==arr.length){
            if(sum==s){
                ++count;
            }
            return;
        }
        dfs(sum+arr[i],i+1,arr,s);
        dfs(sum-arr[i],i+1,arr,s);
    }
}
474. 一和零

也属于0-1背包的题,只不过这里维度增加。理解为两个背包

https://leetcode.cn/problems/ones-and-zeroes/

法一:动态规划,基于三维数组,但原则不变,dp含义+边界值
含义dp[i][j][k]表示从前i个字符串中选择一些字符串,使得包含0和1的个数分别为j和k时,这些串所能构成的最大子集长度。
边界,三维考虑的边界就是xyz三个轴中出现0时的情况
dp[0][j][k]表示从前0个字符串中选择一些字符串,使得包含0和1的个数分别为j和k
其中包含1和0的个数只能是0,所以dp[0][j][k]均为0
dp[i][0][k]表示从前i个字符串中选择一些字符串,使得包含0和1的个数分别为0和k
这个是不确定的,因为每个串中可以只包含1,例如[“1”,“11”]这样的子集就不包含0,所以dp值不一定为0,需要代入动态规划中计算。
dp[i][j][0]同上,需要代入计算,其实就是j和k的取值从0开始

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {                 
        //创建dp数组
        int len = strs.length;
        int[][][] dp = new int[len + 1][m + 1][n + 1];

        for(int i = 1; i <= len; i++) {
            //计算当前串中0和1的个数
            int t0 = 0;            
            for(char c : strs[i-1].toCharArray()) {
                if(c == '0') {
                    t0++;
                } 
            }
            int t1 = strs[i-1].length() - t0;
            //动态规划
            for(int j = 0; j <= m; j++) {
                for(int k = 0; k <= n; k++) {                    
                    if(j >= t0 && k >=t1) {
                        //当两者都满足时,该字符串才可能加入,如果假如子集长度+1
                        dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j-t0][k-t1] + 1);
                    } else {
                        //当两者都不满足时
                        dp[i][j][k] = dp[i-1][j][k];
                    }                    
                }
            }            
        }
        return dp[len][m][n];
    }
}

💡dfs超时

NC309 完全背包

牛客

思路:类似0-1背包,只是动态方程稍有不同,因为完全背包同一个物品可以装多次,dp[i][j],很明显边界都为0,这里就不分析了

提升

比较考验数学和逻辑分析能力

152. 乘积最大子数组

暴力

class Solution {
    public int maxProduct(int[] nums) {        
        int max = nums[0];        
        int l = nums.length;
        for(int i = 1; i < l; i++) {
            int cur = nums[i];
            int dp = cur;
            //往前乘,并记下最大值
            for(int j = i - 1; j >= 0; j--) {
                cur = cur * nums[j];
                if(dp < cur) dp = cur;
            }
            if(max < dp) max = dp;
        }
        return max;
    }
}

动态规划,如果你一开始想不到,可以先尝试暴力,往前乘,然后你会发现这种方式其实就是下面的原始方式
只不过下面有记录前面算过的最大或最小值,而当我们没记录时,当然只能靠暴力
dp[i]表示以第i个元素作为结尾的最大乘积子数组
优化:dp[i] = Max(dp[i-1]) * nums[i] or Min(dp[i-1]) * nums[i]
因为nums[i]可能为正,也可能为负数,所以不一定等于Max(dp[i-1]) * nums[i]
他应该由前一个作为结尾的最小乘积或者最大乘积决定
所以我们还需要一个数组用于记录以第i个元素作为结尾的最小乘积子数组

class Solution {
    public int maxProduct(int[] nums) {        
        int max = nums[0];                
        int l = nums.length;
        int[] dpMax = new int[l];
        int[] dpMin = new int[l];
        dpMax[0] = nums[0];
        dpMin[0] = nums[0];
        for(int i = 1; i < l; i++) {
            int a = nums[i] * dpMax[i-1];
            int b = nums[i] * dpMin[i-1];
            if(a > b) {
                dpMax[i] = Math.max(a, nums[i]);
                dpMin[i] = Math.min(b, nums[i]);
            } else {
                dpMax[i] = Math.max(b, nums[i]);
                dpMin[i] = Math.min(a, nums[i]);
            }
            if(dpMax[i] > max) {
                max = dpMax[i];
            }
        }
        return max;
    }
}



343. 整数拆分

将一个整数拆分为乘积最大的一些数字

力扣传送门

思路:动态规划
2=1×1 ——max=1
3=1×2 ——max=2
4=1×3 或 2×2 ——max=4
5=1×4 或 2×3 ——max=6
6=1×5 或 2×4 或 3×3 ——max=9 ==>双指针即可遍历每个数的不同拆分。
当然,这里只是拆分为两个数,所以还需要动态规划来帮我们获取更细的拆分。用dp[i]表示整数 i 所能拆分后所能获得的最大乘积。例如6=1×5,如果dp[5]大于5,则说明5需要拆分,用dp[5]替换5(dp[1]同理)。其中,计算dp[6]时,dp[5]肯定是前面先算好了。且dp[1]=1,dp[2]=1。动态方程为dp[i] = Math.max(l, dp[l]) * Math.max(r, dp[r]) i = l + r(拆分)

class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n + 1];
        dp[1] = 1;  //初始化
        dp[2] = 1;
        for (int i = 3; i <= n; i++) {
            int l = 1, r = i-l;  //双指针
            while (l <= r) { 
            	//判断是否需要拆分
                int tmp = Math.max(l, dp[l]) * Math.max(r, dp[r]);			
                //更新最大值
                if (tmp > dp[i]) dp[i] = tmp;
                l++;
                r--;
            }
        }
        return dp[n];
    }
}
*279. 完全平方数

用最少数量的完全平方数构造整数n

力扣传送门

💡沿用上一题思路,但是很慢

思路:动态规划,(BFS会更好理解)

用dp[i]表示和为i的完全平方数的最少数量,例如5的最少数量是2,拆为4+1,即dp[5] = 2
dp[0] = 0    dp[1] = dp[1-0] + 1
dp[2] = dp[2-1] + 1	       数量加一代表减去的那个平方数
dp[3] = dp[3-1] + 1		
dp[4] = dp[4-1] + 1 或者 dp[4-4] + 1      
dp[5] = dp[5-1] + 1 或者 dp[5-4] + 1
dp[i] = dp[i-pft] + 1	pft是小于等于i的完全平方数,从1开始算 1*1  2*2   3*3 
可得动态方程:dp[i] =  Math.min(dp[i], dp[i-pft]+1) 
class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            dp[i] = i; //初始化最坏的情况,全是1
            for (int j = 1; j * j <= i; j++) {//1*1  2*2   3*3 ...
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
            }            
        }
        return dp[n];
    }
}
322. 零钱兑换

https://leetcode.cn/problems/coin-change/

类似完全平方数,也可以用bfs
dp[i]表示金额为i所需硬币的最少数量
需要注意的是初始化,虽然不存在应该赋值为-1,但根据动态方程,我们求的是最少数量,所以应该给他赋值一个正无穷的数,更简单一点就是超过总金额即可(big)。最后再根据dp结果是不是等于big,如果是说明不存在,否则返回

class Solution {
    public int coinChange(int[] coins, int amount) {  
        //dp[i]表示金额为i所需硬币的最少数量
        int len = coins.length;
        int big = amount + 1;
        int[] dp = new int[big];
        dp[0] = 0;
        for(int i = 1; i <= amount; i++) {
            dp[i] = big; //初始化,最坏的情况就是不存在,但不能用-1,应该用一个大于amount的数
            for(int j = 0; j < len; j++ ) {
                int coin = coins[j];
                if(coin <= i) {
                    dp[i] = Math.min(dp[i], dp[i-coin] + 1);
                }                 
            }
        }
        return dp[amount] == big ? -1 : dp[amount];
    }
}
*518. 零钱兑换 II

https://leetcode.cn/problems/coin-change-2/

1.dfs 类似组合总和,只是数据的范围变大,速度下降
组合总和题目限定:对于给定的输入,保证和为 target 的不同组合数少于 150 个。
所以本道题 dfs 超时
500
[3,5,7,8,9,10,11]
改进:先排序 再剪枝,结果还是超时
2.改用动态规划
dp[i]表示金额为i的硬币组合数
如何保证组合不重复:123和321
交换coins和amount的遍历顺序即可,也就是先遍历coins再遍历amount,具体可参看官方题解

class Solution {    

    public int change(int amount, int[] coins) {         
        int[] dp = new int[amount+1];
        dp[0] = 1;
        for(int coin : coins) {
            for(int i = 1; i <=amount; i++) {
                if(i >= coin) {
                    dp[i] = dp[i] + dp[i-coin];
                }
            }
        }
        return dp[amount];
    }
}
* 377. 组合总和 Ⅳ

https://leetcode.cn/problems/combination-sum-iv/

思路:动态规划
dp[i]表示的是和为i的组合数
0 1
1 dp[0] 1
2 dp[0] + dp[1] 2
3 dp[0] + dp[1] + dp[2] 4
4 dp[1] + dp[2] + dp[3] 7
类似上一题零钱兑换II,但题目要求组合没有顺序,也就是1 1 2和2 1 1是不一样的

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1; //初始化
        for(int i = 1; i <= target; i++) {
            for(int num: nums) {
                if(i >= num) {
                    dp[i]+=dp[i-num];
                }
            }
        }        
        return dp[target];
    }
}
*139. 单词拆分

求解过程类似题目300. 最长递增子序列
https://leetcode.cn/problems/word-break/solution/dan-ci-chai-fen-by-leetcode-solution/

参见官方

class Solution { 
    public boolean wordBreak(String s, List<String> wordDict) {
        //dp[i]表示s中前i个字符能否分割为字典中的单词
        int l = s.length();
        boolean[] dp = new boolean[l + 1];
        //初始化
        dp[0] = true;
        for(int i = 1; i <= l; i++) {            
            //遍历[0,i)里面的字串,也就是[0,i) [1,i) [2,i) [3,i)...[i-1,i)
            for(int j = 0; j < i; j++) {
                //如果dp[j]为true,说明前j个字符能分割,此时只需判断字典中是否存在j到i子串
                if(dp[j] && wordDict.contains(s.substring(j , i))) {
                    dp[i] = true;
                    //找到就可以跳出了
                    break;
                }
            }
        }
        return dp[l];
    }
}
650. 只有两个键的键盘

https://leetcode.cn/problems/2-keys-keyboard/
其实你可以大致分析一下,就能找到规律,关键在于n这个数能否找到整除的数(除了本身),如果不行它只能是一个c,然后不断ppppp…结果是n个操作,例如3和7
如果行,则能由整除的数来减少cp次数。

2
c p
3
c p p
4
c p c p dp[2] + dp[2]
5
c p p p p
6
c p p c p dp[3] + dp[2]
7
c p p p p p p
8
c p c p c p dp[4] + dp[2]
9
c p p c p p dp[3] + d[3]
10
c p p p p c p dp[5] + [2]

动态方程
对于偶数,无需判断,有多个方式也无所谓,答案唯一,例如 30 = 15 * 2 或 = 5 * 6 ,操作次数是一样。
== dp[i] = dp[i/2] + dp[2]

对于奇数
==如果[2,i/2]不存在被i整除的数dp[i]=i
==否则存在tmp被i整除dp[i] = dp[tmp] + dp[i/tmp]

class Solution {
    public int minSteps(int n) {
        int[] dp = new int[n+1];
        dp[1] = 0;
        if(n >= 2) dp[2] = 2;
        for(int i = 3; i <= n; i++) {            
            if((i & 1) == 1)  {
                // 奇数先赋值最差情况,也就是不能整除[2,i/2],需要操作次数为i,例如3,7
                dp[i] = i;
                // 然后在判断能否被整除
                int j = i / 2;
                int tmp = 2;
                while(tmp <= j) {
                    if((i % tmp) == 0) {
                        dp[i] = dp[tmp] + dp[i/tmp];
                        break;
                    }
                    tmp++;
                }                
            } else {
                //偶数
                int tmp = i / 2;
                dp[i] = dp[tmp] + dp[2];
            }

        }
        return dp[n];
    }
}

待整理

376. 摆动序列

求最长子序列的长度,要求子序列为摆动序列,也就是一大一小的序列。不要求子序列连续。
https://leetcode.cn/problems/wiggle-subsequence/description/

class Solution {
    public int wiggleMaxLength(int[] nums) {        
        int len = nums.length - 1;
        //记录结果
        int ans = 1; 
        //控制递增递减方向,刚开始为0,表示没有要求
        int f = 0; 
        for (int i = 0; i < len; i++) {
            //递增,同时要求上一个为递减
            if(nums[i] < nums[i+1] && f != 1 ) {                
                ans++;                
                f = 1;
            } 
            //递减,同时要求上一个为递增
            if(nums[i] > nums[i+1] && f != -1) {                
                ans++;
                f = -1;
            }
        }
        return ans;
    }
}
5. 最长回文子串

力扣传送门

思路一:字符串中心扩展(双指针)

思路二:动态规划
外扩的方式实现,判断dp[i][j]是不是时,通过判断dp[i+1][j-1]即可

class Solution {
    public String longestPalindrome(String s) {
        int l = s.length();
        int[][] dp = new int[l][l];

        for (int i = 0; i < l; i++) { //长度为1
            dp[i][i] = 1;
        }
        int start = 0, end = 0;    //记录最长字串下标——最后一次的记录必定最长(思考)

        for (int len = 2; len <= l; len++) { //遍历长度为2、长度为3、长……

            int k = l - len;

            for (int i = 0; i <= k; i++) {

                int j = i + len - 1;  //串尾

                if (s.charAt(i) == s.charAt(j)) {
                    if (len == 2 || dp[i + 1][j - 1] == 1) {  //长度为2的需要特殊处理
                        dp[i][j] = 1;
                        start = i;
                        end = j;
                    }
                }
            }
        }
        return s.substring(start, end + 1);
    }
}
*646. 最长数对链

https://leetcode.cn/problems/maximum-length-of-pair-chain/

思路一:贪心
思路二:参考最长递增子序列

class Solution {
    public int findLongestChain(int[][] pairs) {

        Arrays.sort(pairs,new Comparator<int[]>(){
            @Override
            public int compare(int a[], int b[]){
                return a[0]-b[0];
            }
        });

        int len = pairs.length;
        int dp[] = new int[len];

        int ans=-1;
        for(int i=0;i<len;i++){
            dp[i]=1;
            for(int j=0;j<i;j++){
                if(pairs[i][0]>pairs[j][1]){
                    if(dp[j]+1>dp[i]){
                        dp[i]=dp[j]+1;
                    }
                }
            }
            if(ans<dp[i]){
                ans=dp[i];
            }
        }

        return ans;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值