- 847访问所有节点的最短路径
- 934最短桥(用到三色法,层级)
- 126. 单词接龙 II(dfs+bfs,必看)">126. 单词接龙 II(dfs+bfs,必看)
- 386. 字典序排数(巧妙)">386. 字典序排数(巧妙)
- 675. 为高尔夫比赛砍树(寻路算法,很重要)">675. 为高尔夫比赛砍树(寻路算法,很重要)
- 0-1BFS 最短路径问题(双端队列)
- 6081. 到达角落需要移除障碍物的最小数目(比较有细节)">6081. 到达角落需要移除障碍物的最小数目(比较有细节)
- 6081. 到达角落需要移除障碍物的最小数目(比较有细节)">6081. 到达角落需要移除障碍物的最小数目(比较有细节)
847访问所有节点的最短路径
使用三个量来标记 分别是当前的位置,经过的位置,走过的路径长度。这题还有一个要点,使用二进制数的形式来表示经过的路径
class Solution {
public:
int shortestPathLength(vector<vector<int>>& graph) {
int n = graph.size();
queue<tuple<int, int, int>> q;
vector<vector<int>> seen(n, vector<int>(1 << n));
for (int i = 0; i < n; ++i) {
q.emplace(i, 1 << i, 0);//代表经过了第几位
seen[i][1 << i] = true;
}
int ans = 0;
while (!q.empty()) {
auto [u, mask, dist] = q.front();
q.pop();
if (mask == (1 << n) - 1) {//全是1,代表经过了所有节点
ans = dist;//此时路径长度确定
break;
}
// 搜索相邻的节点
for (int v: graph[u]) {
// 将 mask 的第 v 位置为 1
int mask_v = mask | (1 << v);
if (!seen[v][mask_v]) {//减枝操作
q.emplace(v, mask_v, dist + 1);
seen[v][mask_v] = true;//经过的地方设为true;
}
}
}
return ans;
}
};
//这里的主要的关于剪枝的想法,不只是处于相同的节点,还要经过的节点一样。放在题
目中就是说v和mask一样。如果题目要求只看终点而不是经过所有的路径的话,这里凡是经
过相同的节点都要剪枝。
934最短桥(用到三色法,层级)
非常标准的广度优先搜索的题目,找到最短的路径,因为是需要一层层的遍历的,所以需要先入先出的队列来进行遍历。深搜查找第一个岛屿的所有位置,然后使用bfs寻找第二个岛屿,并把过程经过的0赋值为2。
class Solution {
public:
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});//存进去的都是从第一个岛屿出发所能到达的第一层为0的部分。
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);
}
};
126. 单词接龙 II(dfs+bfs,必看)
- 想要进行bfs,因为本题并没有图,所以第一步先建图
注意维护一个哈希表方便查找。
class Solution {
public:
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string> &wordList) {
vector<vector<string>> res;
// 因为需要快速判断扩展出的单词是否在 wordList 里,因此需要将 wordList 存入哈希表,这里命名为「字典」
unordered_set<string> dict = {wordList.begin(), wordList.end()};
// 修改以后看一下,如果根本就不在 dict 里面,跳过
if (dict.find(endWord) == dict.end()) {
return res;
}
// 特殊用例处理
dict.erase(beginWord);
// 第 1 步:广度优先遍历建图
// 记录扩展出的单词是在第几次扩展的时候得到的,key:单词,value:在广度优先遍历的第几层
unordered_map<string, int> steps = {{beginWord, 0}};
// 记录了单词是从哪些单词扩展而来,key:单词,value:单词列表,这些单词可以变换到 key ,它们是一对多关系
unordered_map<string, set<string>> from = {{beginWord, {}}};
int step = 0;
bool found = false;
queue<string> q = queue<string>{{beginWord}};
int wordLen = beginWord.length();
while (!q.empty()) {
step++;
int size = q.size();
for (int i = 0; i < size; i++) {
const string currWord = move(q.front());
string nextWord = currWord;
q.pop();
// 将每一位替换成 26 个小写英文字母
for (int j = 0; j < wordLen; ++j) {
const char origin = nextWord[j];
for (char c = 'a'; c <= 'z'; ++c) {
nextWord[j] = c;
if (steps[nextWord] == step) {//这里的意思是如果同一层也有相同的部分,就可以直接进入from里面,因为后面的dict.erase已经能够扩展出来的,防止后面又出现且距离更远。
from[nextWord].insert(currWord);
}
if (dict.find(nextWord) == dict.end()) {
continue;
}
// 如果从一个单词扩展出来的单词以前遍历过,距离一定更远,为了避免搜索到已经遍历到,且距离更远的单词,需要将它从 dict 中删除
dict.erase(nextWord);
// 这一层扩展出的单词进入队列
q.push(nextWord);
// 记录 nextWord 从 currWord 而来
from[nextWord].insert(currWord);
// 记录 nextWord 的 step
steps[nextWord] = step;
if (nextWord == endWord) {
found = true;
}
}
nextWord[j] = origin;
}
}
if (found) {
break;
}
}
// 第 2 步:深度优先遍历找到所有解,从 endWord 恢复到 beginWord ,所以每次尝试操作 path 列表的头部
if (found) {
vector<string> Path = {endWord};
dfs(res, endWord, from, Path);
}
return res;
}
void dfs(vector<vector<string>> &res, const string &Node, unordered_map<string, set<string>> &from,
vector<string> &path) {
if (from[Node].empty()) {//这是把里面所有路径都走一遍。
res.push_back({path.rbegin(), path.rend()});
return;
}
for (const string &Parent: from[Node]) {
path.push_back(Parent);
dfs(res, Parent, from, path);
path.pop_back();
}
}
};
386. 字典序排数(巧妙)
非常巧妙的使用广搜的方法来写。
class Solution {
public:
vector<int> lexicalOrder(int n) {
vector<int> ret(n);
int number = 1;
for (int i = 0; i < n; i++) {
ret[i] = number;
if (number * 10 <= n) {//这里符合字典排序规则
number *= 10;
} else {
while (number % 10 == 9 || number + 1 > n) {//这个相当于向前退一位
number /= 10;
}
number++;
}
}
return ret;
}
};
675. 为高尔夫比赛砍树(寻路算法,很重要)
题目限定了我们只能按照从低到高的顺序进行砍树,并且图中不存在高度相等的两棵树,这意味着整个砍树的顺序唯一确定,就是对所有树进行高度排序,就是完整的路线。
- 另外一个重要的性质是,点与点之间的最短路径,不会随着砍树过程的进程而发生变化,
综上所述,砍树的路线唯一确定,当我们求出每两个相邻的砍树点最短路径,并进行累加就是答案,求最短路径使用BFS比较好。
class Solution {
public:
int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};//四个方向
//BFS求最短路径,即可
int bfs(vector<vector<int>>& forest, int sx, int sy, int tx, int ty) {
if (sx == tx && sy == ty) {//当起点和终点是同一个位置的时候直接返回0
return 0;
}
int row = forest.size();
int col = forest[0].size();
int step = 0;
queue<pair<int, int>> qu;//队列的先进先出,保存每一层。
vector<vector<bool>> visited(row, vector<bool>(col, false));//记忆化
qu.emplace(sx, sy);
visited[sx][sy] = true;//记忆化
while (!qu.empty()) {
step++;
int sz = qu.size();
for (int i = 0; i < sz; ++i) {
auto [cx, cy] = qu.front();
qu.pop();
for (int j = 0; j < 4; ++j) {
int nx = cx + dirs[j][0];
int ny = cy + dirs[j][1];
if (nx >= 0 && nx < row && ny >= 0 && ny < col) {
if (!visited[nx][ny] && forest[nx][ny] > 0) {
if (nx == tx && ny == ty) {
return step;
}
qu.emplace(nx, ny);
visited[nx][ny] = true;
}
}
}
}
}
return -1;
}
int cutOffTree(vector<vector<int>>& forest) {
vector<pair<int, int>> trees;
int row = forest.size();
int col = forest[0].size();
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j) {
if (forest[i][j] > 1) {
trees.emplace_back(i, j);//把所有的树按照高度排序
}
}
}
sort(trees.begin(), trees.end(), [&](const pair<int, int> & a, const pair<int, int> & b) {
return forest[a.first][a.second] < forest[b.first][b.second];
});
int cx = 0;
int cy = 0;
int ans = 0;
for (int i = 0; i < trees.size(); ++i) {
int steps = bfs(forest, cx, cy, trees[i].first, trees[i].second);//找两个点之间的最短距离
if (steps == -1) {//其中任意两个点无法到达,就直接返回-1
return -1;
}
ans += steps;
cx = trees[i].first;
cy = trees[i].second;
}
return ans;
}
};
0-1BFS 最短路径问题(双端队列)
参考链接
https://blog.csdn.net/Mr_dimple/article/details/1168640526081. 到达角落需要移除障碍物的最小数目(比较有细节)
从起点开始,加入队列。
- while队列非空,取队首元素,用这个节点更新其他节点。
- 如果可以更新的话:
- 1、边权为0,放到队首。(从边权为0的边走,不增加消耗,得多走这样的边)
- 2、边权为其他,放到队尾。(增加消耗,少用)
(这样,队列前面的元素值一定比后面的元素值小(可以手动模拟下),每次求队首元素来更新相连的点的距离,完美的替代了优先队列的功能!
const int d[4][2] = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
class Solution {
public:
int minimumObstacles(vector<vector<int>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<vector<int>> dis(n, vector<int>(m, INT_MAX));
dis[0][0] = 0;
deque<pair<int, int>> dq;
dq.emplace_back(0, 0);
while (!dq.empty()) {
auto [i, j] = dq.front();
dq.pop_front();
for (int k = 0; k < 4; ++k) {
int ni = i + d[k][0], nj = j + d[k][1];
if (ni < 0 || ni >= n || nj < 0 || nj >= m || dis[ni][nj] < INT_MAX)//这里就暗含了记忆化的问题,只要便利过就不要再次便利,这种矩阵最好都使用BFS来写。因为队列前面的元素值一定比后面的小
continue;
dis[ni][nj] = dis[i][j] + grid[ni][nj];
if (grid[ni][nj])
dq.emplace_back(ni, nj);
else
dq.emplace_front(ni, nj);
}
}
return dis[n - 1][m - 1];
}
};