This commit is contained in:
programmercarl
2022-12-20 15:33:57 +08:00
parent 5fc31873aa
commit 77f1c528b7
15 changed files with 290 additions and 292 deletions

View File

@ -121,6 +121,30 @@ public:
```
## 补充
二级剪枝的部分:
```C++
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
```
可以优化为:
```C++
if (nums[k] + nums[i] > target && nums[i] >= 0) {
break;
}
```
因为只要 nums[k] + nums[i] > target那么 nums[i] 后面的数都是正数的话,就一定 不符合条件了。
不过这种剪枝 其实有点 小绕,大家能够理解 文章给的完整代码的剪枝 就够了。

View File

@ -46,8 +46,8 @@ public:
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$
* 时间复杂度O(n^2)
* 空间复杂度O(1)
当然该方法超时了。
@ -72,8 +72,8 @@ public:
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$
* 时间复杂度O(n)
* 空间复杂度O(1)
### 动态规划
@ -157,8 +157,8 @@ public:
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$
* 时间复杂度O(n)
* 空间复杂度O(n)
从递推公式可以看出dp[i]只是依赖于dp[i - 1]的状态。
@ -187,8 +187,8 @@ public:
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$
* 时间复杂度O(n)
* 空间复杂度O(1)
这里能写出版本一就可以了版本二虽然原理都一样但是想直接写出版本二还是有点麻烦容易自己给自己找bug。

View File

@ -52,17 +52,20 @@
一天一共就有五个状态,
0. 没有操作
1. 第一次买入
2. 第一次卖出
3. 第二次买入
4. 第二次卖出
0. 没有操作 (其实我们也可以不设置这个状态)
1. 第一次持有股票
2. 第一次不持有股票
3. 第二次持有股票
4. 第二次不持有股票
dp[i][j]中 i表示第i天j为 [0 - 4] 五个状态dp[i][j]表示第i天状态j所剩最大现金。
需要注意dp[i][1]**表示的是第i天买入股票的状态并不是说一定要第i天买入股票这是很多同学容易陷入的误区**。
例如 dp[i][1] ,并不是说 第i点一定买入股票有可能 第 i-1天 就买入了,那么 dp[i][1] 延续买入股票的这个状态。
2. 确定递推公式
需要注意dp[i][1]**表示的是第i天买入股票的状态并不是说一定要第i天买入股票这是很多同学容易陷入的误区**。
达到dp[i][1]状态,有两个具体操作:
@ -95,11 +98,7 @@ dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
第0天做第一次卖出的操作这个初始值应该是多少呢
首先卖出的操作一定是收获利润整个股票买卖最差情况也就是没有盈利即全程无操作现金为0
从递推公式中可以看出每次是取最大值那么既然是收获利润如果比0还小了就没有必要收获这个利润了。
所以dp[0][2] = 0;
此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入当天卖出所以dp[0][2] = 0;
第0天第二次买入操作初始值应该是多少呢应该不少同学疑惑第一次还没买入呢怎么初始化第二次买入呢
@ -188,6 +187,32 @@ dp[1] = max(dp[1], dp[0] - prices[i]); 如果dp[1]取dp[1],即保持买入股
对于本题,把版本一的写法研究明白,足以!
## 拓展
其实我们可以不设置0. 没有操作’ 这个状态因为没有操作手上的现金自然就是0 正如我们在 [121.买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) 和 [122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html) 也没有设置这一状态是一样的。
代码如下:
``` CPP
// 版本三
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() == 0) return 0;
vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for (int i = 1; i < prices.size(); i++) {
dp[i][1] = max(dp[i - 1][1], 0 - prices[i]);
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[prices.size() - 1][4];
}
};
```
## 其他语言版本
Java:

View File

@ -99,17 +99,15 @@ for (int j = 0; j < 2 * k - 1; j += 2) {
第0天做第一次卖出的操作这个初始值应该是多少呢
首先卖出的操作一定是收获利润整个股票买卖最差情况也就是没有盈利即全程无操作现金为0
此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入当天卖出所以dp[0][2] = 0;
从递推公式中可以看出每次是取最大值那么既然是收获利润如果比0还小了就没有必要收获这个利润了。
第0天第二次买入操作初始值应该是多少呢应该不少同学疑惑第一次还没买入呢怎么初始化第二次买入呢
所以dp[0][2] = 0;
第二次买入依赖于第一次卖出的状态其实相当于第0天第一次买入了第一次卖出了然后在买入一次第二次买入那么现在手头上没有现金只要买入现金就做相应的减少。
第0天第二次买入操作,初始值应该是多少呢?
所以第二次买入操作,初始化为dp[0][3] = -prices[0];
不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少。
第二次买入操作初始化为dp[0][3] = -prices[0];
第二次卖出初始化dp[0][4] = 0;
**所以同理可以推出dp[0][j]当j为奇数的时候都初始化为 -prices[0]**

View File

@ -84,9 +84,9 @@ public:
很多录友可能有疑惑,为什么 以上代码中的dfs函数没有终止条件呢 感觉递归没有终止很危险。
其实终止条件 就写在了调用dfs的地方如果遇到不合法的方向直接不会去调用dfs。
其实终止条件 就写在了 调用dfs的地方如果遇到不合法的方向直接不会去调用dfs。
当然也可以这么写:
当然也可以这么写:
```CPP
// 版本二
@ -122,7 +122,7 @@ public:
};
```
这里大家应该能看出区别了,无疑就是版本一中 调用dfs 的条件放在了 版本二 的 终止条件位置上。
这里大家应该能看出区别了,无疑就是版本一中 调用dfs 的条件判断 放在了 版本二 的 终止条件位置上。
**版本一的写法**是 下一个节点是否能合法已经判断完了只要调用dfs就是可以合法的节点。
@ -137,6 +137,9 @@ public:
其实本题是 dfsbfs 模板题,但正是因为是模板题,所以大家或者一些题解把重要的细节都很忽略了,我这里把大家没注意的但以后会踩的坑 都给列出来了。
本篇我只给出的dfs的写法大家发现我写的还是比较细的那么后面我再单独更本题的bfs写法虽然是模板题但依然有很多注意的点敬请期待

View File

@ -35,6 +35,11 @@
* 所有节点的值都是唯一的。
* p、q 为不同节点且均存在于给定的二叉搜索树中。
# 视频讲解
**《代码随想录》算法视频公开课:[二叉搜索树找祖先就有点不一样了!| 235. 二叉搜索树的最近公共祖先](https://www.bilibili.com/video/BV1Zt4y1F7ww),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
# 思路

View File

@ -14,17 +14,17 @@
示例 1
输入nums = [10,9,2,5,3,7,101,18]
输出4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
* 输入nums = [10,9,2,5,3,7,101,18]
* 输出4
* 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2
输入nums = [0,1,0,3,2,3]
输出4
* 输入nums = [0,1,0,3,2,3]
* 输出4
示例 3
输入nums = [7,7,7,7,7,7,7]
输出1
* 输入nums = [7,7,7,7,7,7,7]
* 输出1
提示:
@ -33,11 +33,21 @@
## 思路
最长上升子序列是动规的经典题目这里dp[i]是可以根据dp[j] j < i推导出来的那么依然用动规五部曲来分析详细一波
首先通过本题大家要明确什么是子序列,“子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”。
本题也是代码随想录中子序列问题的第一题,如果没接触过这种题目的话,本题还是很难的,甚至想暴力去搜索也不知道怎么搜。
子序列问题是动态规划解决的经典问题当前下标i的递增子序列长度其实和i之前的下表j的子序列长度有关系那那又是什么样的关系呢。
接下来,我们依然用动规五部曲来分析详细一波:
1. dp[i]的定义
**dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度**
本题中正确定义dp数组的含义十分重要。
**dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度**
为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为我们在 做 递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小那么两个递增子序列一定分别以nums[j]为结尾 和 nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么 如果算递增呢。
2. 状态转移方程
@ -49,13 +59,15 @@
3. dp[i]的初始化
每一个i对应的dp[i](即最长上升子序列起始大小至少都是1.
每一个i对应的dp[i](即最长递增子序列起始大小至少都是1.
4. 确定遍历顺序
dp[i] 是有0到i-1各个位置的最长升序子序列 推导而来那么遍历i一定是从前向后遍历。
dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来那么遍历i一定是从前向后遍历。
j其实就是0到i-1遍历i的循环在外层遍历j则在内层代码如下
j其实就是遍历0到i-1那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。
遍历i的循环在外层遍历j则在内层代码如下
```CPP
for (int i = 1; i < nums.size(); i++) {

View File

@ -16,9 +16,9 @@
* 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
* 输入: [1,2,3,0,2]
* 输出: 3
* 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
## 思路
@ -36,12 +36,13 @@
dp[i][j]第i天状态为j所剩的最多现金为dp[i][j]。
**其实本题很多同学搞的比较懵,是因为出现冷冻期之后,状态其实是比较复杂度**,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。
具体可以区分出如下四个状态:
* 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
* 卖出股票状态,这里就有两种卖出股票状态
* 状态二:两天前就卖出了股票,度过冷冻期,一直没操作,今天保持卖出股票状态
* 状态三:今天卖出股票
* 状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有
* 不持有股票状态,这里就有两种卖出股票状态
* 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
* 状态三:今天卖出股票
* 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
![](https://img-blog.csdnimg.cn/518d5baaf33f4b2698064f8efb42edbf.png)
@ -57,38 +58,48 @@ j的状态为
从代码上来看确实可以合并,但从逻辑上分析合并之后就很难理解了,所以我下面的讲解是按照这四个状态来的,把每一个状态分析清楚。
**注意这里的每一个状态,例如状态一,是买入股票状态并不是说今天已经就买入股票,而是说保存买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态**
如果大家按照代码随想录顺序来刷的话,会发现 买卖股票最佳时机 1234 的题目讲解中
* [动态规划121.买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)
* [动态规划122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II动态规划.html)
* [动态规划123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html)
* [动态规划188.买卖股票的最佳时机IV](https://programmercarl.com/0188.买卖股票的最佳时机IV.html)
「今天卖出股票」我是没有单独列出一个状态的归类为「不持有股票的状态」,而本题为什么要单独列出「今天卖出股票」 一个状态呢?
因为本题我们有冷冻期,而冷冻期的前一天,只能是 「今天卖出股票」状态,如果是 「不持有股票状态」那么就很模糊,因为不一定是 卖出股票的操作。
如果没有按照 代码随想录 顺序去刷的录友,可能看这里的讲解 会有点困惑,建议把代码随想录本篇之前股票内容的讲解都看一下,领会一下每天 状态的设置。
**注意这里的每一个状态,例如状态一,是持有股票股票状态并不是说今天一定就买入股票,而是说保持买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态**
1. 确定递推公式
达到买入股票状态状态一dp[i][0],有两个具体操作:
**达到买入股票状态**状态一dp[i][0],有两个具体操作:
* 操作一前一天就是持有股票状态状态一dp[i][0] = dp[i - 1][0]
* 操作二:今天买入了,有两种情况
* 前一天是冷冻期状态四dp[i - 1][3] - prices[i]
* 前一天是保持卖出股票状态状态二dp[i - 1][1] - 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], dp[i - 1][3] - prices[i], 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]);
达到保持卖出股票状态状态二dp[i][1],有两个具体操作:
**达到保持卖出股票状态**状态二dp[i][1],有两个具体操作:
* 操作一:前一天就是状态二
* 操作二:前一天是冷冻期(状态四)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
达到今天就卖出股票状态状态三dp[i][2] ,只有一个操作:
**达到今天就卖出股票状态**状态三dp[i][2] ,只有一个操作:
* 操作一:昨天一定是买入股票状态(状态一),今天卖出
昨天一定是持有股票状态(状态一),今天卖出
dp[i][2] = dp[i - 1][0] + prices[i];
达到冷冻期状态状态四dp[i][3],只有一个操作:
**达到冷冻期状态**状态四dp[i][3],只有一个操作:
* 操作一:昨天卖出了股票(状态三)
昨天卖出了股票(状态三)
dp[i][3] = dp[i - 1][2];
@ -105,13 +116,13 @@ dp[i][3] = dp[i - 1][2];
这里主要讨论一下第0天如何初始化。
如果是持有股票状态状态一那么dp[0][0] = -prices[0],买入股票所剩现金为负数
如果是持有股票状态状态一那么dp[0][0] = -prices[0]一定是当天买入股票。
保持卖出股票状态(状态二),第0天没有卖出dp[0][1]初始化为0就行
保持卖出股票状态(状态二),这里其实从 「状态二」的定义来说 ,很难明确应该初始多少,这种情况我们就看递推公式需要我们给他初始成什么数值。
今天卖出了股票状态三同样dp[0][2]初始化为0因为最少收益就是0绝不会是负数
如果i为1第1天买入股票那么递归公式中需要计算 dp[i - 1][1] - prices[i] ,即 dp[0][1] - prices[1],那么大家感受一下 dp[0][1] 即第0天的状态二应该初始成多少只能初始为0。想一想如果初始为其他数值是我们第1天买入股票后 手里还剩的现金数量是不是就不对了
同理dp[0][3]也初始为0。
今天卖出了股票状态三同上分析dp[0][2]初始化为0dp[0][3]也初始为0。
4. 确定遍历顺序
@ -137,18 +148,18 @@ public:
vector<vector<int>> dp(n, vector<int>(4, 0));
dp[0][0] -= prices[0]; // 持股票
for (int i = 1; i < n; i++) {
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
}
return max(dp[n - 1][3],max(dp[n - 1][1], dp[n - 1][2]));
return max(dp[n - 1][3], dp[n - 1][1], dp[n - 1][2]);
}
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$
* 时间复杂度O(n)
* 空间复杂度O(n)
当然空间复杂度可以优化定义一个dp[2][4]大小的数组就可以了,就保存前一天的当前的状态,感兴趣的同学可以自己去写一写,思路是一样的。

