1.深度优先遍历(depth-first-search),遇到新节点就遍历下去。因此遍历需要用先入后出的栈来实现,也可以通过与栈等价的递归来实现。
深度优先搜索也可以用来检测环路:记录每个遍历过的节点的父节点,若一个节点被再次遍历且父节点不同,则说明有环。我们也可以用拓扑排序判断是否有环路,若最后存在入度不为零的点,则说明有环。 有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这 种做法叫做状态记录或记忆化(memoization)。
一般来说,深度优先搜索类型 的题可以分为主函数和辅函数,主函数用于遍历所有的搜索位置,判断是否可以开始搜索,如果 可以即在辅函数进行搜索。辅函数则负责深度优先搜索的递归调用。
2. 广度优先遍历(BFS),先访问起始顶点,然后依次访问起始顶点的领接顶点,再依次访问领接顶点的领接顶点,直到访问完,还有顶点没被访问,重复上述步骤,直到全部顶点都被访问。
利用队列实现搜索。通常解决最短路线问题。
3. 回溯法,又称为试探法,常用于需要记录节点状 态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。
在搜索到某一节点的时候,如果我们发现目前的节点(及 其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。这样的好处是我们可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。在具体的写法上,它与普通的深度优先搜索一样,都有 [修改当前节点状态]→[递归子节点] 的步骤,只是多了回溯的步骤,变成了 [修改当前节点状态]→[递归子节点]→[回改当前节点状态]。
记住两个小诀窍,一是按引用传状态,二是所有的状态修改在递归完成后回改。
回溯法修改一般有两种情况,一种是修改最后一位输出,比如排列组合;一种是修改访问标记,比如矩阵里搜字符串。
695 岛屿的最大面积
书中最优解 栈
对于四个方向的遍历,可以创造一个数组 [-1, 0, 1, 0, -1],每相邻两位即为上右下左四个方向之一,-1,0是向上。按数组走的
一种是先判定是否越界,只有在合法的情况下才进行下一步搜索
vector<int> direction{ -1, 0, 1, 0, -1 };
// 主函数
int maxAreaOfIsland(vector<vector<int>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int max_area = 0;
for (int i = 0; i < grid.size(); ++i) {
for (int j = 0; j < grid[0].size(); ++j) {
//遇到岛屿 调用dfs将相连的岛屿遍历并统计面积
//将最大面积的岛屿 赋给max_area
if (grid[i][j] == 1) {
max_area = max(max_area, dfs(grid, i, j));
}
}
}
return max_area;
}
// 辅函数
int dfs(vector<vector<int>>& grid, int r, int c) {
//判断边界,判断是否到海洋
if (grid[r][c] == 0) return 0;
//为了确保每个土地访问不超过一次,我们每次经过一块土地时,将这块土地的值置为 0。这样我们就不会多次访问同一土地。
grid[r][c] = 0;
int x, y, area = 1;
for (int i = 0; i < 4; ++i) {
//遍历一次,陆地加一
x = r + direction[i], y = c + direction[i + 1];
if (x >= 0 && x < grid.size() && y >= 0 && y < grid[0].size()) {
area += dfs(grid, x, y);
}
}
return area;
}
一种是不管三七二十一先进行下一步搜索,待下一步搜索开始时再判断是否合法(即判断放在辅函数第一行)。
// 主函数
int maxAreaOfIsland(vector<vector<int>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int max_area = 0;
for (int i = 0; i < grid.size(); ++i) {
for (int j = 0; j < grid[0].size(); ++j) {
max_area = max(max_area, dfs(grid, i, j));
}
}
return max_area;
}
// 辅函数
int dfs(vector<vector<int>>& grid, int r, int c) {
if (r < 0 || r >= grid.size() ||
c < 0 || c >= grid[0].size() || grid[r][c] == 0) {
return 0;
}
grid[r][c] = 0;
return 1 + dfs(grid, r + 1, c) + dfs(grid, r - 1, c) +
dfs(grid, r, c + 1) + dfs(grid, r, c - 1);
}
栈
vector<int> direction{ -1, 0, 1, 0, -1 };
int pointers::maxAreaOfIsland(vector<vector<int>>& grid)
{
int m = grid.size(), n = m ? grid[0].size() : 0, local_area, area = 0, x, y;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j]) {
local_area = 1;
grid[i][j] = 0;
stack<pair<int, int>> island;
//Stack(栈)是一种后进先出的数据结构,使用STL的STACK需要的头文件
island.push({ i, j }); //入栈,注意是圆括号里面套花括号
while (!island.empty()) {
auto [r, c] = island.top(); //输出栈顶数据
island.pop(); //弹出栈顶数据
for (int k = 0; k < 4; ++k) {
x = r + direction[k], y = c + direction[k + 1];
if (x >= 0 && x < m &&
y >= 0 && y < n && grid[x][y] == 1) {
grid[x][y] = 0;
++local_area;
island.push({ x, y });
}
}
}
area = max(area, local_area);
}
}
}
return area;
}
547 省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。返回矩阵中 省份 的数量。
解:因为对称性,对角线全是1
书中最优解
int pointers::findCircleNum(vector<vector<int>>& isConnected)
{
int n = isConnected.size(), count = 0;
vector<bool> visited(n, false);
for (int i = 0; i < n; ++i) {
if (!visited[i]) {
dfs(isConnected, i, visited);
++count;
}
}
return count;
}
// 辅函数
void dfs(vector<vector<int>>& isConnected, int i, vector<bool>& visited) {
visited[i] = true; //表示已访问
for (int k = 0; k < isConnected.size(); ++k) {
if (isConnected[i][k] == 1 && !visited[k]) {
dfs(isConnected, k, visited);
}
}
}
417 太平洋大西洋流水问题
有一个 m × n
的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。
这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n
的整数矩阵 heights
, heights[r][c]
表示坐标 (r, c)
上单元格 高于海平面的高度 。
岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。
返回网格坐标 result
的 2D 列表 ,其中 result[i] = [ri, ci]
表示雨水从单元格 (ri, ci)
流动 既可流向太平洋也可流向大西洋 。
书中最优解
vector<int> direction{ -1, 0, 1, 0, -1 };
// 主函数
vector<vector<int>> pacificAtlantic(vector<vector<int>>& matrix) {
if (matrix.empty() || matrix[0].empty()) {
return {};
}
vector<vector<int>> ans;
int m = matrix.size(), n = matrix[0].size(); //m是行数,n是列数
vector<vector<bool>> can_reach_p(m, vector<bool>(n, false));
vector<vector<bool>> can_reach_a(m, vector<bool>(n, false));
for (int i = 0; i < m; ++i) {
dfs(matrix, can_reach_p, i, 0); //太平洋,第一列
dfs(matrix, can_reach_a, i, n - 1); //大西洋,最后一列
}
for (int i = 0; i < n; ++i) {
dfs(matrix, can_reach_p, 0, i); //太平洋,第一行
dfs(matrix, can_reach_a, m - 1, i); //大西洋,最后一行
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; ++j) {
if (can_reach_p[i][j] && can_reach_a[i][j]) { //既流向太平洋又流向大西洋
ans.push_back(vector<int>{i, j});
}
}
}
return ans;
}
// 辅函数
void dfs(const vector<vector<int>>& matrix, vector<vector<bool>>& can_reach,int r, int c) {
if (can_reach[r][c]) {
return;
}
can_reach[r][c] = true;
int x, y;
for (int i = 0; i < 4; ++i) {
x = r + direction[i]; //上下左右标
y = c + direction[i + 1];
if (x >= 0 && x < matrix.size()
&& y >= 0 && y < matrix[0].size() &&
matrix[r][c] <= matrix[x][y]) {
dfs(matrix, can_reach, x, y);
}
}
}
网页答案,辅函数稍作调整,执行用时更快
if (visited[x][y]) return;
visited[x][y] = true;
for (int i = 0; i < 4; i++) { // 向四个方向遍历
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
// 超过边界
if (nextx < 0 || nextx >= heights.size() || nexty < 0 || nexty >= heights[0].size()) continue;
// 高度不合适,注意这里是从低向高判断
if (heights[x][y] > heights[nextx][nexty]) continue;
dfs (heights, visited, nextx, nexty);
}
46 全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
网页答案 相对好理解
技术博客 http://www.cnblogs.com/itdef/
B站算法视频题解 https://space.bilibili.com/18508846
class Solution {
public:
vector<int> vis;
vector<vector<int>> ans;
vector<int> v;
void dfs(vector<int>& nums, int idx)
{
if (idx > nums.size()) return;
if (idx == nums.size()) {
ans.push_back(v);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (vis[i] == 0) {
v[idx] = nums[i];
vis[i] = 1;
dfs(nums, idx + 1);
vis[i] = 0;
v[idx] = -1;
}
}
return;
}
vector<vector<int>> permute(vector<int>& nums) {
int len = nums.size();
vis = vector<int>(len,0);
v = vector<int>(len, -1);
dfs(nums, 0);
return ans;
}
};
网页答案
链接:https ://leetcode.cn/problems/permutations/solutions/2570195/javapython3chui-su-fa-mei-ge-wei-zhi-tia-p9wp/
class Solution {
private:
/**
* 枚举当前nums中未使用过的元素nums[i],填入当前位置ans[idx]
* @param nums: 元素数组
* @param idx: 当前元素要填入的位置
* @param res: 结果数组
* @param ans:排列数组,用于生成一种排列的可能
* @param used:判断枚举的元素是否使用过
*/
void backtracking(vector<int>& nums, int idx, vector<vector<int>>& res, vector<int>& ans, vector<bool>& used) {
if (idx == nums.size()) {
// 填入位置到达终点,说明生成了一种排列可能,加入结果数组
res.emplace_back(ans);
return;
}
for (int i = 0; i < nums.size(); i++) {
// 枚举nums中的每个索引
if (used[i])continue; // 如果当前枚举的索引使用过了,则跳过
used[i] = true; // 标记当前索引已经使用
ans[idx] = nums[i]; // 将元素nums[i]填入当前位置idx
backtracking(nums, idx + 1, res, ans, used); // 处理后续位置
used[i] = false; // ans[idx]填入nums[i]的情况枚举完了,恢复nums[i]是未处理的状态【这样当这个位置填入其他元素时,后续位置还可以使用nums[i]】
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res; // 结果列表
int n = nums.size();
vector<int> ans(n); // 用于生成一种排列的列表,长度为n
vector<bool> used(n); // 用于标记每个索引的元素是否使用过
backtracking(nums, 0, res, ans, used); // 从索引0的位置开始填入元素
return res;
}
};
书中最优解
// 主函数
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ans;
backtracking(nums, 0, ans);
return ans;
}
// 辅函数
void backtracking(vector<int>& nums, int level, vector<vector<int>>& ans) {
if (level == nums.size() - 1) { // 填入位置到达终点,说明生成了一种排列可能,加入结果数组
ans.push_back(nums); //减一是因为最后一个数用不着交换
return;
}
for (int i = level; i < nums.size(); i++) { //循环实现level后面每个元素的交换
swap(nums[i], nums[level]); // 修改当前节点状态,将元素[i]填入当前位置level
backtracking(nums, level + 1, ans); // 递归子节点
swap(nums[i], nums[level]); // 回改当前节点状态,一层层递归后到不交换为止,再换回原位
}
}
77 组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。你可以按 任何顺序 返回答案。
书中最优解
// 主函数
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> ans;
vector<int> comb(k, 0);
int count = 0;
backtracking(ans, comb, count, 1, n, k);
return ans;
}
// 辅函数
void backtracking(vector<vector<int>>& ans, vector<int>& comb, int& count, int
pos, int n, int k) {
if (count == k) {
ans.push_back(comb);
return;
}
for (int i = pos; i <= n; ++i) {
comb[count++] = i; // 修改当前节点状态
backtracking(ans, comb, count, i + 1, n, k); // 递归子节点
--count; // 回改当前节点状态
}
}
79 单词搜索
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
解:回溯+深度优先遍历
书中最优解
// 主函数
bool exist(vector<vector<char>>& board, string word) {
if (board.empty()) return false;
int m = board.size(), n = board[0].size();
vector<vector<bool>> visited(m, vector<bool>(n, false));
bool find = false;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
backtracking(i, j, board, word, find, visited, 0);
}
}
return find;
}
// 辅函数
void backtracking(int i, int j, vector<vector<char>>& board, string& word, bool
& find, vector<vector<bool>>& visited, int pos) {
// 越界、被访问过、当前位置的字符不是word对应位置的字符
if (i < 0 || i >= board.size() || j < 0 || j >= board[0].size()) {
return;
}
if (visited[i][j] || find || board[i][j] != word[pos]) {
return;
}
if (pos == word.size() - 1) { //最后一个字符
find = true;
return;
}
visited[i][j] = true; // 修改当前节点状态
// 递归子节点
backtracking(i + 1, j, board, word, find, visited, pos + 1);
backtracking(i - 1, j, board, word, find, visited, pos + 1);
backtracking(i, j + 1, board, word, find, visited, pos + 1);
backtracking(i, j - 1, board, word, find, visited, pos + 1);
visited[i][j] = false; // 回改当前节点状态
}
网页答案
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
rows = board.size();
cols = board[0].size();
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
if (dfs(board, word, i, j, 0)) return true;
}
}
return false;
}
private:
int rows, cols;
bool dfs(vector<vector<char>>& board, string word, int i, int j, int k) {
if (i >= rows || i < 0 || j >= cols || j < 0 || board[i][j] != word[k]) return false;
if (k == word.size() - 1) return true;
board[i][j] = '\0';
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] = word[k];
return res;
}
};
作者:Krahets
链接:https://leetcode.cn/problems/word-search/solutions/
51 皇后
书中最优解
// 主函数
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> ans;
if (n == 0) {
return ans;
}
vector<string> board(n, string(n, ’.’));
vector<bool> column(n, false), ldiag(2 * n - 1, false), rdiag(2 * n - 1, false);
backtracking(ans, board, column, ldiag, rdiag, 0, n);
return ans;
}
// 辅函数
void backtracking(vector<vector<string>>& ans, vector<string>& board, vector<
bool>& column, vector<bool>& ldiag, vector<bool>& rdiag, int row, int n) {
if (row == n) {
ans.push_back(board);
return;
}
for (int i = 0; i < n; ++i) {
if (column[i] || ldiag[n - row + i - 1] || rdiag[row + i + 1]) {
continue;
}
// 修改当前节点状态
board[row][i] = ’Q’;
column[i] = ldiag[n - row + i - 1] = rdiag[row + i + 1] = true;
// 递归子节点
backtracking(ans, board, column, ldiag, rdiag, row + 1, n);
// 回改当前节点状态
board[row][i] = ’.’;
column[i] = ldiag[n - row + i - 1] = rdiag[row + i + 1] = false;
}
}
934最短的桥
给你一个大小为 n x n
的二元矩阵 grid
,其中 1
表示陆地,0
表示水域。
岛 是由四面相连的 1
形成的一个最大组,即不会与非组内的任何其他 1
相连。grid
中 恰好存在两座岛 。你可以将任意数量的 0
变为 1
,以使两座岛连接起来,变成 一座岛 。
返回必须翻转的 0
的最小数目。
解:先深度优先遍历找其中一个岛,然后广度优先遍历找到另一个岛
书中最优解
vector<int> direction{ -1, 0, 1, 0, -1 };
// 主函数
int shortestBridge(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> points;
// dfs寻找第一个岛屿,并把1全部赋值为2
bool flipped = false;
for (int i = 0; i < m; ++i) {
if (flipped) break;
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
dfs(points, grid, m, n, i, j);
flipped = true;
break;
}
}
}
// bfs寻找第二个岛屿,并把过程中经过的0赋值为2
int x, y;
int level = 0;
while (!points.empty()) {
++level;
int n_points = points.size();
while (n_points--) {
auto [r, c] = points.front();
points.pop();
for (int k = 0; k < 4; ++k) {
x = r + direction[k], y = c + direction[k + 1];
if (x >= 0 && y >= 0 && x < m && y < n) {
if (grid[x][y] == 2) {
continue;
}
if (grid[x][y] == 1) {
return level;
}
points.push({ x, y });
grid[x][y] = 2;
}
}
}
}
return 0;
}
// 辅函数
void dfs(queue<pair<int, int>>& points, vector<vector<int>>& grid, int m, int n, int i, int j) {
if (i < 0 || j < 0 || i == m || j == n || grid[i][j] == 2) {
return;
}
if (grid[i][j] == 0) {
points.push({ i, j });
return;
}
grid[i][j] = 2;
dfs(points, grid, m, n, i - 1, j);
dfs(points, grid, m, n, i + 1, j);
dfs(points, grid, m, n, i, j - 1);
dfs(points, grid, m, n, i, j + 1);
}
网页答案
class Solution {
public:
void dfs(vector<vector<int>>& A, vector<pair<int,int>>& tmp, int r, int c)
{
if(r-1>=0 && 1==A[r-1][c])
{
A[r-1][c]=2;
tmp.push_back(make_pair(r-1,c));
dfs(A,tmp,r-1,c);
}
if(r+1<A.size() && 1==A[r+1][c])
{
A[r+1][c]=2;
tmp.push_back(make_pair(r+1,c));
dfs(A,tmp,r+1,c);
}
if(c-1>=0 && 1==A[r][c-1])
{
A[r][c-1]=2;
tmp.push_back(make_pair(r,c-1));
dfs(A,tmp,r,c-1);
}
if(c+1<A[0].size() && 1==A[r][c+1])
{
A[r][c+1]=2;
tmp.push_back(make_pair(r,c+1));
dfs(A,tmp,r,c+1);
}
}
int shortestBridge(vector<vector<int>>& A)
{
int m=A.size();
int n=A[0].size();
int flag=0;
vector<pair<int,int>> tmp;
int res=0;
for(int i=0;i<m;i++)
{
if(flag)
{
break;
}
for(int j=0;j<n;j++)
{
if(1==A[i][j])
{
tmp.push_back(make_pair(i,j));
A[i][j]=2;
dfs(A,tmp,i,j);
flag=1;
break;
}
}
}
while(flag)
{
vector<pair<int,int>> vec;
for(auto it:tmp)
{
int r=it.first;
int c=it.second;
if(r-1>=0)
{
if(1==A[r-1][c])
{
flag=0;
break;
}
else if(0==A[r-1][c])
{
A[r-1][c]=2;
vec.push_back(make_pair(r-1,c));
}
}
if(r+1<A.size())
{
if(1==A[r+1][c])
{
flag=0;
break;
}
else if(0==A[r+1][c])
{
A[r+1][c]=2;
vec.push_back(make_pair(r+1,c));
}
}
if(c-1>=0)
{
if(1==A[r][c-1])
{
flag=0;
break;
}
else if(0==A[r][c-1])
{
A[r][c-1]=2;
vec.push_back(make_pair(r,c-1));
}
}
if(c+1<A[0].size())
{
if(1==A[r][c+1])
{
flag=0;
break;
}
else if(0==A[r][c+1])
{
A[r][c+1]=2;
vec.push_back(make_pair(r,c+1));
}
}
}
if(flag)
{
res++;
}
tmp=vec; //不理解
}
return res;
}
};
126 单词接龙II
// 主函数
vector<vector<string>> findLadders(string beginWord, string endWord, vector<
string>& wordList) {
vector<vector<string>> ans;
unordered_set<string> dict;
for (const auto& w : wordList) {
dict.insert(w);
}
if (!dict.count(endWord)) {
return ans;
}
dict.erase(beginWord);
dict.erase(endWord);
unordered_set<string> q1{ beginWord }, q2{ endWord };
unordered_map<string, vector<string>> next;
bool reversed = false, found = false;
while (!q1.empty()) {
unordered_set<string> q;
for (const auto& w : q1) {
string s = w;
for (size_t i = 0; i < s.size(); i++) {
char ch = s[i];
for (int j = 0; j < 26; j++) {
s[i] = j + ’a’;
if (q2.count(s)) {
reversed ? next[s].push_back(w) : next[w].push_back(s);
found = true;
}
if (dict.count(s)) {
reversed ? next[s].push_back(w) : next[w].push_back(s);
q.insert(s);
}
}
s[i] = ch;
}
}
if (found) {
break;
}
for (const auto& w : q) {
dict.erase(w);
}
if (q.size() <= q2.size()) {
q1 = q;
}
else {
reversed = !reversed;
q1 = q2;
q2 = q;
}
}
if (found) {
vector<string> path = { beginWord };
backtracking(beginWord, endWord, next, path, ans);
}
return ans;
}
// 辅函数
void backtracking(const string& src, const string& dst, unordered_map<string,
vector<string>> &next, vector<string>& path, vector<vector<string>>& ans) {
if (src == dst) {
ans.push_back(path);
return;
}
for (const auto& s : next[src]) {
path.push_back(s);
backtracking(s, dst, next, path, ans);
path.pop_back();
}
}