一、经验总结
最短路径算法是一种用于找到图或网络中两个节点之间最短路径的算法。它被广泛应用于网络路由、GPS导航、交通规划和其他领域。
单源最短路径
用BFS解决边权为1的单源最短路径问题:
- 利用队列辅助完成BFS
- 定义visited数组或是哈希表标记已访问,防止重复入队列访问
- 创建横纵坐标的变化量数组,可以方便地遍历相邻地四个方向
- 定义变量k统计每层节点的数量,控制一层一层的遍历,遍历到终点立即返回。BFS扩展的层数就是最短路径的长度。
多源最短路径
用BFS解决边权为1的多源最短路径问题:
- 多源:多个源点,一个终点
- 从所有的源点出发进行BFS,其他步骤同单源最短路径相同
二、相关编程题
2.1 单源最短路径
2.1.1 迷宫中离入口最近的出口
题目链接
1926. 迷宫中离入口最近的出口 - 力扣(LeetCode)
题目描述
算法原理
就是求入口到出口的最短路径(单元最短路径),算法原理上面已经讨论过。
编写代码
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>& entrance) {
int m = maze.size(), n = maze[0].size();
bool visited[m][n];
memset(visited, 0, sizeof(visited));
queue<pair<int,int>> que;
que.push({entrance[0], entrance[1]});
visited[entrance[0]][entrance[1]] = true;
int ret = 0;
while(!que.empty())
{
++ret; //扩展的层数就是最短路径的长度
int k = que.size(); //要控制一层一层地向外拓展
while(k--)
{
auto [a,b] = que.front();
que.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 && maze[x][y]=='.' && !visited[x][y])
{
if(x==0 || y==0 || x==m-1 || y==n-1) return ret; //遇到出口直接返回
que.push({x,y});
visited[x][y] = true;
}
}
}
}
return -1; //找不到出口返回-1
}
};
2.1.2 最小基因变化
题目链接
433. 最小基因变化 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
public:
int minMutation(string startGene, string endGene, vector<string>& bank) {
unordered_set<string> hashBank(bank.begin(), bank.end()), visited;
if(!hashBank.count(endGene)) return -1;
if(startGene == endGene) return 0;
string change = "ACGT";
queue<string> que;
que.push(startGene);
visited.insert(startGene);
int cnt = 0;
while(!que.empty())
{
++cnt;
int k = que.size();
while(k--)
{
string gene = que.front();
que.pop();
for(int i = 0; i < gene.size(); ++i)
{
string tmp = gene; //注意要创建临时数组,不能修改原数组
for(int j = 0; j < 4; ++j)
{
tmp[i] = change[j];
if(hashBank.count(tmp) && !visited.count(tmp))
{
if(tmp == endGene) return cnt;
que.push(tmp);
visited.insert(tmp);
}
}
}
}
}
return -1;
}
};
2.1.3 单词接龙
题目链接
127. 单词接龙 - 力扣(LeetCode)
题目描述
算法原理
同上
编写代码
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> dict(wordList.begin(), wordList.end());
unordered_set<string> visited;
if(beginWord == endWord) return 1;
if(!dict.count(endWord)) return 0;
queue<string> que;
que.push(beginWord);
visited.insert(beginWord);
int ret = 1;
while(!que.empty())
{
++ret;
int k = que.size();
while(k--)
{
string word = que.front();
que.pop();
for(int i = 0; i < word.size(); ++i)
{
string tmp = word;
for(char j = 'a'; j <= 'z'; ++j)
{
tmp[i] = j;
if(dict.count(tmp) && !visited.count(tmp))
{
if(tmp == endWord) return ret;
que.push(tmp);
visited.insert(tmp);
}
}
}
}
}
return 0;
}
};
2.1.4 为高尔夫比赛砍树
题目链接
675. 为高尔夫比赛砍树 - 力扣(LeetCode)
题目描述
算法原理
按照树的高度从低到高的顺序砍掉所有的树,计算砍完所有树需要走的最短路径。实际上就是使用多次BFS,按顺序计算一棵树到下一棵树的最短路径,最后将所有途径的最短路径加起来就是最终结果。
编写代码
class Solution {
typedef pair<int,int> PII;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int m, n;
public:
int cutOffTree(vector<vector<int>>& forest) {
m = forest.size(), n = forest[0].size();
//汇总所有树的坐标
vector<PII> trees;
for(int i = 0; i < m; ++i)
for(int j = 0; j < n; ++j)
if(forest[i][j] > 1) trees.push_back({i,j});
//按高度升序排序
sort(trees.begin(), trees.end(), [&](const PII &p1, const PII &p2)
{
return forest[p1.first][p1.second] < forest[p2.first][p2.second];
});
//按照树的高度从低到高砍树
int ret = 0;
PII startp = {0,0};
for(int i = 0; i < trees.size(); ++i)
{
int step = BFS(forest, startp, trees[i]); //计算所站位置到要砍的树的最短距离
if(step == -1) return -1; //如果无法砍完所有的树,返回-1
ret+=step; //将所有的最短距离加起来
startp = trees[i]; //从砍树位置继续找下一棵树
}
return ret;
}
int BFS(vector<vector<int>>& forest, const PII& startp, const PII& endp)
{
auto [sx, sy] = startp;
auto [ex, ey] = endp;
if(sx == ex && sy == ey) return 0;
int visited[m][n];
memset(visited, 0, sizeof(visited));
queue<PII> que;
que.push(startp);
visited[sx][sy] = true;
int step = 0;
while(!que.empty())
{
++step;
int k = que.size();
while(k--)
{
auto [a, b] = que.front();
que.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 && forest[x][y] && !visited[x][y])
{
if(x == ex && y == ey) return step;
que.push({x, y});
visited[x][y] = true;
}
}
}
}
return -1;
}
};
2.2 多源最短路径
2.2.1 矩阵
题目链接
542. 01 矩阵 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
typedef pair<int, int> PII;
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();
//ret[x][y] == -1 表示没有访问过(取代visited数组)
//ret[x][y] > -1 表示到最近0的距离(不需要step记录每层节点的数量)
vector<vector<int>> ret(m, vector<int>(n, -1));
queue<PII> que;
//将所有的源点加入到队列
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(mat[i][j] == 0)
{
ret[i][j] = 0;
que.push({i,j});
}
}
}
//多源BFS
while(!que.empty())
{
auto [a, b] = que.front();
que.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 && ret[x][y] == -1)
{
ret[x][y] = ret[a][b]+1;
que.push({x,y});
}
}
}
return ret;
}
};
2.2.2 飞地的数量
题目链接
1020. 飞地的数量 - 力扣(LeetCode)
题目描述
算法原理
编写代码
class Solution {
typedef pair<int, int> PII;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
public:
int numEnclaves(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> t_grid(m, vector<int>(n)); //拷贝原矩阵
queue<PII> que;
//将所有边界上的1都加入队列,并将矩阵修改为0(已访问)
for(int i = 0; i < m; ++i)
for(int j = 0; j < n; ++j)
{
t_grid[i][j] = grid[i][j];
if((i==0 || j==0 || i==m-1 || j==n-1) && grid[i][j]==1)
{
que.push({i, j});
t_grid[i][j] = 0;
}
}
//以所有边界上的1为起点进行多源BFS,将所有边界上的连接块修改为0
while(!que.empty())
{
auto [a, b] = que.front();
que.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 && t_grid[x][y]==1)
{
t_grid[x][y] = 0;
que.push({x,y});
}
}
}
//统计矩阵中剩余的值为1的陆地
int ret = 0;
for(int i = 0; i < m; ++i)
for(int j = 0; j < n; ++j)
if(t_grid[i][j] == 1) ++ret;
return ret;
}
};
2.2.3 地图中的最高点
题目链接
1765. 地图中的最高点 - 力扣(LeetCode)
题目描述
算法原理
同2.2.1矩阵
编写代码
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>> highestPeak(vector<vector<int>>& isWater) {
int m = isWater.size(), n = isWater[0].size();
vector<vector<int>> ret(m, vector<int>(n, -1));
queue<PII> que;
for(int i=0; i<m; ++i)
for(int j=0; j<n; ++j)
{
if(isWater[i][j] == 1)
{
que.push({i,j});
ret[i][j] = 0;
}
}
while(!que.empty())
{
auto [a,b] = que.front();
que.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 && ret[x][y]==-1)
{
ret[x][y] = ret[a][b]+1;
que.push({x,y});
}
}
}
return ret;
}
};
2.2.4 地图分析
题目链接
1162. 地图分析 - 力扣(LeetCode)
题目描述
算法原理
同2.2.1矩阵,只不过这道题不是要求所有海洋到陆地的最短距离(返回矩阵),而是海洋到陆地最短距离的最大值(单个值)。
因此我们仍然采用多源BFS+visited矩阵+step计层的方案,多源BFS拓展的层数就是海洋到陆地的最短距离,遍历到最后就是最短距离的最大值
当然也可以使用多源BFS+dist矩阵的方案
编写代码
class Solution {
typedef pair<int, int> PII;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
public:
int maxDistance(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<bool>> visited(m, vector<bool>(n, false));
queue<PII> que;
//将所有陆地加入到队列(作为起点)
for(int i=0; i<m; ++i)
for(int j=0; j<n; ++j)
{
if(grid[i][j] == 1)
{
que.push({i,j});
visited[i][j] = true;
}
}
//多源BFS
int step = -1; //step计层
while(!que.empty())
{
++step;
int k = que.size();
while(k--)
{
auto [a,b] = que.front();
que.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 && !visited[x][y])
{
que.push({x,y});
visited[x][y] = true;
}
}
}
}
return step == 0? -1:step; //全是陆地或海洋返回-1
}
};