神秘题目6

神秘题目

题目描述

​ 给定一张 \(n\) 个点,\(m\) 条边的有向图,无重边,无自环。

​ 给定 \(q\) 次询问,每次给出 \(s,t,d\) ,求有多少条起点为 \(s\) 终点为 \(t\) 且中途不经过 \(s\)\(t\) 的路径,答案对 \(1e9+7\) 取模。

​ 注意路径不要求是简单路径,也就是说可以多次经过同⼀个点、同⼀条边。

输入格式

第一行两个整数 \(n,m\)

接下来 \(m\) 行,每行两个整数 \(u,v\) 表示一条 \(u\)\(v\) 的有向边。

接下来一行一个整数 \(q\) 表示询问次数。

接下来 \(q\) 行,每行 \(3\) 个整数 \(s,t,d\) 表示一次询问。

输出格式

\(q\) 行,每行一个整数表示答案。

样例输入

7 15 123456
1 4
3 5
2 6
6 7
2 4
7 1
3 6
4 2
4 5
6 5
6 3
3 4
1 2
3 2
6 2
5
1 2 3
4 7 8
3 6 2
1 7 4
3 5 40

样例输出

0
4
1
1
12512

数据范围与约定

对于 \(30\%\) 的数据 \(n\le 10\)

对于 \(50\%\) 的数据 \(n\le20\)

对于 \(100\%\) 的数据 \(n\le100\),\(0\le m\le n\times(n-1)\),\(1\le d\le50\),\(q\le5\times10^5\)

时间限制:\(3s\)

空间限制:\(256MB\)

题解

30分sb暴力

路径可以转化成从 \(s\) 走 1 步到其他节点,中间xjb走 \(d-2\) 步,最后走 1 步到 \(t\)

中间这 \(d-2\) 步可以用矩阵快速幂加速递推,即 \(F\times A^{d-2}\)

其中 \(F\) 是一个 \(1\) 维向量,第 \(i\) 个元素表示走到 \(i\) 的方案数,\(A\) 是去掉 \(s,t\) 的邻接矩阵。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int P=1000000007;
bool ks;
int n,m,q;
int ANS[110][110][60];
bool js;
struct MATRIX
{
	LL data[130][130];
	void one(){for(int i=n;i>=0;i--)data[i][i]=1;}
	void clear(){memset(data,0,sizeof data);}
	MATRIX operator *(const MATRIX &n2){
		MATRIX TEMP;
		for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){
			TEMP.data[i][j]=0;
			for(int k=1;k<=n;k++)
				(TEMP.data[i][j]+=data[i][k]*n2.data[k][j])%=P;
		}
		return TEMP;
	}
	MATRIX ksm(int K){
		MATRIX Be=*this,As;
		As.clear();
		if(K<0)return As;
		As.one();
		for(;K;K>>=1,Be=Be*Be)
		if(K&1)As=As*Be;
		return As;
	}
}A,B,C;
inline int read()
{
	int x=0,w=0;char ch=0;
	while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return w?-x:x;
}
int calc(int S,int T,int D)
{
	if(ANS[S][T][D]!=-1)return ANS[S][T][D];
	if(D==0)return 0;
	if(D==1)return A.data[S][T];
	B.clear();
	for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
	if(i^S)if(i^T)if(j^S)if(j^T)
		B.data[i][j]=A.data[i][j];
	for(int i=1;i<=n;i++)
	if(i^S)if(i^T)
		C.data[1][i]=A.data[S][i];
	C=C*(B.ksm(D-2));
	int temp=0;
	for(int i=1;i<=n;i++)
	if(i^S)if(i^T)
		temp=(temp+C.data[1][i]*A.data[i][T])%P;
	return ANS[S][T][D]=temp;
}
int main()
{
	memset(ANS,-1,sizeof ANS);
	n=read();m=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read();
		A.data[x][y]=1;
	}
	q=read();
	while(q --> 0){
		int S=read(),T=read(),D=read();
		printf("%d\n",calc(S,T,D));
	}
}

50分暴力

矩阵加速个锤子啊,d就50

而且向量乘矩阵明明是 \(n^2\) 的,非得写成 \(n^3\) 的矩阵乘矩阵。

注意到,\(d\) 只有 50。可以考虑预处理出所有答案,然后 \(O(1)\) 回答。

考虑递推,设 \(f[x][y][i]\) 表示从 \(x\)\(i\) 步走到 \(y\) 中途不经过 \(x,y\) 的方案数。

