[NOI2021] 路径交点
没去 \(\tt NOI\) 现场的菜鸡\(+\)新高二退役狗来写一波题解。
一、题目
二、解法
前置知识:行列式,矩阵乘法,高斯消元,比内\(-\)柯西公式。
你看这题真是奇怪得不行,我以前做过这种题吗?没关系,我们来搞一些 \(\tt observation\):
- 选取的路径数正好是 \(n_1\),它并不是一个任意的数,而是求的一个几乎选满的方案,如果你做过这题就会知道这种特殊的组合问题可能会适用比内\(-\)柯西公式。
- 本题求的方案是偶数交点的方案减去奇数交点的方案,那么一种方案的贡献是 \((-1)^{x}\),再观察交点的定义式 \((p_j-q_j)\times(p_{j+1}-q_{j+1})<0\),明显是逆序对个数啊,那么可以自然的联想到行列式。
那么我们就从行列式的角度来解决这道题吧,首先考虑 \(k=2\) 的情况,设 \(p_i\) 表示 \(X\) 部第 \(i\) 个点匹配的 \(Y\) 部点是 \(p_i\),设 \(f(p)\) 表示排列 \(p\) 的逆序对个数,设 \(g_{i,j}\) 表示 \((i,j)\) 是有边,我们通过枚举 \(p\) 可以暴力写出答案式:
\[\sum_{p}(-1)^{f(p)}\prod_{i=1}^{n_1} g_{i,p_i}
\]
那么不难发现就是矩阵 \(g_{i,j}\) 的行列式,因为我们已经会求二分图的情况,那么分层图的情况我们考虑把每一层一个一个合并上去,归约到二分图的情况,看图:
所以比内\(-\)柯西公式帮助我们完成了从中间这一层 \(m\) 个点中选出 \(n\) 个的任务,选出点后两边可以分别匹配因为满足乘法原理,行列式也可以直接相乘。那么合并操作就相当于把邻接矩阵做矩阵乘法,那么算法流程是我们把每一层的邻接矩阵暴力乘起来,最后对 \(n\times n\) 的矩阵高斯消元,所得行列式即是答案,时间复杂度 \(O(n^4)\)
三、总结
某位巨佬说:像这种偶数减奇数的题正好符合行列式模型,所以往那方面想。
本题讨论二部图的行列式求法个人觉得很难,但和 \(dp\) 做法一样,都是从枚举到优化的过程,但你不去在思想上枚举是想不通的。分层图问题转化为二部图问题用到了归约的思想,值得借鉴!
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 205;
const int MOD = 998244353;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int T,n[M],m[M],k;
struct mat
{
int n,m,a[M][M];
void clear() {n=m=0;memset(a,0,sizeof a);}
mat() {clear();}
mat operator * (const mat &b) const
{
mat r;
r.n=n;r.m=b.m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=1;k<=b.m;k++)
r.a[i][k]=(r.a[i][k]+a[i][j]*b.a[j][k])%MOD;
return r;
}
}A,B;
int qkpow(int a,int b)
{
int r=1;
while(b>0)
{
if(b&1) r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
int gauss(int n,int a[][M])
{
int ans=1;
for(int i=1;i<=n;i++)
{
for(int j=i;j<=n;j++)
if(!a[i][i] && a[j][i])
{
swap(a[i],a[j]);
ans*=-1;
break;
}
ans=ans*a[i][i]%MOD;
for(int j=1;j<=n;j++)
{
if(i==j || !a[j][i]) continue;
int t=a[j][i]*qkpow(a[i][i],MOD-2)%MOD;
for(int k=1;k<=n;k++)
a[j][k]=(a[j][k]-a[i][k]*t)%MOD;
}
}
return (ans%MOD+MOD)%MOD;
}
void work()
{
k=read();
for(int i=1;i<=k;i++) n[i]=read();
for(int i=1;i<k;i++) m[i]=read();
A.clear();A.n=n[1];A.m=n[2];
for(int i=1;i<=m[1];i++)
{
int u=read(),v=read();
A.a[u][v]=1;
}
for(int t=2;t<k;t++)
{
B.clear();
B.n=n[t];B.m=n[t+1];
for(int i=1;i<=m[t];i++)
{
int u=read(),v=read();
B.a[u][v]=1;
}
A=A*B;
}
printf("%lld\n",gauss(n[1],A.a));
}
signed main()
{
T=read();
while(T--) work();
}