1. 程式人生 > 其它 >巨神兵

巨神兵

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]);
}