算法【宽度优先遍历及其扩展】
宽度优先遍历基本内容
1.bfs的特点是逐层扩散,从源头点到目标点扩散了几层,最短路就是多少。
2.bfs可以使用的特征是任意两个节点之间的相互距离相同(无向图)。
3.bfs开始时,可以是单个源头、也可以是多个源头。
4.bfs频繁使用队列,形式可以是单点弹出或者整层弹出。
5.bfs进行时,进入队列的节点需要标记状态,防止同一个节点重复进出队列。
6.bfs进行时,可能会包含剪枝策略的设计。
7.bfs是一个理解难度很低的算法,难点在于节点如何找到路、路的展开和剪枝设计。
01bfs,适用于图中所有边的权重只有0和1两种值,求源点到目标点的最短距离
时间复杂度为O(节点数量+边的数量)
1.distance[i]表示从源点到i点的最短距离,初始时所有点的distance设置为无穷大。
2.源点进入双端队列,distance[源点]=0。
3.双端队列头部弹出x,
A.如果x是目标点,返回distance[x]表示源点到目标点的最短距离。
B.考察从x出发的每一条边,假设某边去y点,边权为w
1)如果 distance[y] > distance[x] + w,处理该边;否则忽略该边。
2)处理时,更新distance[y] = distance[x] + w
如果w==0,y从头部进入双端队列;如果w==1,y从尾部进入双端队列。
3)考察完x出发的所有边之后,重复步骤3。
4.双端队列为空停止。
宽度优先遍历与优先级队列结合,更进一步的内容会在讲Dijkstra算法时说明。
宽度优先遍历与深度优先遍历结合,去生成路径
1.bfs建图。
2.dfs利用图生成路径。
下面通过几个题目加深理解。
题目一
测试链接:https://leetcode.cn/problems/as-far-from-land-as-possible/
分析:这是一个多源宽度优先,将所有的陆地入队列。然后开始按层出队列,如果四周是海洋,则将海洋编号为层数,直到没有编号为0的海洋,分情况返回。代码如下。
class Solution {
public:
int queue[10000][2];
int arr[5] = {0, 1, 0, -1, 0};
int maxDistance(vector<vector<int>>& grid) {
int row = grid.size();
int column = grid[0].size();
int l = 0, r = 0, level = 0;
int temp, cur_row, cur_column;
for(int i = 0;i < row;++i){
for(int j = 0;j < column;++j){
if(grid[i][j] == 1){
queue[r][0] = i;
queue[r++][1] = j;
}
}
}
while (l < r)
{
++level;
temp = r;
for(;l < temp;++l){
cur_row = queue[l][0];
cur_column = queue[l][1];
for(int index = 0;index < 4;++index){
if(cur_row+arr[index] >= 0 && cur_row+arr[index] < row && cur_column+arr[index+1] >= 0 && cur_column+arr[index+1] < column
&& grid[cur_row+arr[index]][cur_column+arr[index+1]] == 0){
queue[r][0] = cur_row+arr[index];
queue[r++][1] = cur_column+arr[index+1];
grid[cur_row+arr[index]][cur_column+arr[index+1]] = level;
}
}
}
}
if((l == 0 && r == 0) || (level == 1)){
return -1;
}else{
return --level;
}
}
};
其中,(l == 0 &&r == 0)代表全为海洋,level == 1代表全为陆地。
题目二
测试链接:https://leetcode.cn/problems/stickers-to-spell-word/
分析:这个使用宽度优先遍历的思路比较巧妙。让target进队列,按层出队列,然后每一层都去遍历贴纸,将target减去遍历到的字符串的字符串进队列,如果此字符串已经在队列则跳过,直到减去之后为空字符串代表拼出了target,且现在使用的贴纸数量最少。代码如下。
class Solution {
public:
vector<vector<int>> graph;
set<string> visited;
string queue[401];
void build(vector<string>& stickers){
vector<int> temp;
graph.assign(26, temp);
int length1 = stickers.size();
int length2;
for(int i = 0;i < length1;++i){
sort(stickers[i].begin(), stickers[i].end());
length2 = stickers[i].size();
for(int j = 0;j < length2;++j){
if((j == 0) || (stickers[i][j] != stickers[i][j-1])){
graph[stickers[i][j] - 'a'].push_back(i);
}
}
}
}
string next(string cur, string sticker){
int length1 = cur.size();
int length2 = sticker.size();
string s = "";
int i, j;
for(i = 0, j = 0;i < length1 && j < length2;){
if(cur[i] == sticker[j]){
++i;
++j;
}else if(cur[i] < sticker[j]){
s += cur[i];
++i;
}else{
++j;
}
}
if(j == length2){
while (i < length1)
{
s += cur[i];
++i;
}
}
return s;
}
int minStickers(vector<string>& stickers, string target) {
sort(target.begin(), target.end());
build(stickers);
int level = 0;
int l = 0, r = 0, temp;
queue[r++] = target;
visited.insert(target);
while (l < r)
{
++level;
temp = r;
for(;l < temp;++l){
string cur = queue[l];
int len = graph[cur[0] - 'a'].size();
for(int i = 0;i < len;++i){
string Next = next(cur, stickers[graph[cur[0] - 'a'][i]]);
if(Next == ""){
return level;
}else if(visited.count(Next) == 0){
queue[r++] = Next;
visited.insert(Next);
}
}
}
}
return -1;
}
};
其中,使用set来实现队列中不会出现重复字符串。
题目三
测试链接:https://leetcode.cn/problems/minimum-obstacle-removal-to-reach-corner/
分析:这个就是开头介绍的01bfs。下一步如果是障碍物则此节点到一下个节点的距离为1,否则为0,流程按01bfs讲解的写就行。代码如下。
class Solution {
public:
int arr[5] = {0, 1, 0, -1, 0};
int deque[100001][2] = {0};
vector<vector<int>> distance;
void build(vector<vector<int>>& grid, int row, int column){
vector<int> temp;
temp.assign(column, -1);
distance.assign(row, temp);
distance[0][0] = 0;
}
int minimumObstacles(vector<vector<int>>& grid) {
int row = grid.size();
int column = grid[0].size();
build(grid, row, column);
int l = 0, r = 0;
deque[r][0] = 0;
deque[r++][1] = 0;
while (true)
{
int cur_row = deque[l][0];
int cur_column = deque[l][1];
l = (l + 1) % 100001;
if(cur_row == row-1 && cur_column == column-1){
return distance[row-1][column-1];
}
for(int index = 0;index < 4;++index){
if(cur_row+arr[index] >= 0 && cur_row+arr[index] < row
&& cur_column+arr[index+1] >= 0 && cur_column+arr[index+1] < column){
if(distance[cur_row+arr[index]][cur_column+arr[index+1]] == -1
|| distance[cur_row][cur_column] + grid[cur_row+arr[index]][cur_column+arr[index+1]] < distance[cur_row+arr[index]][cur_column+arr[index+1]]){
distance[cur_row+arr[index]][cur_column+arr[index+1]] = distance[cur_row][cur_column] + grid[cur_row+arr[index]][cur_column+arr[index+1]];
if(grid[cur_row+arr[index]][cur_column+arr[index+1]] == 1){
deque[r][0] = cur_row+arr[index];
deque[r++][1] = cur_column+arr[index+1];
}else{
l = (l - 1 + 100001) % 100001;
deque[l][0] = cur_row+arr[index];
deque[l][1] = cur_column+arr[index+1];
}
}
}
}
}
}
};
其中,双端队列采用定义一个数组实现。
题目四
测试链接:https://leetcode.cn/problems/minimum-cost-to-make-at-least-one-valid-path-in-a-grid/
分析:这个题和题目三差不多。下一个节点的方向和箭头指向方向一致,则此节点和下一个节点距离为0,否则为1。代码如下。
class Solution {
public:
int move[5][2] = {{0 ,0}, {0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int distance[101][101];
deque<vector<int>> dq;
void build(vector<vector<int>>& grid, int row, int column){
int max_value = -(1 + (1 << 31));
for(int i = 0;i < row;++i){
for(int j = 0;j < column;++j){
distance[i][j] = max_value;
}
}
distance[0][0] = 0;
}
int minCost(vector<vector<int>>& grid) {
int row = grid.size();
int column = grid[0].size();
int cur_row, cur_column, cost, next_row, next_column;
build(grid, row, column);
vector<int> ini;
ini.push_back(0);
ini.push_back(0);
dq.push_back(ini);
while (true)
{
cur_row = dq.front()[0];
cur_column = dq.front()[1];
dq.pop_front();
if(cur_row == row-1 && cur_column == column-1){
return distance[row-1][column-1];
}
for(int index = 1;index <= 4;++index){
next_row = cur_row + move[index][0];
next_column = cur_column + move[index][1];
cost = grid[cur_row][cur_column] == index ? 0 : 1;
if(next_row >= 0 && next_row < row
&& next_column >= 0 && next_column < column
&& distance[cur_row][cur_column] + cost < distance[next_row][next_column]){
distance[next_row][next_column] = distance[cur_row][cur_column] + cost;
vector<int> temp;
temp.push_back(next_row);
temp.push_back(next_column);
if(cost == 0){
dq.push_front(temp);
}else{
dq.push_back(temp);
}
}
}
}
}
};
其中,双端队列采用stl实现。
题目五
测试链接:https://leetcode.cn/problems/trapping-rain-water-ii/
分析:这个题的队列采用优先队列,显然,矩阵的四周是接不到水的。先将矩阵的四周入队列,队列中的元素为行列坐标以及这个位置的水线,水性就是这个位置水能到达的最高高度,四周的水线高度自然就是位置的高度。然后从优先队列中弹出元素,弹出元素时更新接雨水的体积,用水线高度减去当前位置的高度,就是增加的接雨水的体积。将弹出位置四周加入进队列,进队列时的水线高度为弹出元素的水线高度和四周当前位置的高度取最大值。队列空时,得到答案。代码如下。
class Solution {
public:
int arr[5] = {0, 1, 0, -1, 0};
bool visited[200][200] = {false};
int trapRainWater(vector<vector<int>>& heightMap) {
int row = heightMap.size();
int column = heightMap[0].size();
int ans = 0;
vector<int> cur;
auto cmp = [](vector<int> v1, vector<int> v2)->bool{
return v1[2] > v2[2];
};
priority_queue<vector<int>, vector<vector<int>>, decltype(cmp)> q(cmp);
for(int i = 0;i < column;++i){
if(!visited[0][i]){
visited[0][i] = true;
vector<int> temp;
temp.push_back(0);
temp.push_back(i);
temp.push_back(heightMap[0][i]);
q.push(temp);
}
}
for(int i = 0;i < column;++i){
if(!visited[row-1][i]){
visited[row-1][i] = true;
vector<int> temp;
temp.push_back(row-1);
temp.push_back(i);
temp.push_back(heightMap[row-1][i]);
q.push(temp);
}
}
for(int i = 0;i < row;++i){
if(!visited[i][0]){
visited[i][0] = true;
vector<int> temp;
temp.push_back(i);
temp.push_back(0);
temp.push_back(heightMap[i][0]);
q.push(temp);
}
}
for(int i = 0;i < row;++i){
if(!visited[i][column-1]){
visited[i][column-1] = true;
vector<int> temp;
temp.push_back(i);
temp.push_back(column-1);
temp.push_back(heightMap[i][column-1]);
q.push(temp);
}
}
while (!q.empty())
{
cur = q.top();
q.pop();
ans += (cur[2] - heightMap[cur[0]][cur[1]]);
for(int index = 0;index < 4;++index){
if(cur[0]+arr[index] >= 0 && cur[0]+arr[index] < row
&& cur[1]+arr[index+1] >= 0 && cur[1]+arr[index+1] < column
&& visited[cur[0]+arr[index]][cur[1]+arr[index+1]] == false){
visited[cur[0]+arr[index]][cur[1]+arr[index+1]] = true;
vector<int> temp;
temp.push_back(cur[0]+arr[index]);
temp.push_back(cur[1]+arr[index+1]);
temp.push_back((heightMap[cur[0]+arr[index]][cur[1]+arr[index+1]] > cur[2] ?
heightMap[cur[0]+arr[index]][cur[1]+arr[index+1]] : cur[2]));
q.push(temp);
}
}
}
return ans;
}
};
其中,visited数组表示当前位置是否进过队列,false代表没有,true代表进过。
题目六
测试链接:https://leetcode.cn/problems/word-ladder-ii/
分析:这道题采用宽度优先和深度优先结合的方式。先用bfs找到endWord是否能够转化,同时,在bfs的时候把图建好,图代表入度节点可以转换为出度节点。然后使用dfs得到每一个最短转换序列,dfs的开头是endWord,结尾是beginWord,所以需要将序列翻转才是答案。代码如下。
class Solution {
public:
set<string> dict;
set<string> curLevel;
set<string> nextLevel;
map<string, vector<string>> graph;
vector<string> path;
vector<vector<string>> ans;
void build(vector<string>& wordList){
int length = wordList.size();
for(int i = 0;i < length;++i){
dict.insert(wordList[i]);
}
}
bool bfs(string beginWord, string endWord){
bool find = false;
curLevel.insert(beginWord);
string cur_str;
int cur_str_length;
char old;
while (!curLevel.empty())
{
for(set<string>::iterator it = curLevel.begin();it != curLevel.end();++it){
dict.erase((*it));
}
for(set<string>::iterator it = curLevel.begin();it != curLevel.end();++it){
cur_str = *it;
cur_str_length = cur_str.size();
for(int i = 0;i < cur_str_length;++i){
old = cur_str[i];
for(char ch = 'a'; ch <= 'z';++ch){
cur_str[i] = ch;
if(dict.count(cur_str) != 0 && ch != old){
if(endWord.compare(cur_str) == 0){
find = true;
}
if(graph.count(cur_str) == 0){
vector<string> temp;
graph.insert(make_pair(cur_str, temp));
}
(*graph.find(cur_str)).second.push_back((*it));
nextLevel.insert(cur_str);
}
}
cur_str[i] = old;
}
}
if(find){
return true;
}else{
set<string> temp;
temp = curLevel;
curLevel = nextLevel;
nextLevel = temp;
nextLevel.clear();
}
}
return false;
}
void dfs(string word, string aim){
path.push_back(word);
int length;
if(word.compare(aim) == 0){
vector<string> temp(path.begin(), path.end());
reverse(temp.begin(), temp.end());
ans.push_back(temp);
}else if(graph.count(word) != 0){
length = (*graph.find(word)).second.size();
for(int i = 0; i < length;++i){
dfs((*graph.find(word)).second[i], aim);
}
}
path.pop_back();
}
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
build(wordList);
if(dict.count(endWord) == 0){
return ans;
}
if(bfs(beginWord, endWord)){
dfs(endWord, beginWord);
}
return ans;
}
};
其中,bfs中的建图采用邻接表建图;并且判断当前层字符串能否转换为已有的字符串采用将当前层字符串的每个位置替换一次26个字母,判断新字符串是否存在的方法;bfs没有使用队列,而是使用两个层,当前层的字符串从已存在的字符串中删去避免重复遍历,生成好下一层后,当前层和下一层互换,然后清空下一层;考虑到图的意义,所以dfs生成的序列是从endWord到beginWord的,所以需要翻转。