算法 : 深度优先搜索(DFS)、广度优先搜索(BFS)
深度优先搜索(DFS)
深度优先搜索的核心思想就是一条道走到黑,其实就和二叉树的前、中、后序遍历一样,都会先一直沿着一个方向走,当走不通了再往回找其他的路。
//模板
DFS(当前这一步的处理逻辑)
{
1. 判断边界,是否已经一条道走到黑了:向上回退
2. 尝试当下的每一种可能
3. 确定一种可能之后,继续下一步 Dfs(下一步)
}
员工的重要性
给定一个保存员工信息的数据结构,它包含了员工唯一的id,重要度 和 直系下属的id。
比如,员工1是员工2的领导,员工2是员工3的领导。他们相应的重要度为15, 10, 5。那么员工1的数据结构是[1, 15, [2]],员工2的数据结构是[2, 10, [3]],员工3的数据结构是[3, 5, []]。注意虽然员工3也是员工1的一个下属,但是由于并不是直系下属,因此没有体现在员工1的数据结构中。
现在输入一个公司的所有员工信息,以及单个员工id,返回这个员工和他所有下属的重要度之和。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/employee-importance
/*
Definition for Employee.
class Employee {
public:
int id;
int importance;
vector<int> subordinates;
};
*/
class Solution {
public:
void DFS(unordered_map<int, Employee*>& info, int& sum, int id)
{
//加上当前重要度
sum += info[id]->importance;
//继续搜索所有下属员工的重要度
for(auto subId : info[id]->subordinates)
{
DFS(info, sum, subId);
}
}
int getImportance(vector<Employee*> employees, int id) {
//用哈希表进行存储方便查询
unordered_map<int, Employee*> info;
for(const auto& ep : employees)
{
info[ep->id] = ep;
}
int sum = 0;
DFS(info, sum, id);
return sum;
}
};
图像渲染
有一幅以二维整数数组表示的图画,每一个整数表示该图画的像素值大小,数值在 0 到 65535 之间。
给你一个坐标 (sr, sc) 表示图像渲染开始的像素值(行 ,列)和一个新的颜色值 newColor,让你重新上色这幅图像。
为了完成上色工作,从初始坐标开始,记录初始坐标的上下左右四个方向上像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应四个方向上像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为新的颜色值。
最后返回经过上色渲染后的图像。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/flood-fill
这道题的思路很简单,就是从标记点开始往四周进行搜索,如果搜索的位置符合要求,则进行染色
例如下图
class Solution {
public:
//向量矩阵
int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
void DFS(vector<vector<int>>& image, vector<vector<bool>>& visited,
int row, int col, int sr, int sc, int oldColor, int newColor)
{
//染色并进行标记
image[sr][sc] = newColor;
visited[sr][sc] = false;
//向上下左右四个方向进行渲染
for(int i = 0; i < 4; i++)
{
int newRow = sr + nextPos[i][0];
int newCol = sc + nextPos[i][1];
//如果越界则跳过
if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col)
{
continue;
}
//如果该位置为老颜色,并且没有渲染过则进行渲染
if(image[newRow][newCol] == oldColor && visited[newRow][newCol])
{
DFS(image, visited, row, col, newRow, newCol, oldColor, newColor);
}
}
}
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
if(image.empty() || image[0].empty())
{
return image;
}
int row = image.size(), col = image[0].size();
vector<vector<bool>> visited(row, vector<bool>(col, true)); //标记矩阵
int oldColor = image[sr][sc];
DFS(image, visited, row, col, sr, sc, oldColor, newColor);
return image;
}
};
岛屿的周长
给定一个包含 0 和 1 的二维网格地图,其中 1 表示陆地 0 表示水域。
网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/island-perimeter
这道题是图像渲染的类似问题,这道题的核心就是首先找到一个陆地的位置,顺着陆地的位置进行搜索,如果搜索到边界或者海洋则进行计数即可。
为了方便理解我直接套用了上题的模板进行修改,但是这种方法效率并不高(但是能过),如果追求效率还可以看下面的方法。
class Solution {
public:
//向量矩阵
int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
void DFS(vector<vector<int>>& grid, vector<vector<bool>>& visited, int row, int col,
int& count, int sr, int sc)
{
visited[sr][sc] = false;
for(int i = 0; i < 4; i++)
{
int newRow = sr + nextPos[i][0];
int newCol = sc + nextPos[i][1];
//如果为边界或者为水域,则计数后跳过
if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col || grid[newRow][newCol] == 0)
{
count++;
continue;
}
if(grid[newRow][newCol] == 1 && visited[newRow][newCol])
{
DFS(grid, visited, row, col, count, newRow, newCol);
}
}
}
int islandPerimeter(vector<vector<int>>& grid) {
if(grid.empty() || grid[0].empty())
{
return 0;
}
int row = grid.size(), col = grid[0].size();
vector<vector<bool>> visited(row, vector<bool>(col, true));
int count = 0;
//找到一个水池的边界,沿着边界搜索
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
if(visited[i][j] && grid[i][j] == 1)
{
DFS(grid, visited, row, col, count, i, j);
break;
}
}
}
return count;
}
};
简化版本
class Solution {
public:
int islandPerimeter(vector<vector<int>>& grid) {
if(grid.empty() || grid[0].empty())
{
return 0;
}
int row = grid.size(), col = grid[0].size();
int count = 0;
for(int i = 0; i < grid.size(); i++)
{
for(int j = 0; j < grid[0].size(); j++)
{
//如果为岛屿则搜索边界与海洋
if(grid[i][j] == 1)
{
if(i == 0 || grid[i - 1][j] == 0) count++; //向上搜索
if(i + 1 == row || grid[i + 1][j] == 0) count++; //向下搜索
if(j == 0 || grid[i][j - 1] == 0) count++; //向左搜索
if(j + 1 == col || grid[i][j + 1] == 0) count++; //向右搜索
}
}
}
return count;
}
};
被围绕的区域
给定一个二维的矩阵,包含
'X'
和'O'
(字母 O)。找到所有被
'X'
围绕的区域,并将这些区域里所有的'O'
用'X'
填充。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/surrounded-regions/
没错,这还是渲染的变形。
从题目可以得出,我们需要将所有与边界接壤的保留下来,而将内部独立的小岛全部渲染成X。
如果直接渲染小岛这个题目就变得非常麻烦,因为还需要对边界进行处理。
所以我们可以换一个思路,从边界下手,将所有与边界接壤的岛屿全部渲染为一个临时值Y,此时被渲染为Y的陆地就被隔离开来,而剩下没有被渲染为Y的陆地就是完全独立的岛屿,我们只需要将这一部分岛屿全部变为X即可。之后再将我们渲染为临时值Y的边界给恢复为O即可。
class Solution {
public:
//向量矩阵
int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
void DFS(vector<vector<char>>& board, vector<vector<bool>>& visited,
int row, int col, int sr, int sc)
{
//将边界上的全部渲染为Y
board[sr][sc] = 'Y';
visited[sr][sc] = false;
//向上下左右四个方向进行搜索
for(int i = 0; i < 4; i++)
{
int newRow = sr + nextPos[i][0];
int newCol = sc + nextPos[i][1];
//如果越界则跳过
if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col)
{
continue;
}
//渲染与边界相邻的位置
if(board[newRow][newCol] == 'O' && visited[newRow][newCol])
{
DFS(board, visited, row, col, newRow, newCol);
}
}
}
void solve(vector<vector<char>>& board) {
if(board.empty() || board[0].empty())
{
return;
}
int row = board.size(), col = board[0].size();
vector<vector<bool>> visited(row, vector<bool>(col, true)); //标记矩阵
//将与边界接壤的岛屿全部渲染为临时值Y
//寻找左右边界
for(int i = 0; i < row; i++)
{
//左边界
if(board[i][0] == 'O')
{
DFS(board, visited, row, col, i, 0);
}
//右边界
if(board[i][col - 1] == 'O')
{
DFS(board, visited, row, col, i, col - 1);
}
}
//寻找上下边界
for(int j = 0; j < col; j++)
{
//上边界
if(board[0][j] == 'O')
{
DFS(board, visited, row, col, 0, j);
}
//下边界
if(board[row - 1][j] == 'O')
{
DFS(board, visited, row, col, row - 1, j);
}
}
//状态更替
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
//将所有没有被渲染到的内部岛屿变为X
if(board[i][j] == 'O')
{
board[i][j] = 'X';
}
//恢复所有与边界接壤的岛屿
if(board[i][j] == 'Y')
{
board[i][j] = 'O';
}
}
}
}
};
岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-islands
我们发现,这道题其实还是渲染的那个模板,唯一不同的是变成了求岛屿的个数,也就是我们渲染的次数。
所以当我们找到一个陆地时,查询他是否被标记过,如果未被标记则开始进行一次渲染,然后计数。
class Solution {
public:
//向量矩阵
int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
void DFS(vector<vector<char>>& grid, vector<vector<bool>>& visited,
int row, int col, int sr, int sc)
{
visited[sr][sc] = false;
//向上下左右四个方向进行搜索
for(int i = 0; i < 4; i++)
{
int newRow = sr + nextPos[i][0];
int newCol = sc + nextPos[i][1];
//如果越界则跳过
if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col)
{
continue;
}
//如果该位置为老颜色,并且没有渲染过则进行渲染
if(grid[newRow][newCol] == '1' && visited[newRow][newCol])
{
DFS(grid, visited, row, col, newRow, newCol);
}
}
}
int numIslands(vector<vector<char>>& grid) {
if(grid.empty() || grid[0].empty())
{
return 0;
}
int row = grid.size(), col = grid[0].size();
vector<vector<bool>> visited(row, vector<bool>(col, true)); //标记矩阵
int count = 0;
//当找到陆地,并且没有被标记时,说明这是一个岛屿,进行搜索后,统计岛屿数量
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
if(grid[i][j] == '1' && visited[i][j])
{
count++;
DFS(grid, visited, row, col, i, j);
}
}
}
return count;
}
};
岛屿的最大面积
给定一个包含了一些 0 和 1 的非空二维数组 grid 。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/max-area-of-island
这道题是岛屿数量的变形,此时我们要求的是岛屿的最大面积,所以只需要统计每个岛屿的大小,然后再进行多次的搜索,选出最大的一个就行。本质就是多次渲染 + 大小统计
class Solution {
public:
//向量矩阵
int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
void DFS(vector<vector<int>>& grid, vector<vector<bool>>& visited, int& count,
int row, int col, int sr, int sc)
{
count++;
visited[sr][sc] = false;
//向上下左右四个方向进行搜索
for(int i = 0; i < 4; i++)
{
int newRow = sr + nextPos[i][0];
int newCol = sc + nextPos[i][1];
//如果越界则跳过
if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col)
{
continue;
}
//如果该位置为老颜色,并且没有渲染过则进行渲染
if(grid[newRow][newCol] == 1 && visited[newRow][newCol])
{
DFS(grid, visited, count, row, col, newRow, newCol);
}
}
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
if(grid.empty() || grid[0].empty())
{
return 0;
}
int row = grid.size(), col = grid[0].size();
vector<vector<bool>> visited(row, vector<bool>(col, true)); //标记矩阵
int max = 0;
//当找到陆地,并且没有被标记时,说明这是一个岛屿,进行搜索后,统计岛屿数量
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
if(grid[i][j] == 1 && visited[i][j])
{
int count = 0;
DFS(grid, visited, count, row, col, i, j);
max = max > count ? max : count;
}
}
}
return max;
}
};
广度优先搜索(BFS)
深度优先搜索的核心思想就是一石激起千层浪,就和二叉树的层序遍历一样,他会先尝试当前的所有可能,接着再尝试由当前步引出的下一步的所有可能,也就是从内向外,一环一环不断搜索的过程。
//模板
Bfs()
{
1. 建立起始步骤,队列初始化
2. 遍历队列中的每一种可能,whlie(队列不为空)
{
通过队头元素带出下一步的所有可能,并且依次入队
{
判断当前情况是否达成目标:按照目标要求处理逻辑
}
继续遍历队列中的剩余情况
}
}
这里就拿前面DFS第一题员工的重要性来进行一个示范
/*
// Definition for Employee.
class Employee {
public:
int id;
int importance;
vector<int> subordinates;
};
*/
class Solution {
public:
int getImportance(vector<Employee*> employees, int id) {
//用哈希表进行存储方便查询
unordered_map<int, Employee*> info;
for(const auto& ep : employees)
{
info[ep->id] = ep;
}
int sum = 0;
queue<int> q;
q.push(id); //队首入队
//当前的所有可能
while(!q.empty())
{
int cur = q.front();
q.pop();
sum += info[cur]->importance; //加上当前结果
//下一步的所有可能
for(auto subId : info[cur]->subordinates)
{
q.push(subId);
}
}
return sum;
}
};
N叉树的层序遍历
给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/
class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
if(root == nullptr)
{
return {};
}
vector<vector<int>> res;
queue<Node*> q;
q.push(root);
while(!q.empty())
{
vector<int> level; //当前层的所有结果
int size = q.size();
//遍历当前层
while(size--)
{
Node* cur = q.front();
q.pop();
level.push_back(cur->val);
//将下一层加入到队列末尾
for(const auto& sub : cur->children)
{
q.push(sub);
}
}
res.push_back(level);
}
return res;
}
};
腐烂的橘子
在给定的网格中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/rotting-oranges
从题目的得出,每分钟任何与腐烂橘子接触的橘子都会变腐烂,也就是从最开始的腐烂橘子开始,由它往它的周围进行扩散,直到所有的橘子被感染。
这个描述是不是和我们一开始说的BFS一模一样,而我们要求的是最小分钟数,所以这道题的题目其实就是让我们求BFS经历的次数。所以我们只需要在每轮BFS后进行判断,如果当前由橘子感染,则进行一次计数。
class Solution {
public:
int nextPos[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; //方向矩阵
int orangesRotting(vector<vector<int>>& grid) {
if(grid.empty() || grid[0].empty())
{
return 0;
}
int row = grid.size(), col = grid[0].size();
queue<pair<int, int>> q;
//将所有腐烂的橘子入队
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
if(grid[i][j] == 2)
{
q.push(make_pair(i, j));
}
}
}
int count = 0;
while(!q.empty())
{
bool flag = false;
int size = q.size();
//由已腐烂的橘子带入被感染的橘子
while(size--)
{
auto cur = q.front();
q.pop();
//本轮感染的橘子
for(int i = 0; i < 4; i++)
{
int newRow = cur.first + nextPos[i][0];
int newCol = cur.second + nextPos[i][1];
//如果越界则跳过
if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col)
{
continue;
}
//腐烂新鲜橘子
if(grid[newRow][newCol] == 1)
{
flag = true;
grid[newRow][newCol] = 2;
q.push(make_pair(newRow, newCol));
}
}
}
//如果有橘子腐烂才计数
if(flag)
{
count++;
}
}
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
//如果有任何一个新鲜橘子,则说明不可能完成
if(grid[i][j] == 1)
{
return -1;
}
}
}
return count;
}
};
单词接龙
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-ladder
要求最短路径,那么首先就能想到使用BFS,因为BFS每层相当于进行了一次变化的操作,而下一层都是在上一层的基础上完成了一次变化后得到的序列,所以每次得到符合条件的序列都是在上层一步转换得到的。
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
if(beginWord == endWord)
{
return 0;
}
unordered_set<string> dict(wordList.begin(), wordList.end()); //哈希映射,方便查找
unordered_set<string> visited; //标记矩阵
queue<string> q;
q.push(beginWord);
visited.insert(beginWord);
int res = 1; //序列长度包含原字符串
//用BFS必定能求出最小序列,因为BFS是一轮一轮从内往外,每层的结果都只进行了一次变化。
while(!q.empty())
{
int size = q.size();
//对上轮的结果进行一次变化
while(size--)
{
string curStr = q.front();
q.pop();
//对每个位一次变化
for(int i = 0; i < curStr.size(); i++)
{
string newStr = curStr; //副本
for(int j = 0; j < 26; j++)
{
newStr[i] = 'a' + j;
//如果访问过或者词典中不存在则跳过
if(visited.count(newStr) || !dict.count(newStr))
{
continue;
}
//如果得到最终结果,则返回res + 1(本轮)
if(newStr == endWord)
{
return res + 1;
}
q.push(newStr);
visited.insert(newStr);
}
}
}
res++;
}
return 0; //如果中间没返回,说明不存在
}
};
最小基因变化
一条基因序列由一个带有8个字符的字符串表示,其中每个字符都属于 “A”, “C”, “G”, "T"中的任意一个。
假设我们要调查一个基因序列的变化。一次基因变化意味着这个基因序列中的一个字符发生了变化。
例如,基因序列由"AACCGGTT" 变化至 “AACCGGTA” 即发生了一次基因变化。
与此同时,每一次基因变化的结果,都需要是一个合法的基因串,即该结果属于一个基因库。
现在给定3个参数 — start, end, bank,分别代表起始基因序列,目标基因序列及基因库,请找出能够使起始基因序列变化为目标基因序列所需的最少变化次数。如果无法实现目标变化,请返回 -1。
注意:
起始基因序列默认是合法的,但是它并不一定会出现在基因库中。
所有的目标基因序列必须是合法的。
假定起始基因序列与目标基因序列是不一样的。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-genetic-mutation
与上题解法相同。
class Solution {
public:
int minMutation(string start, string end, vector<string>& bank) {
if(start == end)
{
return 0;
}
unordered_set<string> set(bank.begin(), bank.end());
unordered_set<string> visited;
int res = 0;
queue<string> q;
q.push(start);
visited.insert(start);
while(!q.empty())
{
int size = q.size();
while(size--)
{
string curStr = q.front();
q.pop();
for(int i = 0; i < start.size(); i++)
{
string newStr = curStr;
for(int j = 0; j < 26; j++)
{
newStr[i] = 'A' + j;
if(visited.count(newStr) || !set.count(newStr))
{
continue;
}
if(newStr == end)
{
return res + 1;
}
q.push(newStr);
visited.insert(newStr);
}
}
}
res++;
}
return -1;
}
};
打开转盘锁
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以*旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/open-the-lock
与前两题类似,此时的限制从必须出现指定字符串变成了不能出现指定字符串,改变逻辑判断条件即可
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
unordered_set<string> set(deadends.begin(), deadends.end());
unordered_set<string> visited;
if(set.count("0000"))
{
return -1;
}
int res = 0;
queue<string> q;
q.push("0000");
visited.insert("0000");
while(!q.empty())
{
int size = q.size();
while(size--)
{
string curStr = q.front();
q.pop();
if(curStr == target)
{
return res;
}
for(int i = 0; i < target.size(); i++)
{
string newStr1 = curStr; //往前加
string newStr2 = curStr; //往后减
newStr1[i] = (newStr1[i] == '9') ? '0' : newStr1[i] + 1;
newStr2[i] = (newStr2[i] == '0') ? '9' : newStr2[i] - 1;
if(!visited.count(newStr1) && !set.count(newStr1))
{
q.push(newStr1);
visited.insert(newStr1);
}
if(!visited.count(newStr2) && !set.count(newStr2))
{
q.push(newStr2);
visited.insert(newStr2);
}
}
}
res++;
}
return -1;
}
};