[bzoj]noip十連測歐貝里斯克的巨神兵(obelisk)——dag圖DP,狀態壓縮
阿新 • • 發佈:2019-02-09
題目大意:
有一張n個點,m條邊的有向圖,有多少個子圖(選定一個邊集)是沒有環的。答案對1e9+7取模。()
思路:
話說這個題目作為NOIP的模擬題有點過了,畢竟的方法不是太好想(一定我是太菜了)。
先考慮的方法,構造一個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的點是在新新增的這一層的。
設集合為當前的狀態,列舉一個的補集的子集,兩個集合的並集中,有前面定義的最後一層,因為是剛剛新增進去的且只有從後面來的邊,所以必有。對於這個集合的答案,一定是等於所有的的狀態的和,顯然地,如果用這種方法計數,每一個都會被新增進去多次,就是每一個的子集都會代表一次,所以我們在相加的時候多乘以一個容斥係數即可,這樣就可以保證每一個都只會被計算一次。
時間複雜度,好像還是不夠快,再優化我就不會了。。。
/*=========================
* 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;
}