题解:洛谷 P15805 子图最短路

【题目来源】

洛谷:P15805 [GESP202603 八级] 子图最短路 - 洛谷

【题目描述】

给定包含 \(n\) 个结点 \(m\) 条边的带权无向图 \(G\),结点依次以 \(1, 2, \dots, n\) 编号。第 \(i\) (\(1 \le i \le m\))条边连接编号为 \(u_i\)\(v_i\) 的两个结点,权值为 \(w_i\)

对于指定的 \(1 \le \ell \le r \le n\),按以下方式构造图 \(G\) 的子图 \(G(\ell, r)\)

  • 保留 \(G\) 中编号在区间 \([\ell, r]\) 中的结点。删去其它编号不在 \([\ell, r]\) 中的结点以及与之相连的边。剩余的结点和边构成子图 \(G(\ell, r)\)

对于 \(G(\ell, r)\) 中的任意结点 \(u, v\) 应有 \(\ell \le u, v \le r\)。记 \(u, v\) 在子图 \(G(\ell, r)\) 上的最短距离为 \(d(\ell, r, u, v)\)。特殊地,若 \(u, v\) 在子图 \(G(\ell, r)\) 上不连通,则认为 \(d(\ell, r, u, v) = 0\)

你需要求出 \(\sum_{\ell=1}^{n} \sum_{r=\ell}^{n} \sum_{u=\ell}^{r} \sum_{v=u}^{r} d(\ell, r, u, v)\)\(10^9\) 取模的结果。

  • 题目中的英文字母 \(l\) 使用了特殊写法 \(\ell\),以避免英文字母 \(l\) 与数字 \(1\) 混淆。

【输入】

第一行,两个正整数 \(n, m\),表示结点数与边数。

接下来 \(m\) 行,第 \(i\) (\(1 \le i \le m\))行包含三个正整数 \(u_i, v_i, w_i\),表示一条连接结点 \(u_i, v_i\) 的权值为 \(w_i\) 的边。

【输出】

输出一行,一个整数,表示 \(\sum_{\ell=1}^{n} \sum_{r=\ell}^{n} \sum_{u=\ell}^{r} \sum_{v=u}^{r} d(\ell, r, u, v)\)\(10^9\) 取模的结果。

【输入样例】

3 2
1 2 1
2 3 2

【输出样例】

9

【核心思想】

  1. 问题分析:给定带权无向图 \(G\),需要枚举所有连续编号区间 \([\ell, r]\) 构成的子图 \(G(\ell, r)\),计算每个子图中所有连通结点对的最短距离之和,最后累加所有子图的贡献并对 \(10^9\) 取模。这是一个增量式最短路 + 子图枚举问题,核心在于利用子图的连续性,通过增量添加结点的方式高效维护最短路。

  2. 算法选择

    • 增量 Dijkstra:固定左端点 \(\ell\),逐步扩展右端点 \(r\),每次加入新结点时计算其到已有子图的最短路
    • Floyd 增量更新:利用新加入的结点 \(r\) 作为中间点,更新子图内所有结点对的最短距离
    • 邻接表排序优化:对邻接表按目标结点编号排序,在 Dijkstra 中利用单调性提前终止无效遍历
  3. 关键步骤

    • 预处理:读入边并建立邻接表,对每个结点的邻接表按目标结点编号升序排序并去重
    • 枚举左端点\(\ell\)\(1\)\(n-1\)):
      • 初始化距离矩阵 \(d\) 为无穷大,\(d[\ell][\ell] = 0\)
      • 扩展右端点\(r\)\(\ell+1\)\(n\)):
        • \(dijkstra(\ell, r)\):计算结点 \(r\) 到子图 \([\ell, r]\) 内所有结点的最短路,仅遍历编号 \(\geq \ell\)\(< r\) 的邻接点
        • \(floyd(\ell, r)\):以 \(r\) 为中间点,更新所有 \(i, j \in [\ell, r]\)\(d[i][j] = \min(d[i][j], d[i][r] + d[r][j])\)
        • 累加当前子图的所有连通点对距离之和到 \(ans\)
    • 输出结果\(ans \bmod 10^9\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^2 \cdot (m \log n + n^2))\),外层枚举 \(O(n^2)\) 个子图,每个子图 Dijkstra 为 \(O(m \log n)\),Floyd 更新为 \(O(n^2)\)
    • 空间复杂度:\(O(n^2 + m)\),距离矩阵 \(d\)\(O(n^2)\),邻接表为 \(O(m)\)
  5. 增量式最短路的核心思想

    • 连续子图的增量特性:子图 \(G(\ell, r)\) 是在 \(G(\ell, r-1)\) 基础上加入结点 \(r\) 及其相关边得到的,利用这一特性避免对每个子图重新计算全部最短路
    • Dijkstra 的局部性优化:由于子图只包含编号 \(\geq \ell\) 的结点,在松弛时跳过 \(< \ell\) 的结点;邻接表排序后,当遇到 \(\geq r\) 的结点时可提前 \(break\),减少无效遍历
    • Floyd 的增量更新:新结点 \(r\) 加入后,只需以 \(r\) 为中间点更新已有结点对的最短距离,无需三重循环全量更新
    • 连通性判定\(d[i][j] < \infty\) 时累加,不连通时贡献为 \(0\),符合题意
    • 适用于连续区间子图枚举、需要维护动态图最短路的组合计数类问题

