zl程序教程

您现在的位置是:首页 >  其它

当前栏目

P4159 [SCOI2009] 迷路

2023-09-27 14:28:12 时间

\(P4159\) [\(SCOI2009\)] 迷路

题目传送门

前序知识整理
关键词:矩阵+快速幂
P1226 【模板】快速幂||取余运算
矩阵乘法
P3390 【模板】矩阵快速幂
P1939 【模板】矩阵加速(数列)

一、题目描述

题目背景
\(windy\)有向图 中迷路了。

题目描述
该有向图有 \(n\) 个节点,节点从 \(1\)\(n\) 编号,\(windy\) 从节点 \(1\) 出发,他必须恰好在 \(t\) 时刻到达节点 \(n\)

现在给出该有向图,你能告诉 \(windy\) 总共有多少种不同的路径吗?

答案对 \(2009\) 取模。

注意:\(windy\) 不能在某个节点逗留,且通过某有向边的时间严格为给定的时间。

输入格式
第一行包含两个整数,分别代表 \(n\)\(t\)

\(2\) 到第 \((n+1)\) 行,每行一个长度为 \(n\) 的字符串,第 \((i + 1)\)行的第 \(j\) 个字符 \(c_{i, j}\)是一个数字字符,若为 \(0\),则代表节点 \(i\) 到节点 \(j\) 无边,否则代表节点 \(i\) 到节点 \(j\) 的边的长度为 \(c_{i, j}\)

输出格式
输出一行一个整数代表答案对 \(2009\) 取模的结果。

二、题目解析

第一反应:咦?这不是图论吗??? 默默的看了眼\(t\)的范围(\(<=1e9\)),死了心

蒟蒻豆爸解释一下:
为什么看一眼\(t\)的范围就知道不是图论呢?原因就是这个\(t\),一般图论就是用\(floyd,bellmanFord,spfa,dijkstra\)这些东东,而这些东东的时间复杂度最好也就是\(O((n+m)log⁡~m)\),一个\(m\)就干到了\(1e9\),不\(TLE\)就怪了

\(floyd\) \(bellmanFord\) \(spfa\) \(dijkstra\)
\(O(N^3)\) \(O(N\times M)\) \(O(N\times M)\) \(O((n+m)log⁡~m)\)

\(DP\)!! \(DP\)一定可以!!!

蒟蒻豆爸解释一下:
\(DP\)能做到线性复杂度就足够牛\(X\)了吧,就算是线性的,也一样会\(TLE\),为啥呢?因为跑一遍所有边就是\(1e9\)~

默默的看了眼\(t\)的范围,又死了心

那么怎么做呢?

一、边权为\(1\)的有向图中 两点间边权和 恰好是\(k\) 的路径条数

首先,我们把这道题想简单一点,如果题目中的每一条边都没有边权,只用\(1\)\(0\)来表示两个点之间是否存在边,并且用邻接矩阵来存这张图,那么我们又可以得到些什么呢?

举个栗子

我们以下面这张图为例
20221102111851

以邻接矩阵来表示这个矩阵:
\(\begin{bmatrix} 0& 1 & 1\\ 1&0 &1 \\ 1& 0 & 0 \end{bmatrix} \)矩阵\(1\)

其中,\(a_{ij}\) 表示\(i\)\(j\)之间是否有连线;

那么,我们把它 平方 一下,又可以得到什么呢?(友情提示:如果不清楚矩阵乘法,请点这里)

\(\begin{bmatrix} 0& 1 & 1\\ 1&0 &1 \\ 1& 0 & 0 \end{bmatrix} \times \begin{bmatrix} 0& 1 & 1\\ 1&0 &1 \\ 1& 0 & 0 \end{bmatrix}= \begin{bmatrix} 2& 0 & 1\\ 1& 1 & 1 \\ 0& 1 & 1 \end{bmatrix} \)矩阵\(2\)

你又发现了什么呢?

好的,如果还没发现,我们再来将矩阵\(1\) 三次方一下:

\(\begin{bmatrix} 0& 1 & 1\\ 1&0 &1 \\ 1& 0 & 0 \end{bmatrix} \times \begin{bmatrix} 0& 1 & 1\\ 1&0 &1 \\ 1& 0 & 0 \end{bmatrix} \times \begin{bmatrix} 0& 1 & 1\\ 1&0 &1 \\ 1& 0 & 0 \end{bmatrix}= \begin{bmatrix} 2& 0 & 1\\ 1& 1 & 1 \\ 0& 1 & 1 \end{bmatrix} \times \begin{bmatrix} 0& 1 & 1\\ 1&0 &1 \\ 1& 0 & 0 \end{bmatrix} = \begin{bmatrix} 1& 2 & 2\\ 2& 1 & 2 \\ 2& 0 & 1 \end{bmatrix} \) 矩阵\(3\)

什么,你还没发现吗???

那么让我来告诉你吧!!!

矩阵\(1\),我们可以把\(a_{ij}\) 看成通过 一条边 ,由\(i\)到达\(j\)的情况总数
矩阵\(2\),我们可以把\(a_{ij}\) 看成通过 两条边 ,由\(i\)到达\(j\)的情况总数
矩阵\(3\),我们可以把\(a_{ij}\) 看成通过 三条边 ,由\(i\)到达\(j\)的情况总数

