mirror of
https://github.com/youngyangyang04/leetcode-master.git
synced 2025-07-07 15:45:40 +08:00
Update
This commit is contained in:
@ -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] 后面的数都是正数的话,就一定 不符合条件了。
|
||||
|
||||
不过这种剪枝 其实有点 小绕,大家能够理解 文章给的完整代码的剪枝 就够了。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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。
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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]**
|
||||
|
||||
|
@ -84,9 +84,9 @@ public:
|
||||
|
||||
很多录友可能有疑惑,为什么 以上代码中的dfs函数,没有终止条件呢? 感觉递归没有终止很危险。
|
||||
|
||||
其实终止条件 就写在了,调用dfs的地方,如果遇到不合法的方向,直接不会去调用dfs。
|
||||
其实终止条件 就写在了 调用dfs的地方,如果遇到不合法的方向,直接不会去调用dfs。
|
||||
|
||||
当然,也可以这么写:
|
||||
当然也可以这么写:
|
||||
|
||||
```CPP
|
||||
// 版本二
|
||||
@ -122,7 +122,7 @@ public:
|
||||
};
|
||||
```
|
||||
|
||||
这里大家应该能看出区别了,无疑就是版本一中 调用dfs 的条件,放在了 版本二 的 终止条件位置上。
|
||||
这里大家应该能看出区别了,无疑就是版本一中 调用dfs 的条件判断 放在了 版本二 的 终止条件位置上。
|
||||
|
||||
**版本一的写法**是 :下一个节点是否能合法已经判断完了,只要调用dfs就是可以合法的节点。
|
||||
|
||||
@ -137,6 +137,9 @@ public:
|
||||
|
||||
其实本题是 dfs,bfs 模板题,但正是因为是模板题,所以大家或者一些题解把重要的细节都很忽略了,我这里把大家没注意的但以后会踩的坑 都给列出来了。
|
||||
|
||||
本篇我只给出的dfs的写法,大家发现我写的还是比较细的,那么后面我再单独更本题的bfs写法,虽然是模板题,但依然有很多注意的点,敬请期待!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -35,6 +35,11 @@
|
||||
* 所有节点的值都是唯一的。
|
||||
* p、q 为不同节点且均存在于给定的二叉搜索树中。
|
||||
|
||||
# 视频讲解
|
||||
|
||||
**《代码随想录》算法视频公开课:[二叉搜索树找祖先就有点不一样了!| 235. 二叉搜索树的最近公共祖先](https://www.bilibili.com/video/BV1Zt4y1F7ww),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
|
||||
|
||||
|
||||
# 思路
|
||||
|
||||
|
||||
|
@ -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++) {
|
||||
|
@ -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]。
|
||||
|
||||
**其实本题很多同学搞的比较懵,是因为出现冷冻期之后,状态其实是比较复杂度**,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。
|
||||
|
||||
具体可以区分出如下四个状态:
|
||||
|
||||
* 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
|
||||
* 卖出股票状态,这里就有两种卖出股票状态
|
||||
* 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
|
||||
* 状态三:今天卖出了股票
|
||||
* 状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
|
||||
* 不持有股票状态,这里就有两种卖出股票状态
|
||||
* 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
|
||||
* 状态三:今天卖出股票
|
||||
* 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
|
||||
|
||||

