[机房测试]巨神兵

Description

求一个 \(n\leq 17\) 个点 \(m\) 条边的有向图有多少个边的子集不包含环。

Solution

边很多,只能对点状压。考虑一个 DAG 可以被怎么构造。假设 \(S\) 已经是一个拓扑图,那么加上一点集 \(T\),如果只从 \(S\)\(T\) 连边,那么构造出的图一定也还是拓扑图。所以就可以有转移

\[f_{S|T} \gets f_S \times 2^{cnt} \]

\(cnt\) 表示 \(S\)\(T\) 的有向边个数。但是这样会算重。我们考虑一个特定方案被计算了几次。

例如,左边的方案可以从右边三种状态分别转移一次。扩展到更多的点,容易发现会被重复计算 \(2^{c}-1\) 次。\(c\) 是该种特定方案最下面一层的点(没有出度的点)的个数。考虑容斥掉,我们只需要保留一次贡献。容易发现这个 \(2^{c}\) 其实是二项式定理得来的,对于所有有 \(k\) 个出度为零的点在 \(S\) 里面的方案,总共就会产生 \(\binom{c}{k}\) 的贡献,而出度为零的点不可能全部在 \(S\) 里面,所以总共只会有 \(2^c-1\) 次。所以只需要对每个 \(T\)\(S|T\) 的贡献乘上 \((-1)^{|T|+1}\) 的容斥系数,就可以把中间的二项式全部抵掉。

#include<stdio.h>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;

inline int read(){
    int x=0,flag=1; char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')flag=0;c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+c-48;c=getchar();}
    return flag? x:-x;
}

const int N=17;
const int Mod=1e9+7;

int mp[N][N],g[1<<N],in[1<<N],op[1<<N],pw[N*N],f[1<<N];

int main(){
    freopen("obelisk.in","r",stdin);
    freopen("obelisk.out","w",stdout);
    int n=read(),m=read(); pw[0]=f[0]=1;
    for(int i=1;i<=m;i++){
        int u=read(),v=read();
        mp[u-1][v-1]=1,pw[i]=2ll*pw[i-1];
    }
    int rg=(1<<n); op[0]=-1;
    for(int i=1;i<rg;i++) op[i]=-op[i-(-i&i)];
    for(int S=0;S<rg-1;S++){
        for(int i=0;i<n;i++) in[1<<i]=0;
        for(int i=0;i<n;i++){
            if(!((S>>i)&1)) continue;
            for(int j=0;j<n;j++) in[1<<j]+=mp[i][j];
        }
        const int U=rg-S-1; g[0]=0;
        for(int s=U&(U-1);;s=U&(s-1)){
            const int now=U^s;
            g[now]=g[now-(-now&now)]+in[-now&now];
            f[now^S]=(f[now^S]+1ll*f[S]*pw[g[now]]*op[now]%Mod+Mod)%Mod;
            if(!s) break;
        }
    }
    printf("%d",f[rg-1]);
}
posted @ 2021-09-13 17:37  Kreap  阅读(48)  评论(0编辑  收藏  举报