Day57-图论,卡码网53

  1. 寻宝(第七期模拟笔试)
  • 题目描述:在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将 所有岛屿联通起来(注意:这是一个无向图)。 给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。

  • 输入描述:第一行包含两个整数V 和 E,V代表顶点数,E代表边数 。顶点编号是从1到V。例如:V=2,一个有两个顶点,分别是1和2。接下来共有 E 行,每行三个整数 v1,v2 和 val,v1 和 v2 为边的起点和终点,val代表边的权值。

  • 输出描述:输出联通所有岛屿的最小路径总距离

  • 输入示例

  • 7 11

  • 1 2 1

  • 1 3 1

  • 1 5 2

  • 2 6 1

  • 2 4 2

  • 2 3 2

  • 3 4 1

  • 4 5 1

  • 5 6 2

  • 5 7 1

  • 6 7 1

  • 输出示例

  • 6


  • 思路

  • 最小生成树是所有节点的最小连通子图,即:以最小的成本(边的权值)将图中所有节点链接到一起。图中有n个节点,那么一定可以用n-1条边将所有节点连接到一起。最小生成树可以使用prim算法,也可以使用kruskal算法计算出来。

  • prim算法

  • prim算法是从节点的角度采用贪心的策略每次寻找距离最小生成树最近的节点并加入到最小生成树中。

  • prim三部曲

    • 第一步:选距离生成树最近节点,选择距离最小生成树最近的节点,加入到最小生成树,刚开始还没有最小生成树,所以随便选一个节点加入就好
    • 第二步:最近节点加入生成树此时节点1已经算最小生成树的节点。
    • 第三步:更新非生成树节点到生成树的距离(即更新minDist数组,minDist数组用来记录每一个节点距离最小生成树的最近距离)
/**
 * 1. 初始化数据结构
    grid: 邻接矩阵,表示图的连接关系,初始值为10001(表示无穷大)
    minDist: 记录各顶点到当前生成树的最小距离
    isInTree: 标记顶点是否已加入生成树
 * 2. 构建邻接矩阵
    读取边信息,填充邻接矩阵
    因为是无向图,所以需要双向设置权值
 * 3. Prim算法核心
    1.寻找距离当前生成树最近的节点(未加入树且距离最小的节点)
    2.将该节点加入生成树
    3.更新其他未加入树的节点到生成树的最小距离
 * 4. 输出结果
    计算并输出最小生成树的总权重
    slice(2)跳过前两个元素(索引0和1,因为顶点编号从1开始)
    reduce求和所有边的权重

 * Main函数解析
    使用Node.js的readline模块处理输入
    第一行读取顶点数v和边数e
    后续读取e条边的信息(格式:顶点1 顶点2 权重)
    调用prim函数计算结果 
 */
// prim(v, edges) - 实现Prim算法核心逻辑
function prim(v, edges) {
    // 填一个默认最大值,题目描述val最大为10000
    const grid = Array.from({ length: v + 1 }, () => new Array(v + 1).fill(10001)); 
    // 所有节点到最小生成树的最小距离
    const minDist = new Array(v + 1).fill(10001)
    // 这个节点是否在树里
    const isInTree = new Array(v + 1).fill(false)
    // 建立邻接矩阵
    for(const [v1, v2, w] of edges) {
          // 因为是双向图,所以两个方向都要填上
        grid[v1][v2] = w
        grid[v2][v1] = w
    }
    // prim 演算法 只需要循环 n-1次,建立 n - 1条边,就可以把n个节点的图连在一起
    for (let i = 1 ; i < v ; i++) {
        // 选中哪个节点 加入最小生成树
        let cur = -1
        let tempMinDist = Number.MAX_VALUE
        // 1. 第一步,寻找距离生成树最近的节点 
        // 1 - v,顶点编号,这里下标从1开始
        for (let j = 1 ; j < v + 1 ; j++) {
            //  选取最小生成树节点的条件:
            //  (1)不在最小生成树里
            //  (2)距离最小生成树最近的节点
            if (!isInTree[j] && minDist[j] < tempMinDist) {
                tempMinDist = minDist[j]
                cur = j
            }
        }
        // 2. 第二步,最近节点(cur)加入生成树
        isInTree[cur] = true
        // 3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组)
        // cur节点加入之后, 最小生成树加入了新的节点,那么所有节点到 最小生成树的距离(即minDist数组)需要更新一下
        // 由于cur节点是新加入到最小生成树,那么只需要关心与 cur 相连的 非生成树节点 的距离 是否比 原来 非生成树节点到生成树节点的距离更小了
        for (let j = 1 ; j < v + 1 ; j++) {
            // 更新的条件:
            // (1)节点是 非生成树里的节点
            // (2)与cur相连的某节点的权值 比 该某节点距离最小生成树的距离小
            // 很多录友看到自己 就想不明白什么意思,其实就是 cur 是新加入 最小生成树的节点,那么 所有非生成树的节点距离生成树节点的最近距离 由于 cur的新加入,需要更新一下数据了
            if (!isInTree[j] && grid[cur][j] < minDist[j]) {
                minDist[j] = grid[cur][j]
            }
        }
    }
     // 统计结果,不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边
    console.log(minDist.slice(2).reduce((acc, cur) => acc + cur, 0))
}

