目录
BFS解决FloodFill算法
FloodFill算法中文名是洪水灌溉, 本质是在矩阵中找到性质相同的联通块~
一、图像渲染
733. 图像渲染 - 力扣(LeetCode)https://leetcode.cn/problems/flood-fill/description/1.题目解析
给定起始位置,把与起始位置像素值相同的联通块全部修改成指定颜色, 重复该过程~
2.算法分析
bfs算法,从起始位置开始上下左右往外扩,将像素值相同的点颜色都进行修改,重复该过程
3.算法代码
class Solution {
typedef pair<int, int> PII;
//向量数组
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
public:
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color)
{
int prev = image[sr][sc];
if(prev == color) return image; //处理边界情况
int m = image.size(), n = image[0].size();
queue<PII> q;
q.push({sr, sc});
while(!q.empty())
{
auto [a, b] = q.front();
q.pop();
image[a][b] = color;
for(int i = 0; i < 4; i++) //向四周扩展
{
int x = a + dx[i], y = b + dy[i];
if(x >= 0 && x < m && y >= 0 && y <= n && image[x][y] == prev) //注意防越界
q.push({x, y});
}
}
return image;
}
};
二、岛屿数量
200. 岛屿数量 - 力扣(LeetCode)https://leetcode.cn/problems/number-of-islands/1.题目解析
求岛屿的数量, 岛屿是指被水包围的陆地联通块(0表示陆地,1表示水)
2.算法分析
从头开始遍历岛屿,遍历到一个陆地,就用bfs扩展出一个联通块,但是有1个问题,就是扩展到下一层的时候,依旧要上下左右遍历,就会重新扩展回去,因此可以用一个visit数组记录一下,遍历到陆地之后,如果是false, 将visit数组的该位置修改成true即可
3.算法代码
class Solution {
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
int m, n;
bool vis[301][301];
public:
int numIslands(vector<vector<char>>& grid) {
m = grid.size(), n = grid[0].size();
int ret = 0;
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(grid[i][j] == '1' && !vis[i][j])
{
ret++;
bfs(grid, i, j);
}
}
}
return ret;
}
void bfs(vector<vector<char>>& grid, int i, int j)
{
queue<pair<int, int>> q;
q.push({i, j});
vis[i][j] = true;
while(q.size())
{
auto [a, b] = q.front();
q.pop();
for(int k = 0; k < 4; k++)
{
int x = a + dx[k], y = b + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1' && !vis[x][y])
{
q.push({x, y});
vis[x][y] = true;
}
}
}
}
};
三、岛屿的最大面积
695. 岛屿的最大面积 - 力扣(LeetCode)https://leetcode.cn/problems/max-area-of-island/description/1.题目解析
求岛屿的最大面积(岛屿的概念相信题目二已经解释了)
2.算法分析
与题目二基本是完全一样的,区别就是在bfs函数中要求出每块岛屿的面积
3.算法代码
class Solution {
typedef pair<int, int> PII;
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, -1, 1};
bool vis[51][51] = {false};
int m, n;
public:
int maxAreaOfIsland(vector<vector<int>>& grid)
{
m = grid.size(), n = grid[0].size();
int ret = 0;
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(grid[i][j] == 1 && !vis[i][j])
ret = max(ret, bfs(grid, i, j));
}
}
return ret;
}
int bfs(vector<vector<int>>& grid, int i, int j)
{
int area = 0; //记录当前岛屿的面积
queue<PII> q;
q.push({i, j});
area++;
vis[i][j] = true;
while(!q.empty())
{
auto [a, b] = q.front();
q.pop();
for(int k = 0; k < 4; k++)
{
int x = a + dx[k], y = b + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1 && !vis[x][y])
{
q.push({x, y});
area++;
vis[x][y] = true;
}
}
}
return area;
}
};
四、被围绕的区域
130. 被围绕的区域 - 力扣(LeetCode)https://leetcode.cn/problems/surrounded-regions/1.题目解析
找到被'x'字符围绕的'o'区域, 将这些'o'改为'x', 注意边界上的o是不能被修改的,与边界'o'直接相连的'o'也是不能被修改的,因为这些'o'区域并不算被'x'字符给包围了
2.算法分析
本题直接使用bfs做是不太方便的,就是因为本题边界'o'的特殊情况
我们可以采用"正难则反"的策略来解决该问题~
先处理矩阵的四条边,把与边上的'o'相连的联通块全部都修改成'.', 然后把矩阵剩余的'o'联通块修改成'x'即可, 同时把边上的'.'联通块还原成 ‘o’ 即可
3.算法代码
class Solution {
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int m, n;
public:
void solve(vector<vector<char>>& board)
{
m = board.size(), n = board[0].size();
//1.先处理边界上的'O'连通块
for(int j = 0; j < n; j++)
{
if(board[0][j] == 'O') bfs(board, 0, j);
if(board[m-1][j] == 'O') bfs(board, m-1, j);
}
for(int i = 0; i < m; i++)
{
if(board[i][0] == 'O') bfs(board, i, 0);
if(board[i][n-1] == 'O') bfs(board, i, n-1);
}
//2.还原
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if(board[i][j] == 'O') board[i][j] = 'X';
else if(board[i][j] == '.') board[i][j] = 'O';
}
void bfs(vector<vector<char>>& board, int i, int j)
{
queue<pair<int, int>> q;
q.push({i, j});
board[i][j] = '.';
while (q.size())
{
auto [a, b] = q.front();
q.pop();
for(int k = 0; k < 4; k++)
{
int x = a + dx[k], y = b + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'O')
{
q.push({x, y});
board[x][y] = '.';
}
}
}
}
};
BFS解决单源最短路问题
下面几道题目都是边权为1的最短路问题,边权为1(边权一样的最短路问题也归到该类问题) 是最短路问题的一种,所以代码都是大同小异的!
ps: bfs解决最短路问题都是只能解决边权为1的最短路问题~
解法: 从起点开始来一次bfs即可
一、迷宫中离入口最近的出口
1926. 迷宫中离入口最近的出口 - 力扣(LeetCode)https://leetcode.cn/problems/nearest-exit-from-entrance-in-maze/1.题目解析
给定起始点,'+'表示墙,'.'表示空格,位于边上的'.'都是出口,求从起点到出口的最短步数
ps: 位于边上的起始点不能算出口
2.算法分析
bfs解决最短路问题
3.算法代码
class Solution {
//向量数组
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
public:
int nearestExit(vector<vector<char>>& maze, vector<int>& e)
{
int m = maze.size(), n = maze[0].size();
bool visit[m][n]; //记录遍历过的点
memset(visit, 0, sizeof(visit)); //将visit数组初始化成false
queue<pair<int, int>> q;
q.push({e[0], e[1]}); //让入口点入队列
visit[e[0]][e[1]] = true;
int step = 0; //记录扩展的层数,也就是从起点到出口的最短距离
while(!q.empty())
{
step++;
int sz = q.size();
for(int i = 0; i < sz; i++) //让sz个元素出队列
{
auto [a, b] = q.front();
q.pop();
for(int j = 0; j < 4; j++)
{
int x = a + dx[j], y = b + dy[j];
if(x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == '.' && !visit[x][y])
{
if(x == 0 || x == m-1 || y == 0 || y == n-1) //判断是否到达出口
return step;
q.push({x, y});
visit[x][y] = true;
}
}
}
}
return -1;
}
};
二、最小基因变化
433. 最小基因变化 - 力扣(LeetCode)https://leetcode.cn/problems/minimum-genetic-mutation/1.题目解析
给定起始基因序列和最终基因序列(都是八个字符, 从"ACGT"挑选), 每次变化只能变一个字符, 变成的字符依旧是"ACGT", 求最少变化次数
2.算法分析
将问题转化成边权为1的最短路问题
1.把起始基因序列看成起点, 把最终基因序列看成终点, 本质就是求起点到终点的最短路
2.由于变化后的基因序列要在基因库中,因此我们可以提前把基因序列加入哈希表中
3.求变化后的字符串,可以先定义字符串string change = "ACGT", 然后将队头字符串每个字符改成change中的字符即可,但是要注意需要先保存队头字符串,否则队头字符串发生的就不是一次基因变换了!
3.算法代码
class Solution
{
public:
int minMutation(string startGene, string endGene, vector<string>& bank)
{
unordered_set<string> hash(bank.begin(), bank.end()); //将基因库中的基因序列存入哈希表中
unordered_set<string> visit; //记录变化过的基因序列
string change = "ACGT";
if(startGene == endGene) return 0;
if(!hash.count(endGene)) return -1;
queue<string> q;
q.push(startGene);
visit.insert(startGene);
int ret = 0;
while(q.size())
{
ret++;
int sz = q.size();
while(sz--)
{
string t = q.front();
q.pop();
for(int i = 0; i < 8; i++)
{
string tmp = t;
for(int j = 0; j < 4; j++)
{
tmp[i] = change[j];
if(hash.count(tmp) && !visit.count(tmp))
{
if(tmp == endGene) return ret;
q.push(tmp);
visit.insert(tmp);
}
}
}
}
}
return -1;
}
};
三、单词接龙
127. 单词接龙 - 力扣(LeetCode)https://leetcode.cn/problems/word-ladder/1.题目解析
本题和题目二本质一样,不过求的是变化过程中的单词数目, 所以要给变化的次数+1
2.算法分析
注意与题目二的不同即可,比如无法变成最终单词返回0,起始单词和最终单词相等返回1
3.算法代码
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList)
{
unordered_set<string> hash(wordList.begin(), wordList.end());
unordered_set<string> visit;
if(beginWord == endWord) return 1;
if(!hash.count(endWord)) return 0;
queue<string> q;
q.push(beginWord);
visit.insert(beginWord);
int ret = 1;
while(!q.empty())
{
ret++;
int sz = q.size();
while(sz--)
{
string t = q.front();
q.pop();
for(int i = 0; i < t.size(); i++)
{
string tmp = t;
for(char ch = 'a'; ch <= 'z'; ch++)
{
tmp[i] = ch;
if(hash.count(tmp) && !visit.count(tmp))
{
if(tmp == endWord) return ret;
q.push(tmp);
visit.insert(tmp);
}
}
}
}
}
return 0;
}
};
四、为高尔夫比赛砍树
675. 为高尔夫比赛砍树 - 力扣(LeetCode)https://leetcode.cn/problems/cut-off-trees-for-golf-event/1.题目解析
给定一个矩阵,1表示地面,0表示障碍物,大于1的数字表示树的高度,从左上角开始砍树,每次可以上下左右四个方向移动,每次砍到一棵树后该位置就变成地面,但是要求砍树的顺序是树高从小到大,求砍完所有树需要的最少步骤
2.算法分析
问题的本质就是若干次迷宫问题(题目一), 因为要按照树的高度砍树,所以我们需要提前把树的位置(横纵坐标)按照树高从小到大存入到vector中, 问题就转化成了从起点到vector中的每个位置(终点位置)的最短路问题(dfs), 一次dfs之后,将起始位置更新成刚才的终点位置即可
3.算法代码
class Solution {
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
int m, n;
public:
int cutOffTree(vector<vector<int>>& f)
{
//1.确定砍树顺序
m = f.size(), n = f[0].size();
vector<pair<int, int>> trees;
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(f[i][j] > 1) trees.push_back({i, j});
sort(trees.begin(), trees.end(), [&](const pair<int, int>& p1, const pair<int, int>& p2)
{
return f[p1.first][p1.second] < f[p2.first][p2.second];
});
//2.按照顺序砍树, 转化成若干次迷宫问题
int bx = 0, by = 0; //起始位置
int ret = 0; //最终结果
for(auto& [a, b] : trees)
{
int step = bfs(f, bx, by, a, b); //step记录每一次bfs的结果
if(step == -1) return -1;
ret += step;
bx = a, by = b; //更新起始位置
}
return ret;
}
int bfs(vector<vector<int>>& f, int bx, int by, int ex, int ey) //ex, ey记录最终位置
{
if(bx == ex && by == ey) return 0;
queue<pair<int, int>> q;
bool visit[51][51] = {false};
q.push({bx, by});
visit[bx][by] = true;
int step = 0;
while(!q.empty())
{
step++;
int sz = q.size();
for(int i = 0; i < sz; i++)
{
auto [a, b] = q.front();
q.pop();
for(int j = 0; j < 4; j++)
{
int x = a + dx[j], y = b + dy[j];
if(x >= 0 && x < m && y >= 0 && y < n && f[x][y] != 0 && !visit[x][y])
{
if(x == ex && y == ey) return step;
q.push({x, y});
visit[x][y] = true;
}
}
}
}
return -1;
}
};
BFS解决多源最短路问题
单源最短路问题是指只有1个起点和一个终点,而多源最短路问题是有若干个起点的,终点也是只有1个, 求某个起点到终点的最短路~
解法:
1.暴力解法: 从每个起点到终点都进行单源bfs, 求这些路中的最短路(大概率超时)
2.把所有的"源点"变成一个"源点", 问题就转化成了单一的"单源最短路"问题, 一次bfs就解决问题
感性理解: A往外扩会先扩到B, C这一层,因此从A出发到H一定不是最短路,因此我们可以将A, B, C全部等效成一个点即可
一、01矩阵
542. 01 矩阵 - 力扣(LeetCode)https://leetcode.cn/problems/01-matrix/description/1.题目解析
给定一个01矩阵,返回一个同等规模的矩阵,每个元素是原矩阵的该位置离最近的0的距离
2.算法分析
解法一:一个位置一个位置求(超时)
解法二: 多源BFS+正难则反
如果直接用多源BFS, 也就是把所有的1看成一个点,找到离最近的0的距离,但问题是无法知道该距离是哪一个1到0的最短距离,而且无法退回去填表,因此采用正难则反的策略
把所有的0当成起点,1当成终点,分为两步:
1.将所有值为0的位置(横纵坐标)加入到队列中
2.一层一层的向外扩展即可
ps:单源最短路中,我们用到了visit矩阵标记元素是否访问过,step变量记录扩展的层数,size变量记录每次多少元素出队列,而本题中这三个都可以不要,都可以借助最终要求的dist矩阵来解决, 把dist矩阵初始化成-1,表示元素没有访问过,而step和sz变量也都不需要了,因为我们一边扩展,一边填dist矩阵,dist矩阵中存储了扩展到第几层了并且记录了从起点到该位置的最短记录,因此我们并不需要一层一层往外扩展,也就不需要size变量与step变量了~
3.算法代码
class Solution
{
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, -1, 1};
public:
vector<vector<int>> updateMatrix(vector<vector<int>>& mat)
{
int m = mat.size(), n = mat[0].size();
vector<vector<int>> dist(m, vector<int>(n, -1));
queue<pair<int, int>> q;
//1.将所有的源点加入到队列中
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(mat[i][j] == 0)
{
q.push({i, j});
dist[i][j] = 0;
}
}
}
//2.往外扩展
while(q.size())
{
auto [a, b] = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
int x = a + dx[i], y = b + dy[i];
if(x >= 0 && x < m && y >= 0 && y < n && dist[x][y] == -1)
{
dist[x][y] = dist[a][b] + 1; //dist[a][b]已经存储了从起始位置到a, b的最短距离,而[a, b]可以一步到[x, y], 因此直接+1即可
q.push({x, y});
}
}
}
return dist;
}
};
二、飞地的数量
1020. 飞地的数量 - 力扣(LeetCode)https://leetcode.cn/problems/number-of-enclaves/1.题目解析
给定矩阵单元格,1表示陆地,0表示海洋, 每次可以上下左右移动一格, 求无法走出整个矩阵单元格的陆地的数量
2.算法分析
解法一: 一个位置一个位置去判断,也就是对于每个1都进行bfs, 看是否能走出单元格(超时)
优化: 利用visit数组,从某个位置出发进行bfs, 如果能走出单元格,那就再从刚才的位置来一次bfs, 将遍历到的值为1的单元格标记一下; 如果不能走出单元格,那就再从刚才的位置来一次bfs, 将遍历到的值为1的单元格用另一种方式标记一下;最终再遍历一次原始矩阵,统计值为1且没有被访问的单元格数量即可!
缺陷: 要进行两次bfs, 并且代码不太一样
解法二: 正难则反 + 多源bfs
从边界的1开始进行多源bfs,用数组visit标记访问过的单元格,多源bfs完成之后,所有和边界1相连的单元格的visit数组对应位置都被标记成了true, 此时再遍历一遍原始矩阵,统计值为1且没有被访问的单元格数量即可!
3.算法代码
解法二:
class Solution
{
bool visit[501][501];
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
public:
int numEnclaves(vector<vector<int>>& grid)
{
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> q;
//1.将边界的1加入队列
for(int j = 0; j < n; j++)
{
if(grid[0][j] == 1) //第1行
{
q.push({0, j});
visit[0][j] = true;
}
if(grid[m-1][j] == 1) //最后一行
{
q.push({m-1, j});
visit[m-1][j] = true;
}
}
for(int i = 0; i < m; i++)
{
if(grid[i][0] == 1) //第一列
{
q.push({i, 0});
visit[i][0] = true;
}
if(grid[i][n-1] == 1) //最后一列
{
q.push({i, n-1});
visit[i][n-1] = true;
}
}
//2.多源bfs
while(!q.empty())
{
auto [a, b] = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
int x = a + dx[i], y = b + dy[i];
if(x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1 && !visit[x][y])
{
q.push({x, y});
visit[x][y] = true;
}
}
}
//3.统计结果
int ret = 0;
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(grid[i][j] == 1 && !visit[i][j])
ret++;
return ret;
}
};
三、地图中的最高点
1765. 地图中的最高点 - 力扣(LeetCode)https://leetcode.cn/problems/map-of-highest-peak/1.题目解析
给定矩阵,1代表水域,0代表陆地,现在要求我们给每个格子设定高度,要求水域高度为0,相邻的格子(上下左右)高度差不超过1,求最终安排的矩阵中的的最高单元格的高度
2.算法分析
由于水域高度是固定的,因此我们可以先把水域高度都填成0,然后向外扩展,由于最终要高度最高,因此向外扩展时,把所有的单元格高度都设置成1即可,继续向外扩展,设置成2即可,因此本题解法也是多源bfs, 所以本题的代码和题目一"01矩阵"基本一模一样
3.算法代码
class Solution {
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
public:
vector<vector<int>> highestPeak(vector<vector<int>>& isWater)
{
int m = isWater.size(), n = isWater[0].size();
vector<vector<int>> dist(m, vector<int>(n, -1));
queue<pair<int, int>> q;
//1.将所有的源点加入到队列中
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(isWater[i][j] == 1)
{
dist[i][j] = 0;
q.push({i, j});
}
}
}
//2.多源bfs
while(q.size())
{
auto [a, b] = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
int x = a + dx[i], y = b + dy[i];
if(x >= 0 && x < m && y >=0 && y < n && dist[x][y] == -1)
{
dist[x][y] = dist[a][b] + 1;
q.push({x, y});
}
}
}
return dist;
}
};
四、地图分析
1162. 地图分析 - 力扣(LeetCode)https://leetcode.cn/problems/as-far-from-land-as-possible/1.题目解析
给定矩阵,0表示海洋,1表示陆地,求海洋距离最近的陆地的最大距离
2.算法分析
可以从陆地(填成0)出发,向外扩展(+1), 返回扩展结束的数字即可,本质是bfs问题, 所以本题的代码和题目一"01矩阵"基本一模一样
3.算法代码
class Solution {
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
public:
int maxDistance(vector<vector<int>>& grid)
{
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dist(m, vector<int>(n, -1));
queue<pair<int, int>> q;
//1.将所有的源点加入到队列中
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(grid[i][j] == 1)
{
dist[i][j] = 0;
q.push({i, j});
}
}
}
//2.多源bfs
int ret = -1; //保存最大值
while(q.size())
{
auto [a, b] = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
int x = a + dx[i], y = b + dy[i];
if(x >= 0 && x < m && y >=0 && y < n && dist[x][y] == -1)
{
dist[x][y] = dist[a][b] + 1;
q.push({x, y});
ret = max(ret, dist[x][y]);
}
}
}
return ret;
}
};
五、腐烂的苹果
0表示格子为空,1表示苹果完好,2表示苹果腐烂, 每一分钟腐烂的苹果都会将四周扩展,将完好的苹果变腐烂,问
2.算法分析
多源bfs求最短路即可
法一: 每次向外扩展后腐烂一个苹果,就把该苹果位置的值修改为2
法二:不修改原始矩阵,定义bool矩阵标记扩展过程中腐烂的苹果
3.算法代码
法一:
class Solution {
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, -1, 1};
public:
int rotApple(vector<vector<int>>& grid)
{
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> q;
//1.将所有的源点加入到队列中
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(grid[i][j] == 2)
q.push({i, j});
//2.bfs
int ret = 0; //记录扩展了多少层
while(q.size())
{
ret++;
int sz = q.size();
for(int i = 0;i < sz; i++)
{
auto [a, b] = q.front();
q.pop();
for(int j = 0; j < 4; j++)
{
int x = a + dx[j], y = b + dy[j];
if(x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1)
{
grid[x][y] = 2;
q.push({x, y});
}
}
}
}
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(grid[i][j] == 1)
return -1;
return ret - 1; //注意,最后相当于多扩展了一层,因此要-1
}
};
法二:
class Solution {
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, -1, 1};
bool visit[1001][1001];
public:
int rotApple(vector<vector<int>>& grid)
{
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> q;
//1.将所有的源点加入到队列中
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(grid[i][j] == 2)
q.push({i, j});
//2.bfs
int ret = 0; //记录扩展了多少层
while(q.size())
{
ret++;
int sz = q.size();
for(int i = 0;i < sz; i++)
{
auto [a, b] = q.front();
q.pop();
for(int j = 0; j < 4; j++)
{
int x = a + dx[j], y = b + dy[j];
if(x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1 && !visit[x][y])
{
visit[x][y] = true;
q.push({x, y});
}
}
}
}
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(grid[i][j] == 1 && !visit[i][j])
return -1;
return ret - 1; //注意,最后相当于多扩展了一层,因此要-1
}
};
BFS解决拓扑排序问题
在分享下面几道题目前,应该先弄懂几个概念: 有向无环图(DAG图), AOV网(是DAG图的具体应用,点表示活动,边的方向表示活动的先后顺序), 拓扑排序(找到活动的先后顺序), 下面简单介绍一下拓扑排序的过程和实现
拓扑排序过程:
1.找到图中入度为0的点,然后输出
2.删除与该点连接的边
3.重复1、2操作,直到图中没有点为止或者没有入度为0的点为止
因为图中可能有环,所以删除到只剩下这个环时,此时没有入度为0的点了,但图中还有点,说明是个环,拓扑排序也就结束了!!!
所以拓扑排序的一个重要应用: 判断有向图是否有环
ps:由上图也可以看出,由于某一时刻入度为0的点不一定只有1个,选任意一个都可以,因此拓扑排序的结果不是唯一的
拓扑排序实现:
1.把所有入度为0的点加入队列中
2.当队列不为空的时候:
2.1 拿出队头元素,加入到最终结果中
2.2.删除与该元素相连的边
2.3 判断与删除边相连的点,是否入度变为0, 如果入度为0,加入到队列中
一、课程表
207. 课程表 - 力扣(LeetCode)https://leetcode.cn/problems/course-schedule/1.题目解析
抽象一下题目:
给定一个数组,数组的每个元素是一个包含两个元素的数组,其中[a, b]表示,b发生之后才能发生a, 问最终能否将所有的活动排好序进行
2.算法分析
本质就是建立有向图,用拓扑排序判断图中是否有环即可,如何用拓扑排序上文已经介绍了,所以关键就是如何建图
建图有两种方式: 本文主要介绍邻接表
1.邻接矩阵(主要用于稠密图)
2.邻接表(主要用于稀疏图)
3.算法代码
class Solution
{
public:
bool canFinish(int n, vector<vector<int>>& prerequisites)
{
//1.准备工作
unordered_map<int, vector<int>> edges; //邻接表
vector<int> in(n); //存储每一个节点的入度
//2.建图
for(auto& e : prerequisites)
{
int a = e[0], b = e[1];
edges[b].push_back(a); //b->a
in[a]++; //a的入度++
}
//3.拓扑排序
queue<int> q;
//3.1 把所有入度为0的点加入队列
for(int i = 0; i < n; i++)
if(in[i] == 0) q.push(i);
//3.2 bfs
while(q.size())
{
int t = q.front();
q.pop();
for(auto a : edges[t]) //edges[t]是t所连接的所有点构成的数组
{
in[a]--; //将t连接的点的入度--(本质就是删除了与t相连的边)
if(in[a] == 0) q.push(a); //删除之后, 将入度为0的点加入队列
}
}
//4.判断是否有环
for(int i = 0; i < n; i++)
if(in[i]) return false; //还有点入度不为0, 表示有环, 返回false
return true;
}
};
二、课程表 II
210. 课程表 II - 力扣(LeetCode)https://leetcode.cn/problems/course-schedule-ii/description/1.题目解析
抽象题目:
如果没有环,输出拓扑排序的结果即可
2.算法分析
见题目一
3.算法代码
class Solution {
public:
vector<int> findOrder(int n, vector<vector<int>>& prerequisites)
{
//1.准备工作
unordered_map<int, vector<int>> edges;
vector<int> in(n);
//2.建图
for(auto& e : prerequisites)
{
int a = e[0], b = e[1];
edges[b].push_back(a);
in[a]++;
}
//3.拓扑排序
queue<int> q;
vector<int> ret; //记录最终结果
for(int i = 0; i < n; i++)
if(in[i] == 0) q.push(i);
while(q.size())
{
int t = q.front();
q.pop();
ret.push_back(t);
for(auto a : edges[t])
{
in[a]--;
if(in[a] == 0) q.push(a);
}
}
//4.判断是否有环
if(ret.size() == n)
return ret;
return {};
}
};
三、火星字典
LCR 114. 火星词典 - 力扣(LeetCode)https://leetcode.cn/problems/Jf1JuT/description/1.题目解析
给定一个字符串数组,数组每个元素是一个单词,火星上的英文字母顺序和我们地球上的字母顺序不一样,数组中的英文单词就是按照字母顺序给出的,现要求根据给定的字符串数组,给出数组中出现过的英文字母的顺序, 如果不存在合法顺序,返回空串即可
2.算法分析
本题的本质仍然是拓扑排序,而拓扑排序前必须先有图,但难点是如何从题目给定字符串数组中统计出字母顺序的信息,从而能建出图
1. 两层 for循环遍历数组,收集信息(确定出字母的先后顺序),建图
ps: 收集信息时,采用对应字母比较的方式
比如比较: "wrt"与 "wrf"时,依次比较'w'与'w', 'r'与'r', 比较 't'与'f'不相等,于是确定t->f
但是要特殊处理下面这种特殊情况:
"abc" vs "ab",无论火星上的字母顺序如何,都不可能出现"abc"排在"ab"之前的情况,因此不存在合法顺序,直接返回空串
2. 由于本题图的顶点不是数字,而是字母,因此我们选择创建哈希表来存图,而两层for循环遍历的时候,可能会统计冗余信息,比如
比较 "wrf" 与 "er" 和 比较 "wrf" 与 "ett" 时,都能得出 w->e 的信息,但是不能重复存储在图中,因此我们创建哈希表中的value也是一个哈希表: unordered_map<char, unordered_set<char>> hash;
3. 用哈希表统计入度信息 unordered_map<char, int> in;
ps: 必须初始化哈希表,遍历words数组,每遇到一个字母,就将入度初始化成0
3.算法代码
class Solution
{
unordered_map<char, unordered_set<char>> edges; //邻接表存储图
unordered_map<char, int> in; //统计每个节点的入度
bool check; //标记收集信息时的特殊情况
public:
string alienOrder(vector<string>& words)
{
//1.初始化入度哈希表
for(auto& s : words)
for(auto ch : s)
in[ch] = 0;
//2.建图
int n = words.size();
for(int i = 0; i < n; i++)
{
for(int j = i + 1; j < n; j++)
{
add(words[i], words[j]);
if(check) return ""; //特殊情况,直接返回空串
}
}
//3.拓扑排序
queue<char> q;
for(auto& [a, b] : in)
{
if(b == 0) q.push(a); //将入度为0的点加入到队列中
}
string ret; //统计最终结果
while(q.size())
{
char t = q.front();
q.pop();
ret += t;
for(char ch : edges[t])
{
in[ch]--;
if(in[ch] == 0) q.push(ch);
}
}
//4.判断是否有环
for(auto& [a, b] : in)
if(b != 0) return "";
return ret;
}
//收集信息函数(确定字母顺序)
void add(string& s1, string& s2)
{
int n = min(s1.size(), s2.size());
int i = 0;
for(; i < n; i++)
{
if(s1[i] != s2[i])
{
char a = s1[i], b = s2[i]; //a->b
if(!edges.count(a) || !edges[a].count(b)) //图中没有a 或 图中有a但是a没有->b, 都建图
{
edges[a].insert(b);
in[b]++;
}
break; //不要继续往后比较了,只看第一个不相等的字符
}
}
if(i == s2.size() && i < s1.size()) //特殊情况
check = true;
}
};