转移很简单,枚举下一个节点即可。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
bool ks;
const int P=1000000007;
int n,m,q,road[110][110];
LL ANS[110][110][60],f[110][60];
bool js;
inline int read()
{
	int x=0,w=0;char ch=0;
	while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return w?-x:x;
}
void calc(int S,int T,int D)
{
	ANS[S][T][0]=ANS[S][T][2]=0;
	ANS[S][T][1]=road[S][T];
	memset(f,0,sizeof f);
	for(int i=1;i<=n;i++)
		f[i][1]=road[S][i];
	for(int t=1;t<=50;t++)
	for(int i=1;i<=n;i++)
	if(i!=T&&i!=S)
	for(int j=1;j<=n;j++)
	if(j!=T&&j!=S&&road[i][j])
		(f[j][t+1]+=f[i][t])%=P;
	for(int t=2;t<=50;t++)
	for(int i=1;i<=n;i++)
	if(i!=T&&i!=S&&road[i][T])
		(ANS[S][T][t]+=f[i][t-1])%=P;
}
void prepare()
{
	for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
		calc(i,j,50);
}
int main()
{
	freopen("road.in","r",stdin);
	freopen("road.out","w",stdout);
	n=read();m=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read();
		road[x][y]=1;
	}
	prepare();
	q=read();
	while(q --> 0){
		int S=read(),T=read(),D=read();
		if(S==T&&!D)printf("1\n");
		else printf("%lld\n",ANS[S][T][D]);
	}
}

满分做法

考虑总方案数-不合法方案数。

\(f[x][y][i]\) 表示从 \(x\)\(i\) 步走到 \(y\) 中途不经过 \(x,y\) 的方案数。

\(g[x][y][i]\) 表示从 \(x\)\(i\) 步走到 \(y\) 的方案数,中途没限制。

\(h[x][y][i]\) 表示从 \(x\)\(i\) 步走到 \(x\) 中途不经过 \(y\) 的方案数。

首先 \(O(n^4)\) 求出 \(g\) ,很简单,对于一个状态 \(g[x][y][i]\) 枚举 \(y\) 的下一个节点更新即可。

然后大力分类讨论

考虑用 \(g\) 减去不合法的方案求出 \(f,h\)

\(f\)

  • 如果中间经过的第一个不能经过的节点是 \(y\): x --> y --> y

    • 注意,一条路径中途可能多次会经过 \(y\) ,我们只在第一次经过时统计,这样能避免重复。
    • 观察这条路径, y 将其分成了 x->y 和 y->y 两部分,前面一部分就是 \(f\) 了,后面是 \(g\),枚举前面一部分的长度更新 \(f\) 即可。
  • 如果中间经过的第一个不能经过的节点是 \(x\): x --> x --> y

  • 很显然,前一部分是 \(h\) ,后一部分是 \(g\) ,枚举长度更新即可。

\(g\)

  • 路径只有一种,即 x-->y-->x
  • 前面是 \(f\),后面是 \(g\) 。更新方法和上面一模一样。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const mod=1000000007;
bool ks;
int n,m,q,road[110][110];
LL f[110][110][60],g[110][110][60],h[110][110][60];
bool js;
inline int read()
{
	int x=0,w=0;char ch=0;
	while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return w?-x:x;
}
void prepare()
{
	for(int i=1;i<=n;i++)
	g[i][i][0]=1;
	for(int len=0;len<=50;len++)
	for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
	for(int k=1;k<=n;k++)
		(g[i][k][len+1]+=g[i][j][len]*road[j][k])%=mod;
	for(int len=1;len<=50;len++){
		for(int x=1;x<=n;x++)
		for(int y=1;y<=n;y++){
			f[x][y][len]=g[x][y][len];
			for(int i=1;i<len;i++)
				(f[x][y][len]-=f[x][y][i]*g[y][y][len-i])%=mod;
			if(x==y)continue;
			for(int i=1;i<len;i++)
				(f[x][y][len]-=h[x][y][i]*g[x][y][len-i])%=mod;
		}
		for(int x=1;x<=n;x++)
		for(int y=1;y<=n;y++){
			h[x][y][len]=g[x][x][len];
			for(int i=1;i<len;i++)
				(h[x][y][len]-=f[x][y][i]*g[y][x][len-i])%=mod;
			for(int i=1;i<len;i++)
				(h[x][y][len]-=h[x][y][i]*g[x][x][len-i])%=mod;
		}
	}
}
int main()
{
	n=read();m=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read();
		road[x][y]=1;
	}
	prepare();
	q=read();
	while(q --> 0){
		int S=read(),T=read(),D=read();
		printf("%lld\n",(f[S][T][D]%mod+mod)%mod);
	}
}
posted @ 2021-03-26 16:13  zYzYzYzYz  阅读(122)  评论(0编辑  收藏  举报