《代码随想录(Carl)》 数据结构与算法 题目解法关键知识点记录(四)

《代码随想录(Carl)》 数据结构与算法

二算法类题目

2.1 动态规划

23.198. 打家劫舍【初始化:递推公式的基础就是dp[0] 和 dp[1],所以两个都要初始化;从dp[i]的定义上来讲,dp[0] ⼀定是 nums[0],dp[1]就是nums[0]和nums[1]的最⼤值即:dp[1] =max(nums[0], nums[1]);】
24.213. 打家劫舍 II【数组首尾相连成环,分两种情况-不考虑第一间房或最后一间房执行[198. 打家劫舍]解法。】
25.337. 打家劫舍 III【树形DP[二叉树+动态规划,在树的节点上进行递推公式的状态转移],递归三部曲+动规五部曲:
1确定递归函数的参数和返回值:
我们要求⼀个节点被偷与不偷的两个状态所得到的最大⾦钱,那么返回值就是⼀个⻓度为2的数组。
2.1定义dp数组和下标的含义
-dp[2]数组:大小为2的一维数组,下标为0记录不偷该节点所得到的最⼤⾦钱,下标为1记录偷该节点所得到的最⼤⾦钱。
2确定递归终止条件:
如果遇到空节点(nullptr),⽆论偷还是不偷该节点所得到的最大金钱都是0,所以返回vector{0, 0}
2.3dp数组初始化
dp[2]数组初始化为vector{0, 0}
3确定单层递归的逻辑
2.4确定dp数组遍历顺序(二叉树的遍历顺序,后序遍历)
2.2确定dp数组递推公式
// 不偷cur
int val0 = max(left[0], left[1]) + max(right[0], right[1]);
// 偷cur
int val1 = cur->val + left[0] + right[0];
2.5举例推导dp数组
4最后头结点就是 取下标0 和 下标1的最⼤值就是偷得的最⼤⾦钱。】
26.121. 买卖股票的最佳时机【
股票全程只能买卖⼀次,只能选择 某⼀天 买⼊这只股票,并选择在 未来的某⼀个不同的⽇⼦ 卖出该股票。
二维dp数组,分持有和不持有股票两种状态,且不持有状态依赖持有状态
1定义dp数组和下标的含义
dp[i][0] 表示第i天持有股票所得最多现⾦ ;
dp[i][1] 表示第i天不持有股票所得最多现⾦;
2递推公式:
1如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来:
a第i-1天就持有股票,那么就保持现状,所得现⾦就是昨天持有股票的所得现⾦ 即:dp[i - 1][0]
b第i天买⼊股票,所得现⾦就是买⼊今天的股票后所得现⾦ 即:-prices[i]
那么dp[i][0]应该选所得现⾦最⼤的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);
2如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来:
a第i-1天就不持有股票,那么就保持现状,所得现⾦就是昨天不持有股票的所得现⾦ 即:dp[i - 1][1]
b第i天卖出股票,所得现⾦就是按照今天股票价格卖出后所得现⾦ 即:dp[i - 1][0] + prices[i]
同样dp[i][1]取最⼤的,dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
3 dp数组初始化
从递推公式可知,dp[i][0]和dp[i][1]都是从dp[0][0]和dp[0][1]推导出来。 那么dp[0][0]表示第0天持有股票,此时的持有股票就⼀定是买⼊股票了,因为不可能由前⼀天推导出来,所以dp[0][0] -= prices[0];
dp[0][1]表示第0天不持有股票,不持有股票那么现⾦就是0,所以dp[0][1] = 0;】
27.122. 买卖股票的最佳时机 II【[121. 买卖股票的最佳时机]解法增加股票卖出再买入时重新开始计算收益和最后卖出股票时把收益加上的情况;
Carl与[121. 买卖股票的最佳时机] 的唯⼀区别:本题股票可以买卖多次了(注意只有⼀只股票,所以再次购买前要出售掉之前的股票);
在动规五部曲中,这个区别主要是体现在递推公式上:1如果第i天持有股票dp[i][0],b当第i天买⼊股票,所得现⾦就是昨天不持有股票的所得现⾦ 减去 今天的股票价格 即:dp[i - 1][1] - prices[i],那么dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i])。因为⼀只股票可以买卖多次,所以当第i天买⼊股票的时候,所持有的现⾦可能有之前买卖过的利润。】
28.123. 买卖股票的最佳时机 III-困难【
本题最多可以完成 两笔 股票买卖交易(不能同时参与多笔交易,必须在再次购买前出售掉之前的股票)
关键在于⾄多买卖两次,这意味着可以买卖⼀次,可以买卖两次,也可以不买卖。
1定义dp数组和下标的含义
⼀天⼀共就有五个状态,
0. 没有操作
1.第⼀次买⼊
2. 第⼀次卖出
3. 第⼆次买⼊
4. 第⼆次卖出
dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所得最多现⾦。
2递推公式:
0 dp[i][0]没有操作
第i天没有操作,即:dp[i][0] = 0
1 dp[i][1]表示第i天第一次买⼊股票的状态,并不是说⼀定要第i天买⼊股票。
dp[i][1]如果第i天第⼀次买⼊股票,那么可以由两个状态推出来:
a第i天没有操作,那么就保持现状,所得现⾦就是昨天持有股票的所得现⾦,即:dp[i][1] = dp[i - 1][1]
b第i天买⼊股票,所得现⾦就是昨天状态0没有操作状态下的所得现⾦ 减去 今天的股票价格,那么dp[i][1] = dp[i-1][0] - prices[i]
那么dp[i][1]应该选所得现⾦最⼤的,所以 dp[i][1] = max(dp[i - 1][1], dp[i-1][0] - prices[i]);
[昨天状态0没有操作,即dp[i-1][0] = 0 => 所得现⾦就是买⼊今天的股票后所得现⾦即:dp[i][1] = -prices[i];dp[i][1] = max(dp[i - 1][1], - prices[i]);]
2 dp[i][2] 如果第i天第⼀次卖出股票, 那么可以由两个状态推出来:
a第i天没有操作,那么就保持现状,所得现⾦就是昨天卖出股票的所得现⾦,即:dp[i][2] = dp[i - 1][2]
b第i天卖出股票,所得现⾦就是按照今天股票价格卖出后所得现⾦,dp[i][2] = dp[i - 1][1] + prices[i]
那么dp[i][2]应该选所得现⾦最⼤的,所以dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i])
3 dp[i][3]如果第i天第二次买⼊股票,那么可以由两个状态推出来:
a第i天没有操作,那么就保持现状,所得现⾦就是昨天持有股票的所得现⾦,即:dp[i][3] = dp[i - 1][3]
b第i天买⼊股票,所得现⾦就是昨天第一次卖出股票状态下的所得现⾦ 减去 今天的股票价格,那么dp[i][3] = dp[i-1][2] - prices[i]
那么dp[i][3]应该选所得现⾦最⼤的,所以dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
同理可推出剩下状态部分:
4 dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);

