feat: 图算法 130,133,200,207,210,399,909

This commit is contained in:
LouisFonda 2025-06-07 16:11:13 +08:00
parent 0a41e81e58
commit f53e76dcf0
Signed by: yigencong
GPG Key ID: 29CE877CED00E966
7 changed files with 685 additions and 0 deletions

View File

@ -0,0 +1,123 @@
/**
* @param {character[][]} board
* @return {void} Do not return anything, modify board in-place instead.
*/
const solve = function (board) {
};
/*
题目要求我们把被X围绕的O区域修改成X所以只需要找出所有没有被X围绕的O区域把这一部分区域修改成A,最后遍历整个矩阵把所有为O的区域
修改成X所有为A的区域修改成O,所有在边上的O,及其相邻的O一定是没有被包围的所以我们只需要遍历四条边上的O把它们修改成A即可这里使用
dfs
*/
function f1(board) {
// 如果矩阵为空(虽然测试用例保证 1<=m,n
if (board.length === 0 || board[0].length === 0) return;
// 获取矩阵的行m和列n,用于后续遍历
const m = board.length;
const n = board[0].length;
// 遍历矩阵的第一列和最后一列
for (let i = 0; i < m; i++) {
dfs(i, 0);
dfs(i, n - 1);
}
// 遍历矩阵的第一行和最后一行
for (let j = 0; j < n; j++) {
dfs(1, j);
dfs(m - 1, j);
}
// 遍历所有矩阵将O修改成X,将A修改成O
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (board[i][j] === 'O') board[i][j] = 'X';
if (board[i][j] === 'A') board[i][j] = 'O';
}
}
/*
dfs遍历,将O修改为A
注意board直接找外部作用域无需传递
*/
let dfs = (x, y) => {
// 如果越界或者board[x][y] !== 'O'直接return
if (x < 0 || y < 0 || x === m || y === n || board[x][y] !== 'O') return;
board[x][y] = 'A'; // 将O修改成A
// 递归处理四周的O
dfs(board, x - 1, y);
dfs(board, x, y - 1);
dfs(board, x + 1, y);
dfs(board, x, y + 1);
};
}
/*
思路和上面一致使用广度优先遍历BFS
*/
function f2(board) {
// 定义查找偏移量
const dx = [1, -1, 0, 0];
const dy = [0, 0, 1, -1];
// 如果矩阵为空(虽然测试用例保证 1<=m,n
if (board.length === 0 || board[0].length === 0) return;
// 获取矩阵的行m和列n,用于后续遍历
const m = board.length;
const n = board[0].length;
const queue = []; // 广度遍历的队列,是一个二维数组,储存下标[row, col] 表示几行几列
// 遍历第一行和最后一行如果发现O就将他修改成A并且将它四周的位置压入队列继续处理
for (let i = 0; i < n; i++) {
if (board[0][i] === 'O') {
board[0][i] = 'A';
queue.push([0, i]);
}
if (board[m - 1][i] === 'O') {
board[m - 1][i] = 'A';
queue.push([m - 1, i]);
}
}
// 处理第一列和最后一列,这里需要注意四个角落的元素可以不处理,为了统一,这一我就不做处理,对结果不影响
for (let i = 0; i < m; i++) {
if (board[i][0] === 'O') {
board[i][0] = 'A';
queue.push([i, 0]);
}
if (board[i][n - 1] === 'O') {
board[i][n - 1] = 'A';
queue.push([i, n - 1]);
}
}
// 开始广度遍历
while (queue.length > 0) {
const [x, y] = queue.pop(); // 获取位置坐标
// 通过偏移量寻找四周为O的位置将其修改为A然后将其压入栈中继续先四周寻找
for (let i = 0; i < 4; i++) {
const mx = x + dx[i];
const my = y + dy[i];
// 如果越界 或 不等于O直接跳过
if (mx < 0 || mx >= m || y < 0 || my >= n || board[mx][my] !== 'O') continue;
board[mx][my] = 'A';
queue.push([mx, my]);
}
}
// 遍历所有矩阵将O修改成X,将A修改成O
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (board[i][j] === 'O') board[i][j] = 'X';
if (board[i][j] === 'A') board[i][j] = 'O';
}
}
}

View File