|
||||
@ -57,38 +58,48 @@ j的状态为:
|
||||
|
||||
从代码上来看确实可以合并,但从逻辑上分析合并之后就很难理解了,所以我下面的讲解是按照这四个状态来的,把每一个状态分析清楚。
|
||||
|
||||
**注意这里的每一个状态,例如状态一,是买入股票状态并不是说今天已经就买入股票,而是说保存买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态**。
|
||||
如果大家按照代码随想录顺序来刷的话,会发现 买卖股票最佳时机 1,2,3,4 的题目讲解中
|
||||
|
||||
* [动态规划: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]初始化为0,dp[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]大小的数组就可以了,就保存前一天的当前的状态,感兴趣的同学可以自己去写一写,思路是一样的。
|
||||
|
||||
|
@ -13,15 +13,14 @@
|
||||
连续递增的子序列 可以由两个下标 l 和 r(l < 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;
|
||||
|
@ -22,6 +22,11 @@
|
||||
* -10^8 <= val <= 10^8
|
||||
* 新值和原始二叉搜索树中的任意节点值都不同
|
||||
|
||||
# 视频讲解
|
||||
|
||||
**《代码随想录》算法视频公开课:[原来这么简单? | LeetCode:701.二叉搜索树中的插入操作](https://www.bilibili.com/video/BV1Et4y1c78Y),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
|
||||
|
||||
|
||||
# 思路
|
||||
|
||||
其实这道题目其实是一道简单题目,**但是题目中的提示:有多种有效的插入方式,还可以重构二叉搜索树,一下子吓退了不少人**,瞬间感觉题目复杂了很多。
|
||||
|
@ -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数组的定义,更直观一点。
|
||||
|
||||
## 其他语言版本
|
||||
|
||||
|
||||
|
@ -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(广度优先搜索)分不清。
|
||||

|
||||
|
||||
先给大家说一下两者大概的区别:
|
||||
提示:
|
||||
|
||||
* dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,在换方向(换方向的过程就涉及到了回溯)。
|
||||
* bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。
|
||||
* n == graph.length
|
||||
* 2 <= n <= 15
|
||||
* 0 <= graph[i][j] < n
|
||||
* graph[i][j] != i(即不存在自环)
|
||||
* graph[i] 中的所有元素 互不相同
|
||||
* 保证输入为 有向无环图(DAG)
|
||||
|
||||
当然以上讲的是,大体可以这么理解,接下来 我们详细讲解dfs,(bfs在用单独一篇文章详细讲解)
|
||||
## 思路
|
||||
|
||||
## dfs 搜索过程
|
||||
这道题目是深度优先搜索,比较好的入门题。
|
||||
|
||||
上面说道dfs是可一个方向搜,不到黄河不回头。 那么我们来举一个例子。
|
||||
如果对深度优先搜索还不够了解,可以先看这里:[深度优先搜索的理论基础](https://programmercarl.com/图论深搜理论基础.html)
|
||||
|
||||
如图一,是一个无向图,我们要搜索从节点1到节点6的所有路径。
|
||||
我依然总结了深搜三部曲,如果按照代码随想录刷题的录友,应该刷过 二叉树的递归三部曲,回溯三部曲。
|
||||
|
||||

|
||||
**大家可能有疑惑,深搜 和 二叉树和回溯算法 有什么区别呢**? 什么时候用深搜 什么时候用回溯?
|
||||
|
||||
那么dfs搜索的第一条路径是这样的: (假设第一次延默认方向,就找到了节点6),图二
|
||||
我在讲解[二叉树理论基础](https://programmercarl.com/二叉树理论基础.html)的时候,提到过,**二叉树的前中后序遍历其实就是深搜在二叉树这种数据结构上的应用**。
|
||||
|
||||

|
||||
那么回溯算法呢,**其实 回溯算法就是 深搜,只不过 我们给他一个更细分的定义,叫做回溯算法**。
|
||||
|
||||
此时我们找到了节点6,(遇到黄河了,是不是应该回头了),那么应该再去搜索其他方向了。 如图三:
|
||||
那有的录友可能说:那我以后称回溯算法为深搜,是不是没毛病?
|
||||
|
||||

|
||||
理论上来说,没毛病,但 就像是 二叉树 你不叫它二叉树,叫它数据结构,有问题不? 也没问题对吧。
|
||||
|
||||
路径2撤销了,改变了方向,走路径3(红色线), 接着也找到终点6。 那么撤销路径2,改为路径3,在dfs中其实就是回溯的过程(这一点很重要,很多录友,都不理解dfs代码中回溯是用来干什么的)
|
||||
建议是 有细分的场景,还是称其细分场景的名称。 所以回溯算法可以独立出来,但回溯确实就是深搜。
|
||||
|
||||
又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图四中,路径4撤销(回溯的过程),改为路径5。
|
||||
|
||||

|
||||
|
||||
又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图五,路径6撤销(回溯的过程),改为路径7,路径8 和 路径7,路径9, 结果发现死路一条,都走到了自己走过的节点。
|
||||
|
||||

|
||||
|
||||
那么节点2所连接路径和节点3所链接的路径 都走过了,撤销路径只能向上回退,去选择撤销当初节点4的选择,也就是撤销路径5,改为路径10 。 如图图六:
|
||||
|
||||

|
||||
|
||||
|
||||
上图演示中,其实我并没有把 所有的 从节点1 到节点6的dfs(深度优先搜索)的过程都画出来,那样太冗余了,但 已经把dfs 关键的地方都涉及到了,关键就两点:
|
||||
|
||||
* 搜索方向,是认准一个方向搜,直到碰壁之后在换方向
|
||||
* 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。
|
||||
|
||||
|
||||
## 代码框架
|
||||
|
||||
正式因为dfs搜索可一个方向,并需要回溯,所以用递归的方式来实现是最方便的。
|
||||
|
||||
很多录友对回溯很陌生,建议先看看码随想录,[回溯算法章节](https://programmercarl.com/回溯算法理论基础.html)。
|
||||
|
||||
有递归的地方就有回溯,那么回溯在哪里呢?
|
||||
|
||||
就地递归函数的下面,例如如下代码:
|
||||
```
|
||||
void dfs(参数) {
|
||||
处理节点
|
||||
dfs(图,选择的节点); // 递归
|
||||
回溯,撤销处理结果
|
||||
}
|
||||
```
|
||||
|
||||
可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的。
|
||||
|
||||
在讲解[二叉树章节](https://programmercarl.com/二叉树理论基础.html)的时候,二叉树的递归法其实就是dfs,而二叉树的迭代法,就是bfs(广度优先搜索)
|
||||
|
||||
所以**dfs,bfs其实是基础搜索算法,也广泛应用与其他数据结构与算法中**。
|
||||
|
||||
我们在回顾一下[回溯法](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,走换下一个方向。
|
||||
|
||||

|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
我们讲解了,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,那么目前的过程就是这样的:
|
||||
|
||||

|
||||
|
||||
二维数组中,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):
|
||||
|
@ -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. 确定递推公式
|
||||
|
||||
|
@ -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吧,真正的开发利器,值得顶起来!
|
||||
|
||||
|
@ -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 表示开始搜索节点的下标
|
||||
|
Reference in New Issue
Block a user