题解:洛谷 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
【核心思想】
-
问题分析:给定带权无向图 \(G\),需要枚举所有连续编号区间 \([\ell, r]\) 构成的子图 \(G(\ell, r)\),计算每个子图中所有连通结点对的最短距离之和,最后累加所有子图的贡献并对 \(10^9\) 取模。这是一个增量式最短路 + 子图枚举问题,核心在于利用子图的连续性,通过增量添加结点的方式高效维护最短路。
-
算法选择:
- 增量 Dijkstra:固定左端点 \(\ell\),逐步扩展右端点 \(r\),每次加入新结点时计算其到已有子图的最短路
- Floyd 增量更新:利用新加入的结点 \(r\) 作为中间点,更新子图内所有结点对的最短距离
- 邻接表排序优化:对邻接表按目标结点编号排序,在 Dijkstra 中利用单调性提前终止无效遍历
-
关键步骤:
- 预处理:读入边并建立邻接表,对每个结点的邻接表按目标结点编号升序排序并去重
- 枚举左端点(\(\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\)
-
时间/空间复杂度:
- 时间复杂度:\(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)\)
-
增量式最短路的核心思想:
- 连续子图的增量特性:子图 \(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
浙公网安备 33010602011771号