Day50-图论,leetcode797
理论
图论 理论
- 图,核心是表示节点间的连接状态
- 二维坐标中,两点可以连成线,多个点连成的线就构成了图。图也可以就一个节点,甚至没有节点(空图)
图的种类
- 有向图,指 图中边是有方向的
- 无向图,指 图中边是没有方向的
- 权值图
- 加权有向图,就是有向图,图中边是有权值的
- 加权无向图,就是无向图,图中边是有权重的
度、出度、入度
- 无向图中有几条边连接该节点,该节点就有几度。
- 在有向图中,每个节点有出度和入度。
- 出度:从该节点出发的边的个数。
- 入度:指向该节点边的个数。
连通图
- 连通图,指在无向图中,任何两个节点都是可以到达的,称作连通图,如果有节点不能到达其他节点,则为非连通图。
- 强连通图,指在有向图里的连通性,强连通图中任何一个节点可以到达其余所有节点,
- 连通分量,前提也是在无向图里,指极大连通子图,指它的一个子图里边任何一个节点,可以到达这个子图里的所有节点。在无向图中的极大连通子图称之为该图的一个连通分量。
- 强连通分量,指在有向图里,在这个子图里边任何一个节点都能到达其余节点,这个子图才是整个图里边的强连通分量。在有向图中极大强连通子图称之为该图的强连通分量。
图的构造
- 朴素存储
- 定义n乘2的二维数组,把每条边的两个节点记录下来,就是将所有边存下来。
- 邻接矩阵
- 定义n乘n二维数组,下标表示两个节点连接状态,适合稠密图点少边多,
- 邻接表
- 使用数组+链表的方式表示,以边为核心,构造数组,适合稀疏图,边少点多,
图的遍历
- DFS,深度优先搜索
- BFS,广度优先搜索
深度优先搜索理论
-
dfs:沿着一个方向去搜,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
-
bfs:先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。
-
深搜三部曲
-
- 确认递归函数,参数:一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。
-
- 确认终止条件
-
- 处理目前搜索节点出发的路径
const result = []; // 保存符合条件的所有路径 const path = []; // 起点到终点的路径 dfs(图, 目前搜索的节点) { if (终止条件) { 存放结果 return } for(选择:本节点连接的节点) { 处理节点 dfs(图,选择的节点) 回溯,撤销处理的结果 } }
-
- 所有可能的路径
- 给你一个有 n 个节点的 有向无环图(DAG),请你找出从节点 0 到节点 n-1 的所有路径并输出(不要求按特定顺序)。graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)
-
思路
-
- 确认递归函数,参数
- dfs函数一定要存一个图,用来遍历的,还要存一个目前遍历的节点,定义为x,至于 单一路径,和路径集合可以放在全局变量
-
- 确认终止条件:从节点 0 到节点 n-1 的路径并输出,当目前遍历的节点 为 最后一个节点的时候,就找到了一条,从 出发点到终止点的路径。
-
- 处理目前搜索节点出发的路径, 遍历节点n链接的所有节点
- 遍历到的节点加入到路径中来
- 进入下一层递归
- 回溯,撤销本节点
var allPathsSourceTarget = function(graph) { let result = [] // 收集符合条件的路径 let path = [] // 0节点到终点的路径 // x:目前遍历的节点 // graph:存当前的图 const dfs = (graph, x) => { // 要求从节点 0 到节点 n-1 的路径并输出,所以是 graph.length - 1 if (x == graph.length - 1) { // 找到符合条件的一条路径 result.push([...path]) return } for (let i = 0; i < graph[x].length; i++) { // 遍历节点n链接的所有节点 path.push(graph[x][i]) // 遍历到的节点加入到路径中来 dfs(graph, graph[x][i]) // 进入下一层递归 path.pop() // 回溯,撤销本节点 } } path.push(0) // 无论什么路径已经是从0节点出发 dfs(graph, 0) // 开始遍历 return result }
- 卡码网,所有可达路径
-
给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。
-
输入描述:第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边;后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径
-
输出描述:输出所有的可达路径,路径中所有节点之间空格隔开,每条路径独占一行,存在多条路径,路径输出的顺序可任意。如果不存在任何一条路径,则输出 -1。注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是
1 3 5
,而不是1 3 5
, 5后面没有空格!
- 输入示例
- 5 5
- 1 3
- 3 5
- 1 2
- 2 4
- 4 5
- 输出示例
- 1 3 5
- 1 2 4 5
- 思路
- 邻接矩阵写法
// 邻接矩阵写法
/**
* 1. 代码整体结构
作用:使用 Node.js 的 readline 模块异步读取用户输入(适用于算法题的标准输入场景)。
关键点:
r1[Symbol.asyncIterator]() 创建一个异步迭代器,逐行读取输入。
readline() 是一个异步函数,每次调用返回输入的下一行。
* 2. 初始化图(initGraph函数)
输入格式:
第一行:N M(节点数 N,边数 M)。
后续 M 行:每行 u v 表示一条从节点 u 到节点 v 的有向边。
邻接矩阵:
构建一个 (N+1) x (N+1) 的二维数组 graph,graph[u][v] = 1 表示存在边 u → v。
例如,输入 3 3\n1 2\n2 3\n1 3 会生成:
graph = [
[0, 0, 0, 0],
[0, 0, 1, 1], // 1 → 2, 1 → 3
[0, 0, 0, 1], // 2 → 3
[0, 0, 0, 0]
];
3. 深度优先搜索(dfs函数)
功能:从节点 x 出发,递归搜索所有能到达节点 n 的路径。
关键逻辑:
1. 终止条件:当前节点 x 等于目标节点 n 时,将路径 path 的副本存入 result。
2. 遍历邻接节点:检查 graph[x][i] == 1,若存在边 x → i,则继续搜索。
3. 回溯机制:通过 path.push(i) 和 path.pop() 实现路径的探索与回退。
4. 主流程
步骤:
1. 调用 initGraph() 读取输入并构建邻接矩阵。
2. 将起点 1 加入 path,开始 DFS 搜索。
3. 输出所有路径(如 1 2 3)或 -1(无路径)。
*/
// 创建readline接口
const r1 = require('readline').createInterface({ input: process.stdin });
// 创建异步迭代器
let iter = r1[Symbol.asyncIterator]();
const readline = async ()=>(await iter.next()).value;
let graph;
let N, M;
// 收集符合条件的路径
let result = [];
// 1节点到终点的路径
let path = [];
// 创建邻接矩阵,初始化邻接矩阵
async function initGraph(){
let line;
line = await readline();
[N, M] = line.split(' ').map(i => parseInt(i))
// 节点编号从1到n,申请n+1的数组
graph = new Array(N + 1).fill(0).map(() => new Array(N + 1).fill(0))
while(M--){
line = await readline()
const strArr = line ? line.split(' ').map(i => parseInt(i)) : undefined
// 使用邻接矩阵,表示无线图,1表示s与t是相连的
strArr ? graph[strArr[0]][strArr[1]] = 1 : null
}
};
// 深度搜索
function dfs(graph, x, n){
// 当前遍历节点为x, 到达节点为n
if(x == n){ // 找到符合条件的一条路径
result.push([...path])
return
}
// 遍历节点x链接的所有节点
for(let i = 1 ; i <= n ; i++){
// 找到x链接的节点
if(graph[x][i] == 1){
// 遍历到的节点加入到路径中来
path.push(i)
// 进入下一层递归
dfs(graph, i, n )
// 回溯,撤销本节点
path.pop(i)
}
}
};
(async function(){
// 创建邻接矩阵,初始化邻接矩阵
await initGraph();
// 从节点1开始深度搜索
path.push(1);
// 深度搜索
dfs(graph, 1, N );
// 输出
if(result.length > 0){
result.forEach(i => {
console.log(i.join(' '))
})
}else{
console.log(-1)
}
})();
- 邻接表写法
/*
* 前两句用来创建一个异步读取命令行输入的接口,常见于Node.js环境中处理算法题的标准输入。
const r1 = require('readline').createInterface({ input: process.stdin });
let iter = r1[Symbol.asyncIterator]();
* 1. readline模块
Node.js内置模块,用于逐行读取流(如标准输入)。
createInterface() 创建一个可交互的读写接口。
* 2. process.stdin
代表标准输入流(键盘输入或文件重定向)。
* 3. Symbol.asyncIterator
JavaScript内置符号,定义对象的异步迭代器协议。
调用后返回一个异步迭代器对象,可以用 for await...of 或手动调用 next() 读取输入。
*/
// 邻接表写法
const r1 = require('readline').createInterface({ input: process.stdin });
// 创建readline接口
let iter = r1[Symbol.asyncIterator]();
// 创建异步迭代器
const readline = async () => (await iter.next()).value;
let graph;
let N, M;
// 收集符合条件的路径
let result = [];
// 1节点到终点的路径
let path = [];
// 创建邻接表,初始化邻接表
async function initGraph() {
let line;
line = await readline();
[N, M] = line.split(' ').map(i => parseInt(i))
// 节点编号从1到n,申请n+1大的数组
graph = new Array(N + 1).fill(0).map(() => new Array())
while (line = await readline()) {
const strArr = line.split(' ').map(i => parseInt(i))
// 使用邻接表,表示s->t是相连的
strArr ? graph[strArr[0]].push(strArr[1]) : null
}
};
// 深度搜索
async function dfs(graph, x, n) {
// 当前遍历节点为x, 到达节点为n
if (x == n) { // 找到符合条件的一条路径
result.push([...path])
return
}
graph[x].forEach(i => { // 找到x指向的节点
path.push(i) // 遍历到的节点加入到路径中来
dfs(graph, i, n) // 进入下一层递归
path.pop(i) // 回溯,撤销本节点
})
};
(async function () {
// 创建邻接表,初始化邻接表
await initGraph();
// 从节点1开始深度搜索
path.push(1);
// 深度搜索
dfs(graph, 1, N);
// 输出
if (result.length > 0) {
result.forEach(i => {
console.log(i.join(' '))
})
} else {
console.log(-1)
}
})();
广度优先搜索理论基础
- 广度优先搜索(bfs)是一圈一圈的搜索过程(深度优先搜搜(dfs)是一条路跑到黑然后再回溯)
- 广度优先搜索的搜索方式就适合于解决两个点之间的最短路径问题。因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。
/**
* 1. 方向数组:
dir 数组定义了四个移动方向(右、下、左、上)
每个方向是一个包含 [dx, dy] 的小数组
* 2. 队列实现:
JavaScript 没有内置队列,用数组模拟:
push() 入队
shift() 出队
* 3. 访问标记:
visited 数组记录已访问的节点
节点一旦入队就立即标记,避免重复访问
* 4. 边界检查:
检查坐标是否超出网格范围
越界则跳过该方向
*/
// 定义四个方向:右、下、左、上
const dir = [[0, 1], [1, 0], [-1, 0], [0, -1]];
/**
* 广度优先搜索
* @param {character[][]} grid - 二维网格
* @param {boolean[][]} visited - 访问标记数组
* @param {number} x - 起始x坐标
* @param {number} y - 起始y坐标
*/
function bfs(grid, visited, x, y) {
const queue = []; // JavaScript中用数组模拟队列
queue.push([x, y]); // 起始节点入队
visited[x][y] = true; // 标记为已访问
while (queue.length > 0) {
const [curx, cury] = queue.shift(); // 取出队列头部元素
// 遍历四个方向
for (let i = 0; i < 4; i++) {
const nextx = curx + dir[i][0];
const nexty = cury + dir[i][1];
// 检查边界
if (nextx < 0 || nextx >= grid.length ||
nexty < 0 || nexty >= grid[0].length) {
continue; // 越界则跳过
}
// 检查是否已访问
if (!visited[nextx][nexty]) {
queue.push([nextx, nexty]); // 未访问则入队
visited[nextx][nexty] = true; // 立即标记为已访问
}
}
}
}
// 示例网格(1表示可访问,0表示障碍)
const grid = [
['1', '1', '0', '0'],
['1', '0', '1', '0'],
['0', '1', '1', '1']
];
// 初始化访问数组
const visited = Array(grid.length)
.fill()
.map(() => Array(grid[0].length).fill(false));
// 从(0,0)开始BFS
bfs(grid, visited, 0, 0);
console.log(visited);
/* 输出访问标记:
[
[true, true, false, false],
[true, false, false, false],
[false, false, false, false]
]
*/
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。