一、dfs(深搜)
- 深度优先搜索也可以用来检测环路:记录每个遍历过的节点的父节点,若一个节点被再次遍历且父节点不同,则说明有环。我们也可以用之后会讲到的拓扑排序判断是否有环路,若最后存在入度不为零的点,则说明有环。
- 有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这种做法叫做状态记录或记忆化(memoization)。
重点:dfs和 自带备忘录的dfs区别
dfs : 遍历所有情况,但你只取其中一个(最优),返回一个即可
自上而下的自带备忘录的dfs:也是遍历所有情况,但每次保存算的结果(直接计算出来如选和不选),返回最优结果。
例子:
打家劫舍问题
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
dfs(超时)(从下到上/从上到下都行)
class Solution {
public:
int k = 0;//记录所有情况的最大值
int rob(vector<int>& nums) {
dfs(nums,0,0);
return k;
}
void dfs(vector<int>& nums,int i,int sum){
if(i >= nums.size()){
k = max(k,sum);
return;
}
dfs(nums,i+1,sum);//不偷
dfs(nums,i+2,sum+nums[i]);//偷
}
};
dfs + 自上而下备忘录
class Solution {
public:
int sum = 0;
vector<int> f = vector<int>(101,-1);//当前选的最大值
int rob(vector<int>& nums) {
return dfs(nums,nums.size()-1);
}
int dfs(vector<int>& nums,int i){
if(i == 0){
f[i] = nums[i];
return f[i];
}
if(i == 1){
f[i] = max(nums[0],nums[1]);
return f[i];
}
if(f[i]!=-1)
return f[i];//有记录直接放回
f[i] = max(dfs(nums,i-1),dfs(nums,i-2)+nums[i]);//直接计算
//选当前和不选当前的两种状结果,返回
return f[i];
}
};
岛屿最大面积
给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。
示例 1:
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。
示例 2:
输入:grid = [[0,0,0,0,0,0,0,0]]
输出:0
class Solution {
public:
vector<int> dir = {-1,0,1,0,-1};
int dfs(vector<vector<int> > &grid,int i,int j){
if(i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] == 0){
return 0;
}
grid[i][j] = 0;
int a = 1;
for(int k = 0;k < 4;k++){
int x = i + dir[k];
int y = j + dir[k+1];
a += dfs(grid,x,y);
}
return a;
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int m = 0;
for(int i = 0; i < grid.size();i++)
for(int j = 0;j < grid[0].size();j++)
if(grid[i][j] == 1)
m = max(m,dfs(grid,i,j));
return m;
}
};
省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
class Solution {
public:
vector<int> visted = vector<int>(204,0);
void dfs(vector<vector<int> > &isConnected,int i){
visted[i] = 1;
for(int k = 0;k < isConnected.size();k++)
if(isConnected[i][k] && !visted[k])
dfs(isConnected,k);
}
int findCircleNum(vector<vector<int>>& isConnected) {
int k = 0;
for(int i = 0;i < isConnected.size();i++){
if(!visted[i]){
dfs(isConnected,i);
k++;
}
}
return k;
}
};
太平洋大西洋水流问题
给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。
提示:
输出坐标的顺序不重要
m 和 n 都小于150
示例:
给定下面的 5x5 矩阵:
太平洋 ~ ~ ~ ~ ~
~ 1 2 2 3 (5) *
~ 3 2 3 (4) (4) *
~ 2 4 (5) 3 1 *
~ (6) (7) 1 4 5 *
~ (5) 1 1 2 4 *
* * * * * 大西洋
返回:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).
方法一(dfs+剪枝):
class Solution {
public:
bool ul = false;
bool rd = false;
vector<int> dir = {-1,0,1,0,-1};
int visted[152][152] = {0};
void dfs(vector<vector<int> > &heights,int i,int j){
if(ul && rd)return;
visted[i][j] = 1;
for(int k = 0;k < 4;k++){
int x = i + dir[k];
int y = j + dir[k+1];
if(x < 0 || y < 0){//上左
ul = true;
continue;//必须continue,尽管一个到头还得遍历另一边
}
//if(ul && rd)return; 不能在这会改变visited值没办法复原
if(x >= heights.size() || y >= heights[0].size()){//右下
rd = true;
continue;
}
if(x >= 0 && x < heights.size() && y >= 0 && y < heights[0].size() && heights[x][y] <= heights[i][j] && !visted[x][y])
dfs(heights,x,y);
}
visted[i][j] = 0;
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
vector<vector<int> > t;
for(int i = 0;i < heights.size();i++)
for(int j = 0;j < heights[0].size();j++){
ul = false;
rd = false;
dfs(heights,i,j);
if(ul && rd)
t.push_back(vector<int>{i,j});
}
return t;
}
};
方法二(逆向思维,深搜四边界)
class Solution {
public:
vector<int> dir ={-1,0,1,0,-1};
int m,n;
void dfs(vector<vector<int> > &heights,vector<vector<bool> > &reach,int i,int j){
if(reach[i][j])
return;
reach[i][j] = true;
for(int k = 0;k < 4;k++){
int x = i + dir[k];
int y = j + dir[k+1];
if(x >= 0 && x < m && y >= 0 && y < n && heights[x][y] >= heights[i][j]){
dfs(heights,reach,x,y);
}
}
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
if(heights.empty() || heights[0].empty())return {};
m = heights.size(),n = heights[0].size();
vector<vector<bool> > reach_t(m,vector<bool>(n,false));//能到太平洋(左上)
vector<vector<bool> > reach_d(m,vector<bool>(n,false));//能到大西洋(右下)
for(int i = 0;i < m;i++){
dfs(heights,reach_t,i,0);//左边界
dfs(heights,reach_d,i,n-1);//右边界
}
for(int j = 0;j < n;j++){
dfs(heights,reach_t,0,j);//上界
dfs(heights,reach_d,m-1,j);//下界
}
vector<vector<int> > t;
for(int i = 0;i < m;i++)
for(int j = 0;j < n;j++)
if(reach_d[i][j] && reach_t[i][j])
t.push_back({i,j});
return t;
}
};
二、回溯法(目的是为了多次深搜或死循环深搜)
回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。
顾名思义,回溯法的核心是回溯。在搜索到某一节点的时候,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。这样的好处是我们可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。在具体的写法上,它与普通的深度优先搜索一样,都有 [修改当前节点状态]→[递归子节点] 的步骤,只是多了回溯的步骤,变成了 [修改当前节点状态]→[递归子节点]→[回改当前节点状态]。
没有接触过回溯法的读者可能会不明白我在讲什么,这也完全正常,希望以下几道题可以让您理解回溯法。如果还是不明白,可以记住两个小诀窍,
- 是按引用传状态,
- 是所有的状态修改在递归完成后回改。
回溯法修改一般有两种情况,
- 是修改至最后一位输出,比如排列组合;(组合排列)
- 是修改访问标记,比如矩阵里搜字符串。
全排列
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
class Solution {
public:
void dfs(vector<int> &nums,vector<vector<int> > &ans,int i){
if(i == nums.size()){//至最后
ans.push_back(nums);
return;
}
for(int j = i;j < nums.size();j++){
swap(nums[i],nums[j]);
dfs(nums,ans,i+1);
swap(nums[i],nums[j]);
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int> > ans;
dfs(nums,ans,0);
return ans;
}
};
组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
class Solution {
public:
void dfs(int n,int k,int i,vector<int> &p,vector<vector<int> > &ans){
if(k == 0){
ans.push_back(p);
return;
}
for(int j = i;j <= n;j++){
p.push_back(j);
dfs(n,k-1,j+1,p,ans);
p.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<vector<int> > ans;
vector<int> p;
dfs(n,k,1,p,ans);
return ans;
}
};
单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “SEE”
输出:true
示例 3:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCB”
输出:false
class Solution {
public:
vector<int> dir = {-1,0,1,0,-1};
int visted[10][10] = {0};
bool flag = false;
void dfs(vector<vector<char> > &board,string &word,int i,int j,int k){
if(k == word.size() || flag){
flag = true;
return;
}
visted[i][j] = 1;
for(int l = 0;l < 4;l++){
int x = i + dir[l];
int y = j + dir[l+1];
if(x >= 0 && x < board.size() && y >= 0 && y < board[0].size() && board[x][y] == word[k] && !visted[x][y])
dfs(board,word,x,y,k+1);
}
visted[i][j] = 0;
}
bool exist(vector<vector<char>>& board, string word) {
for(int i = 0;i < board.size();i++)
for(int j = 0;j < board[0].size();j++){
if(board[i][j] == word[0]){
dfs(board,word,i,j,1);
}
}
return flag;
}
};
N皇后
class Solution {
public:
int c[10] = {0};//列
int x1[50] = {0};//正对角线
int x2[50] = {0};//反对角线
int m;
void dfs(int i,vector<vector<string> > &ans,vector<string> &t){
if(i == m){
ans.push_back(t);
return;
}
for(int j = 0;j < m;j++){
if(!c[j] && !x1[i+j] && !x2[i-j+m]){
c[j] = x1[i+j] = x2[i-j+m] = 1;
t[i][j] = 'Q';
dfs(i+1,ans,t);
t[i][j] = '.';
c[j] = x1[i+j] = x2[i-j+m] = 0;
}
}
}
vector<vector<string>> solveNQueens(int n) {
m = n;
string tmp;
vector<string> t(n,string(n,'.'));
vector<vector<string> > ans;
dfs(0,ans,t);
return ans;
}
};
三、bfs
广度优先搜索(breadth-first search,BFS)不同与深度优先搜索,它是一层层进行遍历的,因
此需要用先入先出的队列而非先入后出的栈进行遍历。由于是按层次进行遍历,广度优先搜索时
按照“广”的方向进行遍历的,也常常用来处理最短路径等问题。
最短的桥
在给定的二维二进制数组 A 中,存在两座岛。(岛是由四面相连的 1 形成的一个最大组。)
现在,我们可以将 0 变为 1,以使两座岛连接起来,变成一座岛。
返回必须翻转的 0 的最小数目。(可以保证答案至少是 1 。)
示例 1:
输入:A = [[0,1],[1,0]]
输出:1
示例 2:
输入:A = [[0,1,0],[0,0,0],[0,0,1]]
输出:2
示例 3:
输入:A = [[1,1,1,1,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,0,1],[1,1,1,1,1]]
输出:1
// BFS + DFS(标记第一个岛)
class node{
public:
int x;
int y;
int step;
node(int x,int y,int step):x(x),y(y),step(step){
}
};
class Solution {
public:
int dir[5] = {-1,0,1,0,-1};
int m,n;
void dfs(vector<vector<int> > &grid,int &i,int &j,queue<node> &q){
grid[i][j] = 2;
q.push(node{i,j,0});
for(int k = 0;k < 4;k++){
int x = i + dir[k];
int y = j + dir[k+1];
if(x < m && x >= 0 && y < n && y >= 0 && grid[x][y] == 1){
dfs(grid,x,y,q);//第一个岛
}
}
}
int shortestBridge(vector<vector<int>>& grid) {
m = grid.size(),n = grid[0].size();
bool flag = 0;
queue<node> q;
for(int i = 0;i < m;i++){
for(int j = 0;j < n;j++)
if(grid[i][j] == 1){
dfs(grid,i,j,q);
flag = 1;
break;
}
if(flag)break;
}
int x,y;
while(!q.empty()){
node tmp = q.front();
q.pop();
int i = tmp.x;
int j = tmp.y;
int s = tmp.step;
for(int k = 0;k < 4;k++){
x = i + dir[k];
y = j + dir[k+1];
if(x < m && y < n && x >= 0 && y >= 0){
if(grid[x][y] == 1)return s;
if(grid[x][y] == 2)continue;
q.push(node{x,y,s+1});
grid[x][y] = 2;//======================把0的置为2防止重复进出队列。
}
}
}
return 0;
}
};
被围绕的区域(填充包围区域)
给你一个 m x n 的矩阵 board ,由若干字符 ‘X’ 和 ‘O’ ,找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
示例 1:
输入:board = [[“X”,“X”,“X”,“X”],[“X”,“O”,“O”,“X”],[“X”,“X”,“O”,“X”],[“X”,“O”,“X”,“X”]]
输出:[[“X”,“X”,“X”,“X”],[“X”,“X”,“X”,“X”],[“X”,“X”,“X”,“X”],[“X”,“O”,“X”,“X”]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
示例 2:
输入:board = [[“X”]]
输出:[[“X”]]
class Solution {
public:
int visited[201][201] = {0};
int dir[5] = {-1,0,1,0,-1};
int m,n;
void dfs(vector<vector<char>>& board,int i,int j){
visited[i][j] = true;
for(int k = 0;k < 4;k++){
int x = i + dir[k];
int y = j + dir[k+1];
if(x < m && x >= 0 && y < n && y >= 0 && board[x][y] == 'O' && !visited[x][y])
dfs(board,x,y);
}
}
void solve(vector<vector<char>>& board) {
m = board.size();
n = board[0].size();
cout << m << "---" << n << endl;
for(int i = 0;i < m;i++){//四边向内遍历O
if(board[i][0] == 'O')
dfs(board,i,0);
if(board[i][n-1] == 'O')
dfs(board,i,n-1);
}
for(int j = 0;j < n;j++){
if(board[0][j] == 'O')
dfs(board,0,j);
if(board[m-1][j] == 'O')
dfs(board,m-1,j);
}
for(int i = 0;i < m;i++)
for(int j = 0;j < n;j++)
if(board[i][j] == 'O' && !visited[i][j])
board[i][j] = 'X';
}
};