题解:关于图中最短路径的矩阵快速幂算法问题
一、题目背景
给定一个图,图中节点之间存在边,每条边有一个权值。要求从起点 (S) 到终点 (E) 经过恰好 (k) 条边的最短路径长度。通过矩阵快速幂的方法来优化计算过程,避免直接枚举所有可能路径的高时间复杂度。
二、代码整体思路
代码主要通过以下几个步骤来解决问题:
1. 读取图的相关参数,包括经过边的数量 (k)、边的数量 (m)、起点 (S) 和终点 (E)。
2. 对图中的节点进行编号映射(使用 map
实现),将输入的实际节点编号转换为从 (1) 开始的连续编号,方便后续处理。
3. 初始化图的邻接矩阵 (g),根据输入的边信息填充邻接矩阵,同时处理重边(取权值较小的边)。
4. 定义矩阵乘法函数 mul
,用于计算两个矩阵相乘的结果,这里的矩阵乘法规则是取路径和的最小值(类似 Floyd 算法的思想)。
5. 实现矩阵快速幂函数 qmi
,通过不断将邻接矩阵 (g) 自乘(即矩阵快速幂操作),计算出经过 (k) 条边的最短路径矩阵 (res)。
6. 输出从起点 (S) 到终点 (E) 在经过 (k) 条边情况下的最短路径长度(存储在 res[S][E]
中)。
三、代码逐段分析
(一)头文件和全局变量
#include <cstring>
#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
const int N = 210;
int k, n, m, S, E;
int g[N][N];
int res[N][N];
- 头文件:
#include <cstring>
:用于字符串操作和内存初始化函数,如memset
。#include <iostream>
:提供 C++ 风格的输入输出流,如cin
和cout
。#include <algorithm>
:包含一些常用的算法函数,如min
、max
等。#include <map>
:引入map
容器,用于实现节点编号的映射。using namespace std;
:使用标准命名空间,以便直接使用标准库中的函数和类型。
- 常量定义:
const int N = 210
:定义矩阵的最大维度,即图中节点的最大数量。
- 变量定义:
int k, n, m, S, E;
:k
表示从起点到终点要经过的边的数量;n
表示图中节点的数量(在处理过程中动态更新);m
表示图中边的数量;S
表示起点;E
表示终点。int g[N][N];
:二维数组g
用于存储图的邻接矩阵,g[i][j]
表示节点 (i) 和节点 (j) 之间的边权值(初始化为极大值,后续根据输入更新)。int res[N][N];
:二维数组res
用于存储经过 (k) 条边的最短路径矩阵,res[i][j]
表示从节点 (i) 到节点 (j) 经过 (k) 条边的最短路径长度。
(二)mul
函数
void mul(int c[][N], int a[][N], int b[][N])
{
static int temp[N][N];
memset(temp, 0x3f, sizeof temp);
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
temp[i][j] = min(temp[i][j], a[i][k] + b[k][j]);
memcpy(c, temp, sizeof temp);
}
- 定义一个局部静态数组
temp[N][N]
,用于临时存储矩阵乘法的结果。 memset(temp, 0x3f, sizeof temp);
:将temp
数组初始化为一个较大的值(0x3f
表示十六进制的较大数,这里用于表示初始时的无穷距离)。- 通过三层循环实现矩阵乘法:
- 中间层循环变量
k
从 (1) 到 (n),表示中间节点。 - 最外层循环变量
i
从 (1) 到 (n),表示结果矩阵c
的行。 - 内层循环变量
j
从 (1) 到 (n),表示结果矩阵c
的列。 temp[i][j] = min(temp[i][j], a[i][k] + b[k][j]);
:计算从节点i
经过节点k
到节点j
的路径长度,并取较小值更新temp[i][j]
,这与 Floyd 算法中更新最短路径的思想类似。
- 中间层循环变量
memcpy(c, temp, sizeof temp);
:将临时数组temp
的内容复制到结果矩阵c
中。
(三)qmi
函数
void qmi()
{
memset(res, 0x3f, sizeof res);
for (int i = 1; i <= n; i ++ ) res[i][i] = 0;
while (k)
{
if (k & 1) mul(res, res, g); // res = res * g
mul(g, g, g); // g = g * g
k >>= 1;
}
}
- 初始化结果矩阵
res
:memset(res, 0x3f, sizeof res);
:将res
数组初始化为一个较大的值(表示初始时的无穷距离)。for (int i = 1; i <= n; i ++ ) res[i][i] = 0;
:将res
矩阵的对角元素设置为 (0),表示节点到自身的路径长度为 (0)。
- 矩阵快速幂过程:
while (k)
:当 (k) 不为 (0) 时进行循环。if (k & 1) mul(res, res, g);
:如果 (k) 的二进制表示的最低位为 (1),则将结果矩阵res
与邻接矩阵g
相乘(调用mul
函数),更新res
。mul(g, g, g);
:将邻接矩阵g
自乘(调用mul
函数),更新g
。k >>= 1;
:将 (k) 右移一位,即去掉 (k) 的二进制表示的最低位。
(四)main
函数
int main()
{
cin >> k >> m >> S >> E;
memset(g, 0x3f, sizeof g);
map<int, int> ids;
if (!ids.count(S)) ids[S] = ++ n;
if (!ids.count(E)) ids[E] = ++ n;
S = ids[S], E = ids[E];
while (m -- )
{
int a, b, c;
cin >> c >> a >> b;
if (!ids.count(a)) ids[a] = ++ n;
if (!ids.count(b)) ids[b] = ++ n;
a = ids[a], b = ids[b];
g[a][b] = g[b][a] = min(g[a][b], c);
}
qmi();
cout << res[S][E] << endl;
return 0;
}
- 输入图的参数:
cin >> k >> m >> S >> E;
:读取经过边的数量k
、边的数量m
、起点S
和终点E
。
- 初始化邻接矩阵和节点编号映射:
memset(g, 0x3f, sizeof g);
:将邻接矩阵g
初始化为一个较大的值(表示初始时节点之间无连接)。- 创建
map
容器ids
用于存储节点编号的映射关系。 - 检查起点
S
和终点E
是否在ids
中,如果不在则为其分配新的编号(ids[S] = ++ n
和ids[E] = ++ n
),并更新S
和E
为新的编号。
- 读取边的信息并更新邻接矩阵:
- 通过循环
while (m -- )
读取每条边的信息,包括边权值c
和两个端点a
和b
。 - 检查端点
a
和b
是否在ids
中,如果不在则为其分配新的编号,并更新a
和b
为新的编号。 g[a][b] = g[b][a] = min(g[a][b], c);
:更新邻接矩阵g
,如果存在重边,取权值较小的边。
- 通过循环
- 计算最短路径矩阵并输出结果:
qmi();
:调用qmi
函数,通过矩阵快速幂计算经过 (k) 条边的最短路径矩阵res
。cout << res[S][E] << endl;
:输出从起点S
到终点E
经过 (k) 条边的最短路径长度。
- 结束程序:
return 0;
:程序正常结束。
四、时间复杂度和空间复杂度分析
(一)时间复杂度
- 输入和初始化部分:读取输入和初始化邻接矩阵、节点编号映射等操作,时间复杂度为 (O(m + n)),其中 (m) 是边的数量,(n) 是节点的数量(在处理过程中动态更新)。
- 矩阵快速幂部分:矩阵快速幂的时间复杂度为 (O(\log k \times n^3)),其中 (k) 是经过边的数量,(n) 是节点的数量。每次矩阵乘法的时间复杂度为 (O(n^3)),而矩阵快速幂需要进行 (O(\log k)) 次乘法操作。
- 总体时间复杂度为 (O(m + n + \log k \times n^3)),在实际中,当 (m) 和 (n) 较大时,主要时间复杂度为 (O(\log k \times n^3))。
(二)空间复杂度
- 存储邻接矩阵和结果矩阵:邻接矩阵
g[N][N]
和结果矩阵res[N][N]
占用空间均为 (O(n^2)),所以这部分空间复杂度为 (O(n^2))。 - 存储节点编号映射:
map
容器ids
存储节点编号映射,在最坏情况下,每个节点都需要存储,空间复杂度为 (O(n))。 - 总体空间复杂度为 (O(n^2 + n) = O(n^2)),主要由存储矩阵的空间占用决定。
希望以上题解能帮助你理解这道题的代码逻辑和算法原理。如果还有疑问,请随时提问。