题意:
有 \(n\) 个人排成一排,每个点 \(i\) 最多会给出一条限制,形如 \((i,j)\) 表示点 \(i\) 必须站在 \(j\) 的左侧。问有多少种成立的方案数,答案对输入的模数 \(p\) 取模。
对于\(100\%\) 的数:\(n≤2\times 10^5,m≤2\times 10^5,m≤n,n+10≤p≤10^9+7,T≤10\).
题解:
给个不一样的做法,应该好写一点。
我们考虑限制构成什么,先用限制构造边 \(i\to j\),发现如果所有 \(i\) 都有出边那么这玩意会构成外向基环树森林,但是我们会发现如果有环,那一定无解,所以在有解的情况下会构成一棵树。
现在再思考限制的本质,对于 \(i\to j\), 这是一个经典的问题,等价于满足构成的树上 \(i\) 的拓扑序小于 \(j\)。
对于这个经典的有根树版本问题我们有一个经典的结论:
其中 \(siz_i\) 为 \(i\) 的子树大小。考虑怎么把我们的限制构造成有根树,发现我们把 \(i\to j\) 都换成 \(j\to i\) 答案肯定是不变的,把题意理解成 \(i\) 在 \(j\) 的右侧即可。这样一定是有根树。
证明:
考虑一般情况,若发现如果所有 \(i\) 都有出边那么这玩意会构成内向基环树森林。考虑删掉环的一些部分使得有答案,那么你一定可以找到一个根在环上一点。
那么直接做就可以了。时间复杂度 \(O(Tn)\).
细节:预处理逆元可以做到线性,这里还有一个性质值得注意,数据范围里写了 \(n+10\le p\),这使得我们可以直接处理逆元,因为如果 \(n\ge p\) 的话我们就要用 \(lucas\) 定理了。
code:
#include<bits/stdc++.h>
#define pb push_back
#define int long long
#define m(a) memset(a,0,sizeof(a))
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
using namespace std;
const int N=2e5+10;
int n,m,k,T,siz[N],vis[N],cnt,tg,mod,ni[N],ans;
vector<int> g[N];
int ksm(int x,int k){int ans=1;while(k){if(k&1) ans=ans*x%mod;x=x*x%mod;k>>=1;}return ans;
}
void dfs(int u){vis[u]=cnt;siz[u]=1;for(int v:g[u]){if(vis[v]==cnt){tg=1;break; }else if(!vis[v]) dfs(v);siz[u]+=siz[v];}
}
void init(){rep(i,1,n) g[i].clear();tg=0;cnt=0;m(siz);m(vis);ni[1]=1;ans=1;rep(i,2,n) ni[i]=(-mod/i*ni[mod%i]%mod+mod)%mod;
}
signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);cin>>T;while(T--){cin>>n>>m>>mod;init();rep(i,1,m){int x,y;cin>>x>>y;g[y].pb(x);}rep(i,1,n){if(!vis[i]) cnt++,dfs(i);}if(tg==1){cout<<0<<'\n';continue;}rep(i,1,n){ans=ans*i%mod;ans=ans*ni[siz[i]]%mod;}cout<<ans<<'\n';}return 0;
}
官方解法:
算法1
对于 15%的数据 我们可以直接枚举所有置换,再进行判断即可,时间复杂度$ O(T*n!)$
算法2
对于 50%的数据 定义$ f[S]$表示已经选了元素集合为 \(S\) 的合法方案数是多少, 转移方程为 \(f[S]=∑ 𝑓[𝑆 − 2^x ]\),满足 x 是 S 中的元素且所有限制 均不矛盾,可以先预处理每个位置不合法状态的集合,这样 时间复杂度 \(O(n T *2^n)\),如果你写成 \(O(n*n*T*2^n)\),那么 恭喜你,你只有 30(常数小的除外)分了。
算法3
对于余下所有数据, 我们需要观察出一些性质。
如果我们对于一条限制 \((x, y)\), 建一条 \(x->y\) 的边, 就会发 现在这张图中每个点的出边最多只有一条。
如果在这张图中有环, 则答案一定为 0 。
如果这张图无环, 则这些边一定构成一个森林。
所以我们可以跑树形 dp。
定义 \(f[i]\) 表示第 \(i\) 棵子树的方案数是多少。
转移时, 相当于有很多的儿子 \(s_{1}, s_{2} \ldots s_{n}\) (设每个儿子的子树 大小为 size), 相当于对于一个序列中, 先选 1 个位置给 \(s_{1}\),
再在余下的位置中, 选 2 个位置给 \(s_{2} \ldots\), 相当于一些组合数的 乘积再乘以每个儿子内部的方案数。
算法4
对于 \(70 \%\) 的数据
我们可以利用组合恒等式 \(c_{x}^{y}=c_{x-1}^{y}+C_{x-1}^{y-1}\) 预处理组合数, 或是预 处理逆元暴力求组合数, 复杂度均为 \(O\left(T^{*} n * n\right)[m\) 一定小 于等于 \(n\), 所以 \(m\) 近似为 \(n\) ]
算法5
对于 \(100 \%\) 的数据
我们可以先预处理阶乘, 阶乘的逆元, 然后组合数就可以 \(\mathrm{O}(1)\) 了, 总复杂度为 \(O\left(T^{*}(n+m)\right)\)