注意:我这里定义的所有dp的索引相对于实际问题都是从1开始的,也就是空间长度会比实际大1,这样的好处是在部分题目场景下不需要条件判断也不会越界。
字符串/数组类
区间
1.最长回文子串
dp定义
dp[i][j]表示第i个字符到第j个字符组成的子字符串是否是回文串(这里i,j都是从1开始的)
boolean[][] dp = new boolean[n + 1][n + 1];(n是字符串长度)
转移方程
如果从第i+1个字符到第j-1个字符组成的子字符串是回文字符串,那么如果第i个字符和第j个字符相等,那么从第i个字符到第j个字符组成的子字符串是回文字符串,表示如下:
dp[i][j] = dp[i + 1][j - 1] if s.charAt(i - 1) == s.charAt(j - 1)
(ps:s表示字符串,因为索引从0开始所以用i-1和j-1)
2.猜数字大小II
dp定义
通常我们定义都是n+1空间,这里定义n+2,是因为后面区间内dp文件中,右区间刚好为n了,但是需要划分n-1和n+1两部分,如果不定义为n+2,那么n+1这部分作为索引就会越界。
dp[i][j]表示从第i到第j区间的数确保能获胜的最小金额(不管选哪个数字),自然最后结果是返回dp[1][n]了
int[][] dp = new int[n + 2][n + 2];
转移方程
对于区间[i,j]存在选择choice∈[i,j],由于要保证稳赢,那么肯定不能直接choice就选中,没选中的话,就需要花费choice,然后从区间[i,choice-1]和[choice+1,j]中去求解子问题,因为要稳赢所以取左右两个区间中的最大成本花费,由于最后要选择最小成本,所以从所有choice中选择稳赢的且花费最少的,具体表示如下:
//max确保稳赢,min在稳赢的基础上选择最小的成本
dp[i][j] = Math.min(dp[i][j], Math.max(dp[i][choice - 1], dp[choice + 1][j]) + choice]);
前缀
1.解码方法
dp定义
dp[i]表示前i个字符组成的字符串的解码方式数
int[][] dp = new int[n + 1];(n是字符串长度)
转移方程
已知前i-2个字符组成的字符串和前i-1个字符组成的字符串的解码方式数,如果第i个字符不等于0,同时第i-1个字符满足不等于0且和第i位组成的两位数字符不大于26,那么第i个字符和第i-1个字符可以合并解码也可以单独解码,同理其他情况,只能第i位单独解码或者只能第i位和第i-1位字符一起解码,表示如下:
//最后一个字符单独解码码
if(s[i-1]!='0'){
dp[i]+=dp[i-1];
}
//倒数第二位不是0,且倒数两位不大于26,即1~26
if(i>1 && (s[i-2]=='1' || (s[i-2]=='2' && s[i-1]>='0' && s[i-1]<='6'))){
dp[i]+=dp[i-2];
}
总的来说
dp[i]=dp[i-1] 最后一位单独编码
dp[i]=dp[i-2] 最后两位一起编码
dp[i]=dp[i-1]+dp[i-2] 单独一起均可
2.交错字符串
dp定义
dp[i][j]表示s1的前i个元素和s2的前j个元素是否能交错构成s3的前i+j个元素。
boolean[][] dp = new boolean[m + 1][n + 1];
(m表示s1的长度,n表示s2的长度)
转移方程
如果s1的前i-1个元素和s2的前j个元素能交错构成s3的前i+j-1个元素,那么如果s1的第i个元素等于s3的第i+j个元素,那么s1的前i个元素和s2的前j个元素也能交错构成s3的前i+j个元素;对于s2的前j-1个元素的情况也同理,表示如下:
dp[i][j] = dp[i][j] ||
(s1.charAt(i - 1) == s3.charAt(i + j - 1) && dp[i - 1][j]);
dp[i][j] = dp[i][j] ||
(s2.charAt(j - 1) == s3.charAt(i + j - 1) && dp[i][j - 1]);
(ps:在索引s1/s2/s3时候由于其索引从0开始,所以第i个元素索引是i-1
,第i+j个元素索引是i+j-1)
3.单词拆分
dp定义
dp[i]表示前i个字符组成的子串能否被单词拆分
boolean[] dp = new boolean[n + 1];
转移方程
如果长度为len的部分等于单词表中的某个单词,那么dp[i]可以由dp[i - len]转移过来,具体表示如下:
if(i>=len&&s.substr(i-len,len)==wordDict[j]) {
dp[i]=dp[i-len] ||dp[i];
}
4.乘积最大子数组
dp定义
由于这题存在负数,由于负负得正的情况,也需要同时记录连续子数组的最小乘积。其中maxDp[i]表示以i结尾的连续子数组中的最大连续子数组的乘积,minDp[i]表示以i结尾的连续子数组中的最小连续子数组的乘积。
int[] maxDp = new int[n + 1];
int[] minDp = new int[n + 1];
转移方程
由于可能存在一个极小的负数乘上负数为最大值,所以在maxDp转移的时候应该要考虑minDp,同理可能存在一个极大的整数乘上负数为最小值,所以在minDp转移的时候应该要考虑maxDp,当然也有可能nums[i - 1]最大/小,最新的从第i个数开始重新算连续子数组最大/小值。表示如下
maxDp[i] = max(maxDp[i - 1] * nums[i - 1], max(nums[i - 1], minDp[i - 1] * nums[i - 1]));
minDp[i] = min(minDp[i - 1] * nums[i - 1], min(nums[i - 1], maxDp[i - 1] * nums[i - 1]));
5.最长递增子序列
dp定义
dp[i]表示以i结尾的最长子递增子序列长度
int[] dp = new int[n + 1];
转移方程
对于j<i,dp[i]如何由dp[j]转移过来呢?很显然,只要nums[i -1]>nums[j -1]满足严格递增,那么dp[j]+1就可以作为dp[i]的一个备选方案,由于j∈[1,i),所以需要在所有方案中取最大值。具体表示如下:
if(nums[j-1]<nums[i-1]){
dp[i]=max(dp[j]+1,dp[i]);
}
6.买卖股票的最佳时机含冷冻期
dp定义
dp[i][j]表示到第i天后的最大利润,j为0,1,2,3分别表示不同状态,为0时表示此时不持股且非卖出(进一步表示明天不在冷冻期可以直接买),为1表示为卖出状态,为2表示处于冷冻期,为3表示持股状态(即买入)。最后的结果就是从0~2三种状态中取最大值。
int[][] dp = new int[n + 1][4];
转移方程
1.对于为0时,表示此时不持股且非卖出,那么其前一天的状态可能:①冷冻期(即为2时),②此时不持股且非卖出(即为0时)。所以其状态转移方程如下:
dp[i][0]=max(dp[i-1][0],dp[i-1][2]);
2.对于为1的时候,表示卖出状态,那么其前一天的状态可能:①持股状态(即为3)。所以其状态转移方程如下。卖出利润自然需要加上prices[i - 1]:
dp[i][1]=dp[i-1][3]+prices[i-1];
3.对于为2时,表示冷冻期,那么其前一天的状态可能为:①卖出状态(即为1)。所以其状态转移方程如下:
dp[i][2]=dp[i-1][1];
4.对于为3时,表示持股状态,其前一天的状态可能为:①此时不持股且非卖出(即为0),②冷冻期(即为2时),③持股状态(即为3),那么状态转移方程如下,三中情况取最大值:
dp[i][3]=max(dp[i-1][0]-prices[i-1],dp[i-1][2]-prices[i-1]);
dp[i][3]=max(dp[i][3],dp[i-1][3]);(ps:由于不能同时参与多笔交易,必须在再次购买前出售掉之前的股票,所以这里对于3的转移不能继续买入)
7.最大整除子集
dp定义
dp[i]表示前i个数字且以i为结尾的最长整数子集的长度
int[] dp = new int[n + 1];
转移方程
对于j∈[1,i),如果满足nums[i-1]%nums[j-1]==0,那么dp[j]+1和dp[i]中去最大值就可以转移到dp[i]。表示如下:
dp[i] = Math.max(dp[i],d[j]+1);
8.摆动序列
dp定义
其中upDp[i]表示以第i个数为结尾的最长摆动子序列长度,且该子序列最后一位大于倒数第二位;downDp[i]表示第i个数为结尾的最长摆动子序列长度,且该子序列最后一位小于倒数第二位。最后结果就是返回upDp[n]和downDP[n]的最大值。
int[] upDp = new int[n + 1];
int[] downDp = new int[n + 1];
// 可以都初始化为1
Arrays.fill(upDp, 1);
Arrays.fill(downDp, 1);
转移方程
对于第i个数nums[i-1]如果大于第i-1个数nums[i-2],那么upDp[i]就可以由downDp[]i-1]转移过来,长度+1,对于upDp[i]其由upDp[i-1]直接转移得到,长度不变,只是子序列最后一位由nums[i-2]替换为了nums[i-1];同理第i个数nums[i-1]如果小于第i-1个数nums[i-2],那么downDP[i]就可以由upDp[i-1]转移过来,长度+1,对于downDp[i]其由downDp[i-1]直接转移得到,长度不变,只是子序列最后一位由nums[i-2]替换为了nums[i-1];如果相等,那么upDp[i]和downDp[i]分别相对于upDp[i-1]和downDp[i-1]长度没有变,只是子序列最后一位由nums[i-2]替换为了nums[i-1]。具体表示如下:
if(nums[i-2]>nums[i-1]){
downDp[i]=max(downDp[i-1],upDp[i-1]+1);
upDp[i]=upDp[i-1];
}else if(nums[i-2]<nums[i-1]){
upDp[i]=max(upDp[i-1],downDp[i-1]+1);
downDp[i]=downDp[i-1];
}else{
upDp[i]=upDp[i-1];
downDp[i]=downDp[i-1];
}
网格类
1.不同路径
dp定义
dp[i][j]表示走到第i行,第j列(从1开始)的路径条数
int[][]dp = new int[m + 1][n + 1];(m、n表示网格有几行几列)
转移方程
已知走到第i-1行、第j列的路径条数和走到第i行、第j-1行的路径条数,由于只能向下或者向右走,所以两者相加即可得到走到第i行、第j列的路径条数,表示如下:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
2.不同路径 II
和1.不同路径的dp和转移方程一样,不过由于多了障碍物,在初始化和转移的时候需要考虑障碍物的不可达。即加相应条件判断即可。
3.最小路径和
dp定义
dp[i][j]表示走到第i行,第j列(从1开始)的最小路径和
int[][]dp = new int[m + 1][n + 1];(m、n表示网格有几行几列)
转移方程
已知走到第i-1行、第j列的最小路径和和走到第i行、第j-1行的最小路径和,由于只能向下或者向右走一步,所以两者分别加上第i行第j列的网格数取最小值即为走到第i行第j列的最小路径和,表示如下:
dp[i][j]=min(dp[i][j-1],dp[i-1][j])+grid[i-1][j-1]
(ps:grid[i-1][j-1]表示网格第i行第j列的数字)
4.最大正方形
dp定义
dp[i][j]表示以第i行第j列位置为右下角的最大只包含1的正方形的边长,由于题目最后结果是求面积,正方形边长的平方就是面积。
int[][]dp = new int[m + 1][n + 1];(m、n表示网格有几行几列)
转移方程
当第i行第j列位置为1的时候,那么dp[i][j]的值不仅依赖于dp[i-1][j-1]还依赖于dp[i-1][j]和dp[i][j-1],因为即使dp[i-1][j-1]非常大,但如果dp[i-1][j]或dp[i][j-1]较小会使得dp[i][j]如果按照dp[i-1][j-1]+1直接转移过来的话得到的正方形就不是全为1了,所以需要这三者取最小值保证+1后得到的正方形满足全为1。
if(matrix[i-1][j-1]=='1'){
dp[i][j]=min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]))+1;
}
类别待定
1.爬楼梯
dp定义
dp[i]表示爬到第i阶楼梯有多少中爬法
int[] dp = new int[n + 1];
转移方程
已知爬到第i-1阶楼梯和爬到i-2阶楼梯的爬法,由于一次只能爬1或2阶,所以两者爬法相加即为爬到第i阶楼梯的爬法,表示如下:
dp[i] = dp[i - 1] + dp[i - 2];
2.不同的二叉搜索树
dp定义
dp[i]表示i个节点能组成的二叉树个数
int[] dp = new int[n + 1];
转移方程
取j∈[1,i],以j为根节点,第j个数左边部分为左子树,左子树有dp[j - 1]个,右边部分为右子树,右子数有dp[i-j]个,所以以j为根节点的二叉树有dp[j - 1]*dp[i-j],由于j∈[1,i],有多个取值,最终得到 。
3.三角形最小路径和
dp定义
dp[i][j]表示到达i层第j个位置的最小路径和
int[][] dp = new int[n + 1][n + 1];
转移方程
由于每一步只能移动到下一行中相邻的结点上,所以dp[i][j]可以由dp[i-1][j-1]或dp[i-1][j]移动得到。表示如下:
dp[i][j] = Math.min(dp[i-1][j-1],dp[i-1][j]) + triangle[i - 1][j - 1];
(triangle中索引从0开始,所以第i层,第j个数,索引为i-1和j-1)
4.打家劫舍
dp定义
dp[i]表示抢劫到第i个房子时的最大现金。
int[] dp = new int[n + 1];
转移方程
由于不能连续抢劫两间相邻的,所以要准备抢劫第i间时,只能从dp[i-2]转移,也可以选择不抢劫第i间时,从dp[i - 1]转移,表示如下:
dp[i]=max(dp[i-1],dp[i-2]+nums[i-1]);
5.打家劫舍 II
同4.打家劫舍一样,不过由于第一家和最后一家不能共存,在做的时候需要分别考虑两种,即1~n-1和2~n。
6.丑数 II
dp定义
dp[i]表示第i个丑数
int[] dp = new int[n + 1];
转移方程
由于丑数只包含质因数2、3或5的正整数(ps:1也算,是个例外),所以说对于dp[i]的转移一定是从前面的丑数乘上2或者3或者5得到的,也就是说存在j<i,使得dp[i] = dp[j] *(2或3或5)。由于丑数顺序是从小到大,所以需要每次取乘2或3或5的结果的最小值,同时对于乘的不同情况,j的值也会不同,具体表示如下:
dp[i]=min(dp[p2]*2,min(dp[p3]*3,dp[p5]*5));
if(dp[i]==dp[p2]*2) ++p2;
if(dp[i]==dp[p3]*3) ++p3;
if(dp[i]==dp[p5]*5) ++p5;
7.完全平方数
dp定义
dp[i]表示和为i的完全平方数的最小数量
int[] dp = new int[n + 1];
转移方程
由于转移的过程实际上是完全平方数的增加的过程,例如dp[i]就是从dp[i-1],dp[i-4],dp[i-9]……中找出最小的然后+1转移过来的,也就是说dp[i]=dp[i-完全平方数]+1,具体表示如下:
for(int i=1;i*i<=n;i++){
for(int j=i*i;j<=n;j++){
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
8.超级丑数
dp定义
dp[i]表示第i个丑数
int[] dp = new int[n + 1];
转移方程
这题本质上和6.丑数II一样,不过是primes数组从2、3、5变成了还有其他可能罢了,具体表示如下:
for(int i=0;i<m;i++){
min_res=min(min_res,dp[p[i]]*primes[i]);
}
dp[i]=min_res;
for(int i=0;i<m;i++){
if(min_res==dp[p[i]]*primes[i]){
++p[i];
}
}
9.零钱兑换
dp定义
dp[i]表示筹齐总金额i需要的最少硬币个数
int[] dp = new int[n + 1];
转移方程
这题就是1.爬楼梯的一个升级版,爬楼梯相当于只有面值1和2的硬币可选,类比于一次可以爬1阶或者2阶。这题的硬币组合由题目给出,所以每次可以选择一个硬币进行状态转移。具体表现如下,coin是各种可供选择的硬币,最后从各种选择选择最小的:
dp[j]=min(dp[j-coin]+1,dp[j]);
10.打家劫舍III
dp定义
dp(n)表示选择节点n后以n为根节点的子树能盗取的最大金额,由于n是节点,不是数字,不能作为索引,所以这题dp不用数组,用map。同理np(n)表示不选择节点n后以n为根节点的子树能盗取的最大金额。
表示不选择节点n后以n为根节点的子树能盗取的最大金额
Map<TreeNode, Integer> dp = new HashMap<>();
Map<TreeNode, Integer> np = new HashMap<>();
转移方程
假如存在一个根节点,如果选择了该根节点,那么其左右孩子就不能被选,那么以该根节点为根的子树能盗取的最大金额转移表示如下:
dp.put(root, np.getOrDefault(root.left, 0) + np.getOrDefault(root.right, 0) + root.val);
如果没有选择该根节点,那么其左右孩子就可选可不选,那么以该根节点为根的子树能盗取的最大金额转移表示如下:
np.put(root, Math.max(dp.getOrDefault(root.left, 0), np.getOrDefault(root.left, 0))
+ Math.max(dp.getOrDefault(root.right, 0), np.getOrDefault(root.right, 0)));
11.比特位计数
dp定义
dp[i]表示数i的二进制表示中1的个数
int[] dp = new int[n + 1];
转移方程
当一个数与该数-1的按位与为0时,表示这个数是2的次幂,因为2的次幂的二进制表示只有一个1且该1是最高位,减1之后那么自然得到就是最高位为0以及后面位全是1的结果了。应该我们可以用highestBit记录当前的最高位,表示如下:
if ((i & (i - 1)) == 0) {
//说明是2的次幂
highestBit = i;
}
那么我比你多一个最高位,显然就比你多了一个1,转移方程表示如下:
dp[i] = dp[i - highestBit] + 1;
12.整数拆分
dp定义
dp[i]表示数i能拆分成的整数的最大乘积
int[] dp = new int[n + 1];
转移方程
转移的情况明显就是拆或者不拆,设j∈[1,i),那么如果i-j拆分,那么dp[i-j]*j即可转移到dp[i],如果i-j不拆分,那么(i-j)*j可以转移到dp[i],具体表示如下:
dp[i]=max(dp[i],max(dp[i-j]*j,(i-j)*j));
13.统计各位数字都不同的数字个数
dp定义
dp[i]表示给定整数i,满足条件的数的个数。
int[] dp = new int[n + 1];
转移方程
首先对于dp[i]在dp[i-1]的基础上多出来了位数为i的满足各个位上数字都不同的数字,所以对于dp[i-1]到dp[i]的转移可以从这部分多出来了位数为i的满足各个位上数字都不同的数字入手。其次我们要知道dp[i-1]-dp[i-2]就是多出来的位数为i-1的满足各个位上数字都不同的数字,显然由于dp[i-1]-dp[i-2]就是多出来的位数为i-1的满足各个位上数字都不同的数字,那么在确定了前i-1位后,最后一位不能和前面的重复,其选择为10-(i-1),因此满足要求的位数为i的数共有(dp[i-1]-dp[i-2])*(10-(i-1))个。因此转移方程表示如下:
dp[i] = dp[i - 1] + (dp[i - 1]- dp[i -2]) * (10 - (i - 1));
14.组合总数IV
dp定义
dp[i]表示值为i的元素组合数。
int[] dp = new int[n + 1];
转移方程
对于nums数组,存在数组中的数num,dp[i]为所有的dp[i-num]的和。表示如下:
for(auto& num:nums){
if(i>=num && dp[i-num]<INT_MAX-dp[i]){
dp[i]+=dp[i-num];
}
}