Day50-图论,leetcode797

理论

图论 理论

  • 图,核心是表示节点间的连接状态
  • 二维坐标中,两点可以连成线,多个点连成的线就构成了图。图也可以就一个节点,甚至没有节点(空图)

图的种类

  • 有向图,指 图中边是有方向的
  • 无向图,指 图中边是没有方向的
  • 权值图
    • 加权有向图,就是有向图,图中边是有权值的
    • 加权无向图,就是无向图,图中边是有权重的

度、出度、入度

  • 无向图中有几条边连接该节点,该节点就有几度。
  • 在有向图中,每个节点有出度和入度。
    • 出度:从该节点出发的边的个数。
    • 入度:指向该节点边的个数。

连通图

  • 连通图,指在无向图中,任何两个节点都是可以到达的,称作连通图,如果有节点不能到达其他节点,则为非连通图。
  • 强连通图,指在有向图里的连通性,强连通图中任何一个节点可以到达其余所有节点,
  • 连通分量,前提也是在无向图里,指极大连通子图,指它的一个子图里边任何一个节点,可以到达这个子图里的所有节点。在无向图中的极大连通子图称之为该图的一个连通分量。
  • 强连通分量,指在有向图里,在这个子图里边任何一个节点都能到达其余节点,这个子图才是整个图里边的强连通分量。在有向图中极大强连通子图称之为该图的强连通分量。

图的构造

  • 朴素存储
    • 定义n乘2的二维数组,把每条边的两个节点记录下来,就是将所有边存下来。
  • 邻接矩阵
    • 定义n乘n二维数组,下标表示两个节点连接状态,适合稠密图点少边多,
  • 邻接表
    • 使用数组+链表的方式表示,以边为核心,构造数组,适合稀疏图,边少点多,

图的遍历

  • DFS,深度优先搜索
  • BFS,广度优先搜索

深度优先搜索理论

  • dfs:沿着一个方向去搜,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。

  • bfs:先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。

  • 深搜三部曲

      1. 确认递归函数,参数:一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。
      1. 确认终止条件
      1. 处理目前搜索节点出发的路径
      const result = []; // 保存符合条件的所有路径
      const path = []; // 起点到终点的路径
      dfs(图, 目前搜索的节点) {
          if (终止条件) {
              存放结果
              return
          }
          for(选择:本节点连接的节点) {
              处理节点
              dfs(图,选择的节点)
              回溯,撤销处理的结果
          }
      }
      


  1. 所有可能的路径
  • 给你一个有 n 个节点的 有向无环图(DAG),请你找出从节点 0 到节点 n-1 的所有路径并输出(不要求按特定顺序)。graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)

  • 思路

    1. 确认递归函数,参数
    • dfs函数一定要存一个图,用来遍历的,还要存一个目前遍历的节点,定义为x,至于 单一路径,和路径集合可以放在全局变量
    1. 确认终止条件:从节点 0 到节点 n-1 的路径并输出,当目前遍历的节点 为 最后一个节点的时候,就找到了一条,从 出发点到终止点的路径。
    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
    }
    


  1. 卡码网,所有可达路径
  • 给定一个有 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]
]
*/



参考&感谢各路大神

posted @ 2025-07-16 16:29  安静的嘶吼  阅读(7)  评论(0)    收藏  举报