// main() - 处理输入并调用prim函数
async function main() {
    const rl = require('readline').createInterface({ input: process.stdin })
    const iter = rl[Symbol.asyncIterator]()
    const readline = async () => (await iter.next()).value
    const [v, e] = (await readline()).split(" ").map(Number)
    const edges = []
    for (let i = 0 ; i < e ; i++) {
        edges.push((await readline()).split(" ").map(Number))
    }
    prim(v, edges)
}


main()

  • Kruskal,求最小生成树。

  • prim 算法是维护节点的集合,适合点多边少图,稠密图,而 Kruskal 是维护边的集合,适合边少点多图,稀疏图。

  • kruscal的思路:

    • 边的权值排序,因为要优先选最小的边加入到生成树里
    • 遍历排序后的边 (判断两个节点是否在同一个集合,就看图中两个节点是否有绿色的粗线连着)
      • 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环
      • 如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合
  • 并查集主要就两个功能:

    • 将两个元素添加到一个集合中
    • 判断两个元素在不在同一个集合
/**
 * 1. 初始化并查集数据结构 
    father数组用于实现并查集数据结构
    初始化每个顶点的父节点为自己
 * 2. 并查集辅助函数
    find(u): 查找u的根节点,带路径压缩优化
    isSame(u, v): 判断u和v是否在同一集合
    join(u, v): 合并u和v所在的集合
 * 3. Kruskal算法核心
    1.将所有边按权重从小到大排序
    2.依次检查每条边:
        如果边的两个顶点不在同一集合(不会形成环)
        则将边加入最小生成树,并合并两个顶点所在的集合
    3.累计加入边的权重
 * 4. 输出结果
    输出最小生成树的总权重

 * Main函数解析
    使用Node.js的readline模块处理输入
    第一行读取顶点数v和边数e
    后续读取e条边的信息(格式:顶点1 顶点2 权重)
    调用kruskal函数计算结果
 */
function kruskal(v, edges) {
    // 并查集标记节点关系的数组
    // 并查集初始化
    const father = Array.from({ length: v + 1 }, (_, i) => i)

    // 并查集的查找操作
    function find(u){
        if (u === father[u]) {
            return u
        } else {
            father[u] = find(father[u]) // 路径压缩
            return father[u]
        }
        
    }
    
    function isSame(u, v) {
        let s = find(u)
        let t = find(v)
        return s === t
    }
    
    // 并查集的加入集合
    function join(u, v) {
        let s = find(u) // 寻找u的根
        let t = find(v) // 寻找v的根
         // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
        if (s == t) return
        if (s !== t) {
            father[s] = t // 合并两个集合
        }
    }
    
    // 按边的权值对边进行从小到大排序
    edges.sort((a, b) => a[2] - b[2])
    let result = 0
    for (const [v1, v2, w] of edges) {
        // 如果祖先不同,则不在同一个集合
        if (!isSame(v1, v2)) {
            // 这条边可以作为生成树的边
            result += w
            // 两个节点加入到同一个集合
            join(v1 ,v2)
        } 
    }
    console.log(result)
}


async function main() {
    const rl = require('readline').createInterface({ input: process.stdin })
    const iter = rl[Symbol.asyncIterator]()
    const readline = async () => (await iter.next()).value
    const [v, e] = (await readline()).split(" ").map(Number)
    const edges = []
    for (let i = 0 ; i < e ; i++) {
        edges.push((await readline()).split(" ").map(Number))
    }
    kruskal(v, edges)
}


main()




参考&感谢各路大神

posted @ 2025-07-23 10:11  安静的嘶吼  阅读(9)  评论(0)    收藏  举报