刷题记录[SDOI2009]HH去散步(动态规划、矩阵快速乘、点边互换思想)
链接https://www.luogu.org/problemnew/show/P2151
题目描述
HH有个一成不变的习惯,喜欢饭后百步走。所谓百步走,就是散步,就是在一定的时间 内,走过一定的距离。 但是同时HH又是个喜欢变化的人,所以他不会立刻沿着刚刚走来的路走回。 又因为HH是个喜欢变化的人,所以他每天走过的路径都不完全一样,他想知道他究竟有多 少种散步的方法。
现在给你学校的地图(假设每条路的长度都是一样的都是1),问长度为t,从给定地 点A走到给定地点B共有多少条符合条件的路径
输入输出格式
输入格式:
第一行:五个整数N,M,t,A,B。其中N表示学校里的路口的个数,M表示学校里的 路的条数,t表示HH想要散步的距离,A表示散步的出发点,而B则表示散步的终点。
接下来M行,每行一组Ai,Bi,表示从路口Ai到路口Bi有一条路。数据保证Ai != Bi,但 不保证任意两个路口之间至多只有一条路相连接。 路口编号从0到N − 1。 同一行内所有数据均由一个空格隔开,行首行尾没有多余空格。没有多余空行。 答案模45989。
输出格式:
一行,表示答案。
说明
对于30%的数据,N ≤ 4,M ≤ 10,t ≤ 10。
对于100%的数据,N ≤ 50,M ≤ 60,t ≤ 2^30,0 ≤ A,B
关于这道题,首先最容易想到一个线性代数知识点:
若使用一个矩阵表示图上两点的有一条连边,则这个矩阵的N次方中的一个元素表示从点i经过N步到达点j的方案数
可以很容易通过数学归纳法证明
这个方法的优势在与由于矩阵乘法具有结合律,可以使用二分法优化
但是这题有一个特殊的条件:不会立刻沿着刚刚回来的路走回
这导致传统的方法失效了
为了解决这个问题,我们采用点边互换的思想,即将无向边变成两个方向相反的有向边,并对这些有向边进行编号,并当作点处理
显然,两点之间联通的条件为两个点在原图中所对应的两条有向边满足一个边的出发点是另一个边的终点,并且两条边不是来源于同一条无向边。
如何判断是否来自同一条无向边呢?
拆分无向边为两条有向边时我们采用相邻编号,且编号从2开始。则当任意一个编号异或1等于另一个编号时则这两个编号来自同一个无向边。
这样处理之后,我们得到了一个新的矩阵。
在原图中,由于经过m个节点的路径长度为m+1,因此在新的矩阵中我们只需求新矩阵的t-1次幂就能得到答案。接下来有两种做法:
第一种:建立一个新矩阵,设立两个虚点(相当于原图中的虚边)使两个虚点分别与所有从出发点出发的边、所有到终点结束的边联通,用这个矩阵与刚才的矩阵相乘,统计两个虚点之间的方案数输出
第二种:直接统计所有从出发点出发的边、所有到终点结束的边之间的方案数求和输出
我采用了后一种方案
时间复杂度为O()
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
#define mod 45989
#define ll long long
int n,m,t,a,b;
#define N 200
#define next nico
int head[N],to[N],next[N],from[N],tot=1;
void add(int a,int b)
{
to[++tot]=b;
from[tot]=a;
next[tot]=head[a];
head[a]=tot;
}
struct mat
{
int a[130][130];
mat operator *(const mat b)const
{
mat ans;
for(int i = 1 ; i <= tot ; i ++)
for(int j = 1 ; j <= tot ; j ++)
ans.a[i][j]=0;
for(int i = 1 ; i <= tot ; i ++)
for(int j = 1 ; j <= tot ; j ++)
{
for(int k = 1 ; k <= tot ; k ++)
{
ans.a[i][j]=(((ll)a[i][k]*b.a[k][j])%mod+ans.a[i][j])%mod;
}
}
return ans;
}
}buf,answer;
mat pow(mat a,int n)
{
if(n==1)return a;
mat ans = pow(a,n/2);
ans=ans*ans;
if(n%2==1)
ans=ans*a;
return ans;
}
void build()
{
for(int i = 2; i <= tot; i ++)
for(int j = 2 ;j <= tot; j ++)
if(j!=(i^1)&&from[i]==to[j])
{
buf.a[i][j]=1;
}
}
int main()
{
scanf("%d%d%d%d%d",&n,&m,&t,&a,&b);
for(int i = 1; i <= m ; i ++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x+1,y+1);add(y+1,x+1);
}
build();
a++;b++;
answer=pow(buf,t-1);
int ans = 0;
for(int i = 2; i <= tot ; i ++ )
for(int j = 2; j <= tot ; j ++ )
if(from[i]==a&&to[j]==b)
ans=(ans+answer.a[j][i])%mod;
printf("%d",ans);
}