@ -0,0 +1,85 @@
/**
* // Definition for a _Node.
* function _Node(val, neighbors) {
* this.val = val === undefined ? 0 : val;
* this.neighbors = neighbors === undefined ? [] : neighbors;
* };
* https://leetcode.cn/problems/clone-graph/?envType=study-plan-v2&envId=top-interview-150
*/
/**
* @param {_Node} node
* @return {_Node}
*/
const cloneGraph = function (node) {
};
// eslint-disable-next-line no-underscore-dangle, no-unused-vars
function _Node(val, neighbors) {
this.val = val === undefined ? 0 : val;
this.neighbors = neighbors === undefined ? [] : neighbors;
}
/*
只给了我们一个连通图的一个节点叫我们克隆整个图根据两图图的定义从这个节点出发我们能到达途中的任意一个节点(顶点)
所以我们只需要遍历所有的neighbor然后顺着邻居继续遍历邻居即可遍历整个图但是这里有一个问题就是如果通过邻居到达了
自己或者邻居的邻居是自己的邻居就会发生循环这里就遇到使用一种数据结果来缓存已经遍历过的邻居当再次遇到它时跳过就行
抽象过程你需要统计小王家人你只认识小王于是题通过小王认识了它的爸爸妈妈把他们加入到统计表然后小王的爸爸向你介绍了
它的妹妹同时小王向你介绍了它的姑姑但是你发现这两个人是同一个人所以你只统计了一次最后你就获得了所有和小王有关的人
*/
function f1(node) {
const visited = new Map(); // 储存原节点,和克隆节点的映射
/**
*
* @param {*} node 需要克隆的节点
* @returns 返回克隆的节点
*/
function clone(node) {
if (!node) return node;
// 如果这个节点之前克隆过,直接返回其克隆节点
if (visited.has(node)) return visited.get(node);
// 创建这个节点的克隆节点
const cloneNode = new _Node(node.val, []);
// node与cloneNode建立映射
visited.set(node, cloneNode);
// 为克隆节点克隆邻居节点
for (const neighbor of node.neighbors) {
cloneNode.neighbors.push(clone(neighbor));
}
return cloneNode;
}
return clone(node);
}
/*
思路和上面一致但是使用广度优先遍历
*/
function f2(node) {
if (!node) return node;
const visited = new Map();
const queue = [node]; // 用于广度遍历
const cNode = new _Node(node.val, []);
visited.set(node, cNode); // 克隆第一个节点将其存入visited
while (queue.length > 0) {
const n = queue.shift();
// 遍历这个节点的所有邻居如果没有访问过将其clone并加入visited
for (const neighbor of n.neighbors) {
if (!visited.has(neighbor)) {
visited.set(neighbor, new _Node(neighbor.val, []));
queue.push(neighbor);
}
visited.get(n).neighbors.push(visited.get(neighbor));
}
}
return cNode;
}

View File

