## [$AcWing$ $345$ 牛站](https://www.acwing.com/problem/content/347/) ### 一、题目描述 给定一张由 $T$ 条边构成的无向图,点的编号为 $1$∼$1000$ 之间的整数。 求从起点 $S$ 到终点 $E$ **恰好** 经过 $N$ 条边(**可以重复经过**)的 **最短路**。 注意: 数据保证一定有解 **输入格式** 第 $1$ 行:包含四个整数 $N$,$T$,$S$,$E$。 第 $2..T+1$ 行:每行包含三个整数,描述一条边的边长以及构成边的两个点的编号。 **输出格式** 输出一个整数,表示最短路的长度 **数据范围** $2≤T≤100,2≤N≤10^6$ **输入样例**: ```cpp {.line-numbers} 2 6 6 4 11 4 6 4 4 8 8 4 9 6 6 8 2 6 9 3 8 9 ``` **输出样例**: ```cpp {.line-numbers} 10 ``` ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230710080838.png) ### 二、前导知识 > * **图中任意两点间路径数量** [$VUA$~$125$ $Numbering$ $Paths$](https://www.cnblogs.com/littlehb/p/16846789.html) > * **图中两点路径为$k$的方案数** [$P4159$ [$SCOI2009$] 迷路](https://www.cnblogs.com/littlehb/p/16850595.html) 这道题放这不太合适,因为还有拆点,有点难度~ ### 三、题目解析 本题并不是让我们求路径的条数,而是求 **在路径条数限定的情况下,求最短路径长度** #### 1. $(i,j)$之间两条边 设$g$是图的邻接矩阵,**$i$,$j$两个点,如果之间有两条边,那么中间必然只有一个点**,设为$k$,$k$的范围是$ \in [1,n]$: 状态表示 $t[i][j]$:从$i$恰好经过 **两条边** 到达$j$的 **最短路径长度** **状态计算** $$\large t[i][j] = min(g[i][k] + g[k][j])\ k \in [1,n]$$ 那么求两点之间插入第三个点的最短路径代码为: ```cpp {.line-numbers} for(int k = 1;k <= n;k++) for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) t[i][j] = min(t[i][j],g[i][k] + g[k][j]); ``` 这被称作:**广义矩阵乘法** 下面的转化需要一点 **矩阵乘法** 的知识: **矩阵乘法栗子**: 令矩阵 $a$ 为一个 $2 × 2$ 的矩阵: ```cpp {.line-numbers} a = [ 2, 3 ] [ 4, 1 ] ``` 令矩阵 $b$ 为一个 $2 × 3$ 的矩阵: ```cpp {.line-numbers} b = [ 5, 1, 2 ] [ 2, 3, 4 ] ``` 求解 $a × b$: 设$t = a × b$ 矩阵 $t$ 的维度为 $2 × 3$。 ```cpp {.line-numbers} t = [ (2×5 + 3×2), (2×1 + 3×3), (2×2 + 3×4) ] [ (4×5 + 1×2), (4×1 + 1×3), (4×2 + 1×4) ] ``` > **注**:运算规则就是$a$矩阵第$1$行的所有数据,**对应**,乘以$b$矩阵第$1$列的所的数据,**累加和** 放到$t$矩阵的第$1$行第$1$列中去,其它以此类推。 所以,得到的结果矩阵 $t$ 为: ```cpp {.line-numbers} t = [ 14, 11, 14 ] [ 22, 7, 12 ] ``` 矩阵乘法 **模板** 如下: ```cpp {.line-numbers} for (int k = 1; k <= COLS_A; ++k) for (int i = 1; i <= ROWS_A; ++i) for (int j = 1; j <= COLS_B; ++j) t[i][j] += a[i][k] * b[k][j]; ``` 上面的最基本的矩阵乘法,现在,我们改一下需求,不再求$sum$叠加和,而是想求一下$min(a[i][k]+b[k][j])$,然后把这个最小值放到$t[i][j]$里,其实也是一样的道理,这被我们称为 **广义矩阵乘法**,代码就是: ```cpp {.line-numbers} for(int k = 1;k <= n;k++) for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) t[i][j] = min(t[i][j],g[i][k] + g[k][j]); ``` 我们 **惊喜** 的发现,这个代码与上面两点间插入第三个点求最短路径的代码是一模一样的,也就是说,我们可以理解为$t[i][j]$可以通过 **类似于** 乘以矩阵$g[][]$来达到求两点之间插入第三个点获取最短路径。 此时$a[i][k]->g[i][k],b[k][j]->g[k][j]$,噢,这里的$a[][],b[][]$都是$g[][]$,就是$g^2$啊! 结论: 1、两边条,加一个点,乘$g^2$ 2、三边条,加两个点,乘$g^3$ ... #### 2.$(i,j)$之间大于两条边 #### 快速幂对广义矩阵乘法的优化 广义矩阵乘法可以求$i$经过 **两条边** 到达$j$的 **最短路径** 长度,则对$g$做$k$次 **自乘** 就可以求经过$k$条边的 **最短路径** 长度了。 ```cpp {.line-numbers} void mul(int a[][N],int b[][N]){ int t[N][N]; memset(t,0x3f,sizeof t); //预求最小,先设最大 for(int k = 1;k <= n;k++) for(int i = 1;i <= n;i++) for(int j = 1;j <= n;j++) t[i][j] = min(t[i][j],a[i][k] + b[k][j]); // sizeof 不能在此处使用,因为a数组其实是指针传入,无法获取大小 // 有两个办法可以解决: // (1)采用全局的sizeof t进行替代 (简单) // (2)在函数中增加大小这样一个参数,由调用者赋值传入 (麻烦) memcpy(a,t,sizeof t); } void qmi(){ memcpy(f,g,sizeof f); k--; while(k){ if(k & 1) mul(f,g); mul(g,g); k >>= 1; } } ``` #### 3. 离散化 值得注意的是点的编号最大是`1000`,最多只有`100`条边,也就是最多`200`个节点,需要对节点编号做 **离散化**。 > **解释**:$Floyd$算法复杂度$O(N^3)$,如果$N=1000$,妥妥的会挂啊! ### $Code$ ```cpp {.line-numbers} #include using namespace std; const int N = 205; unordered_map id; int s, e; //起点 终点 int n, m; //离散化后的节点号 m条边 int k; //恰好k条边 int g[N][N]; //图 int f[N][N]; //最短距离 //矩阵乘法 void mul(int a[][N], int b[][N]) { int t[N][N]; memset(t, 0x3f, sizeof t); for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) //求指定边数情况下的最短距离 //广义上的矩阵乘法 t[i][j] = min(t[i][j], a[i][k] + b[k][j]); memcpy(a, t, sizeof t); } //快速幂 void qmi() { // 写法1:使用一个空的矩阵,需要执行k次幂 // memset(f, 0x3f, sizeof f); // for (int i = 1; i <= n; i++) f[i][i] = 0; //写法2,使用一个拷贝矩阵,需要执行k-1次幂 memcpy(f, g, sizeof f); k--; while (k) { if (k & 1) mul(f, g); //矩阵快速幂 mul(g, g); k >>= 1; } } // AC 481 ms // 这个速度真是很牛X int main() { scanf("%d %d %d %d", &k, &m, &s, &e); //起点的离散化后号为1 id[s] = ++n; //如果e与s不同,那么e的新号为2,否则为1 if (!id.count(e)) id[e] = ++n; //重新对s和e给定新的号码 s = id[s], e = id[e]; //初始化邻接矩阵 memset(g, 0x3f, sizeof g); while (m--) { int a, b, c; scanf("%d %d %d", &c, &a, &b); if (!id.count(a)) id[a] = ++n; //记录点的映射关系a-> id[a] if (!id.count(b)) id[b] = ++n; //记录点的映射关系b-> id[b] a = id[a], b = id[b]; //对a,b给定新的号码 //利用新的号码将边长c记录到邻接矩阵中 g[a][b] = g[b][a] = min(g[a][b], c); } //快速幂+动态规划思想 qmi(); //输出从起点到终点,恰好经过k条边的最短路径 printf("%d\n", f[s][e]); return 0; } ```