BFS的模板其实已经比较熟练,那么在一些题目中,还会用到BFS双端队列模板,这时候就要看什么情况当前层会遍历结束,进入到下一层。
BFS双端队列
1.LeetCode6081 到达角落需要移除障碍物的最小数目
不难想到,移除的障碍数就是BFS遍历的层数。
在BFS的时候,如果当前位置不是障碍,那么当前位置不会进入下一层。代码中用双端队列(所谓双端队列,你可以从队头或队尾插入/删除一个元素)处理,对于不是障碍的位置加入到队列头部,当前层遍历的索引i–,障碍才会进入到下一层遍历中。
其实,只是对BFS的模板稍微改了一下,用到了双端队列。
class Solution {
final int[] direction = new int[]{1,0,-1,0,1};
public int minimumObstacles(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
Deque<int[]> q = new LinkedList<>();
q.offer(new int[]{0, 0});
boolean[][] visited = new boolean[m][n];
visited[0][0] = true;
int level = 0;
while(!q.isEmpty())
{
int size = q.size();
for(int i=0; i<size; i++)
{
int[] cur = q.poll();
int x = cur[0], y = cur[1];
if(x == m-1 && y == n-1)
{
return level;
}
for(int k=0; k<direction.length-1; k++)
{
int nextX = x + direction[k];
int nextY = y + direction[k+1];
if(nextX < 0 || nextX == m || nextY < 0 || nextY == n || visited[nextX][nextY]) continue;
visited[nextX][nextY] = true;
if(grid[nextX][nextY] == 0)
{
//不是障碍, 加入到双端队列头部, 当前层遍历索引值i--
i--;
q.addFirst(new int[]{nextX, nextY});
}
else
{
q.addLast(new int[]{nextX, nextY});
}
}
}
level++;
}
return level;
}
}
2.LeetCode1368 使网格图至少有一条有效路径的最小代价
还是借用第一题的代码模板, 最少修改次数就是level值,进入到下一层的条件为:当前位置的方向与当前位置到下一个位置的方向不一样,比如,当前位置的方向为右(1),那么BFS遍历向下,这时候实际是下一层了,因为要修改一次当前位置的方向。
但是仅仅修改了条件,第2个用例,我们的结果是1,正确的结果应该是0。
grid = [[1,1,3],[3,2,2],[1,1,4]]
debug代码发现,在下一个位置加入到队列时,标记下一个位置为已访问。此时加入队列分为两种情况,一种是修改当前位置方向值,一种是不需修改。对于不修改的情况,可能需要继续遍历当前层所有位置才能满足,但如果此前该位置被标记为已访问过,那么下一个位置只能被归到下一层。
(第一题在套用BFS模板时,没有这个问题,是因为如果下一个位置为障碍,那么如果需要通过它访问到终点的位置,level必然加1,在入队时标记其为已访问是没问题的,这与正常的BFS模板也是一致的。)
对于第2题这种比较特殊的情况,需要将标记已访问过该位置的语句延迟到BFS遍历到该节点时。这样即使重复把一个位置加入到队列,但它们属于不同的层,如果当前层就BFS结束返回,下一层不会访问到。m和n都小于等于100,因此,这种看似与模板不一致的“错误”写法,既解决了题目的应用问题,也不会导致时间和内存的超出。
class Solution {
final int[] directions = new int[]{1,0,-1,0,1};
public int minCost(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
Deque<int[]> q = new LinkedList<>();
q.offer(new int[]{0, 0});
boolean[][] visited = new boolean[m][n];
int level = 0;
while(!q.isEmpty())
{
int size = q.size();
for(int i=0; i<size; i++)
{
int[] cur = q.poll();
int x = cur[0], y = cur[1];
//将访问标记延迟到遍历该节点时,而不是加入队列时候。实际上,这种写法存在效率问题,但对于解决这道题,这样写更合理。
visited[x][y] = true;
if(x == m-1 && y == n-1)
{
return level;
}
for(int k=0; k<directions.length-1; k++)
{
int direct = getDirection(directions[k], directions[k+1]);
int nextX = x + directions[k];
int nextY = y + directions[k+1];
if(nextX < 0 || nextX == m || nextY < 0 || nextY == n || visited[nextX][nextY]) continue;
if(grid[x][y] == direct)
{
i--;
q.addFirst(new int[]{nextX, nextY});
}
else
{
q.addLast(new int[]{nextX, nextY});
}
}
}
level++;
}
return level;
}
private int getDirection(int x, int y)
{
if(x == 0)
{
if(y == 1) return 1;
else return 2;
}
else
{
if(x == 1) return 3;
else return 4;
}
}
}
3. LCP 56. 信物传送
这题与第2题就是一样了,调下参数即可。
class Solution {
final int[] directions = new int[]{1,0,-1,0,1};
final char[] signs = new char[]{'v', '<', '^', '>'};
public int conveyorBelt(String[] grid, int[] start, int[] end) {
int m = grid.length;
int n = grid[0].length();
Deque<int[]> q = new LinkedList<>();
q.offer(new int[]{start[0], start[1]});
boolean[][] visited = new boolean[m][n];
int level = 0;
while(!q.isEmpty())
{
int size = q.size();
for(int i=0; i<size; i++)
{
int[] cur = q.poll();
int x = cur[0], y = cur[1];
visited[x][y] = true;
if(x == end[0] && y == end[1])
{
return level;
}
for(int k=0; k<directions.length-1; k++)
{
int nextX = x + directions[k];
int nextY = y + directions[k+1];
if(nextX < 0 || nextX == m || nextY < 0 || nextY == n || visited[nextX][nextY]) continue;
if(grid[x].charAt(y) == signs[k])
{
i--;
q.addFirst(new int[]{nextX, nextY});
}
else
{
q.addLast(new int[]{nextX, nextY});
}
}
}
level++;
}
return level;
}
}
关于BFS双端队列先写到这儿。BFS的变异题还有多次使用BFS,掌握了BFS模板,这些题只是脑筋急转弯的思维题,有空继续总结。