@ -0,0 +1,116 @@
/**
* @param {character[][]} grid
* @return {number}
* https://leetcode.cn/problems/number-of-islands/?envType=study-plan-v2&envId=top-interview-150
*/
const numIslands = function (grid) {
};
/*
使用深度优先遍历在character[][]这个二维数组中我们认为相邻的1之间都有一条边首先遍历这个二维数组从遇到的第一个为1的位置递归
的开始向四周查找其他位置是否也为1如果是的话就继续寻找并且把找过的位置设置为0,计数加一即可先当与图中有几个连通分量
*/
function f1(grid) {
/*
递归函数向grid[r][c]四周继续递归寻找为1的位置并把它设置成0,当整个为1位置都为0表示整个岛屿的小时
*/
const dfs = (grid, r, c) => {
// 如果r,c超出边界或者grid[r][c] === 0 表示岛屿的边界结束
if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] === '0') return;
// 将当前位置设置为0表示已经访问过
grid[r][c] = '0';
// 递归的查找四周
dfs(grid, r - 1, c); // 上
dfs(grid, r, c + 1); // 右
dfs(grid, r + 1, c); // 下
dfs(grid, r, c - 1); // 左
};
// 如果邻接矩阵为空或者没有任何数据直接返回0
if (!grid || grid.length === 0) return 0;
let nr = grid.length; // 获取行的个数
let nc = grid[0].length; // 获取列的个数
let lands = 0; // 岛屿数量
for (let r = 0; r < nr; ++r) {
for (let c = 0; c < nc; ++c) {
if (grid[r][c] === '1') { // 发现陆地,必定有一座岛,把相邻的陆地全部清空
lands++;
dfs(grid, r, c); // 从当前点开始DFS
}
}
}
return lands;
}
/*
使用BFS思路和DFS一致
*/
function f2(grid) {
// 获取网格的行数
const nr = grid.length;
if (nr === 0) return 0; // 如果网格为空返回0岛屿
// 获取网格的列数
const nc = grid[0].length;
// 记录岛屿的数量
let num_islands = 0;
// 遍历每个格子
for (let r = 0; r < nr; r++) {
for (let c = 0; c < nc; c++) {
// 如果当前格子是陆地('1'),说明找到了一个新的岛屿
if (grid[r][c] === '1') {
// 增加岛屿数量
num_islands++;
// 将当前格子标记为已访问,防止再次访问
grid[r][c] = '0';
// 创建一个队列来进行BFS
const neighbors = [];
neighbors.push([r, c]); // 将当前岛屿的起始点入队列
// 进行BFS遍历岛屿中的所有陆地
while (neighbors.length > 0) {
// 获取队列的第一个元素
const [row, col] = neighbors.shift();
// 检查上下左右四个方向的相邻格子
// 上
if (row - 1 >= 0 && grid[row - 1][col] === '1') {
neighbors.push([row - 1, col]);
grid[row - 1][col] = '0'; // 将相邻陆地标记为水
}
// 下
if (row + 1 < nr && grid[row + 1][col] === '1') {
neighbors.push([row + 1, col]);
grid[row + 1][col] = '0'; // 将相邻陆地标记为水
}
// 左
if (col - 1 >= 0 && grid[row][col - 1] === '1') {
neighbors.push([row, col - 1]);
grid[row][col - 1] = '0'; // 将相邻陆地标记为水
}
// 右
if (col + 1 < nc && grid[row][col + 1] === '1') {
neighbors.push([row, col + 1]);
grid[row][col + 1] = '0'; // 将相邻陆地标记为水
}
}
}
}
}
// 返回岛屿的数量
return num_islands;
}
/*
利用并查集矩阵里面所有的1都认为是一个小岛屿用一个一维数组parent表示并查集 grid[i][j] === '1' 这个位置为例那么这个
位置在并查集中的下标就是parent[i*col+j] col表示矩阵的列数而parent[i * col + j]表示的是它的父节点默认指向自己定义一个遍历count
记录岛屿的数量
*/
// TODO:

View File

