学习篇|leetcode刷题笔记-DP篇

----代码随想录笔记

题型1、基础动态规划

战略:

基础dp通常通过总结数学规律,如斐波拉契数列这种比较容易观察到的规律。例题没什么好说的,难以总结共性。

典型例题:

  746. 使用最小花费爬楼梯
  509. 斐波那契数
  70. 爬楼梯
  62. 不同路径
  63. 不同路径 II
  343. 整数拆分
  96. 不同的二叉搜索树

题型2、0-1背包

战略:

0-1背包的原理需要掌握,以及滚动数组的优化。在做具体的题目,一般会有变形,以下是变形例子:
1、在背包容量限制下,能装到的最大价值。这个是背包问题能直接套用的,但是要搞清楚题目中哪个是容量,哪个是价值,哪个是重量。
2、方案数。是背包问题的经典变形,是不能直接套用原背包递推公式的。

经典例题:

416. 分割等和子集

关键词:

如果能够分割成2个子集,那么一定存在子集和为数组之和的一半。

问题转化:

在容量是(sum/2)的背包下,求能装到的最大价值,价值数组和重量数组都是nums。因为最大的容量是(sum/2),那么如果能够分割成2个子集,最大容量是一定能够填满的。最后的判断依据就是dp[capacity]==capatity?

细节:

如果sum/2不能被整除,在nums是正整数的情况下,是可以直接返回false的。

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

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

关键词:

尽可能平分成2。换个方式理解,分成三堆a,a,b,其中2堆是重量一样的。total_weight = a+a+b, 如何使得b最小的问题,b>=0, 所以a最大取值是total_weight/2.

问题转化:同416

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = Arrays.stream(stones).sum();
        int capacity = sum/2;
        int[] dp = new int[capacity+1];
        for (int i=0; i=stones[i]; --j) {
                dp[j] = Math.max(dp[j], dp[j-stones[i]]+stones[i]);
            }
        }
        return sum-dp[capacity]*2;
    }
}

494. 目标和

关键词:

1、方案数的求解问题。
2、推导思路,如果数组分成两个部分,两个部分的加和分别是a,b,那么有:
a-b=target
a+b=sum,sum为数组的和
所以a的值是可以确定的a=(sum+target)/2.

问题转化:

本题是求方案数,个人感觉其实很难想到套用0-1背包,原递推公式是不可以直接套用的,方案数有自己的递推公式,具体见代码。

细节:

1、对于target可能是负数,那么capacity也可能是负的,但是题中nums[i]>=0,所以capacity一定要是正的。
2、不能被整除的问题,类似416
3、dp的初始值问题。与0-1背包不同,方案数都只与dp相关,如果没有初始值,那么结果只能是0.

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = Arrays.stream(nums).sum()+target;
        if (sum%2!=0) {
            // 都是整数,不能凑成非整数
            return 0;
        }
        int capacity = sum/2;
        if (capacity<0) capacity = capacity;
        int[] dp = new int[capacity+1];
        dp[0]=1;
        for (int i=0; i=nums[i]; --j) {
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[capacity];
    }
}

474. 一和零

关键词:

非多重背包,因为每个元素只能使用一遍,这个是2维的0-1背包。

问题转化:

直接套用0-1背包递推公式,搞清楚以下问题:
重量是什么?每个元素的0,1个数
价值是什么?元素的个数,能放进背包的即+1
容量是什么?m,n

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[] count0 = new int[strs.length];
        int[] count1 = new int[strs.length];
        for (int i=0; i=count0[i]; --x) {
                for (int y=n; y>=count1[i]; --y) {
                    dp[x][y] = Math.max(dp[x][y],dp[x-count0[i]][y-count1[i]]+1);
                }
            }
        }
        return dp[m][n];
    }
}

以上代码可以优化成:

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m+1][n+1];
        for (int i=0; i=zeroNum; --x) {
                for (int y=n; y>=oneNum; --y) {
                    dp[x][y] = Math.max(dp[x][y], dp[x-zeroNum][y-oneNum]+1);
                }
            }
        }
        return dp[m][n];
    }
}

题型3、完全背包

战略:

