Appearance
图论
图的性质
997. 找到小镇的法官
js
/**
* v1 图论:利用节点的入度和出度
* @param {number} n
* @param {number[][]} trust
* @return {number}
*/
const findJudge = function (n, trust) {
const inDegree = new Array(n + 1).fill(0);
const outDegree = new Array(n + 1).fill(0);
for (let i = 0; i < trust.length; i++) {
const [from, to] = trust[i];
inDegree[to]++; // 入度
outDegree[from]++; // 出度
}
for (let i = 1; i <= n; i++) {
// 法官的入度为 n - 1,出度为0
if (inDegree[i] === n - 1 && outDegree[i] === 0) {
return i;
}
}
return -1;
};1557. 可以到达所有点的最少点数目
js
/**
* v1 图论(出入度)
*
* 算法思想:所有入度为 0 的节点必须作为起点;其余节点因为入度 > 0,
* 至少有一个前驱可达,因此不必作为起点。选取全部入度为 0 的节点即可到达所有点,且集合最小。
* @param {number} n
* @param {number[][]} edges
* @return {number[]}
*/
const findSmallestSetOfVertices = function (n, edges) {
const inDegree = new Array(n).fill(0); // inDegree[i] 表示编号 i 的节点的入度
for (const [from, to] of edges) {
// 统计每个节点的入度(from 未使用,只需累加 to 的入度)
inDegree[to]++;
}
const ans = []; // 收集所有入度为 0 的节点作为最少起点集合
for (let i = 0; i < n; i++) {
// 遍历所有节点,挑选入度为 0 的
if (inDegree[i] === 0) {
ans.push(i);
}
}
return ans; // 返回可以到达所有点的最少点集合
};1615. 最大网络秩
js
/**
* v1 枚举(出入度)
*
* 复杂度:时间 O(n^2 + |E|),空间 O(n + |E|)。
* @param {number} n
* @param {number[][]} roads
* @return {number}
*/
const maximalNetworkRank = function (n, roads) {
const connected = Array.from({ length: n }, () => new Set()); // 邻接集合:connected[i] 存与 i 直连的城市
const degrees = new Array(n).fill(0); // 度数组:degrees[i] 为 i 的连接数
for (const [from, to] of roads) {
// 无向边:两端度各加一,并记录互为直连
degrees[from]++;
degrees[to]++;
connected[from].add(to);
connected[to].add(from);
}
let maxRank = 0;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
// 若 i 与 j 直连,合计度需减 1,避免重复计数这条边
const rank = degrees[i] + degrees[j] - (connected[i].has(j) ? 1 : 0);
maxRank = Math.max(maxRank, rank);
}
}
return maxRank;
};图的遍历
547. 省份数量
js
/**
* v1 DFS
* @param {number[][]} isConnected
* @return {number}
*/
const findCircleNum = function (isConnected) {
const n = isConnected.length;
const visited = new Array(n).fill(false);
let result = 0;
// 遍历每一个省份
for (let i = 0; i < n; i++) {
if (!visited[i]) {
dfs(isConnected, i);
result++;
}
}
return result;
function dfs(graph, i) {
if (visited[i]) return;
visited[i] = true;
// 遍历邻居节点
for (let j = 0; j < graph[i].length; j++) {
if (graph[i][j] === 1 && i !== j) {
dfs(graph, j);
}
}
}
};js
/**
* v1 BFS
* @param {number[][]} isConnected
* @return {number}
*/
const findCircleNum = function (isConnected) {
const n = isConnected.length;
const visited = new Array(n).fill(0);
let result = 0;
// 遍历每一个城市
for (let i = 0; i < n; i++) {
if (!visited[i]) {
bfs(isConnected, i); // 遍历相连的省份
result++;
}
}
return result;
function bfs(graph, i) {
const queue = [i];
visited[i] = true;
while (queue.length > 0) {
const cur = queue.shift();
for (let j = 0; j < graph[cur].length; j++) {
if (graph[cur][j] === 1 && !visited[j]) {
visited[j] = true;
queue.push(j);
}
}
}
}
};js
/**
* v1 并查集
* @param {number[][]} isConnected
* @return {number}
*/
const findCircleNum = function (isConnected) {
const n = isConnected.length;
const uf = new UF(n);
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
// 如果两个城市是相连的,那么它们属于同一个连通分量
if (isConnected[i][j] === 1) {
uf.union(i, j);
}
}
}
return uf.size; // 返回联通分量的数量
};
class UF {
constructor(n) {
this.parent = Array.from({ length: n }, (_, index) => index);
this.size = n;
}
union(i, j) {
const rootI = this.find(i);
const rootJ = this.find(j);
if (rootI === rootJ) return;
this.parent[rootI] = rootJ;
// 两个连通分量合并成一个连通分量
this.size--;
}
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
}841. 钥匙和房间
js
/**
* v1 DFS
* @param {number[][]} rooms
* @return {boolean}
*/
let canVisitAllRooms = function (rooms) {
const n = rooms.length;
const visited = new Array(n).fill(false);
const dfs = (room) => {
if (visited[room]) return;
// 前序位置,标记房间已访问
visited[room] = true;
for (const key of rooms[room]) {
dfs(key);
}
};
dfs(0);
return visited.every((item) => item);
};js
/**
* v1 BFS
* @param {number[][]} rooms
* @return {boolean}
*/
let canVisitAllRooms = function (rooms) {
const n = rooms.length;
const visited = new Array(n).fill(false);
const q = [0];
visited[0] = true;
while (q.length > 0) {
let cur = q.shift();
for (const key of rooms[cur]) {
if (visited[key]) continue;
visited[key] = true;
q.push(key);
}
}
return visited.every((item) => item);
};797. 所有可能的路径
js
/**
* v1 DFS
* @param {number[][]} graph
* @return {number[][]}
*/
const allPathsSourceTarget = function (graph) {
const n = graph.length;
const paths = []; // 记录所有路径
const path = []; // 记录当前遍历中的路径
dfs(0);
return paths;
function dfs(s) {
// 添加节点 s 到路径
path.push(s);
if (s === n - 1) {
// 到达终点
paths.push([...path]);
path.pop(); // 弹出最终节点
return;
}
// 递归每个相邻节点
for (const next of graph[s]) {
dfs(next);
}
// 从路径移出节点 s
path.pop();
}
};1466. 重新规划路线
js
/**
* v1 DFS
* @param {number} n
* @param {number[][]} connections
* @return {number}
*/
const minReorder = function (n, connections) {
const graph = Array.from({ length: n }, () => []);
for (const [from, to] of connections) {
// 构建无向图,并标记原始边1和反向边0
graph[from].push([to, 1]);
graph[to].push([from, 0]);
}
const visited = new Array(n).fill(false);
let ans = 0;
// 从0号节点出发遍历
dfs(graph, 0);
return ans;
function dfs(graph, x) {
visited[x] = true;
for (const [next, type] of graph[x]) {
if (!visited[next]) {
if (type === 1) ans++;
dfs(graph, next);
}
}
}
};js
/**
* v1 BFS
* @param {number} n
* @param {number[][]} connections
* @return {number}
*/
const minReorder = function (n, connections) {
// 使用邻接表构建图
// 由于是有向图,为了能遍历整棵树,我们将其视为无向图存储
const graph = Array.from({ length: n }, () => []);
for (const [from, to] of connections) {
// 标记边的方向:1表示原始边,0表示新增的反向边
graph[from].push([to, 1]);
graph[to].push([from, 0]);
}
const visited = new Array(n).fill(false);
let ans = 0;
// BFS 队列,从节点 0 开始向外扩散
const queue = [0];
visited[0] = true;
while (queue.length > 0) {
const cur = queue.shift();
// 遍历所有相邻节点(无论方向)
for (const [next, type] of graph[cur]) {
if (visited[next]) continue;
visited[next] = true;
// 如果 type === 1,说明原边方向是 cur -> next(背离 0 号节点)
// 为了让所有节点都能到达 0,我们需要将这条边翻转为 next -> cur
if (type === 1) ans++;
// 继续将下一层节点加入队列
queue.push(next);
}
}
return ans;
};✨802. 找到最终的安全状态
js
/**
* v1 DFS(三色标记法)
* @param {number[][]} graph
* @return {number[]}
*/
const eventualSafeNodes = function (graph) {
// 如果起始节点位于一个环内,或者能到达一个环,则该节点不是安全的
const n = graph.length;
// 0: 未访问, 1: 访问中 (正在当前递归路径上), 2: 安全
const colors = new Array(n).fill(0);
const ans = [];
for (let i = 0; i < n; i++) {
if (dfs(i)) {
ans.push(i);
}
}
return ans;
function dfs(i) {
if (colors[i] > 0) {
// 1 (灰色) -> false
// 2 (黑色) -> true
return colors[i] == 2;
}
colors[i] = 1; // 标记为访问中
for (const next of graph[i]) {
if (!dfs(next)) {
return false;
}
}
// 所有邻居都安全,标记为安全
colors[i] = 2;
return true;
}
};✨1129. 颜色交替的最短路径
js
/**
* @param {number} n
* @param {number[][]} redEdges
* @param {number[][]} blueEdges
* @return {number[]}
*/
const shortestAlternatingPaths = function (n, redEdges, blueEdges) {
const graph = buildGraph(n, redEdges, blueEdges);
// 记录从节点0到节点i的最短路径,初始为-1表示无法到达
const answer = new Array(n).fill(-1);
// 在交替路径中,同一个节点可能需要被访问两次(分别通过红边和蓝边到达)
// visited[node][color] 表示是否通过color颜色的边到达过node。
// 0:red 1:blue
const visited = Array.from({ length: n }, () => [false, false]);
visited[0][0] = true;
visited[0][1] = true;
// [节点, 最短距离,上一条边的颜色(-1表示起始节点)]
const queue = [[0, 0, -1]];
while (queue.length > 0) {
const [cur, dis, lastColor] = queue.shift();
if (answer[cur] === -1) {
answer[cur] = dis;
}
for (const [next, colorType] of graph[cur]) {
// 只有当当前边的颜色与上一条边的颜色不同,且该状态未被访问过时才处理
if (lastColor !== colorType && !visited[next][colorType]) {
visited[next][colorType] = true;
queue.push([next, dis + 1, colorType]);
}
}
}
return answer;
};
function buildGraph(n, redEdges, blueEdges) {
// from,to,1/0 : 0标识red 1表示blue
const graph = Array.from({ length: n }, () => []);
for (const [from, to] of redEdges) {
graph[from].push([to, 0]);
}
for (const [from, to] of blueEdges) {
graph[from].push([to, 1]);
}
return graph;
}✨1376. 通知所有员工所需的时间
js
/**
* v1 DFS
* @param {number} n
* @param {number} headID
* @param {number[]} manager
* @param {number[]} informTime
* @return {number}
*/
const numOfMinutes = function (n, headID, manager, informTime) {
let maxTime = 0;
// 使用领接表优化下属的查找过程
const graph = new Array(n).fill(0).map(() => []);
for (let i = 0; i < manager.length; i++) {
if (manager[i] !== -1) {
graph[manager[i]].push(i);
}
}
dfs(headID, 0);
return maxTime;
function dfs(id, currentTime) {
// 如果是叶子节点(没有下属),更新最大时间
if (graph[id].length === 0) {
maxTime = Math.max(maxTime, currentTime);
return;
}
// 前序位置
currentTime += informTime[id];
// 查找下属
for (const next of graph[id]) {
dfs(next, currentTime);
}
// 后序位置
// 值传递不需要手动回溯
// currentTime -= informTime[id]
}
};js
/**
* v1 BFS
* @param {number} n
* @param {number} headID
* @param {number[]} manager
* @param {number[]} informTime
* @return {number}
*/
const numOfMinutes = function (n, headID, manager, informTime) {
// 构建邻接表
const graph = new Array(n).fill(0).map(() => []);
for (let i = 0; i < manager.length; i++) {
if (manager[i] !== -1) {
graph[manager[i]].push(i);
}
}
let maxTime = 0;
// 存放该员工id和通知到达他的时间
const queue = [[headID, 0]];
while (queue.length > 0) {
const [cur, time] = queue.shift();
if (time > maxTime) maxTime = time;
for (const next of graph[cur]) {
queue.push([next, time + informTime[cur]]);
}
}
return maxTime;
};1306. 跳跃游戏 III
js
/**
* v1 DFS
* @param {number[]} arr
* @param {number} start
* @return {boolean}
*/
const canReach = function (arr, start) {
let n = arr.length;
const visited = new Array(n).fill(false);
let canArrive = false;
const dfs = (start) => {
// 索引越界、已经遍历过、已经找到了
if (start < 0 || start >= n || visited[start] || canArrive) return;
visited[start] = true;
const step = arr[start];
if (step === 0) canArrive = true;
dfs(start - step);
dfs(start + step);
};
dfs(start);
// 无法到达目标
return canArrive;
};js
/**
* v1 BFS
* @param {number[]} arr
* @param {number} start
* @return {boolean}
*/
const canReach = function (arr, start) {
const n = arr.length;
// 记录访问过的索引,防止重复访问导致死循环
const visited = new Array(n).fill(false);
// 初始化队列,将起始位置加入队列
const queue = [start];
visited[start] = true;
while (queue.length > 0) {
const cur = queue.shift(); // 取出当前位置
// 如果当前位置的值为 0,说明找到了目标,返回 true
if (arr[cur] === 0) return true;
// 遍历当前位置能跳到的两个方向:向右跳 (cur + arr[cur]) 和 向左跳 (cur - arr[cur])
for (const next of [cur + arr[cur], cur - arr[cur]]) {
// 检查边界条件:索引不能越界,且未被访问过
if (next < 0 || next >= n || visited[next]) continue;
// 标记为已访问并加入队列
visited[next] = true;
queue.push(next);
}
}
// 如果遍历完所有可能的路径都无法到达值为 0 的位置,返回 false
return false;
};1926. 迷宫中离入口最近的出口
js
/**
* v1 BFS
* @param {character[][]} maze
* @param {number[]} entrance
* @return {number}
*/
const nearestExit = function (maze, entrance) {
const m = maze.length;
const n = maze[0].length;
const visited = Array.from({ length: m }, () => new Array(n).fill(false));
const q = [entrance];
visited[entrance[0]][entrance[1]] = true;
let directions = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
];
let step = 0;
while (q.length > 0) {
let sz = q.length;
for (let i = 0; i < sz; i++) {
let [row, col] = q.shift();
// 到达出口
if (
(row === 0 || row === m - 1 || col === 0 || col === n - 1) &&
(row !== entrance[0] || col !== entrance[1])
) {
return step;
}
for (const dir of directions) {
const nextRow = row + dir[0];
const nextCol = col + dir[1];
if (
nextRow >= m ||
nextRow < 0 ||
nextCol >= n ||
nextCol < 0 ||
visited[nextRow][nextCol] ||
maze[nextRow][nextCol] === "+"
)
continue;
visited[nextRow][nextCol] = true;
q.push([nextRow, nextCol]);
}
}
step++;
}
return -1;
};1091. 二进制矩阵中的最短路径
js
/**
* v1 BFS
* @param {number[][]} grid
* @return {number}
*/
const shortestPathBinaryMatrix = function (grid) {
if (grid[0][0] === 1) return -1;
const n = grid.length;
const visited = new Set();
const q = [[0, 0]];
visited.add("00");
const directions = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
[-1, -1],
[-1, 1],
[1, -1],
[1, 1],
];
let step = 0;
while (q.length > 0) {
const sz = q.length;
for (let i = 0; i < sz; i++) {
const [row, col] = q.shift();
if (row === n - 1 && col === n - 1) {
return step + 1;
}
for (let dir of directions) {
const nextRow = row + dir[0];
const nextCol = col + dir[1];
if (
nextRow >= n ||
nextRow < 0 ||
nextCol >= n ||
nextCol < 0 ||
visited.has(`${nextRow}${nextCol}`) ||
grid[nextRow][nextCol] === 1
) {
continue;
}
visited.add(`${nextRow}${nextCol}`);
q.push([nextRow, nextCol]);
}
}
step++;
}
return -1;
};433. 最小基因变化
js
/**
* @param {string} startGene
* @param {string} endGene
* @param {string[]} bank
* @return {number}
*/
const minMutation = function (startGene, endGene, bank) {
const visited = new Set();
const q = [startGene];
visited.add(startGene);
let step = 0;
while (q.length > 0) {
const sz = q.length;
for (let i = 0; i < sz; i++) {
const curGene = q.shift();
if (curGene === endGene) return step;
for (const gene of getNeighbor(curGene)) {
if (visited.has(gene) || !bank.includes(gene)) continue;
visited.add(gene);
q.push(gene);
}
}
step++;
}
return -1;
};
/**
*
* @param {string} gene
* @returns
*/
const getNeighbor = (gene) => {
const res = [];
for (let i = 0; i < 8; i++) {
for (const g of ["A", "C", "G", "T"]) {
if (g === gene[i]) continue;
res.push(gene.slice(0, i) + g + gene.slice(i + 1));
}
}
return res;
};✨542. 01 矩阵
js
/**
* v1 BFS
*
* 使用多源广度优先搜索
* 从所有的 0 开始向外扩散,计算每个 1 到最近的 0 的距离
* @param {number[][]} mat
* @return {number[][]}
*/
const updateMatrix = function (mat) {
const m = mat.length;
const n = mat[0].length;
// 初始化结果矩阵,全部填充为 -1 表示未访问
// 这样可以区分哪些位置已经计算过最短距离
const res = Array.from({ length: m }, () => new Array(n).fill(-1));
const queue = [];
// 1. 初始化队列:将所有值为 0 的点作为起点加入队列
// 这些点的距离为 0
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (mat[i][j] === 0) {
res[i][j] = 0;
queue.push([i, j]);
}
}
}
// 定义上下左右四个方向
const directions = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
];
// 2. 开始 BFS
while (queue.length > 0) {
const [x, y] = queue.shift();
// 遍历四个相邻方向
for (const [dx, dy] of directions) {
const nextX = x + dx;
const nextY = y + dy;
// 检查边界条件和是否已访问
// 如果越界或者 res[nextX][nextY] !== -1 (说明已经求出更短距离了),则跳过
if (
nextX < 0 ||
nextX >= m ||
nextY < 0 ||
nextY >= n ||
res[nextX][nextY] !== -1 // 跳过0
) {
continue;
}
// 此时一定是1
// 更新距离:当前点距离 + 1
res[nextX][nextY] = res[x][y] + 1;
// 将新访问的点加入队列
queue.push([nextX, nextY]);
}
}
return res;
};✨863. 二叉树中所有距离为 K 的结点
js
/**
* v1 BFS
* @param {TreeNode} root
* @param {TreeNode} target
* @param {number} k
* @return {number[]}
*/
let distanceK = function (root, target, k) {
// 1. 将树转化为图:构建子节点到父节点的映射
// 这样我们可以从任意节点向父节点移动
const parent = new Map();
function traverse(node, parentNode) {
if (node === null) return;
parent.set(node.val, parentNode);
traverse(node.left, node);
traverse(node.right, node);
}
traverse(root, null);
// 2. 使用 BFS 从 target 节点开始向外扩散搜索
const visited = new Set(); // 记录访问过的节点值,避免回头
const queue = [target];
visited.add(target.val);
let currentDistance = 0;
while (queue.length > 0) {
// 如果当前层级的距离等于 k,那么队列中所有的节点就是我们要找的结果
if (currentDistance === k) {
return queue.map((node) => node.val);
}
const sz = queue.length;
for (let i = 0; i < sz; i++) {
const cur = queue.shift();
// 遍历当前节点的三个邻居:左子节点、右子节点、父节点
const neighbors = [cur.left, cur.right, parent.get(cur.val)];
for (const next of neighbors) {
// 如果邻居存在且未被访问过
if (next && !visited.has(next.val)) {
visited.add(next.val);
queue.push(next);
}
}
}
currentDistance++;
}
// 如果搜索结束还没找到,返回空数组
return [];
};✨934. 最短的桥
js
/**
* v BFS(DFS标记岛屿,BFS记录最短路径)
* @param {number[][]} grid
* @return {number}
*/
const shortestBridge = function (grid) {
const m = grid.length;
const n = grid[0].length;
// 标记已访问节点,避免重复处理
const visited = Array.from({ length: m }, () => new Array(n).fill(false));
const queue = [];
const directions = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
];
// 1. 寻找第一座岛屿
// 只要找到第一个为1的点,就通过DFS将整个岛屿找出并加入BFS队列
let findFirstLand = false;
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === 1) {
dfs(i, j); // 启动DFS
findFirstLand = true;
break;
}
}
if (findFirstLand) break;
}
// DFS: 标记第一座岛屿的所有节点,并将它们全部加入 BFS 队列作为起点
function dfs(x, y) {
// 边界检查 + 访问检查 + 是否为陆地检查
if (
x < 0 ||
x >= m ||
y < 0 ||
y >= n ||
visited[x][y] ||
grid[x][y] === 0
) {
return;
}
visited[x][y] = true;
// 将第一座岛屿的节点入队,距离初始为 0
queue.push([x, y, 0]);
for (const dir of directions) {
dfs(x + dir[0], y + dir[1]);
}
}
// 2. 多源 BFS 向外层层扩展,寻找第二座岛屿
while (queue.length > 0) {
let [curX, curY, count] = queue.shift();
for (const dir of directions) {
const nextX = curX + dir[0];
const nextY = curY + dir[1];
// 越界或已访问过则跳过
if (
nextX < 0 ||
nextX >= m ||
nextY < 0 ||
nextY >= n ||
visited[nextX][nextY]
)
continue;
visited[nextX][nextY] = true;
// 如果遇到了未访问过的陆地(1),说明触碰到了第二座岛屿
// 当前的 count 即为最短桥的长度(跨越的水域数)
if (grid[nextX][nextY] === 1) {
return count;
}
// 遇到水域(0),继续扩展,距离+1
queue.push([nextX, nextY, count + 1]);
}
}
return 0;
};127. 单词接龙
js
/**
* v1 BFS
* @param {string} beginWord
* @param {string} endWord
* @param {string[]} wordList
* @return {number}
*/
const ladderLength = function (beginWord, endWord, wordList) {
// 将 wordList 转换为 Set 以提高查找效率 (O(1))
const wordSet = new Set(wordList);
// 如果 endWord 不在字典中,无法转换,直接返回 0
if (!wordSet.has(endWord)) return 0;
// BFS 队列,初始包含 beginWord
const queue = [beginWord];
// 记录已访问过的单词,避免重复处理
const visited = new Set();
visited.add(beginWord);
// 序列长度初始化为 1 (包含 beginWord 本身)
let len = 1;
while (queue.length > 0) {
const sz = queue.length;
// 遍历当前层的所有节点
for (let i = 0; i < sz; i++) {
const cur = queue.shift();
// 找到目标单词,返回当前序列长度
if (cur === endWord) return len;
// 尝试变换当前单词的每一个字符
for (const next of getNeighbor(cur)) {
// 如果变换后的单词在字典中且未访问过
if (wordSet.has(next) && !visited.has(next)) {
visited.add(next);
queue.push(next);
}
}
}
// 当前层遍历结束,长度 +1
len++;
}
// 队列为空仍未找到 endWord,表示无法到达
return 0;
};
// 辅助函数:生成所有可能的邻居节点(只改变一个字符)
function getNeighbor(word) {
const chars = word.split("");
const res = [];
for (let i = 0; i < word.length; i++) {
const originalChar = chars[i];
for (let j = 0; j < 26; j++) {
let char = String.fromCharCode(j + 97);
if (char === originalChar) continue;
// 拼接新单词
chars[i] = char;
const newWord = chars.join("");
res.push(newWord);
}
chars[i] = originalChar;
}
return res;
}岛屿问题
200. 岛屿数量
js
/**
* v1 DFS 遍历二维数组
* 岛屿问题
* @param {character[][]} grid
* @return {number}
*/
let numIslands = function (grid) {
let m = grid.length;
let n = grid[0].length;
let result = 0;
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === "1") {
// 找到岛屿
result += 1;
dfs(grid, i, j);
}
}
}
return result;
};
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
function dfs(grid, i, j) {
let m = grid.length;
let n = grid[0].length;
// 超出索引边界
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
if (grid[i][j] === "0") {
// 已经是海水了
return;
}
// 将grid[i][j]变成海水
grid[i][j] = "0";
dfs(grid, i - 1, j); // 上
dfs(grid, i + 1, j); // 下
dfs(grid, i, j - 1); // 左
dfs(grid, i, j + 1); // 右
}1020. 飞地的数量
js
/**
* v1 dfs 岛屿问题
* 封闭岛屿中陆地的数量
* @param {number[][]} grid
* @return {number}
*/
let numEnclaves = function (grid) {
let m = grid.length;
let n = grid[0].length;
let result = 0;
for (let i = 0; i < m; i++) {
dfs(grid, i, 0); // 左边岛屿淹了
dfs(grid, i, n - 1); // 右边岛屿淹了
}
for (let j = 0; j < n; j++) {
dfs(grid, 0, j); // 上边岛屿淹了
dfs(grid, m - 1, j); // 下边岛屿淹了
}
// 剩下的岛屿都是封闭岛屿
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === 1) {
result++; // 统计封闭岛屿中陆地的个数
}
}
}
return result;
};
let directions = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
];
// 从 (i, j) 开始,将相邻的陆地都变成海水
function dfs(grid, i, j) {
let m = grid.length;
let n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
if (grid[i][j] === 0) {
return;
}
grid[i][j] = 0;
for (let dir of directions) {
dfs(grid, i + dir[0], j + dir[1]);
}
}1254. 统计封闭岛屿的数目
js
/**
* v1 dfs 岛屿问题
* @param {number[][]} grid
* @return {number}
*/
let closedIsland = function (grid) {
let result = 0;
let m = grid.length;
let n = grid[0].length;
for (let j = 0; j < n; j++) {
dfs(grid, 0, j); // 把靠上的岛屿淹掉
dfs(grid, m - 1, j); // 把靠下的岛屿淹掉
}
for (let i = 0; i < m; i++) {
dfs(grid, i, 0); // 把靠左的岛屿淹掉
dfs(grid, i, n - 1); // 把靠右的岛屿淹掉
}
// 剩下的岛屿都是封闭岛屿
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === 0) {
result++;
// 把周围陆地淹了
dfs(grid, i, j);
}
}
}
return result;
};
let directions = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
];
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
function dfs(grid, i, j) {
let m = grid.length;
let n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
// 已经是海水了
if (grid[i][j] === 1) {
return;
}
// 将 (i, j) 变成海水
grid[i][j] = 1;
// 淹没上下左右的陆地
for (const dir of directions) {
nextI = i + dir[0];
nextJ = j + dir[1];
dfs(grid, nextI, nextJ);
}
}695. 岛屿的最大面积
js
/**
* v1 DFS 岛屿问题
* @param {number[][]} grid
* @return {number}
*/
let maxAreaOfIsland = function (grid) {
let m = grid.length;
let n = grid[0].length;
let maxArea = 0; // 记录岛屿的最大面积
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === 1) {
// 淹没岛屿,并更新最大岛屿面积
let area = dfs(grid, i, j);
maxArea = Math.max(maxArea, area);
}
}
}
return maxArea;
};
let directions = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
];
// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
function dfs(grid, i, j) {
let m = grid.length;
let n = grid[0].length;
let area = 0;
// 超出索引边界
if (i < 0 || j < 0 || i >= m || j >= n) {
return 0;
}
// 已经是海水了
if (grid[i][j] === 0) {
return 0;
}
area++;
grid[i][j] = 0; // 将 (i, j) 变成海水
for (let dir of directions) {
area += dfs(grid, i + dir[0], j + dir[1]);
}
return area;
}417. 太平洋大西洋水流问题
js
/**
* v1 BFS
* @param {number[][]} heights
* @return {number[][]}
*/
const pacificAtlantic = function (heights) {
const m = heights.length;
const n = heights[0].length;
// 标记可以流入太平洋的坐标
const visitedP = Array.from({ length: m }, () => new Array(n).fill(false));
const queueP = [];
// 第一行和第一列一定可以流入太平洋
for (let i = 0; i < n; i++) {
visitedP[0][i] = true;
queueP.push([0, i]);
}
for (let j = 1; j < m; j++) {
visitedP[j][0] = true;
queueP.push([j, 0]);
}
// bfs搜索
bfs(heights, queueP, visitedP);
// 标记可以流入大西洋的坐标
const visitedO = Array.from({ length: m }, () => new Array(n).fill(false));
const queueO = [];
// 最后一行和最后一列一定可以流入太平洋
for (let i = 0; i < n; i++) {
visitedO[m - 1][i] = true;
queueO.push([m - 1, i]);
}
for (let j = 0; j < m - 1; j++) {
visitedO[j][n - 1] = true;
queueO.push([j, n - 1]);
}
// bfs搜索
bfs(heights, queueO, visitedO);
const res = [];
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (visitedO[i][j] && visitedP[i][j]) {
res.push([i, j]);
}
}
}
return res;
};
const bfs = (heights, queue, visited) => {
const m = heights.length;
const n = heights[0].length;
const directions = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
];
while (queue.length > 0) {
const pos = queue.shift();
for (const dir of directions) {
const x = pos[0] + dir[0];
const y = pos[1] + dir[1];
if (x < 0 || x >= m || y < 0 || y >= n || visited[x][y]) continue;
if (heights[x][y] < heights[pos[0]][pos[1]]) continue;
visited[x][y] = true;
queue.push([x, y]);
}
}
};环检测
✨207. 课程表
js
/**
* v1.1 环检测(DFS)
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
let canFinish = function (numCourses, prerequisites) {
// 创建有向图
let graph = buildGraph(numCourses, prerequisites);
// 记录在当前路径上的节点
let onPath = new Array(numCourses).fill(false);
// 记录图中是否有环
let hasCircle = false;
// 记录遍历过的节点
let visited = new Array(numCourses).fill(false);
function dfs(graph, v, onPath) {
// 如果存在环则返回
if (hasCircle) {
return;
}
// v 已经在递归路径上,说明成环了
if (onPath[v]) {
hasCircle = true;
return;
}
// 如果该节点已经遍历过,代表从该节点出发进行的环检测已经完成,则直接返回
if (visited[v]) {
return;
}
visited[v] = true;
// 前序位置:记录路径上的节点
onPath[v] = true;
for (let s of graph[v]) {
dfs(graph, s, onPath);
}
// 后续位置:撤销节点记录
onPath[v] = false;
}
// 因为不是强连通图,所以需要每个节点都检测一遍(存在重复需要剪枝)
for (let i = 0; i < numCourses; i++) {
if (!visited[i]) {
dfs(graph, i, onPath);
}
}
// 只要没有循环依赖可以完成所有课程
return !hasCircle;
};
// 创建有向图
let buildGraph = function (numCourses, prerequisites) {
// 节点:课程
// 边:课程的依赖关系,先修课程 -> 课程
let graph = Array.from({ length: numCourses }, () => []); // 邻接表
for (let [to, from] of prerequisites) {
graph[from].push(to);
}
return graph;
};js
/**
* v1 环检测(BFS)
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
let canFinish = function (numCourses, prerequisites) {
// 创建有向图
let graph = buildGraph(numCourses, prerequisites);
// 记录所有节点的入度(依赖关系)
let inDegree = new Array(numCourses).fill(0);
for (let [to, from] of prerequisites) {
inDegree[to]++;
}
let queue = [];
for (let i = 0; i < inDegree.length; i++) {
if (inDegree[i] === 0) {
// 节点 i 没有入度,即没有依赖的节点
// 可以作为拓扑排序的起点,加入队列
queue.push(i);
}
}
// 记录遍历的节点数量
let count = 0;
// 开始执行 BFS 循环
while (queue.length !== 0) {
let sz = queue.length;
for (let i = 0; i < sz; i++) {
// 弹出节点 cur,并将它指向的节点入度 -1
let cur = queue.shift();
count++;
for (let next of graph[cur]) {
inDegree[next]--;
if (inDegree[next] === 0) {
// 如果入度变为 0,说明 next 依赖的节点都已被遍历,可以入队
queue.push(next);
}
}
}
}
// 如果所有节点都被遍历过,说明不成环
return count === numCourses;
};
// 创建有向图
let buildGraph = function (numCourses, prerequisites) {
// 节点:课程
// 边:课程的依赖关系,先修课程 -> 课程
let graph = Array.from({ length: numCourses }, () => []); // 邻接表
for (let [to, from] of prerequisites) {
graph[from].push(to);
}
return graph;
};拓扑排序
✨210. 课程表 II
js
/**
* v1 拓扑排序
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {number[]}
*/
let findOrder = function (numCourses, prerequisites) {
// 记录是否成环
let hasCycle = false;
// 存放后续遍历结果
let postorder = [];
// 记录当前路径上的节点
let onPath = new Array(numCourses).fill(false);
// 记录遍历过的节点
let visited = new Array(numCourses).fill(false);
let graph = buildGraph(numCourses, prerequisites);
function traverse(graph, v) {
// 当前路径上存在已遍历节点,则说明存在环
if (onPath[v]) {
hasCycle = true;
return;
}
// 存在环或者,已经该节点已经遍历过则直接返回
// 已经遍历过表示,从该节点出发的环检测已经完成,不需要重复进行
if (hasCycle || visited[v]) {
return;
}
// 前序位置
visited[v] = true;
onPath[v] = true;
for (let s of graph[v]) {
traverse(graph, s);
}
// 后续位置
onPath[v] = false;
postorder.push(v);
}
for (let i = 0; i < numCourses; i++) {
traverse(graph, i);
}
// 存在环则无法进行拓扑排序
if (hasCycle) {
return [];
}
// 逆后续遍历结果就是拓扑排序
return postorder.reverse();
};
let buildGraph = function (numCourses, prerequisites) {
// 节点:课程
// 边:课程的依赖关系,先修课程->课程
let graph = Array.from({ length: numCourses }, () => []); // 邻接表
for (let [to, from] of prerequisites) {
graph[from].push(to);
}
return graph;
};js
/**
* v1 拓扑排序(BFS)
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {number[]}
*/
let findOrder = function (numCourses, prerequisites) {
// 记录拓扑排序结果
let path = [];
// 构建有向图
let graph = buildGraph(numCourses, prerequisites);
// 记录所有节点的入度
let inDegree = new Array(numCourses).fill(0);
for (let [to, from] of prerequisites) {
inDegree[to]++;
}
// 初始化:将所有入度为零(无依赖)节点入队
let queue = [];
for (let i = 0; i < inDegree.length; i++) {
if (inDegree[i] === 0) {
queue.push(i);
}
}
// 记录遍历过的节点数量
let count = 0;
// BFS算法模板
while (queue.length !== 0) {
let cur = queue.shift(); // 节点出队
path.push(cur);
count++;
// 更新邻居节点的入度
for (let next of graph[cur]) {
inDegree[next]--;
// 当节点入度为零(无依赖)就入队
if (inDegree[next] === 0) {
queue.push(next);
}
}
}
// 存在环,拓扑排序不存在
if (count !== numCourses) {
return [];
}
return path;
};
let buildGraph = function (numCourses, prerequisites) {
// 节点:课程
// 边:课程的依赖关系,先修课程->课程
let graph = Array.from({ length: numCourses }, () => []); // 邻接表
for (let [to, from] of prerequisites) {
graph[from].push(to);
}
return graph;
};二分图判定
✨785. 判断二分图
js
/**
* v1 二分图判断问题 DFS
* @param {number[][]} graph
* @return {boolean}
*/
let isBipartite = function (graph) {
// 记录图是否符合二分图性质
let ok = true;
let visited = new Array(graph.length).fill(false); // 记录访问过的节点,防止环
// 记录每个节点的颜色: false 和 true 代表两种颜色
let colors = new Array(graph.length).fill(false);
function dfs(v) {
// 如果已经确定不是二分图了,就不用浪费时间再递归遍历了
if (!ok) return;
// 前序位置
visited[v] = true;
let neighbors = graph[v];
for (let next of neighbors) {
// 没遍历过的领居
if (!visited[next]) {
// 那么应该给节点 next 涂上和节点 v 不同的颜色
colors[next] = !colors[v];
dfs(next); // 继续遍历
} else {
// 已经遍历过的邻居,根据颜色判断是否是二分图
if (colors[next] === colors[v]) {
ok = false;
return;
}
}
}
}
// 因为图不一定是联通的,可能存在多个子图
// 所以要把每个节点都作为起点进行一次遍历
// 如果发现任何一个子图不是二分图,整幅图都不算二分图
for (let i = 0; i < graph.length; i++) {
if (!visited[i]) {
dfs(i);
}
}
return ok;
};js
/**
* v1 二分图判断问题 BFS
* @param {number[][]} graph
* @return {boolean}
*/
let isBipartite = function (graph) {
// 记录图是否符合二分图性质
let ok = true;
// 记录访问过的节点,防止环
let visited = new Array(graph.length).fill(false);
// 记录每个节点的颜色: false 和 true 代表两种颜色
let colors = new Array(graph.length).fill(false);
// 从 v 节点开始进行 BFS 遍历
function bfs(v) {
let queue = [v];
visited[v] = true;
while (queue.length !== 0 && ok) {
let cur = queue.shift();
// 从节点 cur 向所有相邻节点扩散
for (let next of graph[cur]) {
// 没遍历过的领居
if (!visited[next]) {
// 那么应该给节点 next 涂上和节点 v 不同的颜色
colors[next] = !colors[cur];
// 标记 w 节点,并放入队列
visited[next] = true;
queue.push(next);
} else {
// 已经遍历过的邻居,根据颜色判断是否是二分图
if (colors[next] === colors[cur]) {
ok = false;
return;
}
}
}
}
}
// 因为图不一定是联通的,可能存在多个子图
// 所以要把每个节点都作为起点进行一次遍历
// 如果发现任何一个子图不是二分图,整幅图都不算二分图
for (let i = 0; i < graph.length; i++) {
if (!visited[i]) {
bfs(i);
}
}
return ok;
};886. 可能的二分法
js
/**
* v1 二分图判断 DFS
* @param {number} n
* @param {number[][]} dislikes
* @return {boolean}
*/
let possibleBipartition = function (n, dislikes) {
// 构建无向图
let graph = buildGraph(n, dislikes);
// 是否可以分组成功
let ok = true;
// 记录访问过的节点
let visited = new Array(n).fill(false);
// 记录节点的颜色
let colors = new Array(n).fill(false);
function dfs(v) {
if (!ok) return;
visited[v] = true;
for (let next of graph[v]) {
// 邻居节点为访问时,染上不同颜色
if (!visited[next]) {
colors[next] = !colors[v];
dfs(next);
} else {
// 领居节点已经反问时,判断颜色是否相同
if (colors[next] === colors[v]) {
ok = false;
return;
}
}
}
}
// 可能存在多个子图,所有要遍历所有的节点
for (let i = 0; i < n; i++) {
if (!visited[i]) {
dfs(i);
}
}
return ok;
};
// 构建无向图
let buildGraph = function (n, dislikes) {
let graph = Array.from({ length: n }, () => []);
for (let [v1, v2] of dislikes) {
// 将图节点编号转换为 0 - n-1
graph[v1 - 1].push(v2 - 1);
graph[v2 - 1].push(v1 - 1);
}
return graph;
};js
/**
* v1 二分图判断 BFS
* @param {number} n
* @param {number[][]} dislikes
* @return {boolean}
*/
let possibleBipartition = function (n, dislikes) {
// 构建无向图
let graph = buildGraph(n, dislikes);
// 是否可以分组成功
let ok = true;
// 记录访问过的节点
let visited = new Array(n).fill(false);
// 记录节点的颜色
let colors = new Array(n).fill(false);
function bfs(v) {
let queue = [v];
visited[v] = true;
while (queue.length !== 0 && ok) {
let cur = queue.shift();
for (let next of graph[cur]) {
// 邻居节点为访问时,染上不同颜色
if (!visited[next]) {
colors[next] = !colors[cur];
queue.push(next);
visited[next] = true;
} else {
// 领居节点已经反问时,判断颜色是否相同
if (colors[next] === colors[cur]) {
ok = false;
return;
}
}
}
}
}
// 可能存在多个子图,所有要遍历所有的节点
for (let i = 0; i < n; i++) {
if (!visited[i]) {
bfs(i);
}
}
return ok;
};
// 构建无向图
let buildGraph = function (n, dislikes) {
let graph = Array.from({ length: n }, () => []);
for (let [v1, v2] of dislikes) {
// 将图节点编号转换为 0 - n-1
graph[v1 - 1].push(v2 - 1);
graph[v2 - 1].push(v1 - 1);
}
return graph;
};并查集
✨990. 等式方程的可满足性
js
/**
* v1 并查集
* @param {string[]} equations 等式数组,每个等式形如 "a==b" 或 "a!=b"
* @return {boolean} 返回这些等式是否能同时成立
*/
let equationsPossible = function (equations) {
// 创建并查集,大小为26(对应26个小写字母)
let union = new UF(26);
// 第一步:处理所有相等关系
for (let equ of equations) {
if (equ[1] === "=") {
// 检查是否是相等关系
// 将字母转换为0-25的索引
let x = equ.charCodeAt(0) - "a".charCodeAt(0);
let y = equ.charCodeAt(3) - "a".charCodeAt(0);
// 将相等字母构成连通分量
union.union(x, y);
}
}
// 第二步:检查所有不等关系是否与已建立的相等关系冲突
for (let equ of equations) {
if (equ[1] === "!") {
// 检查是否是不等关系
let x = equ.charCodeAt(0) - "a".charCodeAt(0);
let y = equ.charCodeAt(3) - "a".charCodeAt(0);
// 如果不等关系的两个变量连通,说明产生矛盾
if (union.connected(x, y)) {
return false; // 发现矛盾,返回false
}
}
}
return true;
};
class UF {
constructor(n) {
// 连通分量的个数,初始时每个元素自成一个连通分量
this._count = n;
// 存储每个节点的父节点,初始时指向自己
this.parent = Array.from({ length: n }, (_, index) => index);
}
union(p, q) {
let rootP = this.find(p);
let rootQ = this.find(q);
if (rootP === rootQ) {
return;
}
// 将p所在的树连接到q所在的树
this.parent[rootP] = rootQ;
// 两个连通分量合并成一个连通分量
this._count--;
}
connected(p, q) {
let rootP = this.find(p);
let rootQ = this.find(q);
return rootP === rootQ;
}
find(p) {
if (this.parent[p] !== p) {
// 路径压缩:将沿路的所有节点都直接指向根节点
this.parent[p] = this.find(this.parent[p]);
}
return this.parent[p];
}
count() {
return this._count;
}
}684. 冗余连接
js
/**
* 解题思路:使用并查集检测无向图中的环
*
* 1. 问题分析:
* - 输入是一个包含 n 条边的无向图,节点编号从 1 到 n
* - 这些边中有一条是"多余的",导致图中形成了环
* - 如果有多个答案,返回在输入中最后出现的那条边
*
* 2. 并查集解法:
* - 并查集用于动态维护图中节点的连通性
* - 对于每条边(u,v),如果 u 和 v 已经连通,则这条边导致了环
* - 由于只添加了一条边,则要找的边,就是发现环的这条边
*
* 3. 实现细节:
* - 由于节点编号从 1 开始,使用时需要转换为 0-based 索引
* @param {number[][]} edges 输入的边数组,每个元素是一个长度为2的数组 [from, to]
* @return {number[]} 返回导致环的最后一条边
*/
let findRedundantConnection = function (edges) {
// 获取最大节点编号
const n = edges.reduce((max, [from, to]) => Math.max(max, from, to), 0);
// 初始化并查集
const union = new UF(n);
// 遍历每条边,检查是否形成环
for (const [from, to] of edges) {
// 由于节点编号从 1 开始所以这里要 - 1
if (union.connected(from - 1, to - 1)) {
return [from, to]; // 如果已连通,该边即为所求
}
union.union(from - 1, to - 1);
}
};
/**
* 并查集(Union-Find)数据结构
* 用于高效地维护元素的连通性关系
*/
class UF {
/**
* 初始化并查集
* @param {number} n 节点个数(0-based)
*/
constructor(n) {
// 连通分量个数,初始时每个节点都是一个独立的连通分量
this._count = n;
// 存储每个节点的父节点,初始时每个节点的父节点是自己
this.parent = Array.from({ length: n }, (_, index) => index);
}
/**
* 将节点 p 和节点 q 连通
* @param {number} p 第一个节点
* @param {number} q 第二个节点
*/
union(p, q) {
const rootP = this.find(p);
const rootQ = this.find(q);
if (rootP === rootQ) {
return;
}
this.parent[rootQ] = rootP;
// 两个连通分量合并成一个连通分量
this._count--;
}
// 判断节点 p 和节点 q 是否连通
connected(p, q) {
const rootP = this.find(p);
const rootQ = this.find(q);
return rootP === rootQ;
}
/**
* 查找节点x的根节点
* 使用路径压缩优化:在查找的同时,将路径上的所有节点都直接连接到根节点
* @param {number} x 要查找的节点
* @returns {number} 根节点的编号
*/
find(x) {
// 递归查找根节点,并在回溯时进行路径压缩
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
/**
* 获取当前的连通分量数
* 初始时为节点数,每次合并两个不同的连通分量时减1
* @returns {number} 连通分量的个数
*/
count() {
return this._count;
}
}130. 被围绕的区域
js
/**
* 解题思路:
* 1. 本题可以转化为连通性问题:
* - 与边界相连的'O'永远不会被'X'包围
* - 所有未被包围的'O'都直接或间接与边界'O'相连
*
* 2. 使用并查集解决:
* - 创建一个虚拟节点dummy,作为所有边界'O'的根节点
* - 所有能与dummy连通的'O'都是不被包围的
* - 其余的'O'都是被包围的,需要变成'X'
*
* 3. 算法步骤:
* a) 将所有边界上的'O'与dummy连通
* b) 将内部的'O'与相邻的'O'连通
* c) 检查每个'O'是否与dummy连通,不连通则替换为'X'
* @param {character[][]} board 输入的字符矩阵
* @return {void} 直接修改输入矩阵,不返回值
*/
let solve = function (board) {
let m = board.length;
let n = board[0].length;
// 虚拟节点 dummy 的索引设置为 m*n,确保不会与其他节点的索引冲突
let dummy = m * n;
// 创建大小为 m*n+1 的并查集,额外的1个位置用于 dummy 节点
let union = new UF(m * n + 1);
// 步骤1:处理边界情况
// 将四个边界上的'O'都与 dummy 节点连通
// 处理左右边界
for (let i = 0; i < m; i++) {
if (board[i][0] === "O") {
// 左边界上的'O'
union.union(getIndex(i, 0), dummy);
}
if (board[i][n - 1] === "O") {
// 右边界上的'O'
union.union(getIndex(i, n - 1), dummy);
}
}
// 处理上下边界
for (let i = 0; i < n; i++) {
if (board[0][i] === "O") {
// 上边界上的'O'
union.union(getIndex(0, i), dummy);
}
if (board[m - 1][i] === "O") {
// 下边界上的'O'
union.union(getIndex(m - 1, i), dummy);
}
}
// 定义四个方向:上、下、左、右
let direction = [
[-1, 0], // 上
[1, 0], // 下
[0, -1], // 左
[0, 1], // 右
];
// 步骤2:处理内部节点
// 遍历除边界外的所有格子
for (let i = 1; i < m - 1; i++) {
for (let j = 1; j < n - 1; j++) {
if (board[i][j] === "O") {
// 对于每个'O',检查其四个方向
for (let dir of direction) {
let x = i + dir[0];
let y = j + dir[1];
// 如果相邻位置也是'O',则将两个位置连通
if (board[x][y] === "O") {
union.union(getIndex(i, j), getIndex(x, y));
}
}
}
}
}
// 步骤3:替换被包围的'O'
// 遍历除边界外的所有格子
for (let i = 1; i < m - 1; i++) {
for (let j = 1; j < n - 1; j++) {
if (board[i][j] === "O") {
// 如果一个'O'和 dummy 不连通,说明它被包围了
if (!union.connected(getIndex(i, j), dummy)) {
board[i][j] = "X";
}
}
}
}
function getIndex(i, j) {
return i * n + j;
}
};
class UF {
// n 为图中节点的个数
constructor(n) {
// 连通分量个数
this._count = n;
// 存储每个节点的父节点
this.parent = Array.from({ length: n }, (_, index) => index);
}
// 将节点 p 和节点 q 连通
union(p, q) {
const rootP = this.find(p);
const rootQ = this.find(q);
if (rootP === rootQ) {
return;
}
this.parent[rootQ] = rootP;
// 两个连通分量合并成一个连通分量
this._count--;
}
// 判断节点 p 和节点 q 是否连通
connected(p, q) {
const rootP = this.find(p);
const rootQ = this.find(q);
return rootP === rootQ;
}
// !路径压缩
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
// 返回图中的连通分量个数
count() {
return this._count;
}
}js
/**
* v1 DFS 岛屿问题
* @param {character[][]} board
* @return {void} Do not return anything, modify board in-place instead.
*/
let solve = function (board) {
let m = board.length;
let n = board[0].length;
// 标记无法捕获区域
for (let i = 0; i < m; i++) {
dfs(board, i, 0); // 标记左边界
dfs(board, i, n - 1); // 标记有边界
}
for (let i = 0; i < n; i++) {
dfs(board, 0, i); // 标记上边界
dfs(board, m - 1, i); // 标记下边界
}
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
// 将可以捕获的区域标记为X
if (board[i][j] === "O") {
board[i][j] = "X";
// 将不能捕获E的区域标记为O
} else if (board[i][j] === "E") {
board[i][j] = "O";
}
}
}
};
let dfs = function (board, i, j) {
let m = board.length;
let n = board[0].length;
let direction = [
[1, 0],
[0, 1],
[-1, 0],
[0, -1],
];
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
if (board[i][j] !== "O") return;
board[i][j] = "E"; // 标记为无法捕获的区域
for (let dir of direction) {
dfs(board, i + dir[0], j + dir[1]);
}
};最小生成树
将所有边按照权重从小到大排序,从权重最小的边开始遍历,如果这条边和 mst 中的其它边不会形成环,则这条边是最小生成树的一部分,将它加入 mst 集合;否则,这条边不是最小生成树的一部分,不要把它加入 mst 集合
✨1584. 连接所有点的最小费用
js
/**
* 连接所有点的最小费用问题 - 最小生成树(Minimum Spanning Tree, MST)
*
* 1. 问题分析:
* - 给定平面上的n个点,需要用线段连接所有点
* - 线段的费用是两点间的曼哈顿距离
* - 要求所有点连通,且总费用最小
* - 本质是带权无向图的最小生成树问题
*
* 2. 解题思路:使用 Kruskal 算法
* a) 将所有可能的边生成并排序
* b) 使用并查集检测环,贪心选择最小费用的边
* c) 构建最小生成树直到所有点连通
*
* 3. 算法步骤:
* a) 生成所有可能的边及其权重(费用)
* b) 按照费用从小到大排序所有边
* c) 使用并查集,依次添加不会形成环的最小费用边
*
* @param {number[][]} points 坐标点数组,每个点是[x,y]格式
* @return {number} 连接所有点的最小总费用
*/
let minCostConnectPoints = function (points) {
let n = points.length;
// 步骤1:生成所有可能的边及其费用
let costs = [];
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
let p1 = points[i];
let p2 = points[j];
// 计算曼哈顿距离作为边的权重
let cost = Math.abs(p1[0] - p2[0]) + Math.abs(p1[1] - p2[1]);
// 用点的索引代替坐标
costs.push([i, j, cost]);
}
}
// 步骤2:按照费用升序排序(贪心策略)
costs.sort((a, b) => a[2] - b[2]);
// 步骤3:执行Kruskal算法构建最小生成树
let mst = 0; // 最小生成树的总费用
let uf = new UF(n); // 初始化并查集
for (let [from, to, cost] of costs) {
// 如果连接这条边会形成环,跳过
if (uf.connected(from, to)) {
continue;
}
// 若这条边不会产生环,则属于最小生成树
mst += cost;
uf.union(from, to);
}
return mst;
};
class UF {
constructor(n) {
// 连通分量的个数,初始时每个元素自成一个连通分量
this._count = n;
// 存储每个节点的父节点,初始时指向自己
this.parent = Array.from({ length: n }, (_, index) => index);
}
union(p, q) {
let rootP = this.find(p);
let rootQ = this.find(q);
if (rootP === rootQ) {
return;
}
// 将p所在的树连接到q所在的树
this.parent[rootP] = rootQ;
// 两个连通分量合并成一个连通分量
this._count--;
}
connected(p, q) {
let rootP = this.find(p);
let rootQ = this.find(q);
return rootP === rootQ;
}
find(p) {
if (this.parent[p] !== p) {
// 路径压缩:将沿路的所有节点都直接指向根节点
this.parent[p] = this.find(this.parent[p]);
}
return this.parent[p];
}
count() {
return this._count;
}
}最短路径算法
TIP
Dijkstra 算法是一种用于计算图中单源最短路径的算法,其本质是标准 BFS 算法 + 贪心思想。
如果图中包含负权重边,会让贪心思想失效,所以 Dijkstra 只能处理不包含负权重边的图。
Dijkstra 算法和标准的 BFS 算法的区别只有两个:
1、标准 BFS 算法使用普通队列,Dijkstra 算法使用优先级队列,让距离起点更近的节点优先出队(贪心思想的体现)。
2、标准 BFS 算法使用一个 visited 数组记录访问过的节点,确保算法不会陷入死循环;Dijkstra 算法使用一个 distTo 数组,确保算法不会陷入死循环,同时记录起点到其他节点的最短路径。
优先级队列实现代码
js
// 优先级队列
class PriorityQueue {
constructor(comparator) {
this.heap = [];
this.size = 0;
this.comparator = comparator || ((a, b) => a - b); // 默认整型小顶堆
}
// 返回堆的大小
getSize() {
return this.size;
}
// 判断堆是否为空
isEmpty() {
return this.size === 0;
}
// 左子节点的索引
left(node) {
return node * 2 + 1;
}
// 右子节点的索引
right(node) {
return node * 2 + 2;
}
// 父节点的索引
parent(node) {
return Math.floor((node - 1) / 2);
}
// 交换数组的两个元素
swap(i, j) {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
}
// 查,返回堆顶元素,时间复杂度 O(1)
peek() {
if (this.isEmpty()) {
throw new Error("heap is empty");
}
return this.heap[0];
}
// 删,删除堆顶元素,时间复杂度 O(logN)
dequeue() {
if (this.isEmpty()) {
throw new Error("heap is empty");
}
let res = this.heap[0];
// 把堆底元素放到堆顶
this.swap(0, this.size - 1);
// 避免对象游离
this.heap[this.size - 1] = undefined;
this.size--;
// 然后下沉到正确位置
this.sink(0);
return res;
}
// 增,向堆中插入一个元素,时间复杂度 O(logN)
enqueue(value) {
// 把新元素追加到最后
this.heap[this.size] = value; // this.heap.push(value);
// 然后上浮到正确位置
this.swim(this.size);
this.size++;
}
// 下沉操作,时间复杂度是树高 O(logN)
sink(node) {
while (this.left(node) < this.size) {
// 与左右子节点比较找到最小的
let min = node;
let left = this.left(node);
let right = this.right(node);
if (
left < this.size &&
this.comparator(this.heap[left], this.heap[min]) < 0
) {
// 左子节点较小
min = left;
}
if (
right < this.size &&
this.comparator(this.heap[right], this.heap[min]) < 0
) {
// 右子节点较小
min = right;
}
// 当前节点就是最小节点
if (min === node) {
break;
}
this.swap(node, min);
node = min;
}
}
// 上浮操作,时间复杂度是树高 O(logN)
swim(node) {
while (
node > 0 &&
this.comparator(this.heap[this.parent(node)], this.heap[node]) > 0
) {
this.swap(node, this.parent(node));
node = this.parent(node);
}
}
}✨743. 网络延迟时间
js
/**
* 网络延迟时间(Dijkstra算法)
*
* 1. 问题分析:
* - times[i] = [u, v, w] 表示从节点u到v需要w时间
* - 求从节点k出发,所有节点收到信号的最短时间,若有节点不可达返回-1
* - 本质是单源最短路径问题,且无负权边,适合用Dijkstra算法
*
* 2. 算法思路:
* a) 用邻接表构建有向图
* b) 用Dijkstra算法计算k到所有节点的最短路径
* c) 取所有最短路径的最大值作为答案
* d) 若有节点不可达,返回-1
*
* @param {number[][]} times 边信息数组 [u, v, w]
* @param {number} n 节点总数
* @param {number} k 起点编号(1-based)
* @return {number} 所有节点收到信号的最短时间,若不可达返回-1
*/
let networkDelayTime = function (times, n, k) {
// Dijkstra算法实现
let dijkstra = function (graph, src) {
// timeTo[i]: 从src到i的最短时间,-1表示未知
let timeTo = new Array(graph.length).fill(-1);
// 小顶堆优先队列,按累计时间从小到大出队
let pq = new PriorityQueue((a, b) => a.timeFromStart - b.timeFromStart);
// 起点入队,累计时间为0
pq.enqueue(new State(src, 0));
while (!pq.isEmpty()) {
let cur = pq.dequeue();
let node = cur.node;
let timeFromStart = cur.timeFromStart;
// 如果已确定最短时间,跳过
if (timeTo[node] !== -1) continue;
// 第一次出队即为最短路径
timeTo[node] = timeFromStart;
// 遍历邻居节点
for (let [to, time] of graph[node]) {
if (timeTo[to] !== -1) continue;
pq.enqueue(new State(to, timeFromStart + time));
}
}
return timeTo;
};
// 步骤1:构建邻接表
let graph = buildGraph(times, n);
// 步骤2:Dijkstra求最短路径
let timeTo = dijkstra(graph, k);
// 步骤3:取所有最短路径的最大值
let res = 0;
for (let i = 1; i < timeTo.length; i++) {
// 节点编号从1开始
if (timeTo[i] === -1) return -1; // 有节点不可达
res = Math.max(res, timeTo[i]);
}
return res;
};
// 构造图
let buildGraph = function (times, n) {
// 节点编号是从 1 开始的,所以要一个大小为 n + 1 的邻接表
let graph = Array.from({ length: n + 1 }, () => []);
for (let [from, to, time] of times) {
// 邻接表存储图结构,同时存储权重信息
graph[from].push([to, time]);
}
return graph;
};
class State {
constructor(node, timeFromStart) {
this.node = node;
// 从起点 k 到当前节点需要的时间
this.timeFromStart = timeFromStart;
}
}1631. 最小体力消耗路径
js
/**
* 最小体力消耗路径(Dijkstra变形)
*
* 1. 问题分析:
* - 给定一个高度矩阵,从左上角到右下角,每次只能上下左右移动
* - 每次移动的体力消耗为相邻格子的高度差的绝对值
* - 路径的总代价定义为路径上所有消耗的最大值
* - 求所有路径中总代价最小的那一条
*
* 2. 算法思路:
* - 本质是最短路问题,但路径权重是“最大边权”而不是“边权和”
* - 用 Dijkstra 算法,每次扩展的不是距离和,而是到当前点的最大体力消耗
* - distTo[node] 表示从起点到 node 的最小体力消耗(即最大边权的最小值)
*
* @param {number[][]} heights 高度矩阵
* @return {number} 最小体力消耗
*/
let minimumEffortPath = function (heights) {
let m = heights.length;
let n = heights[0].length;
// 记录从起点 (0, 0) 到每个节点的最小体力消耗
let distTo = Array.from({ length: m }, () => new Array(n).fill(Infinity));
// 优先队列,按最大体力消耗从小到大出队
let pq = new PriorityQueue((a, b) => a.effortFromStart - b.effortFromStart);
pq.enqueue(new State(0, 0, 0));
while (!pq.isEmpty()) {
let { row, col, effortFromStart } = pq.dequeue();
// 已经有更优解,跳过
if (distTo[row][col] !== Infinity) continue;
// 第一次出队即为最优体力消耗
distTo[row][col] = effortFromStart;
// 到达终点直接返回
if (row === m - 1 && col === n - 1) {
return effortFromStart;
}
// 扩展所有合法邻居
for (let [x, y] of getNeighbor(heights, row, col)) {
if (distTo[x][y] !== Infinity) continue;
// 路径最大体力消耗取决于当前路径和新边的最大值
let newFromStart = Math.max(
effortFromStart,
Math.abs(heights[x][y] - heights[row][col])
);
pq.enqueue(new State(x, y, newFromStart));
}
}
function getNeighbor(heights, x, y) {
let m = heights.length;
let n = heights[0].length;
let neighbors = [];
// 四个方向
let dirs = [
[0, 1], // 右
[0, -1], // 左
[1, 0], // 下
[-1, 0], // 上
];
for (let [dx, dy] of dirs) {
let nx = x + dx;
let ny = y + dy;
// 判断是否越界
if (nx >= 0 && nx < m && ny >= 0 && ny < n) {
neighbors.push([nx, ny]);
}
}
return neighbors;
}
};
class State {
constructor(row, col, effortFromStart) {
// 当前节点的横坐标
this.row = row;
// 当前节点的纵坐标
this.col = col;
// 从起点到节点 node 需要消耗的体力值
this.effortFromStart = effortFromStart;
}
}