P4159 迷路
2023-08-09 20:31:27 # OI # Problem

前置

image.png

nn \le 100

k\le 10910^9

邻接矩阵存图

image.png

f[i][j]f[i][j]代表走了ii步到达jj的方案数

初始化:

f[0][1]=1f[0][1]=1

f[0][2n]=0f[0][{2\to n}]=0

image.png

代码中可以出现相同变量名,前提是它们的作用域不同

image.png

如何精确访问?

::a代表访问全局变量

正常的DP写法:

cin>>n>>k;
   for(int i=1;i<=n;i++)
       for(int j=1;j<=n;j++)
           cin>>z[i][j];
   //z[i][j]=0/1;i->j
   f[0][1]=1;
   for(int a=1;a<=k;a++)//走了a步
       for(int b=1;b<=n;b++)//走到了b
           for(int c=1;c<=n;c++)//第a-1步在c
           {
               if(z[c][b]==1) f[a][b]+=f[a-1][c];//如果有边
           }
   ans=f[k][n];//走k步到n

O(n3)O(n^3)复杂度肯定过不了

优化一下:

// if(z[c][b]=1) f[a][b]+=f[a-1][c];
        f[a][b]+=f[a-1][c]*z[c][b];

解释:cbc \to b有边的时候,z[c][b]=1z[c][b]=1 就相当于转移

cbc \to b没有边的时候,z[c][b]=0z[c][b]=0,就相当于加上00

那这样做有什么用呢? ——别急,往下看

升一下维度,中间始终为11

cin>>n>>k;
   for(int i=1;i<=n;i++)
       for(int j=1;j<=n;j++)
           cin>>z[i][j];
   //z[i][j]=0/1;//i->j
   f[0][1]=1;
   for(int a=1;a<=k;a++)//走了a步
       for(int d=1;d<=1;d++)//无意义
           for(int b=1;b<=n;b++)//走到了b
               for(int c=1;c<=n;c++)//第a-1步在c
               {
                   // if(z[c][b]=1) f[a][b]+=f[a-1][c];
                   f[a][d][b]+=f[a-1][d][c]*z[c][b];
               }
   ans=f[k][1][n];

加维度、把判断变为相乘的目的就是为了把形式凑成矩阵乘法

f[a]f[a]当作变量

image.png

image.png

然后用下矩阵快速幂就搞定了

O(n3×logk)O(n^{3\times}log k)

正文

P4159

这道题和前置的区别就是,这个题的每条边的边权不为1

注意到:

image.png

11 \le边权9\le 9

于是可以拆

拆边还是拆点呢?

众所周知,边的数量\ge点的数量

所以拆点更优

怎么拆?

就是把一条很长的边拆成若干条长度为1的边

像这样:

image.png

为了减少时间复杂度,我们要尽量少拆

image.png

最多延申九个点,加上原来的点,最多十个点,这样就可以避免做许多无用功,可以直接往外延伸

image.png

cin>>n;
    for(int i=1;i<=n;i++)
    {   
        z[i][n+(i-1)*9+1]=1;//链
        for(int j=1;j<9;j++)//n之外点的相互连边
        {
            z[n+(i-1)*9+j][n+(i-1)*9+j+1]=1;
        }
    }

image.png

这是z[i][n+(i-1)*9+1]=1;//链做的工作

image.png

最终建出的图:

graph.png

(距离为9的最后一个节点其实没有必要)

由图可知,这个矩阵长度和宽度是十倍的n

第一步是指1n1\to n

第二步是指n+1n+2n+3n+1 \to n+2 \to n+3 \ldots

假设n=5n=5

加入对141\to 4有一条长度为66的边,那么就是:

graph (1).png

假如是353\to 5长度为9

那么就是:

graph (2).png

对长度为11特判一下,直接连边

对长度为00特判一下,直接continue

假如242 \to 4长度为11

graph (3).png

f[i][j][k]f[i][j][k]DPDP意义实际上就是以jj为起点,走了ii步到达点kk的方案数,只不过这里的起点是11

基于上方矩阵快速幂的思想可以把后面二维看作一个矩阵乘法

也就是要从f0fnf_{0}\to f_n

这样要转移tt次,也就是乘aatt

matrix ans=F*ksm(a,t);

FF就是一个1×n1 \times n的一个矩阵,初始化就把f0.z[1][1]f_0.z[1][1]初始化为11(因为从11号点走00步到11号点的方案数为11,到其它点的方案数为00

所以就写完啦,最后答案为f.z[1][n]

下面奉上AC代码

#include<bits/stdc++.h>
using namespace std;
const int mod=2009;
int nn,t;
struct matrix{
    int n,m;
    int z[185][185];
    matrix()
    {
        n=m=0;
        memset(z,0,sizeof(z));
    }
};
matrix a,F;
matrix operator*(const matrix &a,const matrix &b)
{
    matrix c;
    c.n=a.n;
    c.m=b.m;
    for(int i=1;i<=c.n;i++)
        for(int k=1;k<=a.m;k++)
            for(int j=1;j<=c.m;j++)
            {
                c.z[i][j]+=(a.z[i][k] * b.z[k][j]);
                c.z[i][j]%=mod;
            }
    return c;
}
matrix ksm(matrix x,int y)
{
    if(y==1) return x;
    matrix zz=ksm(x,y/2);
    zz=zz*zz;
    if(y%2==1) zz=zz*x;
    return zz;
}
signed main()
{
    scanf("%d%d",&nn,&t);
    a.n=10*nn;
    a.m=10*nn;
    for(int i=1;i<=nn;i++)//n=5
    {   
        a.z[i][nn+(i-1)*9+1]=1;
        for(int j=1;j<9;j++)//枚举长度//i=2,j=1
        {
            a.z[nn+(i-1)*9+j][nn+(i-1)*9+j+1]=1;
        }
    }
    char c;
    for(int i=1;i<=nn;i++)
    {
        c=getchar();//读取换行
        for(int j=1;j<=nn;j++)//2->5 cc=1
        {
            c=getchar();
            int cc=c-'0';
            if(cc==0)
            {
                continue;
            }
            if(cc==1) 
            {
                a.z[i][j]=1;
                continue;
            }
            a.z[nn+(i-1)*9+cc-1][j]=1;
        }
    }
    // F0.z[1][1]=1;
    // F0.z[1][2-->n]=0;
    // F1.z[1][1-->n]<----->f[1][1-->n]
    F.n=1;
    F.m=nn;
    F.z[1][1]=1;
    matrix ans=F*ksm(a,t);//F_0乘以t次a变成F_t
    printf("%d",ans.z[1][nn]);
    return 0;
}