@ -0,0 +1,42 @@
/**
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {boolean}
*/
const canFinish = function (numCourses, prerequisites) {
};
/*
这个题目是一个典型的拓扑排序问题只需要判断拓扑排序最终能否输出所有元素如果可以则表明是一个有向无环图符合
题目要求否则不符合
*/
function f1(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const inDegree = Array(numCourses).fill(0);
// 构建图和入度表
for (const [a, b] of prerequisites) {
graph[b].push(a);
inDegree[a]++;
}
// 找出入度为零的顶点,将其加入队列
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
// 从队列中输出所有入度为零的顶点,如果输出的顶点数量和课程数量一致,表明可以拓扑排序,否则有环
let count = 0;
while (queue.length > 0) {
const node = queue.shift();
count++;
for (const neighbor of graph[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
return count === numCourses;
}

View File

@ -0,0 +1,94 @@
/**
* 课程安排 II - 拓扑排序DFS
* @param {number} numCourses - 课程总数
* @param {number[][]} prerequisites - 每一对 [a, b] 表示上课程 a 需要先完成课程 b
* @return {number[]} - 返回一个可行的课程学习顺序如果无法完成所有课程则返回 []
*/
const findOrder = function (numCourses, prerequisites) {
return dfsTopoSort(numCourses, prerequisites);
};
/**
* 使用 DFS 实现拓扑排序并判断是否存在环
* @param {number} numCourses
* @param {number[][]} prerequisites
* @return {number[]}
*/
function dfsTopoSort(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []); // 邻接表表示图
const visited = Array(numCourses).fill(0); // 节点状态0=未访问1=访问中2=已完成
const result = []; // 用于存放拓扑排序结果(逆序)
let hasCycle = false; // 用于标记图中是否存在环
// 构建图b -> a 表示先学 b 才能学 a
for (const [a, b] of prerequisites) {
graph[b].push(a);
}
/**
* 深度优先搜索当前节点
* @param {number} node
*/
function dfs(node) {
if (visited[node] === 1) {
// 当前节点正在访问中,说明存在环
hasCycle = true;
return;
}
if (visited[node] === 2 || hasCycle) {
// 节点已处理,或已发现环,直接返回
return;
}
visited[node] = 1; // 标记为访问中
for (const neighbor of graph[node]) {
dfs(neighbor);
}
visited[node] = 2; // 标记为已完成
result.push(node); // 所有邻居处理完后再加入结果,确保当前节点排在后面
}
// 遍历所有节点,防止图不连通
for (let i = 0; i < numCourses; i++) {
dfs(i);
}
// 如果存在环,则无法完成所有课程
return hasCycle ? [] : result.reverse();
}
/*
使用kahn算法来实现一个顶点能从其他顶点过来表明有一个入度有多少个顶点能到达这个顶点就有多少个入度这里直接copy207
修改一下即可
*/
function f2(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const inDegree = Array(numCourses).fill(0);
const result = [];
// 构建图和入度表
for (const [a, b] of prerequisites) {
graph[b].push(a);
inDegree[a]++;
}
// 找出入度为零的顶点,将其加入队列
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
// 从队列中输出所有入度为零的顶点,如果输出的顶点数量和课程数量一致,表明可以拓扑排序,否则有环
while (queue.length > 0) {
const node = queue.shift();
// 将入度为零的顶点存入结果集合
result.push(node);
for (const neighbor of graph[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
// 如果结果集中的顶点的数量小于numCourses那么表明有环放回[],否则返回结果集
return result.length < numCourses ? [] : result;
}

View File

@ -0,0 +1,96 @@
/**
* @param {string[][]} equations
* @param {number[]} values
* @param {string[][]} queries
* @return {number[]}
*/
const calcEquation = function (equations, values, queries) {
};
/*
思路通过equations和values建立一张有权图edges:[[[1, 2],...], [[2, 3],...]],在这里使用邻接表的方式表示edges表示的是0号顶点可以到一号顶点
并且权值为21顶点到2号顶点的权值是3如果0号顶点表示a,1号顶点表示b,2号顶点表示c那么这个图在这个题目里面的意思就是a/b=2,b/c=2,如果要我们
求a/c的值不就是把a->b->c这个路径里面的权值相乘吗所以这个题目可以分为下面几步
步骤
1.通过equationsvalues建立有权图edges
2.遍历queries,比如要我们求a/e那么我们就从a为起点出发开始广度优先遍历知道找到a->e的路径之后加权相乘如果没找到就是-1
*/
function f1(equations, values, queries) {
// Step 1: 给每个变量编号,变量名映射到编号
let nvars = 0; // 变量的数量
const variables = new Map(); // 存储变量名到编号的映射
// 处理 equations给每个变量一个唯一的编号
const n = equations.length;
for (let i = 0; i < n; i++) {
// 如果变量没有编号,就给它一个编号
if (!variables.has(equations[i][0])) {
variables.set(equations[i][0], nvars++);
}
if (!variables.has(equations[i][1])) {
variables.set(equations[i][1], nvars++);
}
}
// Step 2: 创建图结构,存储每个节点与其他节点的比值
const edges = new Array(nvars).fill(0); // 创建一个数组来存储每个节点的邻接点和比值
for (let i = 0; i < nvars; i++) {
edges[i] = []; // 初始化每个节点的邻接表
}
// 遍历 equations构建图每条边的权值是它们之间的比值
for (let i = 0; i < n; i++) {
const va = variables.get(equations[i][0]); // 获取变量 a 的编号
const vb = variables.get(equations[i][1]); // 获取变量 b 的编号
edges[va].push([vb, values[i]]); // a → b 的比值
edges[vb].push([va, 1.0 / values[i]]); // b → a 的比值是 1 / (a → b)
}
// Step 3: 处理查询,计算每个查询的结果
const queriesCount = queries.length; // 查询的数量
const ret = []; // 存储每个查询的结果
for (let i = 0; i < queriesCount; i++) {
const query = queries[i]; // 当前查询
let result = -1.0; // 默认结果为 -1.0,表示无法计算
// 只有当查询中的两个变量都存在时,才进行计算
if (variables.has(query[0]) && variables.has(query[1])) {
const ia = variables.get(query[0]); // 获取查询中第一个变量的编号
const ib = variables.get(query[1]); // 获取查询中第二个变量的编号
// 如果两个变量是同一个,直接返回 1.0
if (ia === ib) {
result = 1.0;
} else {
// Step 4: 使用 DFS 或 BFS 遍历图,查找从 ia 到 ib 的比值
const points = []; // 存储当前正在遍历的节点
points.push(ia); // 从 ia 开始遍历
const ratios = new Array(nvars).fill(-1.0); // 存储每个节点的比值,初始值为 -1.0,表示不可达
ratios[ia] = 1.0; // 起点 ia 到 ia 的比值是 1.0
// 使用 DFS 方式遍历图
while (points.length && ratios[ib] < 0) {
const x = points.pop(); // 从栈中取出一个节点
// 遍历当前节点的所有邻接点
for (const [y, val] of edges[x]) {
// 如果邻接点 y 尚未访问过ratios[y] < 0
if (ratios[y] < 0) {
ratios[y] = ratios[x] * val; // 更新 y 的比值
points.push(y); // 将 y 加入栈中继续遍历
}
}
}
result = ratios[ib]; // 查询的结果是 ib 到达的比值
}
}
// 将当前查询的结果存入结果数组
ret[i] = result;
}
// 返回所有查询的结果
return ret;
}

View File

@ -0,0 +1,129 @@
/**
* @param {number[][]} board
* @return {number}
*/
const snakesAndLadders = function (board) {
};
/*
/*
把棋盘的每一个位置看成图的一个顶点每一个顶点都有六个有向边指向后面的六个位置也就是后面的六个顶点有的顶点比较特殊
它不指向后面的六个顶点而是指向其他的顶点我们要求的就是到达最后一个位置的顶点所需的最少步数到这里我们很容易发现
是一个图的BFS题目我们从最初的位置一层一层的往外扩需要用几层就是几步
*/
function f1(board) {
// 定义一个长度为n*n+1的一维数组将board压缩减少计数难度
const n = board.length;
const size = n * n;
const arr = new Array(size + 1); // 0位置不需要这样就能和board一一对应
let idx = 1; // 棋盘初始位置
let leftToRight = true; // s行遍历board
for (let i = n - 1; i >= 0; i--) {
if (leftToRight) {
for (let j = 0; j < n; j++) {
arr[idx++] = board[i][j];
}
} else {
for (let j = n - 1; j >= 0; j--) {
arr[idx++] = board[i][j];
}
}
leftToRight = !leftToRight;
}
// bfs队列从第一个位置开始[1,0]表示,到达第一个位置最少只需要一步
const queue = [[1, 0]];
const visited = new Array(size + 1).fill(false); // 防止每一层的元素重复加入到下一层
visited[1] = true;
while (queue.length) {
const [cur, step] = queue.shift();
// 查看邻接的六个顶点
for (let i = 1; i <= 6; i++) {
let next = cur + i; // 下一个顶点位置
// 越界,忽略这个顶点
if (next > size) {
continue;
}
// 如果遇到蛇和梯子立即调整指定位置
if (arr[next] !== -1) {
next = arr[next];
}
// 如果邻接顶点就是目标位置直接放回到达当前顶点的步数在加1
if (next === size) {
return step + 1;
}
// 如果元素没有被访问将其加入队列,扩展下一层
if (!visited[next]) {
visited[next] = true;
queue.push([next, step + 1]);
}
}
}
return -1; // 不可达
}
/*
将二维数组变成一维数组需要log(n*n)的时间复杂度n为棋牌大小相等于一开始就遍历了整个棋盘一遍如果能实时计算当前位置到其他位置
在board中的位置可以大大减少时间复杂度
思考假设我们有一个大小为n的棋牌计数位置place(1 ~ n*n)在board中的坐标首先计算行行非常好计算r = (place - 1) / n,那么列
还是 (place-1) % n吗在这个题目不是的因为棋牌是按照s行走的偶数行从左往右奇数行从右往左偶数行显然是(place-1)%n那么技术行只需要将n
减去它即可
*/
const id2rc = (id, n) => {
const r = Math.floor((id - 1) / n);
let c = (id - 1) % n;
if (r % 2 === 1) {
c = n - 1 - c;
}
return [n - 1 - r, c];
};
function f2(board) {
const n = board.length;
const size = n * n;
// BFS
const queue = [[1, 0]];
const visited = new Array(size + 1).fill(false); // 防止每一层的元素重复加入到下一层
visited[1] = true;
while (queue.length) {
const [cur, step] = queue.shift();
// 查看邻接的六个顶点
for (let i = 1; i <= 6; i++) {
let next = cur + i; // 下一个顶点位置
// 越界,忽略这个顶点
if (next > size) {
continue;
}
const [r, c] = id2rc(next, n);
// 如果遇到蛇和梯子立即调整指定位置
if (board[r][c] !== -1) {
next = board[r][c];
}
// 如果邻接顶点就是目标位置直接放回到达当前顶点的步数在加1
if (next === size) {
return step + 1;
}
// 如果元素没有被访问将其加入队列,扩展下一层
if (!visited[next]) {
visited[next] = true;
queue.push([next, step + 1]);
}
}
}
return -1; // 不可达
}