跳到主要内容

最短的桥

934. 最短的桥

problem

题目分析

下面是一个水域左上角有一个岛右下角有一个岛找到两岛间的桥
0100
1100
0001
0011

大致想法:我们不需要将两个岛全部都找到,只需要找到第一个岛,然后从这个岛开始扩张,每次向外扩张一圈,直到找到第二个岛,那么我们的扩张次数就是这两个岛间的最短的桥了

逐步实现:
首先是如何找到第一个岛?

我们可以先使用广度优先算法

  • 首先遍历二维数组得到第一个 1 的位置(这就意味着我们摸到了第一个岛的一个角)
/**
* @param {number[][]} grid
* @return {number}
*/
var shortestBridge = function (grid) {
for (let i = 0; i < edge; i++) {
for (let j = 0; j < edge; j++) {
if (grid === 1) {
// 找到第一个岛
}
}
}
};
  • 找到这个岛之后,我们需要知道这个岛具体有多大?它的边界在哪里?

  • 我们可以使用广度优先算法来找到这个岛的全貌:我们现在得到了这个岛的一个 1,将它放入一个“待搜查”队列 queue 中,对于 queue 队列中的每一个点我们搜查它周围所有的 1 ,新找到的 1 也放入 queue 队列,用完的 1 扔给 island 数组,直到 queue 中没有点排队了,这样我们就将整个岛都纳入 island 数组了。

/**
* @param {number[][]} grid
* @return {number}
*/
var shortestBridge = function (grid) {
let edge = grid.length; // 水域的边界
let island = []; // 存放岛的数组
let queue = []; //存放待搜查的点的队列
let qlen; //队列的长度
let help = [
[1, 0],
[-1, 0],
[0, 1],
[0, -1],
]; //帮助我们找到周围的点的数组
let step = 0; //步数

for (let i = 0; i < edge; i++) {
for (let j = 0; j < edge; j++) {
if (grid[i][j] === 1) {
grid[i][j] = 2; //将找到的岛的点标记为2,以防止重复搜索
qlen = queue.push([i, j]); //排队等待搜查
whlie(qlen != 0); //若全搜查完毕就跳出循环
{
let check = queue.shift(); //取出排在最前面的点
qlen--;
let x = check[0];
let y = check[1]; //保存
island.push(check); //放入岛的数组中
for (let k = 0; k < 4; k++) {
//四次循环找上下左右的邻居
let nbx = x + help[k][0];
let nby = y + help[k][1]; //找到邻居的坐标
//判断邻居是否在水域内,且是否为岛
if (
nbx >= 0 &&
nby >= 0 &&
nbx < edge &&
nby < edge &&
grid[nbx][nby] === 1
) {
queue.push([nbx, nby]); //如果是就让去排队等待检查
qlen++;
grid[nbx][nby] = 2; //改变标记
}
}
}
}
}
}
};
  • 通过这些工作,我们就找到了第一个岛,并把岛的所有点的坐标都存在了 island 数组中,接下来我们就可以开始扩张去找第二个岛了。

  • 还是使用广度优先算法来扩张,每一次扩张遍历 island 数组,检查每一个点的四周,如果是水域,就将它划入岛的范围内,直到某一次扩张遇到了第二个岛,这时候我们扩张的次数就是最短的桥的长度了。

/**
* @param {number[][]} grid
* @return {number}
*/
var shortestBridge = function (grid) {
let edge = grid.length; // 水域的边界
let island = []; // 存放岛的数组
let queue = []; //存放待搜查的点的队列
let qlen; //队列的长度
let help = [
[1, 0],
[-1, 0],
[0, 1],
[0, -1],
]; //帮助我们找到周围的点的数组
let step = 0; //步数

for (let i = 0; i < edge; i++) {
for (let j = 0; j < edge; j++) {
if (grid[i][j] === 1) {
grid[i][j] = 2; //将找到的岛的点标记为2,以防止重复搜索
qlen = queue.push([i, j]); //排队等待搜查
while (qlen != 0) {
//若全搜查完毕就跳出循环
let check = queue.shift(); //取出排在最前面的点
qlen--;
let x = check[0];
let y = check[1]; //保存
island.push(check); //放入岛的数组中
for (let k = 0; k < 4; k++) {
//四次循环找上下左右的邻居
let nbx = x + help[k][0];
let nby = y + help[k][1]; //找到邻居的坐标
//判断邻居是否在水域内,且是否为岛
if (
nbx >= 0 &&
nby >= 0 &&
nbx < edge &&
nby < edge &&
grid[nbx][nby] === 1
) {
queue.push([nbx, nby]); //如果是就让去排队等待检查
qlen++;
grid[nbx][nby] = 2; //改变标记
}
}
}
for (let n of island) {
queue.push(n); //将岛的所有点都放入队列中
}
while (queue.length != 0) {
//开始第二次广度优先搜索
//在每一轮的扩张中队列的长度是不变的,新增的点需要在下一轮扩张中被检查
let qlen = queue.length;
for (let k = 0; k < qlen; k++) {
//每一轮扩张
let check = queue.shift();
let x = check[0];
let y = check[1];
for (let l = 0; l < 4; l++) {
//四次循环找上下左右的邻居
let nbx = x + help[l][0];
let nby = y + help[l][1];
if (nbx >= 0 && nby >= 0 && nbx < edge && nby < edge) {
if (grid[nbx][nby] === 1) {
//如果找到岛,直接返回次数
console.log(step + "===");
return step;
} else if (grid[nbx][nby] === 0) {
//如果是水域,就将它划入岛的范围内
queue.push([nbx, nby]);
grid[nbx][nby] = 2;
}
}
}
}
step++; //每一轮扩张结束后步数加一
}
}
}
}
};

总结

这一题中寻找第一个岛全部点还可以使用深度优先算法,有时间的时候可以尝试一下。

本题的广度优先思想使用了两次,一次是在获取所有岛的点,一次是在扩张岛的范围。两者有一个共同点: 虽说都是基于现有的点来计算,但是被计算的主体并不是 island 数组。究竟哪些点需要计算其周围的情况,是由 queue 队列来决定的。 这是控制时间与空间复杂度的关键。一个 queue 队列可以减少大量的重复运算。