【解题思路】

【算法标签】

普及+ #Dijkstra

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 105, mod = 1e9;  // 模数
int n, m;  // n: 节点数, m: 边数
int d[N][N];  // 距离矩阵
int ans;  // 最终答案
bool st[N];  // Dijkstra中的访问标记

// 边结构体
struct Node
{
    int v, w;  // v: 目标节点, w: 边权
};

vector<Node> a[N];  // 邻接表
priority_queue<Node, vector<Node> > pq;  // 优先队列(大顶堆)

// 优先队列比较函数:边权小的优先
bool operator<(Node x, Node y)
{
    return x.w > y.w;  // 注意:priority_queue默认是大顶堆,这里重载<使小边权优先
}

// 排序比较函数:先按目标节点编号排序,再按边权排序
bool cmp(Node x, Node y)
{
    if (x.v == y.v)  // 如果目标节点相同
    {
        return x.w < y.w;  // 边权小的在前
    }
    return x.v < y.v;  // 目标节点编号小的在前
}

// Dijkstra算法,计算从节点idx到其他节点的最短距离
// l: 当前考虑的最小节点编号
void dijkstra(int l, int idx)
{
    memset(st, 0, sizeof(st));  // 重置访问标记
    pq.push({idx, 0});  // 起点入队,距离为0

    while (!pq.empty())
    {
        int u = pq.top().v;  // 当前节点
        int w = pq.top().w;  // 当前距离
        pq.pop();

        if (st[u] == true)  // 如果已访问,跳过
        {
            continue;
        }
        st[u] = true;  // 标记为已访问

        d[idx][u] = d[u][idx] = w;  // 更新距离矩阵(对称)

        // 遍历当前节点的所有邻接边
        for (int i = 0; i < a[u].size(); i++)
        {
            int v = a[u][i].v;  // 邻接节点

            if (v < l)  // 如果目标节点编号小于l,跳过
            {
                continue;
            }
            if (v >= idx)  // 如果目标节点编号大于等于当前源点idx,跳过
            {
                break;  // 因为邻接表已排序,所以可以提前退出
            }

            if (d[idx][v] > a[u][i].w + w)  // 如果可以更新最短距离
            {
                d[idx][v] = a[u][i].w + w;  // 更新距离
                pq.push({v, d[idx][v]});  // 入队
            }
        }
    }
}

// Floyd算法更新距离矩阵,并计算当前子图的距离和
void floyd(int l, int idx)
{
    int sum = 0;  // 当前子图的距离和

    // 遍历所有节点对(i,j),其中l ≤ i < j ≤ idx
    for (int i = l; i < idx; i++)
    {
        for (int j = i + 1; j <= idx; j++)
        {
            // Floyd核心:通过节点idx更新i和j之间的距离
            d[i][j] = d[j][i] = min(d[i][j], d[i][idx] + d[idx][j]);

            if (d[i][j] < 0x3f3f3f3f)  // 如果i和j连通
            {
                sum = (sum + d[i][j]) % mod;  // 累加到当前子图的距离和
            }
        }
    }

    ans = (ans + sum) % mod;  // 将当前子图的距离和累加到最终答案
}

int main()
{
    cin >> n >> m;  // 输入节点数和边数

    // 输入图的边
    while (m--)
    {
        int u, v, w;
        cin >> u >> v >> w;
        a[u].push_back({v, w});  // 无向图
        a[v].push_back({u, w});  // 无向图
    }

    // 预处理:对每个节点的邻接表排序并去重
    for (int i = 1; i <= n; i++)
    {
        sort(a[i].begin(), a[i].end(), cmp);  // 排序

        // 去重:如果相邻两条边的目标节点相同,保留边权小的
        for (int j = a[i].size() - 1; j > 0; j--)
        {
            if (a[i][j].v == a[i][j - 1].v)
            {
                a[i].erase(a[i].begin() + j);
            }
        }
    }

    // 主算法:枚举所有子图
    for (int i = 1; i < n; i++)  // i是子图的最小节点编号
    {
        memset(d, 0x3f, sizeof(d));  // 初始化距离矩阵为无穷大
        d[i][i] = 0;  // 自己到自己的距离为0

        for (int j = i + 1; j <= n; j++)  // j是当前考虑加入子图的最大节点
        {
            dijkstra(i, j);  // 计算节点j到子图中其他节点的最短距离
            floyd(i, j);  // 更新子图,并计算当前子图的距离和
        }
    }

    cout << ans << endl;  // 输出最终答案

    return 0;  // 程序正常结束
}

【运行结果】

3 2
1 2 1
2 3 2
9
posted @ 2026-07-09 23:25  团爸讲算法  阅读(11)  评论(0)    收藏  举报