完全背包的原理,换元的推导方法,以及滚动数组的优化。具体的题目有以下变形:
1、直接套用完全背包的公式。
2、方案数,方案数有又分组合数和排序数,组合是先背包后容量。排序数是先容量后背包。
3、利用换元法推导出来的。这种感觉都不像是背包问题了。

经典例题:

518. 零钱兑换 II

关键词:

1.组合方案数

问题转化:

无论是0-1,还是完全,方案数的递推公式是dp[j]+=dp[j]+dp[j-nums[i]
推导过程:
dp[i][j] , 到第i个数位置,价值为j的方案数有dp[i][j]个
dp[i][j] = dp[i-1][j-k*nums[i]]

dp[i][j-nums[i]] = dp[i-1][j-(k+1)*nums[i]]

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

降维优化:和完全背包的思路一样,不写了。结果是:dp[j] = dp[j] + dp[j-nums[i]]

细节:

1、与0-1相似,方案数一定要初始话dp[0] = 1; 方案数完全通过dp转的,所以如果没有初始化,结果一定是0.

class Solution {
    public int change(int amount, int[] coins) {
        int[] dp = new int[amount+1];
dp[0] = 1;
        for (int i=0; i

377. 组合总和 Ⅳ

关键词:

1、排序方案数

问题转化:

与518不一样的是,518不用考虑顺序,而此题需要。
例子:
如果是零钱{2,3}, 组合成5的方式有,如果是518那么就是1,而此题则为2.

为什么for循环的先后顺序会有两种不同的结果呢?以的例子分析。i是背包,j是容量。

组合:先背包后容量

倒数第2次遍历,i=0,j=2~5,这个循环结束后,dp = {1,0,1,0,1,0}, 单从含义来理解,就是到2这个数位置,组成0,2,4分别有一种方式,其他为0.
最后一次遍历,i=1,j=3~5,j=4时,dp={1,0,1,1,1,0}, j=5时,dp={1,0,1,1,1,1}, dp[5]的位置只被计算了一遍。

排序:先容量后背包

倒数第2次外循环遍历,j=4,i=0~1,这个外循环结束后,dp={1,0,1,1,1,0},单从含义理解,就是到3这个数,组合成0,2,3,4分别有1种方法。
最后一次外循环遍历,j=5,i=0~1,在这个循环中,可以看到dp[5]的位置被计算了两遍,先加上了dp[2],后加上了dp[3]。先加了dp[2],表示组合为2的方案数之后自己补3;再加上了dp[3],表示组合为3的方案数之后自己补2,从这里看出来,排序方案数时不在乎顺序的;而组合方案数只考虑了加上dp[3],后面自己补2。
———
感觉这里关于顺序的思考还没有触达本质,后面再想想吧。

细节:

1、求方案数要初始化,第三遍了~

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target+1];
        dp[0] = 1;
        for (int j=1; j<=target; ++j) {
            for(int i=0; i=nums[i]) {
                dp[j] = dp[j] + dp[j-nums[i]];

                }
            }
        }
        return dp[target];
    }
}

322. 零钱兑换

关键词:

1、求min的推导,类似完全背包问题

问题转化:

推导过程:
dp[i][j]:到第i个数为止,组合成j的硬币的个数
dp[i][j] = min(dp[i-1][j], dp[i-1][j-v[i]]+1, dp[i-1][j-2v[i]]+2, ···)
dp[i][j-v[i]] = min(dp[i-1][j-v[i]], dp[i-1][j-2
v[i]]+1, dp[i-1][j-3*v[i]]+2, ···)
dp[i][j] = min(dp[i-1][j], dp[i][j-v[i]]+1);

细节:

1、初始化问题,因为是求min的,所以要初始化为最大值占位,这个站位可以认为找不到方案,又因为dp[0]是推导的开始,所以要初始化为一个有意义的值,dp[0]=0


class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount+1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i=0; i

279. 完全平方数

类似322,跳过

139. 单词拆分

关键词:dp定义+推导

问题转化:

这一题感觉不是很想背包,和322一样,推导的过程中和背包很像。
推导:
dp[i]:字符串前i个字符能否被拆分
dp[i+1]的递推:
dp[0]==true && 0~i(包含i)是否存在字典数组中 或
dp[1]==true && 1~i(包含i)是否存在字典数组中 或
···
dp[i]==true && i~i(包含i) 是否存在字典数组中


