P6190 [NOI Online #1 入门组] 魔法 解题报告
P6190 [NOI Online #1 入门组] 魔法 解题报告
1. 问题简述
我们要在一张有向有权图上,从城市 1 走到城市 n。我们有一个特殊能力:最多可以使用 k
次魔法。每次使用魔法,可以让下一条要走的道路的费用 t
变为 -t
。我们的目标是找到一条从 1 到 n 的路径,使得总费用最小。
2. 思路分析:从简到难,逐步深入
这道题最关键的变量是魔法次数 k
。它的取值范围很大(最大可达 \(10^6\)),这通常暗示着我们需要一个与 log(k)
相关的算法。但我们先不考虑这么复杂的情况,而是从 k
比较小的时候开始分析,寻找规律。
第一步:当 k=0
时,一次魔法也不能用
如果不能使用魔法,问题就变成了最经典的单源最短路问题。我们需要求从 1 到 n 的最小花费。
不过,考虑到后续步骤可能需要任意两个城市之间的最短路径,我们可以直接用 Floyd-Warshall 算法,预处理出图中任意两个城市 i
和 j
之间的最短路 dist[i][j]
。Floyd-Warshall 的时间复杂度是 \(O(n^3)\),对于 \(n \le 100\) 的数据范围来说是完全可以接受的。
我们将这个 k=0
时的全源最短路矩阵记为 D_0
。
第二步:当 k=1
时,最多能用一次魔法
现在我们有一次使用魔法的机会。从城市 i
到城市 j
的最短路程,有两种可能:
-
不使用魔法:最短路程就是我们刚刚算出来的
D_0[i][j]
。 -
使用一次魔法:我们必须选择图中的某条边
(u, v)
(费用为w
)来施法。施法后,这条边的费用变为-w
。那么,整条路径就可以看作三段:- 从起点
i
不用魔法走到u
。 - 经过施了魔法的边
(u, v)
。 - 从
v
不用魔法走到终点j
。
为了让总费用最小,我们应该走最短的路径。所以,这条路径的总费用就是
D_0[i][u] - w + D_0[v][j]
。我们只需要遍历图中的每一条边(u, v)
,计算这个值,然后取其中的最小值。 - 从起点
综合以上两种情况,从 i
到 j
最多用 1 次魔法的最短路,就是 min(不使用魔法的费用, 使用一次魔法的最小费用)
。
我们把这个结果记为矩阵 D_1
,其中 D_1[i][j]
表示从 i
到j
最多用1次魔法的最短路。
第三步:推广到 k
次魔法,发现“矩阵乘法”的奥秘
我们如何从 D_1
(最多用1次)推导出 D_2
(最多用2次)呢?
考虑一条从 i
到 j
最多用2次魔法的路径。我们可以把它拆成两段:从 i
到某个中转点 p
,再从 p
到 j
。
- 如果
i -> p
最多用1次魔法,p -> j
也最多用1次魔法,那么i -> p -> j
整条路最多就用了1+1=2
次魔法。 - 这条路径的费用是
D_1[i][p] + D_1[p][j]
。 - 为了找到最短的路,我们需要遍历所有可能的中转点
p
,取一个最小值。
所以,D_2[i][j] = min(D_1[i][p] + D_1[p][j])
,其中 p
从 1 到 n
。
这个式子是不是很眼熟?
- 标准矩阵乘法:
C[i][j] = Σ (A[i][p] * B[p][j])
- 我们的新运算:
D_2[i][j] = min (D_1[i][p] + D_1[p][j])
它们的形式惊人地相似!我们定义一种新的“矩阵乘法” ⊗
, (A ⊗ B)[i][j] = min(A[i][p] + B[p][j])
。
那么,我们就有:
D_2 = D_1 ⊗ D_1
D_3 = D_2 ⊗ D_1 = (D_1 ⊗ D_1) ⊗ D_1
- 以此类推,
D_k = D_1 ⊗ D_1 ⊗ ... ⊗ D_1
(k
个D_1
相乘)
这就变成了 D_k = (D_1)^k
。
第四步:使用矩阵快速幂加速
计算 A^k
,当 k
很大时,最快的方法就是快速幂。普通快速幂是把 a*a
变成 a^2
,我们这里就是把 D_1 ⊗ D_1
变成 (D_1)^2
。
这种新的“矩阵乘法”满足结合律 (A ⊗ B) ⊗ C = A ⊗ (B ⊗ C)
,所以我们可以放心地使用快速幂。
算法流程总结:
- 初始化 (
k=0
): 用 Floyd-Warshall 算法计算出任意两点间不使用魔法的最短路矩阵D_0
。 - 构造基本矩阵 (
k=1
): 根据D_0
,计算出最多使用 1 次魔法的最短路矩阵D_1
。 - 矩阵快速幂: 使用我们定义的新“矩阵乘法”,通过矩阵快速幂计算出
(D_1)^k
。 - 最终答案: 矩阵
(D_1)^k
中的(1, n)
位置的元素,就是从 1 到 n 最多用k
次魔法的最小费用。
特殊情况:如果 k=0
,答案就是 D_0[1][n]
。
3. 代码实现解读
下面我们结合代码来理解这个过程。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
// 存储边的信息
struct edge
{
int u,v,w;
}e[2505];
int n,m,k;
// 定义矩阵结构和我们的新“乘法”
struct mat
{
long long a[105][105];
mat(int x=63) // 构造函数,默认用一个很大的数(近似无穷大)填充
{
memset(a,x,sizeof(a));
}
// 重载 * 运算符,实现 A ⊗ B
mat operator*(const mat&b)const
{
mat ans; // 结果矩阵,已初始化为无穷大
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
// 这就是 D_new[i][j] = min(D_new[i][j], D_old[i][k] + D_another[k][j])
ans.a[i][j]=min(ans.a[i][j],a[i][k]+b.a[k][j]);
return ans;
}
}a; // a 将作为我们的基本矩阵 D_1
long long f[105][105]; // f 矩阵,用于存储 D_0
// 矩阵快速幂
mat fpow(mat x,int y)
{
mat ans;
// 初始化 ans 矩阵。这里直接用 f 矩阵(D_0)初始化。
// 这是一个小技巧:D_0 ⊗ M = M,所以 D_0 在此运算中是“单位矩阵”
// 因此 ans = D_0, 之后 ans = ans ⊗ x 就相当于 ans = x
// 最终结果 (D_0) ⊗ (D_1)^k = (D_1)^k,逻辑是正确的
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
ans.a[i][j]=f[i][j];
while(y)
{
if(y&1)ans=ans*x; // 如果 y 是奇数,ans = ans ⊗ x
x=x*x; // x = x ⊗ x
y>>=1; // y = y / 2
}
return ans;
}
int main()
{
// 1. 初始化 (k=0)
memset(f,63,sizeof(f));
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
f[i][i]=0; // 从一个点到自己,费用为0
for(int i=1;i<=m;i++)
{
cin>>e[i].u>>e[i].v>>e[i].w;
f[e[i].u][e[i].v]=min((long long)e[i].w, f[e[i].u][e[i].v]); // 防止重边
}
// 运行 Floyd-Warshall, 得到 D_0
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
// 2. 构造基本矩阵 a (即 D_1)
for(int k=1;k<=m;k++)
{
int u=e[k].u,v=e[k].v,w=e[k].w;
// 遍历所有 i, j
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
// D_1[i][j] = min( D_0[i][j], min over all edges (D_0[i][u] - w + D_0[v][j]) )
// a.a[i][j] 的初值是无穷大,所以第一次赋值直接min即可
a.a[i][j]=min(a.a[i][j],min(f[i][j],f[i][u]+f[v][j]-w));
}
// 3. 根据 k 的值计算并输出
if(k==0)
{
cout<<f[1][n]<<endl; // k=0,直接用 D_0 的结果
}
else
{
// k>0, 用矩阵快速幂计算 (D_1)^k,并输出 [1][n] 位置的结果
cout<<fpow(a,k).a[1][n]<<endl;
}
return 0;
}
4. 总结
本题的核心思想是动态规划和矩阵快速幂优化。我们通过分析 k
从 0 到 1 再到 2 的变化,发现了一个类似 Floyd-Warshall 的递推关系。这个关系可以抽象成一种新的矩阵乘法 (min, +)
。对于求解 k
次操作后的状态,正好对应于这个新矩阵的 k
次方。由于 k
非常大,我们使用矩阵快速幂将 \(O(k \cdot n^3)\) 的朴素递推优化到了 \(O(\log k \cdot n^3)\),从而高效地解决了问题。