带标号的DAG计数(N^2做法),无向图三元环四元环计数和带标号二分图计数(O(N)做法)
一、带标号DAG计数:
前置芝士:
DAG:有向无环图
带标号:有序的DAG(1->2->3和1->3->2计算成两种不同的DAG)
容斥原理:
带标号DAG计数 I:
给定N个点,求带标号DAG数量,不强制联通。N <= 5000
假设 f[i] 代表 i 个点的答案
考虑选 j 个点,使他们的入度为0,连向剩余的点,剩余的点任意DAG均可。
这样做是一定不会出现环的,因为新建的点(j个入度为0的点)不会有边连向他们。
但是有一个问题,不能保证剩余的点中没有入度为0的点,于是上容斥原理。
有递推式:
四项分别代表: 容斥系数,在i个中选j个,j个点连向i-j个点连边方案数,i-j个点任意DAG个数
f[N]即为答案
p2p[0]=1,C[0][0]=1;
for(int i=1;i<=N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
}
for(int i=1;i<=N*N;i++) p2p[i]=(p2p[i-1]<<1)%mod;
f[0]=1;
for(int i=1;i<=N;i++){
for(int j=1;j<=i;j++){
ll opt=(j&1)?1:-1;
f[i]=(f[i]+opt*p2p[j*(i-j)]*C[i][j]%mod*f[i-j]%mod))%mod;
}
}
cout<<((ll)f[N]+mod)%mod<<endl;
带标号DAG计数 II:
给定N个点,求带标号DAG数量,要求弱联通。N <= 5000
假设g[i]代表i个点弱联通的答案。
那么只需要用f[i]减去不联通的DAG数量即可。
我们可以考虑任意选一个点T,枚举包含T的联通DAG大小,其余的点不与此DAG联通。
有递推式(这里的j表示的是不与T联通的点数):
这三项分别表示:在除T外的i-1个点中选j个,j个点任意DAG,i-j个点任意联通DAG个数
这样做是不会算重也不会少算的,因为任意一个不连通的DAG一定包含两部分:和T联通的DAG和剩余的。
当这两个其中至少一个不同DAG就会不同。
g[N]即为答案
C[0][0]=1,p2p[0]=1,f[0]=1;
for(int i=1;i<=N;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
}
}
for(int i=1;i<=N*N;i++) p2p[i]=(p2p[i-1]<<1)%mod;
for(int i=1;i<=N;i++){
for(int j=1;j<=i;j++){
ll opt=(j&1)?1:-1;
f[i]=(f[i]+opt*C[i][j]*p2p[j*(i-j)]%mod*f[i-j]%mod)%mod;
}
}
for(int i=1;i<=N;i++){
g[i]=f[i];
for(int j=1;j<i;j++){
g[i]=(g[i]-(ll)C[i-1][j]*f[j]%mod*g[i-j]%mod)%mod;
}
}
cout<<(g[N]+mod)%mod<<endl;
在以上的基础上使用多项式求逆可以达到nlogn的复杂度,这里先不提。
二、无向图三元环四元环计数:
给定一个无向图,求三元环数量。 N <= 100000 M <= 200000
无向图三元环计数(根号分治):
先制定一个点之间比较的规则,然后将无向图改为有向图,将无向边改成由小的点指向大的点的有向边。
这样做是显然不会出现环的。
那么新图的三元环(i,j,k)一定就是这样:
那我们就枚举每一个点u,先把所有能直接到的点v打上标记,再枚举这些点v出边到达的点看是否有点u的标记即可。
很明显枚举(u,v)时每条边只会被枚举一次,时间复杂度就是(d[i]代表i的出度):
考虑改变比较规则来优化时间:
- 当两个点的度数不同时,比较两个点的度数。
- 当两个点的度数相同时,比较两个点的编号。
当一个点在原图出度小于时,显然新图出度不会变大,时间。
当一个点在原图出度大于时,由于比较的规则,这个点在新图只会连向出度也大于的点,因为出度大于的点最多只有个,所以时间最多。
综上,时间复杂度为O()。
struct chr{
int a,b;
}e[maxn];
bool cmp(int a,int b){
return d[a]==d[b]?a<b:d[a]<d[b];
}
int MAIN(){
cin>>N>>M;
for(int i=1;i<=M;i++){
int a,b;
scanf("%d%d",&a,&b);
chr now=(chr){a,b};
d[a]++,d[b]++;
e[i]=now;
}
for(int i=1;i<=M;i++){
if(cmp(e[i].a,e[i].b)) add(e[i].a,e[i].b);
else add(e[i].b,e[i].a);
}
int ans=0;
for(int i=1;i<=N;i++){
for(auto j:ed[i]) vis[j]=i;
for(auto j:ed[i]){
for(auto k:ed[j]){
ans+=vis[k]==i;
}
}
}
cout<<ans<<endl;
return 0;
}
无向图三元环计数(bitset):
这种做法就是用bitset优化暴力。
给每一个点开一个bitset,代表这个点能直接到达的点。
直接枚举每条边,将两个端点按位与,1的个数就是这条边所在三元环的个数,加到ans上即可。
由于每个环都被加了3次,所以最后ans要除以3。
时间O() 空间O()
有些题比较卡空间,可以类似分块,设置一个阈值B。
度数大于B的就预处理出完整的bitset,小于的可以在计算时算出。
大于B的点不会超过个。
时间O() 空间O()
bitset<maxn>b[640],p,q;
int MAIN(){
cin>>N>>M;
for(int i=1;i<=M;i++){
int a,b;
scanf("%d%d",&a,&b);
d[a]++,d[b]++;
add(a,b),add(b,a);
}
int sqt=sqrt(M<<1);
int cnt=0;
for(int i=1;i<=N;i++){
if(d[i]>sqt){
tag[i]=++cnt;
for(auto c:ed[i]) b[cnt][c]=1;
}
}
int ans=0;
for(int i=1;i<=N;i++){
bitset<maxn> *A,*B;
if(tag[i]) A=&b[tag[i]];
else{
p.reset();
for(auto l:ed[i]) p[l]=1;
A=&p;
}
for(auto j:ed[i]){
if(tag[j]) B=&b[tag[j]];
else{
q.reset();
for(auto l:ed[j]) q[l]=1;
B=&q;
}
ans+=(*A&*B).count();
}
}
cout<<ans/6<<endl;//这里除以6是因为我每个边都算了两遍
return 0;
}
无向图四元环计数:
给定一个无向图,求四元环数量。 N <= 100000 M <= 200000
类比三元环的根号分治,将无向图改为有向无环图。
四元环在新图中有这几种表现形式:
容易发现一定会有一个这样的结构:
余下的两条边方向任意。
于是我们枚举一个点u,作为这个结构对面的点,之后枚举原图所有能到达的点v,再枚举v在新图能到达的点w,每次将ans加上cnt[w],再让cnt[w]++。
相当于两半拼成一个完整的环。
但是有一个问题,就是上面的第三种情况会算重,所以仅当w>u时才可以进行。
时间复杂度同三元环 O()。
bool cmp(int a,int b){
return d[a]==d[b]?a<b:d[a]<d[b];
}
int MAIN(){
cin>>N>>M;
for(int i=1;i<=M;i++){
int a,b;
scanf("%d%d",&a,&b);
d[a]++,d[b]++;
add(a,b),add(b,a);
}
for(int i=1;i<=N;i++){
for(auto j:ed[i]){
if(cmp(i,j)) addg(i,j);
}
}
int ans=0;
for(int i=1;i<=N;i++){
for(auto j:ed[i]){
for(auto k:edg[j]){
if(cmp(i,k)) ans+=cnt[k]++;
}
}
for(auto j:ed[i]){
for(auto k:edg[j]){
if(cmp(i,k)) cnt[k]=0;
}
}
}
cout<<ans<<endl;
return 0;
}
三、带标号二分图计数:
给定点数N,求带标号二分图个数。 N <= 1000000
这里的二分图染成了两种颜色,边不同或者点的颜色不同都算不同。
枚举黑色点的个数,再任意将黑白点之间连边即可。
于是就有:
int qp(int x,ll y){
int ans=1;
while(y){
if(y&1) ans=((ll)ans*x)%mod;
y>>=1;
x=((ll)x*x)%mod;
}
return ans;
}
int C(int a,int b){
return (ll)jc[a]*jcny[b]%mod*jcny[a-b]%mod;
}
int MAIN(){
cin>>N;
jc[0]=1,jcny[0]=1;
for(int i=1;i<=N;i++) jc[i]=jc[i-1]*(ll)i%mod;
jcny[N]=qp(jc[N],mod-2);
for(int i=N-1;i;--i) jcny[i]=(ll)jcny[i+1]*(i+1)%mod;
int ans=0;
for(int i=0;i<=N;i++){
ans=(ans+(ll)C(N,i)*qp(2,(ll)i*(N-i))%mod)%mod;
}
cout<<ans<<endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!