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 算法,预处理出图中任意两个城市 ij 之间的最短路 dist[i][j]。Floyd-Warshall 的时间复杂度是 \(O(n^3)\),对于 \(n \le 100\) 的数据范围来说是完全可以接受的。

我们将这个 k=0 时的全源最短路矩阵记为 D_0

第二步:当 k=1 时,最多能用一次魔法

现在我们有一次使用魔法的机会。从城市 i 到城市 j 的最短路程,有两种可能:

  1. 不使用魔法:最短路程就是我们刚刚算出来的 D_0[i][j]

  2. 使用一次魔法:我们必须选择图中的某条边 (u, v)(费用为 w)来施法。施法后,这条边的费用变为 -w。那么,整条路径就可以看作三段:

    • 从起点 i 不用魔法走到 u
    • 经过施了魔法的边 (u, v)
    • v 不用魔法走到终点 j

    为了让总费用最小,我们应该走最短的路径。所以,这条路径的总费用就是 D_0[i][u] - w + D_0[v][j]。我们只需要遍历图中的每一条边 (u, v),计算这个值,然后取其中的最小值。

综合以上两种情况,从 ij 最多用 1 次魔法的最短路,就是 min(不使用魔法的费用, 使用一次魔法的最小费用)

我们把这个结果记为矩阵 D_1,其中 D_1[i][j] 表示从 ij最多用1次魔法的最短路。

第三步:推广到 k 次魔法,发现“矩阵乘法”的奥秘

我们如何从 D_1(最多用1次)推导出 D_2(最多用2次)呢?

考虑一条从 ij 最多用2次魔法的路径。我们可以把它拆成两段:从 i 到某个中转点 p,再从 pj

  • 如果 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_1kD_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),所以我们可以放心地使用快速幂。

算法流程总结:

  1. 初始化 (k=0): 用 Floyd-Warshall 算法计算出任意两点间不使用魔法的最短路矩阵 D_0
  2. 构造基本矩阵 (k=1): 根据 D_0,计算出最多使用 1 次魔法的最短路矩阵 D_1
  3. 矩阵快速幂: 使用我们定义的新“矩阵乘法”,通过矩阵快速幂计算出 (D_1)^k
  4. 最终答案: 矩阵 (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)\),从而高效地解决了问题。

posted @ 2025-07-09 17:18  surprise_ying  阅读(50)  评论(0)    收藏  举报