3 dp数组初始化
第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;
第0天做第⼀次买⼊的操作,dp[0][1] = -prices[0];
第0天做第⼀次卖出的操作,这个初始值应该是多少呢?
⾸先卖出的操作⼀定是收获利润,整个股票买卖最差情况也就是没有盈利即全程⽆操作现⾦为0,从递推公式中可以看出每次是取最⼤值,那么既然是收获利润如果⽐0还⼩了就没有必要收获这个利润 了。
所以dp[0][2] = 0;
第0天第⼆次买⼊操作,初始值应该是多少呢?
不⽤管第⼏次,现在⼿头上没有现⾦,只要买⼊,现⾦就做相应的减少。 所以第⼆次买⼊操作,初始化为:dp[0][3] = -prices[0];
同理第⼆次卖出初始化dp[0][4] = 0;】
29.188. 买卖股票的最佳时机 IV【[123. 买卖股票的最佳时机 III]解法的扩展,最⼤的区别就是这⾥要类⽐j为奇数是买,偶数是卖时的状态-所得最多现⾦。
1定义dp数组和下标的含义
⼀天⼀共就有2 * k + 1个状态,
0. 没有操作
1.第⼀次买⼊
2. 第⼀次卖出
3. 第⼆次买⼊
4. 第⼆次卖出
。。。
2 * k – 1. 第k次买⼊
2 * k. 第k次卖出
2递推公式:
dp[i][0] = dp[i - 1][0]; // 第i天没有操作,继续沿⽤前⼀天的状态
for (int j = 1; j <= 2 * k; j++)
{
if ((j & 1) == 1)
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]); // dp[i][j]表示第i天第j次买⼊股票所得最多现⾦
} else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]); // dp[i][j]表示第i天第j次卖出股票所得最多现⾦
}
}
3dp数组初始化
for (int i = 0; i <= 2 * k; i++)
{
if ((i & 1) == 0)
{
dp[0][i] = 0; // 第0天无操作或第k次卖出股票所得最多现⾦
} else {
dp[0][i] = -prices[0]; // 第0天第k次买入股票所得最多现⾦
}
}】
30.309. 最佳买卖股票时机含冷冻期-中等【在[122.买卖股票的最佳时机II]多次买卖的基础上增加一个不持有股票后冷冻期的状态,即不持有股票所得最多现⾦ 整体向后平移一天;
Carl: 细分为四个状态,其状态转移也⼗分清晰;
1定义dp数组和下标的含义
⼀天具体可以区分出如下四个状态:
0.状态⼀:买⼊股票状态(之前就买⼊了股票然后没有操作,或者是今天买⼊股票)
卖出股票状态,这⾥就有两种卖出股票状态
1.状态⼆:两天前就卖出了股票,度过了冷冻期,⼀直没操作,今天保持卖出股票状态
2.状态三:今天卖出了股票
3.状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有⼀天!
dp[i][j]中 i表示第i天,j为 [0 - 3] 四个状态,dp[i][j]表示第i天状态j所得最多现⾦。
2递推公式:
0.达到买⼊股票状态(状态⼀)即:dp[i][0],有两个具体操作:
操作⼀:前⼀天就是持有股票状态(状态⼀),dp[i][0] = dp[i - 1][0]
操作⼆:今天买⼊了,有两种情况
前⼀天是冷冻期(状态四),dp[i - 1][3] - prices[i]
前⼀天是保持卖出股票状态(状态⼆),dp[i - 1][1] - prices[i]
所以操作⼆取最⼤值,即:max(dp[i - 1][3], dp[i - 1][1]) - prices[i]
那么dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
1.达到保持卖出股票状态(状态⼆)即:dp[i][1],有两个具体操作:
操作⼀:前⼀天就是状态⼆
操作⼆:前⼀天是冷冻期(状态四)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
2.达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有⼀个操作:
操作⼀:昨天⼀定是买⼊股票状态(状态⼀),今天卖出
即:dp[i][2] = dp[i - 1][0] + prices[i];
3.达到冷冻期状态(状态四),即:dp[i][3],只有⼀个操作:
操作⼀:昨天卖出了股票(状态三)
dp[i][3] = dp[i - 1][2];
3 dp数组初始化
第0天初始化。
如果是持有股票状态(状态⼀)那么:dp[0][0] = -prices[0],买⼊股票所得现⾦为负数。
保持卖出股票状态(状态⼆),第0天没有卖出dp[0][1]初始化为0就⾏,
今天卖出了股票(状态三),同样dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。
同理dp[0][3]也初始为0。】
31.714. 买卖股票的最佳时机含手续费【相对于[122.买卖股票的最佳时机II]的唯一区别:本题只需要在计算卖出股票操作的时候减去⼿续费就可以;
2递推公式:
2如果第i天不持有股票即dp[i][1]的情况,b第i天卖出股票,所得现⾦就是按照今天股票价格卖出后所得现⾦,注意这⾥需要多⼀个减去⼿续费的操作。即:
dp[i - 1][0] + prices[i] – fee。所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);】
32.股票问题总结篇
1第一次出现多个dp数组在一个循环中求解,且dp数组之间还相互依赖;【121,122】
2将求解的问题抽象成几个状态的dp数组,然后根据状态之间的先后关系确定递推公式;【121,123,309】
3增加的额外条件要求添加到某个状态下【122,714】
33.300. 最长递增子序列【本题最关键的是要想到dp[i]由哪些状态可以推出来,并取最大值。那么很自然就能想到递推公式:位置i的最长严格递增子序列的长度等于j从0到i-1各个位置的最长严格递增子序列的长度 + 1 的最大值。所以:
for (int j = 0; j < i; j++)
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
本题中,正确定义dp数组的含义十分重要。
dp[i]表示i之前包括i的以nums[i]结尾的最长严格递增子序列(可以不连续)的长度。】
34.674. 最长连续递增子序列【自己定义dp:以下标i为结尾的数组的最大连续递增的⼦序列⻓度为dp[i]。
Carl本题相对于[300.最⻓递增⼦序列]最⼤的区别在于“连续”。
1 dp[i]:以下标i为结尾的数组的连续递增的⼦序列⻓度为dp[i]。
2递推公式
如果 nums[i] > nums[i - 1],那么以 i为结尾的数组的连续递增的⼦序列⻓度 ⼀定等于 以i-1为结尾的
数组的连续递增的⼦序列⻓度 + 1 。即:dp[i] = dp[i - 1] + 1;
if (nums[i] > nums[i-1])
dp[i] = dp[i-1] + 1;
因为本题要求连续递增⼦序列,所以就必要⽐较nums[i]与nums[i-1],⽽不⽤去⽐较nums[i]与
nums[j] (j是在0到i-1之间遍历)。那么也不⽤两层for循环,本题⼀层for循环就⾏。
要联动起来,才能理解递增⼦序列怎么求,递增连续⼦序列⼜要怎么求。
概括来说:不连续递增⼦序列的跟前0-i个状态有关,连续递增的⼦序列只跟前⼀个状态有关。】
35.718. 最⻓重复⼦数组【本题其实是动规解决的经典题目,我们只要想到 用二维数组可以记录两个字符串的所有比较情况,这样就比较好推 递推公式了。
1定义dp数组和下标的含义
dp[i][j]:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最⻓重复⼦数组(连续子系列 )⻓度为dp[i][j]。
特别注意: “以下标i - 1为结尾的A” 表明一定是 以A[i-1]为结尾的字符串。
其实dp[i][j]的定义也就决定着,我们在遍历dp[i][j]的时候i 和 j都要从1开始。
2. 确定递推公式
根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始!
3. dp数组如何初始化
根据dp[i][j]的定义,dp[i][0]和dp[0][j]其实都是没有意义的!
但dp[i][0] 和dp[0][j]要初始化,因为 为了⽅便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;
所以dp[i][0] 和dp[0][j]初始化为0。
4. 确定遍历顺序
外层for循环遍历A,内层for循环遍历B。外层for循环遍历B,内层for循环遍历A-是⼀样的。
同时题⽬要求⻓度最⻓的⼦数组的⻓度。所以在遍历的时候顺便把dp[i][j]的最⼤值记录下来。
暴力解法TO(n3)+SO(n)=>TO(n2)+SO(n^2)】
36.1143. 最长公共子序列【自己代码: TO(n2)+SO(n2)实现时间复杂度优化TO(n4)+SO(n2),dp[i][j]保存更新i-1和j-1之前的最大值 代替 2层for循环搜索i-1和j-1之前的最大值。与[300. 最长递增子序列]因为是要保持严格递增,j从0到i-1各个位置的最长严格递增子序列的长度不同,也无法继承,因为不知道当前i会用到哪一个j。
Carl
2. 确定递推公式
如果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]);
dp[text1.size()][text2.size()]为最终结果。】
37.1035. 不相交的线【直线不能相交,这就是说明在字符串A中找到⼀个与字符串B相同的⼦序列,且这个⼦序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。本题说是求绘制的最⼤连线数,其实就是求两个字符串的最⻓公共⼦序列的⻓度!与[1143. 最长公共子序列]解法相同。】
38.53. 最大子数组和【自己代码完全从数学计算结果推导的递推公式;
Carl按照dp思路
2. 确定递推公式
dp[i]只有两个⽅向可以推出来:
dp[i - 1] + nums[i],即:nums[i]加⼊当前连续⼦序列,和 nums[i],即:从头开始计算当前连续⼦序列和,⼀定是取最⼤的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);】
39.392. 判断子序列【自己dp解法:当文本串t为空,模式串s(连续)一定不是t的子序列;模式串s为空,模式串s一定是t的子序列;与[1143. 最长公共子序列]解法相同。TO(n2)+SO(n2)。
大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,依次检查它们是否为 T 的子序列。
双指针法-TO(n)+SO(1)
因为子序列顺序没变,逐次比较s和t中字符是否相等。定义指针i指向文本串t,指针j指向模式串s,当if (s[j] == t[i])时,++j;继续比较下一对字符,最后当模式串s的字符全部比较完if (j == size1)时,确定s 为 t 的子序列。】
40.115. 不同的子序列【自己dp解法:C++测试⽤例存在使得递推公式dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1];两个数相加超过int的数据,所以需要在if判断条件⾥加上限制条件if (t[i] == s[j - 1] && (j - 1 >= i) && (dp[i][j - 1] <= INT_MAX - dp[i - 1][j - 1]) )。结合递推公式和初始化两个步骤尝试出最后结果:
1.定义dp数组和下标的含义
dp[i][j]:s中以下标j - 1为结尾的子序列中出现 t中以下标i为结尾的字符串(连续)出现的个数。
2. 确定递推公式和dp数组初始化
int size1 = t.size(), size2 = s.size(); // t模式串,s文本串
vector dp(size1, vector(size2 + 1, 0) ); // t模式串固定行i,s文本串可变列j
for (int j = 1; j <= size2; j++) // dp[0][j]初始化,t的第一个字符与s中字符匹配
{
if (t[0] == s[j - 1])
{
dp[0][j] = dp[0][j - 1] + 1;
} else {
dp[0][j] = dp[0][j - 1];
}
}

