注意:我这里定义的所有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]);
3.预测赢家
dp定义
dp[i][j]表示在区间[i,j](表示从第i个元素到第j个元素组成的区间)内玩家能赢得对方的最大分数(这个玩家可以指玩家1也可以指玩家2,但是在该区间内是先手),最后结果即在区间[1,n]内先手玩家(就是玩家1)赢的最大分数,不小于0即可获胜,返回dp[1][n]>=0
int [][] dp = new int[n + 1][n + 1];
转移方程
从dp[i+1][j]或者dp[i][j-1]转移到dp[i][j]无非就是选i或者选j的问题,如果选i,那么我就获得了nums[i-1]分,对方玩家在剩余[i+1,j]中能赢我的最大分数为dp[i+1][j],所以选择i后,可以由nums[i-1]-dp[i+1][j]转移过来,选择j的时候同理,最后取两种选择的最大值。具体表示如下:
dp[i][j] = Math.max(nums[i - 1] - dp[i + 1][j], nums[j - 1] - dp[i][j - 1]);
// 由于这里涉及i+1到i的转移,所以外层i的for循环要逆序
4.最长回文子序列
dp定义
dp[i][j]表示从第i个字符到第j个字符的区间中的最长回文子序列长度
int[][] dp = new int[n + 1][n + 1];
转移方程
对于区间[i+1, j-1]如果第i个字符等于第j个字符,那么可以长度+2转移到区间[i,j],如果不相等,那么dp[i,j]由dp[i+1][j]和dp[i][j-1]的最大值转移过来,具体表示如下:
if(s.charAt(i - 1) == s.charAt(j - 1)) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
5.最优除法
dp定义
dp[i][j]表示区间i到j组成的字符串的除法最大值和最小值,以及各自对应状态的除法表达式。最后输出结果为dp[1][n].maxStr
class Interval {
double maxVal, minVal;
String minStr, maxStr;
public Interval() {
this.minVal = 10000.0;
this.maxVal = 0.0;
}
}
Interval[][] dp = new Interval[n + 1][n + 1];
转移方程
在区间[i,j]中,存在k∈[i,j),将其拆为两个区间[i,k]和[k+1,j],所以那么dp[i][j]可以由dp[i][k]和dp[k+1][j]转移得到,dp[i][j]的除法最大值由dp[i][k]的最大值和dp[k+1][j]的最小值相除转移得到,而dp[i][j]的除法最小值由dp[i][k]的最小值和dp[k+1][j]的最大值相除转移得到。具体表示如下(区间结尾用i+step替代j):
if(dp[i][i + step].maxVal < dp[i][k].maxVal / dp[k + 1][i + step].minVal) {
// 赋值
dp[i][i + step].maxVal = dp[i][k].maxVal / dp[k + 1][i + step].minVal;
// 构建字符串
if(k + 1 == i + step) {
dp[i][i + step].maxStr = dp[i][k].maxStr + "/" + dp[k + 1][i + step].minStr;
} else {
dp[i][i + step].maxStr = dp[i][k].maxStr + "/(" + dp[k + 1][i + step].minStr + ")";
}
}
if(dp[i][i + step].minVal > dp[i][k].minVal / dp[k + 1][i + step].maxVal) {
dp[i][i + step].minVal = dp[i][k].minVal / dp[k + 1][i + step].maxVal;
// 构建字符串
if(k + 1 == i + step) {
dp[i][i + step].minStr = dp[i][k].minStr + "/" + dp[k + 1][i + step].maxStr;
} else {
dp[i][i + step].minStr = dp[i][k].minStr + "/(" + dp[k + 1][i + step].maxStr + ")";
}
}
6.回文子串
dp定义
dp[i][j]表示字符串的i到j组成的部分子串是否是回文子串。
boolean[][] dp = new boolean[n + 1][n + 1];
转移方程
很明显如果[i+1,j-1]区间部分是回文子串,且第i子字符等于第j个字符,那么很明显,[i,j]也是回文子串。具体表示如下:
if(s.charAt(i - 1) == s.charAt(j - 1) && (dp[i + 1][j - 1] || j - i <= 1)) {
dp[i][j] = true;
++res;
}
7.有效的括号字符串
dp定义
dp[i][j]表示从i到j的字符创是否是有效括号字符串
boolean[][] dp = new boolean[n + 1][n + 1];
转移方程
很明显这题有两种转移情况,第一种是中间部分是有效括号串,那么往两边扩大转移,第二种是两个有效括号串可以合并成一个大的有效括串,具体表示如下:
// 1.内部有效括号串,往两边扩散
if((s.charAt(i - 1) == '(' || s.charAt(i - 1) == '*') && (s.charAt(j - 1) == ')' || s.charAt(j - 1) == '*')) {
dp[i][j] = dp[i + 1][j - 1];
}
// 2.两个有效括号串拼接
for(int k = i; k < j && !dp[i][j]; k++) {
// 这里k+1大于i,所以i需要倒序,j不需要
dp[i][j] = dp[i][k] && dp[k + 1][j];
}
8.石子游戏
dp定义
dp[i][j]表示从i到j推的石子继续游戏,先手玩家能比后手玩家多获取的石子
int[][] dp = new int[n + 1][n + 1];
转移方程
那么转移无非就是选择i到j推这个区间的左边还是右边,选左边,那么剩下的i+1到j堆的就由对面先手了;同理选右边,那么剩下的i到j-1堆的就也由对面先手。
dp[i][j] = Math.max(piles[i - 1] - dp[i + 1][j], piles[j - 1] - dp[i][j - 1]);
前缀
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。
ps:上面这句特别解释一下,为什么定义的upDp[i]和downDp[i]都不是以第i个数为结尾的,我却可以用第i-1和第i个数的大小来确定downDp[i-1]到upDp[i]的转移呢?假入downDp[i-1]是以第m个数结尾的,其中m<=i-1,显然m等于i-1,是可以直接转移。讨论的是m<i-1的情况,如果第i-1个数大于等于第m个数,很明显,num[i - 1] > nums[i-2]>=nums[m-1]显然成立;如果第i-1个数小于第m个数,那么其实downDp[i-1]也可能看做以第i-1个数为结尾来替换第m个数,子序列长度不变。
对于downDP[i]其由downDP[i-1]直接转移得到,长度不变,且结尾的数也相同;
②同理第i个数nums[i-1]如果小于第i-1个数nums[i-2],那么downDP[i]就可以由upDp[i-1]转移过来,长度+1,对于upDp[i]其由upDp[i-1]直接转移得到,长度不变,且结尾的数也相同;
③如果相等,那么upDp[i]和downDp[i]分别相对于upDp[i-1]和downDp[i-1]长度没有变,只是子序列最后一位可以选择nums[i-2]替换为了nums[i-1],当然如果upDp[i-1]和downDp[i-1]不是以第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];
}
9.等差数列划分
dp定义
dp[i]表示以第i个数结尾的等差数列子数组个数。所以最后的结果是i∈[3,n)的所有以i结尾的数的个数的和
int[] dp = new int[n + 1];
转移方程
当nums[i-1]-nums[i-2]等于nums[i-2]-nums[i-3]时,那么dp[i]可以由dp[i-1]转移过来,并且多了一个满足要求的子数组,即[nums[i-3],nums[i-2],nums[i-1]]。
dp[i] = dp[i - 1] + 1;
10.无重叠区间
dp定义
dp[i]表示前i个区间的不重叠区间个数,最后结果是返回区间个数减去dp中的最大值
int[] dp = new int[intervals.length + 1];
转移方程
我们需要提前将区间按照右区间从小到大排序,那么对于j∈[1,i),如果第j个区间的右区间不大于第i个区间的左区间,那么dp[i]的不重叠区间个数就是dp[j]+1。具体表示如下:
if (intervals[i - 1][0] >= intervals[j - 1][1]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
11.环绕字符串中唯一的子字符串
dp定义
dp[i]表示在字符串中以字符i为结尾的满足条件的子字符串的个数。这题子串可能存在重复,因为长的必然包含短,如果用常规思想dp[i]表示以字符串中第i个字符为结尾的满足条件的子字符串的个数很难处理重复的情况。这题本质就是求以某个字符为结尾的连续“递增”字符串的长度,然后所有字符的结果加起来。由于有26个字母,所以开辟的空间为26.
int[] dp = new int[26];
转移方程
这题无非就是求以某个字符为结尾的连续“递增”字符串的长度,这里的“递增”指的是满足环绕字符串的字母顺序的,如果字符出现了多次,为了去重,我们要取以该字符为结尾的最长字符串,其长度就是以该字符为结尾的满足条件的子字符串个数。所以碰到满足“递增”序,那么cnt加1,否则置为1重新开始,通过max更新保留以各个字符结尾的最长的字符串长度。具体表示如下:
dp[s.charAt(0) - 'a'] = 1;
int cnt = 1;
for(int i = 1; i < n; i++){
if(s.charAt(i) - s.charAt(i - 1) == 1 || s.charAt(i) - s.charAt(i - 1) == -25) {
++cnt;
} else {
cnt = 1;
}
dp[s.charAt(i) - 'a'] = Math.max(dp[s.charAt(i) - 'a'], cnt);
}
12.目标和
dp定义
dp[i][j]表示前i个数能够得到的目标和为j的方法数,由于这题类比于01背包问题,所以可以把第一维去掉,定义为dp[i]表示目标和为i的方法数。(neg定义见下面转移方程描述)
int[][] dp = new int[n + 1][neg + 1];
// 本质是一个01背包问题,可去掉第一维
int[] dp = new int[neg + 1];
转移方程
为了使结果等于target,这题存在将一些数取负号,假如要被取负号的数的原始和为neg>=0,nums数组的所有数的和为sum,那么取正号那部分的数的和为sum-neg,那么表示出来target=(sum-neg)+(-neg),得到neg*2 = sum - target,这样可以求出neg,那么这题就转化为了从nums中取数求和为neg的方法数为多少?具体转移方程如下,由于是01背包问题,所以num循环在外层,且因为省略了第一维内层倒序遍历。
dp[j] += dp[j - num];
13.两个字符串的删除操作
dp定义
dp[i][j]表示word1的前i个字符和word2的前j个字符的最长公共子序列长度。这题求删除字符使得word1和word2相同所需的最小步数,其实等价于word1加上word2的长度减去两倍的最长公共子序列长度。
int [][] dp = new int[m + 1][n + 1];
转移方程
对于dp[i][j],如果word1.charAt(i-1)和word2.charAt(j-1)相等,那么dp[i-1][j-1]+1就可以转移到dp[i][j],如果不相等,那么就取dp[i][j-1]和dp[i-1][j]的最大值。具体表示如下:
if(word1[i-1]==word2[j-1]){
//相等,直接+1
dp[i][j]=dp[i-1][j-1]+1;
}else{
//不相等,则直接跳过word1的第i个字符或者word2的第j个字符
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
14.最长数对链
dp定义
dp[i]表示以第i个对为结尾的最长数对链的长度。
int[] dp = new int[n + 1];
转移方程
很明显这题能转移的前提是先根据数组中对的第一个进行排序。然后判断是否下一个对的第一个数大于当前对的第二个,满足就+1转移。具体表示如下:
if(pairs[i - 1][0] > pairs[j - 1][1]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
15.最长递增子序列的个数
dp定义
dp[i]表示以i为结尾的最长递增子序列的长度,count[i]表示以i为结尾的满足最长递增子序列长度的序列个数。
int[] dp = new int[n + 1];
int[] count = new int[n + 1];
转移方程
很明显,存在j∈[1,i),如果num[j-1]小于nums[i-1],那么dp[i]可以从dp[j]+1转移过来,也就是dp[i]=Math.max(d[i],dp[j]+1),同时我们需要记录满足最大长度的序列个数。具体表示如下:
if(nums[j-1]<nums[i-1]){
// dp[i]=max(dp[i],dp[j]+1);
//最长子序列更新,记录个数
if(dp[j]+1>dp[i]){
dp[i]=dp[j]+1;
count[i]=count[j];
}
//最长子序列长度相同,长度不变,记录个数增加
else if(dp[j]+1==dp[i]){
//长度不用更新
count[i]+=count[j];
}
}
16.两个字符串的最小ASCII删除和
dp定义
dp[i][j]表示s1的前i位和s2的前j位的ascii码和最大的公共子序列的ascii码和*2,那么本题的结果就转化为两个字符串所有字符的ascii码和减去dp[m][n]
int[][] dp = new int[m + 1][n + 1];
转移方程
很明显这题转移就类似于常见的公共子序列问题,如果s1的第i个字符和s2的第j个字符相等,那么dp[i][j]可以由dp[i-1][j-1]加上这两字符的ascii转移得到,不相等的话,选择dp[i-1][j]和dp[i][j-1]中两者较大值转移。
if(s1.charAt(i - 1) == s2.charAt(j - 1)) {
// 字符在int运算时会自动转为ascii码
dp[i][j] = dp[i - 1][j - 1] + s1.charAt(i - 1) + s2.charAt(j - 1);
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
17.最长重复子数组
dp定义
dp定义,dp[i][j]表示nums1的前个元素和nums2的前j个元素的分别以i和j结尾的最长重复子数组长度
int[][] dp = new int[m + 1][n + 1];
转移方程
很显然,如果nums1的第i位和nums2的第j位相等,那么dp[i-1][j-1]+1就可以转移到dp[i][j],如果不相等,dp[i][j]直接为0,因为不存在以i和j分别为结尾的重复子数组。具体表示如下:
if(nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = 0;
}
18.旋转数字
dp定义
dp[i]为0表示数的前i位不是好数也不是旋转之后不变,为1表示不是好数但是旋转后不变,为2表示是好数。
int[] dp = new int[n + 1];
转移方程
很明显当前i-1位是1或者2,即要么是好数要么是旋转后不变,那么当第i位是好数时,那么前i位显然也是好数,其次如果第i位是旋转后不变的数,那么dp[i-1]和dp[i]的状态相同。具体表示如下:
if(cur == 3 || cur == 4 || cur == 7) {
continue;
} else if(cur == 0 || cur == 1 || cur == 8){
dp[i] = dp[pre];
} else {
// 好数(pre为1或者2都可以转移为好数)
dp[i] = dp[pre] == 0 ? 0 : 2;
}
res += dp[i] == 2 ? 1 : 0;
19.最大平均值和的分组
dp定义
dp[i][j]表示前i个数被划分为j个区间的平均和。
double[][] dp = new double[n + 1][n + 1];
转移方程
对于dp[i][j],存在c∈[j-1,i),使得划分出一个区间(c,i](求出这部分的平均和),剩下的[0,c]可以最多划分为j-1个区间(即dp[c][j-1]).。具体转移如下:
// 划出一个区间(c,i],那么剩余[0,c]可以最多划分j-1个区间
double temp = (pre[i] - pre[c]) / (i - c);
dp[i][j] = Math.max(dp[i][j], dp[c][j - 1] + temp);
20.数组中的最长山脉
dp定义
dpL[i]表示以第i个数为结尾的递增子数组长度,dpR[I]表示以第i个数为开头的递减子数组长度。最后结果是取最大的dpL+dpR-1。
int[] dpL = new int[n + 1];
int[] dpR = new int[n + 1];
转移方程
对于dpL[i],很明显,只要满足第i个数大于i-1就可以转移,同理dpR[i],只要满足第i个数大于第i+1个数可以转移。具体表示如下:
for(int i = 2; i <= n; i++) {
if(arr[i - 1] > arr[i - 2]) {
dpL[i] = dpL[i - 1] + 1;
}
}
for(int i = n - 1; i >= 1; i--) {
if(arr[i - 1] > arr[i]) {
dpR[i] = dpR[i + 1] + 1;
}
}
21.最长的斐波那契子序列长度
dp定义
dp[i][j]表示以第i个数和第j个数作为子序列最后两位数的最长斐波那契子序列长度。
int[][] dp = new int[n + 1][n + 1];
转移方程
很明显,要实现转移,需要存在k( k < i < j),使得arr[k - 1] + arr[i-1] = arr[j-1],那么dp[i][j]就可以由dp[k][i]+1转移过来。具体表示如下:
int k = map.getOrDefault(arr[j - 1] - arr[i - 1], -1);
if(k != -1) {
// 存在k,使得arr[k - 1] + arr[i-1] = arr[j-1] (k < i < j)
// 先固定j,枚举满足的k和i进行转移
dp[i][j] = Math.max(dp[k][i] + 1, 3);
}
22.将字符串翻转到单调递增
dp定义
dp[i][j]表示第i个数字是j(可能是0或者1)的翻转次数
int[][] dp = new int[n + 1][2];
转移方程
很明显如果当前位是1,那么前一位可以是0或者1,当前位是0,前一位必须是0。
对于dp[i][0]要求第i位为0,那么i-1位必须是0,如果第i位是1则需要翻转一次,反之不需要翻转;对于dp[i][1]要求第i位为1,那么i-1位可以是0也可以是1,如果第i位是0则需要额外翻转一次,反之则不需要翻转。
if(s.charAt(i - 1) == '1') {
dp[i][0] = dp[i - 1][0] + 1;
dp[i][1] = Math.min(dp[i - 1][0], dp[i - 1][1]);
} else {
dp[i][0] = dp[i - 1][0];
dp[i][1] = Math.min(dp[i - 1][0], dp[i - 1][1]) + 1;
}
23.最长湍流子数组
dp定义
dp[i][0]以第i个数为结尾的最长湍流子数组长度且最后一个数小于倒数第二个数,dp[i][1]以第i个数为结尾的最长湍流子数组长度且最后一个数大于倒数第二个数
int[][] dp = new int[n + 1][2];
转移方程
很显然,如果第i个数大于第i-1个数,那么dp[i][1]可以由dp[i-1][0]转移得到;如果第i个数小于第i-1个数,那么dp[i][0]可以由dp[i-1][1]转移得到。具体表示如下:
if(arr[i - 2] > arr[i - 1]) {
dp[i][0] = dp[i - 1][1] + 1;
}else if(arr[i - 2] < arr[i - 1]) {
dp[i][1] = dp[i - 1][0] + 1;
}
24.最长等差数列
dp定义
dp[i][d]表示分别以第i个数为结尾的公差为d的等差子序列长度,其中由于公差可能为负数,且最大差值为500,因此,我们可以将统一将公差加上500,这样公差的范围就从[-500,500]变成了 [0,1000]。
int[][] dp = new int[n + 1][1001];
转移方程
显然对于i,存在j∈[1,j),公差d为nums[i - 1] - nums[j - 1] + 500,可以从dp[j][d]转移到dp[i][d]。具体表示如下:
int d = nums[i - 1] - nums[j - 1] + 500;
dp[i][d] = Math.max(dp[i][d], dp[j][d] + 1);
25.视频拼接
dp定义
dp[i]表示覆盖[0,i]需要的最少片段数
int[] dp = new int[time + 1];
转移方程
对于任意已存在的片段[a,b],存在i∈(a,b],那么dp[a]可以转移到dp[i]。具体表示如下:
for(int[] cl : clips) {
if(i > cl[0] && i <= cl[1]) {
dp[i] = Math.min(dp[i], dp[cl[0]] + 1);
}
}
26.不相交的线
dp定义
要求不想交就是要求顺序匹配,所以这题等价于求两个数组的最长公共子序列,dp[i][j]表示nums1的前i位和nums2的前j位的最长公共子序列。
int[][] dp = new int[m + 1][n + 1];
转移方程
如果nums1的第i位和nums2的第j位相等,那么dp[i-1][j-1]可以转移到dp[i][j],如果不相等,那么可以由dp[i][j-1]或者dp[i-1][j]转移过来。具体表示如下:
if(nums1[i - 1] == nums2[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]);
}
27.最长括号子串
dp定义
dp[i]表示以i结尾的有效括号子串长度
int[] dp = new int[n + 1];
转移方程
对于当前i,状态的转移主要分为两类:①后面拼接:第i-1位和第i位分别为(和),因此dp[i]可以由dp[i-2]加上2转移得到;②两边扩展:第i - dp[i - 1] - 1为(,第i位为)。那么dp[i]由dp[i - dp[i - 1] - 2]+ dp[i - 1] +2转移过来。具体如下:
if(s.charAt(i - 2) == '(') {
// 后面拼接
dp[i] = dp[i - 2] + 2;
} else if(i - dp[i - 1] - 2 >= 0
&& s.charAt(i - dp[i - 1] - 2) == '(') {
// 两边扩展
//实际为dp[i - dp[i - 1] - 2] ( dp[i - 1] )
dp[i] = dp[i - dp[i - 1] - 2] + 2 + dp[i - 1];
}
28.正则表达式匹配
dp定义
dp[i][j]表示str前i个字符和pattern前j个字符是否匹配。
boolean[][] dp = new boolean[m + 1][n + 1];
转移方程
很明显对于str的第i个字符和pattern的第j个字符,如果其相等或者第j个字符为.(可以表示任意一个字符),那么的dp[i][j]可以由dp[i - 1][j - 1]转移过来。如果第j个字符为*,那么有两种情况,*让第j-i个字符出现0次,所以dp[i][j]可以由dp[i][j - 2]转移过来;或者让第j-i个字符多出现一次,即当第j-i个字符为.或者与第i个字符相等时,dp[i][j]可以由dp[i-1][j]转移过来。
if(pattern.charAt(j - 1) == '.' ||
pattern.charAt(j - 1) == str.charAt(i - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else if(pattern.charAt(j - 1) == '*') {
if(j - 2 >= 0) {
// *让其出现0次
dp[i][j] |= dp[i][j - 2];
}
if(j - 2 >= 0 && (pattern.charAt(j - 2) == '.' ||
pattern.charAt(j - 2) == str.charAt(i - 1))) {
// *'让其多出现一次
dp[i][j] |= dp[i - 1][j];
}
}
29.编辑距离(一)
dp定义
dp[i][j]表示str1的前i位和str2的前j位字符子串的编辑距离,最终结果返回dp[m][n]。
int[][] dp = new int[m + 1][n + 1];
转移方程
对于str1的第i个字符和str2的第j个字符,如果相等,那么不需要操作,所以dp[i][j]可以由dp[i-1][j-1]转移得到。如果不相等,那么有三种方式:①修改str1的第i个字符或str2的第j个字符保证其相等;②删掉str2的第j个字符得到过去状态dp[i][j - 1];③删掉str1的第i个字符得到过去状态dp[i - 1][j]。这些都需要操作一次。
if(str1.charAt(i - 1) == str2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
// ①不同修改一下即可修改成相同
// ②和③删除即可保证
dp[i][j] = Math.min(dp[i - 1][j - 1],
Math.min(dp[i][j - 1], dp[i - 1][j])) + 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;
}
5.01矩阵
dp定义
dp[i][j]表示第i行第j列对应元素到最近的0的距离。
int[][]dp = new int[m][n];(m、n表示矩阵有几行几列)
转移方程
由于要确定一个位置对应元素到最近的0的距离需要同时考虑上下左右,那么一次遍历显然是顾及不到四周的,所以需要分两次遍历,第一次,从左上到右下,那么dp[i][j]就是选择由其上边或者左边转移而来;第二次,从右下到左上,那么dp[i][j]就是选择由其下边或者右边转移而来,由于本题是取最小值,显然都需要取最小的情况。具体表示如下:
//对于f[i][j]是其上面或者左边还是自己更小
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(mat[i][j]!=0){
if(i>0){
f[i][j]=min(f[i][j],f[i-1][j]+1);
}
if(j>0){
f[i][j]=min(f[i][j],f[i][j-1]+1);
}
}
}
}
//对于f[i][j]是其下面或者右边还是自己更小
for(int i=m-1;i>=0;i--){
for(int j=n-1;j>=0;j--){
if(mat[i][j]!=0){
if(i<m-1){
f[i][j]=min(f[i][j],f[i+1][j]+1);
}
if(j<n-1){
f[i][j]=min(f[i][j],f[i][j+1]+1);
}
}
}
}
6.出界的路径数
dp定义
dp[m][i][j]表示m次移动后到i、j位置的路径数
int[][][] dp = new int[maxMove + 1][m + 1][n + 1];
转移方程
这题的转移也涉及到四周,但不同于上一题01矩阵,这题只需要遍历一次,因为我们存在移动次数的维度,即使涉及到i+1转移到i或者j+1转移到j,但由于我整体是从m转移到m+1的(也就是移动次数存在转移),所以没有问题,不需要像上面01矩阵一样。具体表示如下:
int ways = dp[i][j][k];
if(ways > 0) {
for(int p = 0; p < 4; p++) {
int jj = j + directions[p], kk = k + directions[p + 1];
if(jj >= 1 && jj <= m && kk >= 1 && kk <= n) {
dp[i + 1][jj][kk] = (dp[i + 1][jj][kk] + ways) % MOD;
} else {
// 越界了
res = (res + ways) % MOD;
}
}
}
7.骑士在棋盘上的概率
dp定义
dp[k][i][j]表示从i,j位置出发走了k步还留在棋盘上的概率
double[][][] dp = new double[k + 1][n + 1][n + 1];
转移方程
很明显这题的转移就涉及到八个方向,从八个方向走过来,其实就是每个方向的概率乘1/8然后求和。具体表示如下:
// 根据8个方向的概率*1/8然后求和
for(int p = 0; p < 8; p++) {
int newI = i + directions[p];
int newJ = j + directions[p + 1];
if(newI >= 1 && newI <= n && newJ >= 1 && newJ <= n) {
dp[s][i][j] += dp[s - 1][newI][newJ] / 8;
}
}
8.最大加号标志
dp定义
dp[i][j]表示以i,j为中心的最大加号的阶数
int[][] dp = new int[n + 1][n + 1];
转移方程
在思考转移的时候,我们应该在更进一步考虑一个维度,即左右上下,比如dp[i][j][0]表示处于ij位置的左边为1的最大长度,那么显然根据dp[i][j-1][0]来转移,其余方向同理,具体表示如下:
我们最终要得到的是ij位置中四个方向的最小值作为其加号标志的阶数,最后取最大阶数的位置作为结果。具体四种转移表示如下:
// 左
// dp[i][j][0] = dp[i][j-1][0] + 1
for(int j = 1; j <= n; j++) {
if(dp[i][j] != 0) {
++cnt;
} else {
cnt = 0;
}
dp[i][j] = Math.min(dp[i][j], cnt);
}
cnt = 0;
// 右
// dp[i][j][1] = dp[i][j+1][1] + 1
for(int j = n; j >= 1; j--) {
if(dp[i][j] != 0) {
++cnt;
} else {
cnt = 0;
}
dp[i][j] = Math.min(dp[i][j], cnt);
}
// 上
// dp[i][j][2] = dp[i-1][j][2] + 1
for(int i = 1; i <= n; i++) {
if(dp[i][j] != 0) {
++cnt;
} else {
cnt = 0;
}
dp[i][j] = Math.min(dp[i][j], cnt);
}
cnt = 0;
// 下
// dp[i][j][3] = dp[i+1][j][3] + 1
for(int i = n; i >= 0; i--) {
if(dp[i][j] != 0) {
++cnt;
} else {
cnt = 0;
}
dp[i][j] = Math.min(dp[i][j], cnt);
}
9.香槟塔
dp定义
dp[i][j]表示第i行第j列杯子所经过的水的总量(会大于1)
double[][] dp = new double[query_row + 2][query_row + 2];
转移方程
由于下方相邻位置的杯子会接受到上方溢出的水,那么以dp[i][j]为起始状态转移到下一个状态,可能为dp[i+1][j]/dp[i+1][j+1]。溢出的水除2则是转移给下方杯子的水。如果没有溢出就不需要转移。具体表示如下:
if(dp[i][j] <= 1) {
// 没满
continue;
}
// 以dp[i][j]为起始状态转移到下一个状态,可能为dp[i+1][j]/dp[i+1][j+1]
dp[i + 1][j] += (dp[i][j] - 1) / 2;
dp[i + 1][j + 1] += (dp[i][j] - 1) / 2;
10.下降路径最小和
dp定义
dp[i][j]表示到达第i行第j列的下降路径的最小和
int[][] dp = new int[m + 1][n + 1];
转移方程
由于在选择的时候,在下一行选择的元素和当前行所选元素最多相隔一列。因此对于dp[i][j]有三种选择,即dp[i-1][j-1]、dp[i-1][j]、dp[i-1][j+1]。
// 上面一行三个
int min_temp = dp[i - 1][j];
if(j - 1 >= 1) {
min_temp = Math.min(min_temp, dp[i - 1][j - 1]);
}
if(j + 1 <= n) {
min_temp = Math.min(min_temp, dp[i - 1][j + 1]);
}
dp[i][j] = min_temp + matrix[i - 1][j - 1];
11.骑士拨号器
dp定义
dp[i][j]表示长度为i且此时跳到数字j的号码数,结果就是对于j∈[0,9],所有的dp[n][j]加起来。
int[][] dp = new int[n + 1][10];
转移方程
很明显对于dp[i][j],如果arr可以直达j,那么可以从dp[i-1][arr]转移过来。具体表示如下:
for(int arr : path[j]) {
dp[i][j] = (dp[i][j] + dp[i - 1][arr]) % MOD;
}
图/树问题
1.K 站中转内最便宜的航班
dp定义
dp[i][j]表示出发经过i次航班(经过i次航班实际上只中转了i-1次)后到达城市j的最小花费。
int[][] dp = new int[k + 2][n];
转移方程
很明显这题就是涉及到航班的选择,我是否选择这个航班,选择的话,就加上航班的花费,从该次航班状态的开始点转移至结束点,具体表示如下:
for(int[] flight : flights) {
dp[i][flight[1]] = Math.min(dp[i][flight[1]], dp[i - 1][flight[0]] + flight[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]表示以第i个数为根节点的带因子的二叉树的个数
long[] dp = new long[n + 1];
转移方程
很明显这题转移的条件的是存在l∈[1,r]、r∈[l,i-1),使得l和r节点值相乘等于i,那么以l和r为子节点,i为根节点的满足条件二叉树个数明显是其左右子树的满足条件二叉树个数的乘积,更进一步,如果左右子树不相同,那么左右子树可以互换,那么乘积还需要乘2,表示如下:
if(l == r) {
// 两个根相同,那么以i为根的个数是两个子树的个数相乘
dp[i] = (dp[i] + dp[l] * dp[r]) % mod;
} else {
// 不同的话,由于可以互换左右子树位置,所以个数相乘还得乘2
dp[i] = (dp[i] + dp[l] * dp[r] * 2) % mod;
}
4.所有可能的真二叉树
dp定义
dp.get(i)表示i个节点能组成的真二叉树集合。
List<List<TreeNode>> dp = new ArrayList<>();
转移方程
对于i个节点组成的真二叉树,存在p+q=i-1,p个节点组成的真二叉树集合作为左子树,q个节点组成的真二叉树集合作为右子树。
for(TreeNode l : dp.get(p)) {
for(TreeNode r : dp.get(q)) {
TreeNode root = new TreeNode(0);
root.left = l;
root.right = r;
dp.get(i).add(root);
}
}
背包问题
01背包
1.目标和
见前面的字符串/数组类中的前缀的12。
2.分割等和子集
dp定义
dp[i]表示数组中的元素能否组成和为i。w是数组中所有元素和的一半。
boolean[] dp = new boolean[w+1];
转移方程
很明显这题被类比为了01背包问题,状态转移即是否选择该数字nums[i-1]具体表示如下:
dp[j]=dp[j]||dp[j-nums[i-1]];
3.一和零
dp定义
dp[i][j]表示使用了不超过i个0和j个1能得到的最大子集长度。
int[][] dp = new int[m + 1][n + 1];
转移方程
这题0和1的最大个数可以看成背包的两种容量,而一个字符串的0和1的个数可以看成物品的两种属性,最后求最大子集长度(也就是尽可能装进去更多的字符串),所以本质是一个背包问题,不过相对于常规背包问题有两个容量限制。因此以是否选择该字符串这个过程进行状态转移,具体如下:
// 是否选择该字符串这个过程可以进行状态转移
for(int j = m; j >= c0; j--) {
for(int k = n; k >= c1; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - c0][k - c1] + 1);
}
}
ps:这里c0表示遍历到的字符串的0的个数,c1表示1的个数,为什么ij要逆序遍历,这个问题和背包问题同理,因为我们这里有一个代表前几个字符串的维度被省略了(真实的dp定义如下:dp[i][j][k]表示在前i个字符串中使用了不超过j个0和k个1能得到的最大子集长度),因为我们这里用到的dp[j - c0][k - c1]是上一个字符串(i-1)的,如果正序遍历,那么它们就会被当前字符串(i)进行了更新。
完全背包
1.零钱兑换 II
dp定义
dp[i]表示凑成金额为i的硬币的组合数。
int[] dp = new int[amount + 1];
转移方程
选择一种硬币就是一种状态转移,硬币类比背包问题中的物品,一种面值的硬币有无限个,很明显是完全背包问题,具体表示如下:
dp[j]+=dp[j-coin];
2.大礼包
dp定义
这题的dp定义为函数,具体如下,其中curNeeds表示当前的购物清单(随着购物动态变化的),返回值是购买curNeeds所花费的最小价格,也就是说该dp表示给定price和special,购买curNeeds所花费的最小价格。
public int dp(List<Integer> price, List<List<Integer>> special, List<Integer> curNeeds, int n)
转移方程
其实转移状态无非涉及到原价买还是通过大礼包买,取最小值即可。
// 初始化minPrice,即按照清单不购买任何大礼包原价购买的价格
int minPrice = 0;
for(int i = 0; i < n; i++) {
minPrice += curNeeds.get(i) * price.get(i);
}
if(currNeeds.size() == n) {
// 都满足清单数量要求(未超出),那么大礼包才可以购买
minPrice = Math.min(minPrice, dp(price, special, currNeeds, n) + curPrice);
}
多状态DP
1.多米诺和托米诺平铺
dp定义
dp[i][0/1/2/3]表示前i-1列被全覆盖,第i列没有一个被覆盖/上边被覆盖/下边被覆盖/全被覆盖
int[][] dp = new int[n + 1][4];
转移方程
这题的状态转移用图比较直观,参考官方图如下:
对于dp[i][0]这里需要尤其注意:虽然我们能够在上一步留空第i−1列,然后在i-1列竖放一块1×2的骨牌,但我们不能从dp[i−1][0]转移到dp[i][0],因为此时放置的骨牌,仅对第i-1列产生影响,不会对第i列产生影响,该决策所产生的方案数,已在dp[i−1][X]时被统计。
具体表示如下:
dp[i][0] = dp[i - 1][3];
dp[i][1] = (dp[i - 1][0] + dp[i - 1][2]) % MOD;
dp[i][2] = (dp[i - 1][0] + dp[i - 1][1]) % MOD;
dp[i][3] = (((dp[i - 1][0] + dp[i - 1][1]) % MOD + dp[i - 1][2]) % MOD + dp[i - 1][3]) % MOD;
2.最低票价
dp定义
dp[i][0/1/2/3]表示在第i天时复用之前的/购买一天/购买七天/购买三十天的最低消费
int[][] dp = new int[n + 1][4];
转移方程
对于状态0:什么时候可以复用之前的?当然是前七天内购买了七天票或者前三十天内购买了三十天票。
对于状态1/2/3,当天都需要买对应价位的票,前天可以是复用可以是买了一天的票然后使用完。
具体表示如下:
// 状态0的转移
if(days[i - 1] - days[j - 1] < 7) {
dp[i][0] = Math.min(dp[i][0], dp[j][2]);
}
if(days[i - 1] - days[j - 1] < 30) {
dp[i][0] = Math.min(dp[i][0], dp[j][3]);
}
// 状态1的转移,购买一天
dp[i][1] = Math.min(dp[i][1], Math.min(dp[i - 1][0], dp[i - 1][1]) + costs[0]);
// 状态2的转移,购买七天
dp[i][2] = Math.min(dp[i][2], Math.min(dp[i - 1][0], dp[i - 1][1]) + costs[1]);
// 状态3的转移,购买三十天
dp[i][3] = Math.min(dp[i][3], Math.min(dp[i - 1][0], dp[i - 1][1]) + costs[2]);
函数
1.多边形三角剖分的最低得分
dp定义
dp(i,j,value)表示从第i个点到第j个点构成了j-1+1边形进行三角形剖析后可以得到的最低分。最后结果是dp(1, values.length, values)。
public int dp(int i, int j, int[] values) {}
转移方程
如果j == i + 2,那么刚好3个点,是三角形,直接返回三个点的乘积;如果j<i+2那么凑不成3个点,返回0;其他情况,存在k∈(i,j),把i到j划分为三个部分:① i到k ② i、j、k组成的三角形 ③ k到j。具体表示如下:
int minScore = Integer.MAX_VALUE;
// 存在k把i到j划分为三个部分:① i到k ② i、j、k组成的三角形 ③ k到j
for(int k = i + 1; k < j; k++) {
minScore = Math.min(minScore, dp(i, k, values) + values[i - 1] * values[k - 1] * values[j - 1] + dp(k, j, values));
}
类别待定
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];
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];
}
}
15.旋转函数
dp定义
dp[i]表示旋转i次后计算得到的F的结果,很显然i属于0~n-1,取最大的dp为最终的结果。(ps,这题没有定义n+1,因为旋转次数最大取值为n-1,其余取值没有意义了,取不到n)
int[] dp = new int[n];
转移方程
对于dp[i-1]和dp[i]之间的关系:
数组长度为n,每次旋转后dp[i-1]的前n-1个数都向后移动了一位,其索引值+1,那么dp[i-1]加上这前n-1个数就等于旋转dp[i]的后n-1个数的值,对于dp[i-1]其最后一个数nums[n-i]的索引从n-1旋转后变为0,总的来说dp[i]需要加上n-1个nums[n-i]才能和dp[i-1]加上dp[i-1]表示的旋转数组前n-1个数等价。
也就是说dp[i-1]+dp[i-1]中表示的旋转数组中前n-1个数=dp[i]+(n-1)*dp[i-1]表示的旋转数组的最后一个数(即nums[n-i])
=>等式两边加上dp[i-1]表示的旋转数组的最后一个数(即nums[n-i]),化为下面
d[i-1]+sum = dp[i] + n*nums[n-i](ps:其中sum表示数组的所有元素的和),具体表示如下:
dp[i] = dp[i - 1] + sum - n * nums[n - i];
16.整数替换
dp定义
这题我一开始第一想法是dp定义如下:
其中dp[i]表示数i变为1需要的最小替换次数,但是后面考虑状态转移时由于奇数同时涉及到i-1和i+1,顺序遍历无法正常转移。
int[] dp = new int[n+1];
所以选择了递归的方式,将dp表示为一个函数,代表的意思仍然不变。
public int dp(long i) {}
转移方程
对于dp[i],当i为偶数时,可以由dp[i/2]转移过来,当i为奇数时,可以由dp[n-1]或者dp[i+1]转移过来,具体表示如下:
if(i % 2 == 0) {
return dp(i /2) + 1;
} else {
return Math.min(dp(i - 1), dp(i + 1)) + 1;
}
17.只有两个键的键盘
dp定义
dp[i]表示i个A需要的最少操作数
int[] dp = new int[n+1];
转移方程
如果i能被j除尽,那么在dp[j](其中j∈[1,i/2])的基础上只需要通过一次复制加上i/j-1次粘贴共计i/j次操作就可以得到dp[i]。具体表示如下:
if(i%j==0){
dp[i]=min(dp[i],dp[j]+i/j);
}
18.买卖股票的最佳时机含手续费
dp定义
定义dp,dp[i][0]代表第i天处于卖出状态的最大利润,dp[i][1]代表第i天处于买入状态的最大利润
int[][] dp = new int[n + 1][2];
转移方程
对于当前是卖出状态,前一天有两种可能:也是卖出状态;买入状态(今天卖出,赚到卖出的钱,但要扣掉手续费)。对于当前是买入状态,前一天有两种可能:也是买入状态;卖出状态(今天买入,扣掉购买费)。具体表示如下:
// 前一天也是卖出状态,前一天是买入状态(今天卖出,赚到价钱,扣掉手续费)
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1] - fee);
// 前一天也是买入状态,前一天是卖出状态(今天买入,扣掉购买费)
dp[i][1] = Math.max(dp[i - 1][1], dp[i- 1][0] - prices[i - 1]);
19.删除并获得点数
dp定义
dp[i]表示选择到了第i个数得到的最大点数
int[] dp = new int[max + 1];
其中max和对应的sum由题目中所给的nums求出
int max = Arrays.stream(nums).max().getAsInt();
int[] sum = new int[max + 1];
// sum[i]表示nums中值为i的元素相加的结果
for(int i = 0; i < n; i++) {
sum[nums[i]] += nums[i];
}
转移方程
这题就等价于打家劫舍问题了,相邻的数不能选,所以转移方程表示如下:
// 相邻的数不能选
dp[i] = Math.max(dp[i - 1], dp[i- 2] + sum[i]);
20.使用最小花费爬楼梯
dp定义
dp[i]表示爬到第i阶的最低花费
int[] dp = new int[n + 1];
转移方程
由于可以选择爬一个或者两个台阶,所以具体表示如下:
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
21.分汤
dp定义
dp[i][j]表示A还剩下i份B还剩下j份时所求的概率值。这里的份数是以毫升数/25得到,将四种操作量化为(4,0),(3,1),(2,2),(1,3)。
double[][] dp = new double[n + 1][n + 1];
转移方程
由于需要求概率,那么初始化比较重要:当前AB都是空时,此时返回的结果概率值为1的一半,即0.5,如果只有A为空,那么结果为1。具体如下:
dp[0][0] = 1.0 / 2.0;
for(int j = 1; j <= n; j++) {
dp[0][j] = 1.0;
}
很明显这题的转移无非就是四种操作的选择,因为要求概率所以需要*025。具体如下:
// 对应四种操作
dp[i][j] = 0.25 * (dp[Math.max(i - 4, 0)][j] + dp[Math.max(i - 3, 0)][Math.max(j - 1, 0)] +
dp[Math.max(i - 2, 0)][Math.max(j - 2, 0)] + dp[Math.max(i - 1, 0)][Math.max(j - 3, 0)]);
由于如果汤的剩余量不足以完成某次操作,我们将尽可能分配,所以上面代码中需要将剩余量与0取最大值。
新21点
dp定义
dp[i]表示从得分i开始游戏并获胜的概率,所以最后结果返回的是dp[0]。
double[] dp = new double[k + maxPts];
// 理论上可以抽取的最大值是k-1+maxPts
转移方程
很明显从 [1, maxPts]选择,且概率相同,那么状态转移就是基于maxPts种选择得到,由于求的是概率,需要除maxPts。具体表示如下:
for(int choice = 1; choice <= maxPts; choice++) {
dp[i] += dp[i + choice] / maxPts;
}
最佳观光组合
dp定义
dp直接记录最大的values[i] + i,不需要用数组装载,初始化为values[0] + 0。
dp = values[0] + 0
转移方程
对于i∈[1,j),使得dp来记录最大values[i] + i,用于和values[j] - j相加得到最大的结果maxRes。具体表示如下
// 加上最大的values[j] - j
maxRes = Math.max(maxRes, dp + values[i] - i);
// 更新得到最大的 values[i] + i;
dp = Math.max(dp, values[i] + i);