class Solution {
    public boolean wordBreak(String s, List wordDict) {
        boolean[] dp = new boolean[s.length()+1];
        dp[0] = true;
        for (int i=1; i

题型4、打家劫舍

战略:

每家有且只有2种选择,偷or不偷,所以要有2维数组来存储状态。在遍历顺序上,根据前面的值算出当前值。

经典例题

198. 打家劫舍

关键词:


class Solution {
    public int rob(int[] nums) {
        int[][] dp = new int[2][nums.length];
        dp[0][0] = 0;
        dp[1][0] = nums[0];
        for (int i=1; i

213. 打家劫舍 II

关键词:
1、环形,类似于滑动窗口的思想,固定长度的窗口,只考虑窗口内部的


class Solution {
    public int rob(int[] nums) {
        if (nums.length==1) {
            return nums[0];
        }
        if (nums.length==2) {
            return Math.max(nums[0], nums[1]);
        }
        int result1 = robWithInterval(nums, 1, nums.length-2);
        int result2 = robWithInterval(nums, 2, nums.length-1);
        return Math.max(result1, result2);
    }
    
    public int robWithInterval(int[] nums, int start, int end) {
        if (start>end) {
            return 0;
        }
        int[][] dp = new int[2][nums.length];
        dp[0][start-1] = 0;
        dp[1][start-1] = nums[start-1];
        for (int i=start; i<=end; ++i) {
            dp[0][i] = Math.max(dp[0][i-1], dp[1][i-1]);
            dp[1][i] = dp[0][i-1]+nums[i];
        }
        return Math.max(dp[0][end], dp[1][end]);
    }
}

337. 打家劫舍 III

关键词:

1、后序遍历
2、依旧需要2维数组来表示状态,2维数组包含了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[] res = robDfs(root);
        return Math.max(res[0], res[1]);
    }
    
    public int[] robDfs(TreeNode root) {
        int[] res = new int[2];
        if (root==null) {
            return res;
        }
        int[] left = robDfs(root.left);
         int[] right = robDfs(root.right);
        res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        res[1] = left[0]+right[0]+root.val;
        return res;
    }
}

题型5、股票买卖

战略:

需要用数组来表示不同状态下的股票情况,这点和打家劫舍类似。数组的状态主要有一下几种:
1、持有、不持有,这里有个问题:如果当天卖掉了,算持有吗?--不算。这种分法主要是只有对买卖的次数没有要求。
2、如果对买卖的次数有要求,就要明确是第几次买卖。
3、冷冻期的情况,就要分成:买入、卖出、冷冻这3中情况。

经典例题:

121. 买卖股票的最佳时机

关键词:

1、只能买卖一次

问题转化:

是战略里面的第一种,需要记录2种状态

细节

只能买卖一次,怎么体现这个一次?下一题讲到。

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

关键词:

1、可以买卖多次,对次数不做要求

问题转化:

是战略里面的第一种

细节:

和121的区别在于,持有的计算需要建立在i-1天不持有的基础上,二只能买卖一次不需要。

714. 买卖股票的最佳时机含手续费

关键词:

1、卖出的时候减掉手续费

问题转化:

是战略里面的第一种,本质上和121没有太大区别。

123. 买卖股票的最佳时机 III

关键词:

1、限制次数

问题转化:

是战略里面的第二种,有一下5种状态:
没有操作
第一次买入
第一次卖出
第二次买入
第二次卖出

细节

初始化的问题,第一天,第2次买入可以理解在第一天买入-卖出-买入。

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

关键词:

1、限制次数

问题转化

是123的抽象版

309. 最佳买卖股票时机含冷冻期

关键词:

1、冷冻期

问题转化:

是战略里面的第三种。

题型6、子序列

战略:

1、一定要确定dp数组的定义,下标和值各有定义。
关于定义,我怎么知道要怎么定义呢?
a.如果题目要求是递增或者递减, 不管是否有子序列是否连续的要求,一般都要定义为:以第i个结尾的最长子序列的长度是dp[i].一般dp种的最大值是所求。

b.如果题目要求子序列不连续:遍历到第i个为止(不一定以i结尾)的子序列的长度是dp[i].一般dp[length-1]就是所求。

c.如果题目要求子序列连续:以第i个结尾的连续子序列的长度是dp[i].一般dp种的最大值是所求。

注意以上b,c两种区别,它们的递推公式差异也很大。
如果是b,一般是和遍历到的所有都有关系,一般是max(xxx)这种。
如果是c,一般只和dp[i-1]相关。

2、确定递推公式的时候,要学会怎么划分,要做到不重不漏,但是有例外,就是求min,max的情况下,重复不会影响结果的。但是最好还是按照不重不漏的标准来划分情况,然后推导公式。

经典例题:

300. 最长递增子序列

关键词:

1、递增
2、不连续
3、dp定义,dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度

问题转化:

因为要求是递增的,所以采用战略中的第一种,需要明确知道上个末尾的数字是多少。

细节:

数组初始化为1

674. 最长连续递增序列

关键词:

1、递增
2、连续
3、dp定义

问题转化:

因为要求递增,而且是连续的,所以不管从哪个角度,dp定义都是:dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]。

细节:

数组初始化为1

718. 最长重复子数组

关键词:

1、连续
2、dp定义

问题转化:

没有要求是递增或者递减,所以看是否连续,这里题目的要求是连续的,是战略里面的c。

实现细节:

初始化第一行,第一列要注意,但是单独拿出来,代码不好看,所以在初始化dp数组的时候,dp[length+1],把长度+1,遍历的时候从1
开始,就不用特地初始首行首列。

1143. 最长公共子序列

关键词:

1、不连续
2、dp定义

问题转化:

没有要求是递增或者递减,所以看是否连续,这里题目的要求是不连续的,是战略里面的b。可以堪称是300题的升维处理。

实现细节:

还是要注意首行首列的初始化,巧妙的做法如718题。

1035. 不相交的线

关键词:

1、就是求公共子序列

问题转化:

有点像应用题,其实和1143是一样的,代码实现也一样。

392. 判断子序列

关键词:

1、求公共子序列的变形,只要长度和s一致就好了

问题转化:

也像应用题,其实本质和1143一样,代码稍作改动。

583. 两个字符串的删除操作

关键词:

1、最小值

问题转化:

是求公共子序列的变形,所以先要确定dp,dp[i][j],word1长度是i,word2长度是j,需要达到相等,所需要删除元素的最少次数。

实现细节:

dp长度还是length+1,但是这里的首行首列需要初始化了,它是有意义的。

class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        for (int i = 0; i < word1.length() + 1; i++) dp[i][0] = i;
        for (int j = 0; j < word2.length() + 1; j++) dp[0][j] = j;
        
        for (int i = 1; i < word1.length() + 1; i++) {
            for (int j = 1; j < word2.length() + 1; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];  // 相等,可以作为公共子序列的一部分,所以不需要额外的操作
                }else{
                    dp[i][j] = Math.min(dp[i - 1][j - 1] + 2,
                                        Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));  // 不可以作为公共子序列的一部分,只能删掉了
                }
            }
        }
        
        return dp[word1.length()][word2.length()];
    }
}

53. 最大子数组和

关键词:

1、连续

问题转化:

不要求递增,但是要求连续,是战略中c的类别。dp定义:dp[i]:以第i个数结尾的最大连续子序列和为dp[i]。

115. 不同的子序列

关键词:

问题转化:

这一题比较特别,感觉不是很好套模型。先给出dp定义
注意这里不考虑实现细节的优化!!!
dp[i][j]:以i为结尾的s子序列中出现以j为结尾的t的个数为dp[i][j]。

  1. s[i]==t[j],
  2. s[i]!=t[j], 那么t(0j)出现在s(0i)中的次数,和出现在(0~i-1)中一样的。比如t=ab,s=abc,当j=1, i=2时。

实现细节:

init dp长度是length+1, 同时注意首列的初始化为1

class Solution {
    public int numDistinct(String s, String t) {
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        for (int i = 0; i < s.length() + 1; i++) {
            dp[i][0] = 1;
        }
        
        for (int i = 1; i < s.length() + 1; i++) {
            for (int j = 1; j < t.length() + 1; j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                }else{
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        
        return dp[s.length()][t.length()];
    }
}

你可能感兴趣的:(学习篇|leetcode刷题笔记-DP篇)