for (int i = 1; i < size1; i++)
{
for (int j = 1; j <= size2; j++)
{
if (t[i] == s[j - 1] && (j - 1 >= i) && (dp[i][j - 1] <= INT_MAX - dp[i - 1][j - 1]) )
{
dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1]; // 当前字符相等时,用s[j - 1]匹配的个数(上一个字符的个数)+不用s[j - 1]匹配的个数
} else {
dp[i][j] = dp[i][j - 1]; // 当前字符不相等时,不用s[j - 1]匹配的个数,继承前一列
}
// cout << “dp[” << i << “][” << j << "]= " << dp[i][j] << ’ ';
}
// cout << endl;
}
Carl
1.确定dp数组(dp table)以及下标的含义
dp[i][j]:以i-1为结尾的s⼦序列中出现以j-1为结尾的t的个数为dp[i][j]。s文本可变行i,t模式固定j
2. 确定递推公式
这⼀类问题,基本是要分析两种情况
s[i - 1] 与 t[j - 1]相等
s[i - 1] 与 t[j - 1] 不相等
2.1当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。
⼀部分是⽤s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。
⼀部分是不⽤s[i - 1]来匹配,个数为dp[i - 1][j]。s是可变的,t固定不变。
这⾥可能有同学不明⽩了,为什么还要考虑 不⽤s[i - 1]来匹配,都相同了指定要匹配啊。
例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,当⽤s[3]来匹配,即:s[0]s[1]s[3]组成的bag;但是字符串s也可以不⽤s[3]来匹配,即⽤ s[0]s[1]s[2]组成的bag。
所以当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
2.2当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有⼀部分组成,不⽤s[i - 1]来匹配,即:dp[i - 1][j]
所以递推公式为:dp[i][j] = dp[i - 1][j]; // s可变,继承上一行
3. dp数组初始化
从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][0] 和dp[0][j]是⼀定要初始化的。
每次当初始化的时候,都要回顾⼀下dp[i][j]的定义,不要凭感觉初始化。
3.1dp[i][0]表示什么呢?
dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。
那么dp[i][0]⼀定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。 3.2dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。
那么dp[0][j]⼀定都是0,s如论如何也变成不了t。
最后就要看⼀个特殊位置了,即:dp[0][0] 应该是多少。
3.3dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。】
41.583. 两个字符串的删除操作【自己代码与[1143. 最长公共子序列]解法相同。两个字符串分别减去最长公共子序列的长度就是所需的最小删除步数。
Carl[115. 不同的子序列]两个字符串都可以删除字符。延续[115. 不同的子序列]思路,两个字符串相等的最小删除次数,递推公式分相等和不相等两个情况,
1.确定dp数组(dp table)以及下标的含义
dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等(连续),所需要删除元素 的最少次数。
2. 确定递推公式
这⼀类问题,基本是要分析两种情况
当word1[i - 1] 与 word2[j - 1]相同的时候
当word1[i - 1] 与 word2[j - 1]不相同的时候
2.1当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];
2.2当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:
情况⼀:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1
情况⼆:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2 那最后当然是取最⼩值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
3. dp数组如何初始化
从递推公式中,可以看出来,dp[i][0] 和 dp[0][j]是⼀定要初始化的。
dp[i][0]:word2为空字符串,以i-1为结尾的字符串word2要删除多少个元素,才能和word1相同呢,很 明显dp[i][0] = i。dp[0][j]的话同理。
动规的题目主要是第一步定义dp数组(dp table)以及下标的含义和第二步确定递推公式。】
42.72. 编辑距离【自己代码:[583. 两个字符串的删除操作]解法思路相似:
1.确定dp数组(dp table)以及下标的含义
dp[i][j]:以i-1为结尾的字符串word1 转换成 以j-1位结尾的字符串word2(连续),所需要插入,删除和替换一个字符的最少操作次数。
2.2当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:
情况⼀:不考虑word1[i - 1]时,用插入或删除操作,最少操作次数为dp[i - 1][j] + 1
情况⼆:不考虑word2[j - 1]时,用插入或删除操作,最少操作次数为dp[i][j - 1] + 1
情况三:同时不考虑word1[i - 1]和word2[j - 1],再上次匹配的基础上采用替换的操作时,操作的最少次数为dp[i - 1][j - 1] + 1
那最后当然是取最⼩值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 1, dp[i - 1][j] + 1, dp[i][j - 1] + 1});】
43.647. 回文子串【自己代码:两层for循环,找到相等字符作为区间起始位置和终⽌位置,向内扩判断对称从而判断这个区间是否为回文子串(连续),暴力解法TO(n^3)。
动态规划解法
自己思路:与Carl类似,都是按照回文子串的定义从两个方向确定一个范围内的回文子串个数;不同之处是自己延续之前思维定势,原字符串以i-1下标为结尾的从0开始的子串,和反向原字符串以j-1下标为结尾的从0开始的子串,两者之间的回文子串个数,这样就出现了重复。
Carl思路类似暴力解法,两层for循环遍历原字符串,两个位置字符是否相等,相等时判断以该相等字符作为起始终⽌的区间是否为回文子串,尤其是区间长度大于2时,取决于起始终止向内各扩一位的内部更小的区间是否为回文子串,就是这一关键的动规递推公式降低暴力解法的时间复杂度为TO(n^2)。
1.确定dp数组(dp table)以及下标的含义
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的⼦串是否是回⽂⼦串,如果是dp[i][j]为
true,否则为false。
2. 确定递推公式
整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。
当s[i]与s[j]相等时,这就复杂⼀些了,有如下三种情况
情况⼀:下标i 与 j相同,同⼀个字符例如a,当然是回⽂⼦串
情况⼆:下标i 与 j相差为1,例如aa,也是⽂⼦串
情况三:下标:i 与 j相差⼤于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是 不是回⽂⼦串就看aba是不是回⽂就可以了,那么aba的区间就是 i+1 与j-1区间,这个区间是不是
回⽂就看dp[i + 1][j - 1]是否为true。
4. 确定遍历顺序
⾸先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进⾏赋值true的。
dp[i + 1][j - 1] 在 dp[i][j]的左下⻆,所以⼀定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
双指针法优化动规的空间复杂度
自己思路:采用一维dp数组递推公式确定回文子串以及个数的思路,没有二维dp数组记录所有匹配的情况,所以总有不适用的情况。
Carl思路类似暴力解法,一层for循环遍历,利用双指针法以当前字符为中心,和 以当前字符和下一个字符两个字符为中心向外扩判断是否对称,即是否为回文子串,是回文子串时个数增加1;少用了一层for循环确定终止位置,直接用起始位置作为中心点向外侧两边扩,降低了暴力解法的时间复杂度TO(n^2),没用dp数组保存求解出来的小问题最优解,降低了动规解法的空间复杂度SO(1)。
⾸先确定回⽂串,就是找中⼼然后想两边扩散看是不是对称的就可以了。
在遍历中⼼点的时候,要注意中⼼点有两种情况。
⼀个元素可以作为中⼼点,两个元素也可以作为中⼼点。
result += extend(s, i, i, s.size()); // 以i为中⼼
result += extend(s, i, i + 1, s.size()); // 以i和i+1为中⼼】
44.516. 最长回文子序列【自己代码:[647. 回文子串] 思路改进,dp数组表示回文子序列长度;
1dp[i][j]:表示区间范围[i,j](注意是左闭右闭)的⼦串中回⽂⼦序列最大的长度。
2 if (s[i] == s[j - 1]) // 相等时
{
if ( (j - i >= 1) && (j - i <= 2) ) {
dp[i][j] = dp[i][j - 1] + 1; // 同一个字符或相邻两个字符,回⽂⼦串的长度加1
} else {
dp[i][j] = dp[i + 1][j - 1] + 2; // 删除s[i]和s[j - 1]的子串的回⽂⼦串的长度加2
}
} else { // 不相等时
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); // 删除s[i]或s[j - 1]的子串的回⽂⼦串的长度最大值
}
3由于递推公式中有dp[i][j] = dp[i][j - 1] + 1;,且只有当dp[size - 2]行时才会出现s[i] != s[j - 1]的情况,即需要用到dp[i + 1][j],所以初始化dp数组为dp[size][size + 1]。
vector dp(size, vector(size + 1, 0) );
Carl 动态规划
3⾸先要考虑当i 和j 相同的情况。当i与j相同,那么dp[i][j]⼀定是等于1的,即:⼀个字符的回⽂⼦序列⻓度就 是1。所以需要⼿动初始化⼀下。
for (int i = 0; i < size; i++)
dp[i][i] = 1;
4同时,设置第二层循环的起始位置为j = i + 1,for (int j = i + 1; j < s.size(); j++),则递推公式计算不到i = j的情况。
2if (s[i] == s[j]) // 相等时,由于不计算i=j的dp,所以简化了计算
{
dp[i][j] = dp[i + 1][j - 1] + 2; // 删除s[i]和s[j - 1]的子串的回⽂⼦序列的长度加2
} else { // 不相等时
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); // 删除s[i]或s[j - 1]的子串的回⽂⼦序列的长度最大值
}

双指针法优化动规的空间复杂度
双指针+递归计算回文子序列最长长度,时间复杂度?测试用例大数据时计算超时,可能存在大量重复计算的情况;】

感谢Carl对于LeetCode中的数据结构与算法题目的系统讲解工作,可查看网页 linkhttps://programmercarl.com
和 linkhttps://github.com/youngyangyang04/leetcode-master 中的讲解文档。
按照类型从基础到提高循序渐进的过程选择经典题目高频面试题来讲解,大大提高了学习数据结构与算法知识和刷题效率,解决了浪费的时间主要三个问题点:1找题;2找到了不应该现阶段做的题;3没有全套的优质题解可以参考。
按照如下类型来练习:数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心->动态规划->图论->高级数据结构,从简单刷起,做了几个类型题目之后,再慢慢做中等题目、困难题目。
经过系统的知识学习和技能练习现对整个知识体系的掌握有了一个质的飞跃。

你可能感兴趣的:(数据结构与算法,动态规划,算法,数据结构)