1. 程式人生 > >[bzoj]noip十連測歐貝里斯克的巨神兵(obelisk)——dag圖DP,狀態壓縮

[bzoj]noip十連測歐貝里斯克的巨神兵(obelisk)——dag圖DP,狀態壓縮

題目大意:

有一張n個點,m條邊的有向圖,有多少個子圖(選定一個邊集)是沒有環的。答案對1e9+7取模。(n17)

思路:

話說這個題目作為NOIP的模擬題有點過了,畢竟n17的方法不是太好想(一定我是太菜了)。
先考慮n10的方法,構造一個dag圖,一個好的方法就是可以根據拓撲序來分層,一個圖的拓撲序一定是唯一的,所以我們可以一層一層地新增點集進去。將一開始入度為0的點放第一層,將這些點去掉,剩下出現的新的入度為0的點為第二層…以此類推,用f[i][j]表示總共選的點集和最後一層的點集就好了。注意轉移的時候要滿足新一層的拓撲序要全部都在原來的後面,即一定要滿足有邊連向上一層的點。

/*=========================
 * Author : ylsoi
 * Problem : obelisk
 * Algorithm : DP
 * Time : 2018.5.23
 * =======================*/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<climits>
#include<vector>
using namespace std; void File(){ freopen("obelisk.in","r",stdin); freopen("obelisk.out","w",stdout); } template<typename T>bool chkmax(T &_,T __){return _<__ ? (_=__,1) : 0;} template<typename T>bool chkmin(T &_,T __){return _>__ ? (_=__,1) : 0;} #define REP(i,a,b) for(register int i=a;i<=b;++i)
#define DREP(i,a,b) for(register int i=a;i>=b;--i) #define MREP(i,x) for(register int i=beg[x];i;i=E[i].last) #define mem(a) memset(a,0,sizeof(a)) #define ll long long #define inf INT_MAX const int maxn=17+10; const ll mod=1e9+7; int n,m; vector<int>from[maxn]; ll dp[(1<<10)+10][(1<<10)+10],all,ans; ll qpow(ll x,int b){ ll base=x,ret=1ll; while(b){ if(b&1)ret=ret*base%mod; base=base*base%mod; b>>=1; } return ret; } void add(ll &_,ll __){_=((_+__)%mod+mod)%mod;} void mul(ll &_,ll __){_=(_*__%mod+mod)%mod;} bool in(int x,int S){return (1<<(x-1))&S;} void out(int x){ if(!x)return; out(x/2); printf("%d",x%2); } int main(){ File(); scanf("%d%d",&n,&m); REP(i,1,m){ int u,v; scanf("%d%d",&u,&v); from[v].push_back(u); } all=(1<<n)-1; REP(i,1,all)dp[i][i]=1ll; REP(i,1,all){ for(int j=i;j;j=(j-1)&i){ if(!dp[i][j])continue; if(i==all)add(ans,dp[i][j]); int left=all-i; for(int k=left;k;k=(k-1)&left){ bool flag=true; ll mul1=1ll,mul2=1ll; REP(v,1,n){ if(!in(v,k))continue; bool flag1=false; int size=from[v].size()-1,cnt1=0,cnt2=0; REP(p,0,size){ int u=from[v][p]; if(in(u,i) && !in(u,j))++cnt1; if(in(u,j)){flag1=true;++cnt2;} } if(!flag1){ flag=false; break; } mul(mul1,qpow(2,cnt1)); mul(mul2,qpow(2,cnt2)-1); } if(!flag)continue; add(dp[i|k][k],dp[i][j]*mul1%mod*mul2%mod); } } } printf("%lld\n",ans); return 0; }

好像這個方法就已經很不錯了,但是並不滿足我們的需要,考慮怎麼把第二維給去掉。設f[i]表示選擇了點集i的方案總數,自然是要一層一層地轉移(要不然就會出錯),但是依然按照拓撲序來分層地話就會難以實現,因為沒有記錄上一層狀態的緣故,新新增的點不一定是拓撲序中最後面的。那麼我們便按照出度為0的規則來新增點集,即出度為0的點是在新新增的這一層的。
設集合i為當前的狀態,列舉一個i的補集的子集j,兩個集合的並集i|j中,有前面定義的最後一層k,因為j是剛剛新增進去的且只有從後面來的邊,所以必有jk。對於i|j這個集合的答案,一定是等於所有的k的狀態的和,顯然地,如果用這種方法計數,每一個k都會被新增進去多次,就是每一個k的子集j都會代表k一次,所以我們在相加的時候多乘以一個容斥係數(1)size(j)+1即可,這樣就可以保證每一個k都只會被計算一次。
時間複雜度O(3nm),好像還是不夠快,再優化我就不會了。。。

/*=========================
 * Author : ylsoi
 * Problem : obelisk
 * Algorithm : dp
 * Time : 2018.5.23
 * ======================*/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<climits>
#include<vector>
using namespace std;
void File(){
    freopen("obelisk_better.in","r",stdin);
    freopen("obelisk_better.out","w",stdout);
}
#define REP(i,a,b) for(register int i=a;i<=b;++i)
#define DREP(i,a,b) for(register int i=a;i>=b;--i)
#define MREP(i,x) for(register int i=beg[x];i;i=E[i].last)
#define mem(a) memset(a,0,sizeof(a))
#define ll long long
#define inf INT_MAX
const int maxn=17+5;
const ll mod=1e9+7;
int n,m,all,c[(1<<17)+10][maxn];
bool dis[maxn][maxn];
ll dp[(1<<17)+10];
void add(ll &_,ll __){_=((_+__)%mod+mod)%mod;}
void mul(ll &_,ll __){_=(_*__%mod+mod)%mod;}
ll qpow(ll x,int b){
    ll base=x,ret=1ll;
    while(b){
        if(b&1)mul(ret,base);
        mul(base,base);
        b>>=1;
    }
    return ret;
}
bool in(int x,int S){return (1<<(x-1))&S;}
void init(){
    scanf("%d%d",&n,&m);
    all=(1<<n)-1;
    REP(i,1,m){
        int u,v;
        scanf("%d%d",&u,&v);
        dis[u][v]=1;
    }
    REP(i,1,all){
        REP(v,1,n){
            if(!in(v,i))continue;
            REP(u,1,n)c[i][u]+=dis[u][v];
        }
    }
}
void work(){
    dp[0]=1ll;
    int siz,cnt;
    REP(i,0,all){
        if(!dp[i])continue;
        int left=all-i;
        for(register int j=left;j;j=(j-1)&left){
            siz=__builtin_popcount(j),cnt=0;
            REP(u,1,n)if(in(u,i))cnt+=c[j][u];
            add(dp[i|j],dp[i]*qpow(2,cnt)%mod*qpow(-1,siz+1)%mod);
        }
    }
    printf("%lld\n",dp[all]);
}
int main(){
    File();
    init();
    work();
    return 0;
}