View File

@ -13,15 +13,14 @@
连续递增的子序列 可以由两个下标 l 和 rl < r确定如果对于每个 l <= i < r都有 nums[i] < nums[i + 1] 那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列
示例 1
输入nums = [1,3,5,4,7]
输出3
解释最长连续递增序列是 [1,3,5], 长度为3
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的因为 5 7 在原数组里被 4 隔开
* 输入nums = [1,3,5,4,7]
* 输出3
* 解释最长连续递增序列是 [1,3,5], 长度为3尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的因为 5 7 在原数组里被 4 隔开
示例 2
输入nums = [2,2,2,2,2]
输出1
解释最长连续递增序列是 [2], 长度为1
* 输入nums = [2,2,2,2,2]
* 输出1
* 解释最长连续递增序列是 [2], 长度为1
提示
@ -41,27 +40,27 @@
1. 确定dp数组dp table以及下标的含义
**dp[i]以下标i为结尾的数组的连续递增的子序列长度为dp[i]**
**dp[i]以下标i为结尾的连续递增的子序列长度为dp[i]**
注意这里的定义一定是以下标i为结尾并不是说一定以下标0为起始位置
2. 确定递推公式
如果 nums[i + 1] > nums[i],那么以 i+1 为结尾的数组的连续递增的子序列长度 一定等于 以i为结尾的数组的连续递增的子序列长度 + 1 。
如果 nums[i] > nums[i - 1],那么以 i 为结尾的连续递增的子序列长度 一定等于 以i - 1为结尾的连续递增的子序列长度 + 1 。
dp[i + 1] = dp[i] + 1;
dp[i] = dp[i - 1] + 1;
**注意这里就体现出和[动态规划300.最长递增子序列](https://programmercarl.com/0300.最长上升子序列.html)的区别!**
因为本题要求连续递增子序列所以就必要比较nums[i + 1]与nums[i]而不用去比较nums[j]与nums[i] j是在0到i之间遍历
因为本题要求连续递增子序列所以就必要比较nums[i]与nums[i - 1]而不用去比较nums[j]与nums[i] j是在0到i之间遍历
既然不用j了那么也不用两层for循环本题一层for循环就行比较nums[i + 1] 和 nums[i]。
既然不用j了那么也不用两层for循环本题一层for循环就行比较nums[i] 和 nums[i - 1]。
这里大家要好好体会一下!
3. dp数组如何初始化
以下标i为结尾的数组的连续递增的子序列长度最少也应该是1即就是nums[i]这一个元素。
以下标i为结尾的连续递增的子序列长度最少也应该是1即就是nums[i]这一个元素。
所以dp[i]应该初始1;
@ -72,9 +71,9 @@
本文在确定递推公式的时候也说明了为什么本题只需要一层for循环代码如下
```CPP
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
dp[i + 1] = dp[i] + 1; // 递推公式
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) { // 连续记录
dp[i] = dp[i - 1] + 1;
}
}
```
@ -96,15 +95,16 @@ public:
if (nums.size() == 0) return 0;
int result = 1;
vector<int> dp(nums.size() ,1);
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
dp[i + 1] = dp[i] + 1;
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) { // 连续记录
dp[i] = dp[i - 1] + 1;
}
if (dp[i + 1] > result) result = dp[i + 1];
if (dp[i] > result) result = dp[i];
}
return result;
}
};
```
* 时间复杂度O(n)
@ -112,7 +112,7 @@ public:
### 贪心
这道题目也可以用贪心来做也就是遇到nums[i + 1] > nums[i]的情况count就++否则count为1记录count的最大值就可以了。
这道题目也可以用贪心来做也就是遇到nums[i] > nums[i - 1]的情况count就++否则count为1记录count的最大值就可以了。
代码如下:
@ -123,8 +123,8 @@ public:
if (nums.size() == 0) return 0;
int result = 1; // 连续子序列最少也是1
int count = 1;
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) { // 连续记录
count++;
} else { // 不连续count从头开始
count = 1;

View File

@ -22,6 +22,11 @@
* -10^8 <= val <= 10^8
* 新值和原始二叉搜索树中的任意节点值都不同
# 视频讲解
**《代码随想录》算法视频公开课:[原来这么简单? | LeetCode701.二叉搜索树中的插入操作](https://www.bilibili.com/video/BV1Et4y1c78Y),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
# 思路
其实这道题目其实是一道简单题目,**但是题目中的提示:有多种有效的插入方式,还可以重构二叉搜索树,一下子吓退了不少人**,瞬间感觉题目复杂了很多。

View File

@ -13,11 +13,10 @@
示例:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出3
解释:
长度最长的公共子数组是 [3, 2, 1] 。
* A: [1,2,3,2,1]
* B: [3,2,1,4,7]
* 输出3
* 解释:长度最长的公共子数组是 [3, 2, 1] 。
提示:
@ -27,7 +26,12 @@ B: [3,2,1,4,7]
## 思路
注意题目中说的子数组其实就是连续子序列这种问题动规最拿手动规五部曲分析如下
注意题目中说的子数组其实就是连续子序列
要求两个数组中最长重复子数组如果是暴力的解法 只要需要先两层for循环确定两个数组起始位置然后在来一个循环可以是for或者while来从两个起始位置开始比较取得重复子数组的长度
本题其实是动规解决的经典题目我们只要想到 用二维数组可以记录两个字符串的所有比较情况这样就比较好推 递推公式了
动规五部曲分析如下
1. 确定dp数组dp table以及下标的含义
@ -39,7 +43,7 @@ dp[i][j] 以下标i - 1为结尾的A和以下标j - 1为结尾的B
那有同学问了我就定义dp[i][j] 以下标i为结尾的A和以下标j 为结尾的B最长重复子数组长度不行么
行倒是行 但实现起来就麻烦一点大家看下面的dp数组状态图就明白
行倒是行 但实现起来就麻烦一点需要单独处理初始化部分在本题解下面的拓展内容里我给出了 第二种 dp数组的定义方式所对应的代码和讲解大家比较一下就了解
2. 确定递推公式
@ -73,14 +77,15 @@ dp[i][j] 以下标i - 1为结尾的A和以下标j - 1为结尾的B
代码如下
```CPP
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= B.size(); j++) {
if (A[i - 1] == B[j - 1]) {
for (int i = 1; i <= nums1.size(); i++) {
for (int j = 1; j <= nums2.size(); j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
```
@ -93,14 +98,15 @@ for (int i = 1; i <= A.size(); i++) {
以上五部曲分析完毕C++代码如下
```CPP
// 版本一
class Solution {
public:
int findLength(vector<int>& A, vector<int>& B) {
vector<vector<int>> dp (A.size() + 1, vector<int>(B.size() + 1, 0));
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = 0;
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= B.size(); j++) {
if (A[i - 1] == B[j - 1]) {
for (int i = 1; i <= nums1.size(); i++) {
for (int j = 1; j <= nums2.size(); j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
@ -111,8 +117,8 @@ public:
};
```
* 时间复杂度$O(n × m)$n 为A长度m为B长度
* 空间复杂度$O(n × m)$
* 时间复杂度O(n × m)n 为A长度m为B长度
* 空间复杂度O(n × m)
## 滚动数组
@ -127,6 +133,7 @@ public:
**此时遍历B数组的时候就要从后向前遍历这样避免重复覆盖**
```CPP
// 版本二
class Solution {
public:
int findLength(vector<int>& A, vector<int>& B) {
@ -148,6 +155,49 @@ public:
* 时间复杂度$O(n × m)$n 为A长度m为B长度
* 空间复杂度$O(m)$
## 拓展
前面讲了 dp数组为什么定义以下标i - 1为结尾的A和以下标j - 1为结尾的B最长重复子数组长度为dp[i][j]。
我就定义dp[i][j] 以下标i为结尾的A和以下标j 为结尾的B最长重复子数组长度不行么
当然可以就是实现起来麻烦一些
如果定义 dp[i][j] 以下标i为结尾的A和以下标j 为结尾的B那么 第一行和第一列毕竟要经行初始化如果nums1[i] nums2[0] 相同的话对应的 dp[i][0]就要初始为1 因为此时最长重复子数组为1 nums2[j] nums1[0]相同的话同理
所以代码如下
```CPP
// 版本三
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = 0;
// 要对第一行,第一列经行初始化
for (int i = 0; i < nums1.size(); i++) if (nums1[i] == nums2[0]) dp[i][0] = 1;
for (int j = 0; j < nums2.size(); j++) if (nums1[0] == nums2[j]) dp[0][j] = 1;
for (int i = 0; i < nums1.size(); i++) {
for (int j = 0; j < nums2.size(); j++) {
if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
```
大家会发现 这种写法 一定要多写一段初始化的过程。
而且为了让 `if (dp[i][j] > result) result = dp[i][j];` 收集到全部结果两层for训练一定从0开始遍历这样需要加上 `&& i > 0 && j > 0`的判断。
相对于版本一来说还是多写了不少代码。而且逻辑上也复杂了一些。 优势就是dp数组的定义更直观一点。
## 其他语言版本

View File

@ -8,195 +8,44 @@
# 797.所有可能的路径
本题是一道 原汁原味的 深度优先搜索dfs模板题那么用这道题目 来讲解 深搜最合适不过了。
[力扣题目链接](https://leetcode.cn/problems/all-paths-from-source-to-target/)
接下来给大家详细讲解dfs
给你一个有 n 个节点的 有向无环图DAG请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)
## dfs 与 bfs 区别
graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。
先来了解dfs的过程很多录友可能对dfs深度优先搜索bfs广度优先搜索分不清。
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221203135439.png)
先给大家说一下两者大概的区别
提示
* dfs是可一个方向去搜不到黄河不回头直到遇到绝境了搜不下去了在换方向换方向的过程就涉及到了回溯
* bfs是先把本节点所连接的所有节点遍历一遍走到下一个节点的时候再把连接节点的所有节点遍历一遍搜索方向更像是广度四面八方的搜索过程。
* n == graph.length
* 2 <= n <= 15
* 0 <= graph[i][j] < n
* graph[i][j] != i即不存在自环
* graph[i] 中的所有元素 互不相同
* 保证输入为 有向无环图DAG
当然以上讲的是,大体可以这么理解,接下来 我们详细讲解dfsbfs在用单独一篇文章详细讲解
## 思路
## dfs 搜索过程
这道题目是深度优先搜索比较好的入门题
上面说道dfs是可一个方向搜不到黄河不回头。 那么我们来举一个例子。
如果对深度优先搜索还不够了解可以先看这里[深度优先搜索的理论基础](https://programmercarl.com/图论深搜理论基础.html)
如图一是一个无向图我们要搜索从节点1到节点6的所有路径。
我依然总结了深搜三部曲如果按照代码随想录刷题的录友应该刷过 二叉树的递归三部曲回溯三部曲
![图一](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707093643.png)
**大家可能有疑惑,深搜 和 二叉树和回溯算法 有什么区别呢** 什么时候用深搜 什么时候用回溯
那么dfs搜索的第一条路径是这样的 假设第一次延默认方向就找到了节点6图二
我在讲解[二叉树理论基础](https://programmercarl.com/二叉树理论基础.html)的时候提到过**二叉树的前中后序遍历其实就是深搜在二叉树这种数据结构上的应用**。
![图二](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707093807.png)
那么回溯算法呢**其实 回溯算法就是 深搜只不过 我们给他一个更细分的定义叫做回溯算法**。
此时我们找到了节点6遇到黄河了是不是应该回头了那么应该再去搜索其他方向了。 如图三:
那有的录友可能说那我以后称回溯算法为深搜是不是没毛病
![图三](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707094011.png)
理论上来说没毛病 就像是 二叉树 你不叫它二叉树叫它数据结构有问题不 也没问题对吧
路径2撤销了改变了方向走路径3红色线 接着也找到终点6。 那么撤销路径2改为路径3在dfs中其实就是回溯的过程这一点很重要很多录友都不理解dfs代码中回溯是用来干什么的
建议是 有细分的场景还是称其细分场景的名称 所以回溯算法可以独立出来但回溯确实就是深搜
又找到了一条从节点1到节点6的路径又到黄河了此时再回头下图图四中路径4撤销回溯的过程改为路径5。
![图四](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707094322.png)
又找到了一条从节点1到节点6的路径又到黄河了此时再回头下图图五路径6撤销回溯的过程改为路径7路径8 和 路径7路径9 结果发现死路一条,都走到了自己走过的节点。
![图五](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707094813.png)
那么节点2所连接路径和节点3所链接的路径 都走过了撤销路径只能向上回退去选择撤销当初节点4的选择也就是撤销路径5改为路径10 。 如图图六:
![图六](https://code-thinking-1253855093.file.myqcloud.com/pics/20220707095232.png)
上图演示中,其实我并没有把 所有的 从节点1 到节点6的dfs深度优先搜索的过程都画出来那样太冗余了但 已经把dfs 关键的地方都涉及到了,关键就两点:
* 搜索方向,是认准一个方向搜,直到碰壁之后在换方向
* 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。
## 代码框架
正式因为dfs搜索可一个方向并需要回溯所以用递归的方式来实现是最方便的。
很多录友对回溯很陌生,建议先看看码随想录,[回溯算法章节](https://programmercarl.com/回溯算法理论基础.html)。
有递归的地方就有回溯,那么回溯在哪里呢?
就地递归函数的下面,例如如下代码:
```
void dfs(参数) {
处理节点
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
```
可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的。
在讲解[二叉树章节](https://programmercarl.com/二叉树理论基础.html)的时候二叉树的递归法其实就是dfs而二叉树的迭代法就是bfs广度优先搜索
所以**dfsbfs其实是基础搜索算法也广泛应用与其他数据结构与算法中**。
我们在回顾一下[回溯法](https://programmercarl.com/回溯算法理论基础.html)的代码框架:
```
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
```
回溯算法其实就是dfs的过程这里给出dfs的代码框架
```
void dfs(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
}
```
可以发现dfs的代码框架和回溯算法的代码框架是差不多的。
下面我在用 深搜三部曲,来解读 dfs的代码框架。
## 深搜三部曲
在 [二叉树递归讲解](https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%80%92%E5%BD%92%E9%81%8D%E5%8E%86.html)中,给出了递归三部曲。
[回溯算法](https://programmercarl.com/回溯算法理论基础.html)讲解中,给出了 回溯三部曲。
其实深搜也是一样的,深搜三部曲如下:
1. 确认递归函数,参数
```
void dfs(参数)
```
通常我们递归的时候,我们递归搜索需要了解哪些参数,其实也可以在写递归函数的时候,发现需要什么参数,再去补充就可以。
一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局遍历,避免让我们的函数参数过多。
例如这样:
```
vector<vector<int>> result; // 保存符合条件的所有路径
vector<int> path; // 起点到终点的路径
void dfs (图,目前搜索的节点)
```
但这种写法看个人习惯,不强求。
2. 确认终止条件
终止条件很重要很多同学写dfs的时候之所以容易死循环栈溢出等等这些问题都是因为终止条件没有想清楚。
```
if (终止条件) {
存放结果;
return;
}
```
终止添加不仅是结束本层递归,同时也是我们收获结果的时候。
另外其实很多dfs写法没有写终止条件其实终止条件写在了 下面dfs递归的逻辑里了也就是不符合条件直接不会向下递归。这里如果大家不理解的话没关系后面会有具体题目来讲解。
* 841.钥匙和房间
* 200. 岛屿数量
3. 处理目前搜索节点出发的路径
一般这里就是一个for循环的操作去遍历 目前搜索节点 所能到的所有节点。
```
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
```
不少录友疑惑的地方,都是 dfs代码框架中for循环里分明已经处理节点了那么 dfs函数下面 为什么还要撤销的呢。
如图七所示, 路径2 已经走到了 目的地节点6那么 路径2 是如何撤销,然后改为 路径3呢 其实这就是 回溯的过程撤销路径2走换下一个方向。
![图七](https://code-thinking-1253855093.file.myqcloud.com/pics/20220708093544.png)
## 总结
我们讲解了dfs 和 bfs的大体区别bfs详细过程下篇来讲dfs的搜索过程以及代码框架。
最后还有 深搜三部曲来解读这份代码框架。
以上如果大家都能理解了,其实搜索的代码就很好写,具体题目套用具体场景就可以了。
## 797. 所有可能的路径
### 思路
接下来我们使用深搜三部曲来分析题目
1. 确认递归函数参数
@ -218,7 +67,7 @@ void dfs (vector<vector<int>>& graph, int x)
当目前遍历的节点 为 最后一个节点的时候,就找到了一条,从 出发点到终止点的路径。
当前遍历的节点我们定义为x最后一点节点就是 graph.size() - 1。
当前遍历的节点我们定义为x最后一点节点就是 graph.size() - 1(因为题目描述是找出所有从节点 0 到节点 n-1 的路径并输出)
所以 但 x 等于 graph.size() - 1 的时候就找到一条有效路径。 代码如下:
@ -248,7 +97,13 @@ path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来
```
当前遍历的节点就是 `graph[x][i]` 了,所以进入下一层递归
一些录友可以疑惑这里如果找到x 链接的节点的例如如果x目前是节点0那么目前的过程就是这样的
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20221204111937.png)
二维数组中graph[x][i] 都是x链接的节点当前遍历的节点就是 `graph[x][i]` 。
进入下一层递归
```C++
dfs(graph, graph[x][i]); // 进入下一层递归
@ -264,8 +119,7 @@ for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节
}
```
### 本题代码
本题整体代码如下:
```c++
class Solution {
@ -296,11 +150,19 @@ public:
```
# 总结
本题是比较基础的深度优先搜索模板题,这种有向图路径问题,最合适使用深搜,当然本题也可以使用广搜,但广搜相对来说就麻烦了一些,需要记录一下路径。
而深搜和广搜都适合解决颜色类的问题,例如岛屿系列,其实都是 遍历+标记,所以使用哪种遍历都是可以的。
至于广搜理论基础,我们在下一篇在好好讲解,敬请期待!
## 其他语言版本
### Java
## Java
### Python
## Python
```python
class Solution:
def __init__(self):

View File

@ -49,7 +49,7 @@ dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符
有同学会问:为什么要定义长度为[0, i - 1]的字符串text1定义为长度为[0, i]的字符串text1不香么
这样定义是为了后面代码实现方便,如果非要定义为为长度为[0, i]的字符串text1也可以大家可以试一试!
这样定义是为了后面代码实现方便,如果非要定义为为长度为[0, i]的字符串text1也可以我在 [动态规划718. 最长重复子数组](https://programmercarl.com/0718.最长重复子数组.html) 中的「拓展」里 详细讲解了区别所在其实就是简化了dp数组第一行和第一列的初始化逻辑。
2. 确定递推公式

View File

@ -1,6 +1,7 @@
# 人生苦短我用VIM| 最强vim配置
> Github地址[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim)
> Gitee地址[https://gitee.com/programmercarl/power-vim](https://gitee.com/programmercarl/power-vim)
熟悉我的录友应该都知道我是vim流无论是写代码还是写文档Markdown都是vim都没用IDE。
@ -96,5 +97,7 @@ sh install.sh
Github地址[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim)
Gitee地址[https://gitee.com/programmercarl/power-vim](https://gitee.com/programmercarl/power-vim)
最后因为这个vim配置因为我一直没有宣传所以star数量很少哈哈哈录友们去给个star吧真正的开发利器值得顶起来

View File

@ -54,22 +54,22 @@
其实,我们仅仅需要一个容器,能保存我们要遍历过的元素就可以,**那么用队列,还是用栈,甚至用数组,都是可以的**。
用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针。
**用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针**
因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。
如果用栈的话,可能就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历。
**如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历**
因为栈是先进后出,加入元素和弹出元素的顺序改变了。
那么广搜需要注意 转圈搜索的顺序吗? 不需要!
所以用队列,还是用栈都是可以的,但大家都习惯用队列了,所以下面的讲解用我也用队列来讲,只不过要给大家说清楚,并不是非要用队列,用栈也可以。
所以用队列,还是用栈都是可以的,但大家都习惯用队列了,**所以下面的讲解用我也用队列来讲,只不过要给大家说清楚,并不是非要用队列,用栈也可以**
下面给出广搜代码模板,该模板针对的就是,上面的四方格的地图: (详细注释)
```CPP
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点不要重复访问
// x,y 表示开始搜索节点的下标