不信?我们举个栗子:
从点\(1\)到点\(1\),且通过 一条边 的情况不存在,记为\(0\)
从点\(1\)到点\(1\),且通过 两条边 的情况共两种(\(1->2->1\) \(and\) \(1->3->1\)),记为\(2\)
从点\(1\)到点\(1\),且通过 三条边 的情况仅有一种(\(1->2->3->1\)),记为\(1\)

再回头看看矩阵吧!!!是不是完全满足这个条件呢???
所以我们就可以得出结论啦:

在矩阵\(A^x\)中,\(A^x_{ij}\)表示由\(i\)\(j\)经过\(x\)条边的情况总数

所以这就可以运用 快速幂 啦!!!

仔细算一下时间复杂度,\(O(n*logn)\),稳稳滴!!!

那么,这道题就可以很快打出来啦——吗?

显然是不可以的。

可能你已经发现了,我们所有的推论都建立在边权为\(1\)的情况上,可是这道题目呢?

接下来有 \(N\)行,每行一个长度为\(N\)的字符串。第\(i\)行第\(j\)列为 \(0\)表示从节点\(i\)到节点\(j\)没有边,为\(1\)\(9\)表示从节点\(i\)到节点\(j\)需要耗费的时间。

呀呀呀,这道题目的边权不只是\(1\)呀!

!(⊙ o ⊙)!

怎么办呢?

二、边权不为\(1\)的有向图中 两点间边权和 恰好是\(k\) 的路径条数

虽然我们发现不能直接使用我们的结论,但是最大边权是\(9\)\(N\)也不超过\(10\)!都不算大!

那我们就可以采用一种叫做 拆点 的方法:把 一个点拆成\(9\)个点(本质是按边权拆的)

并且,我们发现即使如此拆点,\(N\)也不会超过\(100\),妥妥的可以呀!

但怎么拆点呢?

我们先来试一下拆一个边权不超过\(2\)的图吧!

可得矩阵

\[\large \begin{bmatrix} 0 & 2 \\ 2 & 1 \end{bmatrix}\]

将其拆点:

可得到新矩阵 :

\[\large \begin{bmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 1 \\ 1 & 0 & 0 & 0 \end{bmatrix}\]

将其平方:

\[\large \begin{bmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 1 \\ 1 & 0 & 0 & 0 \end{bmatrix} \times \begin{bmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 1 \\ 1 & 0 & 0 & 0 \end{bmatrix} = \begin{bmatrix} 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 1 \\ 1 & 0 & 1 & 1 \\ 0 & 1 & 0 & 0 \end{bmatrix} \]

验算一下:

原来有点\(1\)到点\(2\)并用经过\(2\)边权的方案总数有一种(\(1->2\),边权为\(2\));

现在来说,点\(1\)变为点\(1'\),点\(2\)变为点\(3'\),经过\(2\)边权的方案总数依旧是\(2\)(\(1'->2'->3'\),边权均为\(1\));

那么则说明我们的拆点是正确的。

可以发现这样的话仍然是满足题意的

这是为什么呢?为啥要这样拆点呢?

因为可以更新答案的那条路径,一定走到了\(x_i\)号节点,正准备走到原来的另一个节点

不是很好解释,自己多画几张图试试就知道了(我知道你们懒,给你们画一个)

拆点 操作

那么既然我们通过拆点操作将所有点之间的边权都变成了\(1\),那么我们就可以用刚才得到的 结论 啦!!!

时间复杂度

\(O(n^3\log ~t)\)

实现代码

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int MOD = 2009;
const int N = 110;

int g[N][N], a[N][N];
int n, t;

//矩阵乘法
void mul(int c[][N], int a[][N], int b[][N]) {
    int t[N][N] = {0};
    int M = n * 9;
    for (int i = 1; i <= M; i++) {
        for (int j = 1; j <= M; j++)
            for (int k = 1; k <= M; k++)
                t[i][j] = (t[i][j] + (LL)(a[i][k] * b[k][j]) % MOD) % MOD;
    }
    memcpy(c, t, sizeof t);
}

int main() {
    // n个节点,t时刻
    scanf("%d %d", &n, &t);

    for (int i = 1; i <= n; i++) { //原图有n个节点

        /* 1点拆9点
          1-> 1~9
          2-> 10~18
          3-> 19~17
          ...

          拆开的点之间的权值是1
         */
        for (int j = 1; j < 9; j++) // 1点拆9点,注意收尾处是小于号
            g[(i - 1) * 9 + j][(i - 1) * 9 + j + 1] = 1;

        //从下标1开始读入原图中i号节点与其它各节点间的边权
        char s[11];
        scanf("%s", s + 1);

        //遍历一下输入的各点间关系图
        for (int j = 1; j <= n; j++)
            //如果i->j 存在边,并且边权 = s[j]
            //比如 1->8, 边权为3; 则 从3'->64'创建一条边权为1的边
            if (s[j] > '0') g[(i - 1) * 9 + s[j] - '0'][(j - 1) * 9 + 1] = 1;
    }

    //复制原始底图
    memcpy(a, g, sizeof g);

    //因为是复制出来,t次幂就变成了t-1次幂
    t--;

    //矩阵快速幂
    while (t) {
        if (t & 1) mul(g, g, a);
        mul(a, a, a);
        t >>= 1;
    }

    //输出结果
    printf("%d\n", g[1][(n - 1) * 9 + 1]);
    return 0;
}