遇到深度优先搜索(DFS)相关的算法题,逻辑有时候不是那么好想,归根结底还是自己没把这东西吃透,因此打算对相关的自己认为是经典的 DFS 算法题进行总结。同时干脆也把广度优先搜索(BFS)一并复习总结了。因为 LeetCode 题有空就做,所以这个笔记一直在更新中。
前言
深度优先搜索(DFS),通俗地说,就是往深了搜索,搜到底不能再深了,就回溯。深度优先搜索可以用栈或递归(其实递归就是隐式地用栈)实现。举个简单的例子,二叉树前、中、后序遍历即可用深度优先搜索实现。
广度优先搜索(BFS),通俗地说,就是往广了搜索,一个节点广泛得发散。广度优先搜索可以用队列实现。举个简单的例子,二叉树层序遍历即可用广度优先搜索实现。
注1:我原来是把DFS和BFS的题目分开了整理的,但是这样整理有一个问题,很多题目用两种思路都能解,而且解题思路都挺清晰。这样一来把用这两种思想解的题分开来整理就有些不合理了,因此我就一起整理了,只在题解中说明用到了那种思想。
注2:后续再看的时候发现有把单纯的回溯算法独立出来的,以和DFS区别,然而我这里就整理在一起了,因为我个人认为思路相似。
二叉树中的最大路径和(困难)
来源:力扣(LeetCode)
题目链接:https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/
给定一个非空二叉树,返回其最大路径和。本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
示例1
输入: [1,2,3]
1
/ \
2 3
输出: 6
示例2
输入: [-10,9,20,null,null,15,7]
-10
/ \
9 20
/ \
15 7
输出: 42
思路:这个题写着是个困难题,但解题逻辑还是挺好想的,硬把 DFS 套上去也能试出来。这里是把每个节点都想象成根节点,包含根节点的最大路径和为:
即为max(包含左子节点的左子树的最大路径和,0) + max(包含右子节点的右子树的最大路径和,0) + 根节点的值。显然求最大路径和的时候若 lvalue、rvalue 小于0,那么加上就亏了,因此这里要是某个和小于 0 就直接加 0。继续显然,lvalue 和 rvalue 用 DFS 得到。
(一开始我居然没看出来没思路“白眼”)
C++代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int res = INT_MIN;
int dfs(TreeNode* node){
if(node == nullptr) return 0;
int lvalue = max(dfs(node->left),0);
int rvalue = max(dfs(node->right),0);
res = max(res,lvalue + rvalue + node -> val);
return max(lvalue,rvalue) + node -> val;
//注意所谓路径是要连续的,因此这里返回值要选个大的加
}
int maxPathSum(TreeNode* root) {
dfs(root);
return res;
}
};
求根到叶子节点数字之和(中等)
来源:力扣(LeetCode)
题目链接:https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/
给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。
例如,从根到叶子节点路径 1->2->3
代表数字 123
。
计算从根到叶子节点生成的所有数字之和。
说明: 叶子节点是指没有子节点的节点。
示例 1:
输入: [1,2,3]
1
/ \
2 3
输出: 25
解释:
从根到叶子节点路径 1->2 代表数字 12.
从根到叶子节点路径 1->3 代表数字 13.
因此,数字总和 = 12 + 13 = 25.
示例 2:
输入: [4,9,0,5,1]
4
/ \
9 0
/ \
5 1
输出: 1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495.
从根到叶子节点路径 4->9->1 代表数字 491.
从根到叶子节点路径 4->0 代表数字 40.
因此,数字总和 = 495 + 491 + 40 = 1026.
思路:显然可以用深度优先搜索,递归逻辑比较好想。计算的时候,父节点的值算出来的值传给子节点再算,值的更新公式为:value = 10*value + node->val。然后再传,直到到达叶子节点,就把算到的值加到总和上。由于这里只是单纯的传值,因此回到上一层递归时值还是老值。
C++代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int res = 0;
void dfs(TreeNode* node, int value){
if(node == nullptr) return;
value = 10*value + node->val;
if(node->left == nullptr && node->right == nullptr){
res += value;
return;
}
dfs(node->left,value);
dfs(node->right,value);
}
int sumNumbers(TreeNode* root) {
dfs(root,0);
return res;
}
};
全排列(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
思路:这个题目的思路其实挺好想的,这个例子太简单,换个稍微复杂的 [1,2,3,4]。原始状况就算一个排列,然后控制1,2不动,3,4交换算一个,然后1不动,2,3交换算一个,在2,3交换的情况下后面2,4也能换,等等等等,如果要自己写,就是这么个思路。最后答案是:
[1,2,3,4],[1,2,4,3],[1,3,2,4],[1,3,4,2],[1,4,3,2],[1,4,2,3],[2,1,3,4],[2,1,4,3],[2,3,1,4],[2,3,4,1],[2,4,3,1],[2,4,1,3],[3,2,1,4],[3,2,4,1],[3,1,2,4],[3,1,4,2],[3,4,1,2],[3,4,2,1],[4,2,3,1],[4,2,1,3],[4,3,2,1],[4,3,1,2],[4,1,3,2],[4,1,2,3]
但代码不好想啊。显然是要用到回溯的,但就是不会写代码,真是急死人了。我很难想到有一种很好的描述方式能让人一下子看懂,因为我自己也不那么懂,过几天说不定又想不出了。反正思路就是固定住前面的数,后面的数换一次固定一次,直到到最后,就成为了一种排列。比如执行swap(nums[0], nums[2])
,就是把位置0的数和位置2的数交换了,之后要递归,执行backtrack(nums, 1, n)
,就是说位置0固定了,对后面的数,即位置1(第二个数)开始的数进行排列,里面也执行同样的操作,让后面的每个数都来一次位置1,在里面再往后排序。当这一套搞完后,要进行所谓的回溯,就是把换走的数换回去,回到最后又执行一次swap(nums[0], nums[2])
,这时位置0的数和位置2的数又还原了,然后++i
,就表示该把位置3的数换到前面来了。当然,这些话只是对照着代码复述其作用,如果能够自己想出这一套,就能把代码写出来了。
注:回溯和深度优先搜索虽然是两个不同的名词,但我总觉得二者相差不大。这个题用到回溯的思想,但这个题目的搜索不能不说是一个深度优先搜索的过程。
C++代码:
循环套递归,还是有点不好想。
class Solution {
vector<vector<int>> res;
void backtrack(vector<int> nums, int pos, int n){
if(pos == n){
res.push_back(nums);
return;
}
for(int i = pos; i < n; ++i){
swap(nums[pos], nums[i]);
backtrack(nums, pos+1, n);
swap(nums[pos], nums[i]);
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
backtrack(nums, 0, n);
return res;
}
};
课程表(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/course-schedule
你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
提示:
输入的先决条件是由 边缘列表 表示的图形,而不是 邻接矩阵 。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
1 <= numCourses <= 10^5
思路:这个题一看就是个有向图找环的题目,课程相当于有向图中的节点,课程之间的先修后修关系确定边以及方向。瞬间想到拓扑排序。有关拓扑排序的内容我在图论算法(杂)中有所记录。当然这个题目很简单了,不用搞麻烦的映射。当然这个题也可以用深度优先搜索做,不过我觉得想到有向图找环拓扑排序用队列广度优先搜索是顺理成章的事。
C++代码:
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> indegrees(numCourses);//入度数组
vector<vector<int>> adjacencylist(numCourses);//邻接表
queue<int> Q;
for(auto edge : prerequisites){
++indegrees[edge[0]];
adjacencylist[edge[1]].push_back(edge[0]);
}
for(int i = 0; i < numCourses; ++i){
if(indegrees[i] == 0) Q.push(i);
}
while(!Q.empty()){
int cur = Q.front();
Q.pop();
for(auto course : adjacencylist[cur]){
if(--indegrees[course] == 0)
Q.push(course);
}
numCourses--;
}
return numCourses == 0;
}
};
单词搜索(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-search
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false
提示:
board 和 word 中只包含大写和小写英文字母。
1 <= board.length <= 200
1 <= board[i].length <= 200
1 <= word.length <= 10^3
思路:这个题目显然是可以用DFS,要注意的是题目中的“同一个单元格内的字母不允许被重复使用”条件。为了满足这个条件,在搜索的时候可以先让当前点换成一个不可能存在的值,比如'*'
,这样在搜索后面的点时肯定就不会重复使用了,之后搜索完在改回来。
C++代码:
class Solution {
int m, n, len;
bool dfs(vector<vector<char>>& board, string &word, int i, int j, int k){
if(i<0 || i>=m || j<0 || j>=n || board[i][j] != word[k]){
return false;
}
if(k == len - 1){
return true;
}
char tmp = board[i][j];
board[i][j] = '*';
bool res = dfs(board, word, i-1, j, k+1) || dfs(board, word, i+1, j, k+1) ||
dfs(board, word, i, j-1, k+1) || dfs(board, word, i, j+1, k+1);
board[i][j] = tmp;
return res;
}
public:
bool exist(vector<vector<char>>& board, string word) {
m = board.size(), n = board[0].size(), len = word.size();
for(int i = 0; i < m; ++i){
for(int j = 0; j < n; ++j){
if(dfs(board, word, i, j, 0) == true)
return true;
}
}
return false;
}
};
括号生成(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/generate-parentheses
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的括号组合。
示例:
输入:n = 3
输出:[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
思路:这个题用到回溯的思想,不过要注意“有效的括号”这个条件。’(‘
的数量要是没超过')'
的数量,就不能加')'
。对于他们给出的实例,搜索及回溯过程如下图所示。
C++代码:
class Solution {
void dfs(vector<string> &res, string &tmp, int N_left, int N_right, int n){
if(tmp.size() == 2*n){
res.push_back(tmp);
return;
}
if(N_left > 0){
tmp += '(';
dfs(res, tmp, N_left - 1, N_right, n);
tmp.pop_back();
}
if(N_right > N_left){
tmp += ')';
dfs(res, tmp, N_left, N_right - 1, n);
tmp.pop_back();
}
}
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
string tmp;
dfs(res, tmp, n, n, n);
return res;
}
};
验证二叉搜索树(中等)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/validate-binary-search-tree
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
示例 2:
输入:
5
/ \
1 4
/ \
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
思路:一看就想到深度优先搜索,不过要注意一个小问题,在搜索过程中不能单单比较左子节点的值比当前节点的值小及右子节点的值比当前节点的值大,应该维护一个上下限。因为保证了左子节点的值比当前节点的值小,并不能保证左子节点的右子树中不存在大于当前节点的值,若存在则不符合二叉搜索树的性质。当然这个题还有一种解法,就是中序遍历这个二叉树,看序列是否有序,因为一颗二叉树若是二叉搜索树,那么他的中序遍历一定是有序的。
法1 C++代码:
class Solution {
bool res = true;
void dfs(TreeNode *node, long lower, long upper) {
if(node == nullptr) return;
if(node->val < upper && node->val > lower){
dfs(node->left, lower, node->val);
dfs(node->right, node->val, upper);
}
else{
res = false;
return;
}
}
public:
bool isValidBST(TreeNode* root) {
dfs(root, LONG_MIN, LONG_MAX);
return res;
}
};
法2 C++代码:
class Solution {
public:
bool isValidBST(TreeNode* root) {
stack<TreeNode*> S;//辅助栈
TreeNode* pre = nullptr;
TreeNode* node = root;
while (!S.empty() || node != nullptr)
{
while (node)
{
S.push(node);
node = node->left;
}
node = S.top();
S.pop();
if (pre != nullptr && pre->val >= node->val)
return false;
pre = node;
node = node->right;
}
return true;
}
};