1. 程式人生 > >[ZJOI2016]小星星&[SHOI2016]黑暗前的幻想鄉(容斥)

[ZJOI2016]小星星&[SHOI2016]黑暗前的幻想鄉(容斥)

這兩道題思路比較像,所以把他們放到一塊。

[ZJOI2016]小星星

題目描述

小Y是一個心靈手巧的女孩子,她喜歡手工製作一些小飾品。她有n顆小星星,用m條彩色的細線串了起來,每條細線連著兩顆小星星。

有一天她發現,她的飾品被破壞了,很多細線都被拆掉了。這個飾品只剩下了n-1條細線,但通過這些細線,這顆小星星還是被串在一起,也就是這些小星星通過這些細線形成了樹。小Y找到了這個飾品的設計圖紙,她想知道現在飾品中的小星星對應著原來圖紙上的哪些小星星。如果現在飾品中兩顆小星星有細線相連,那麼要求對應的小星星原來的圖紙上也有細線相連。小Y想知道有多少種可能的對應方式。

只有你告訴了她正確的答案,她才會把小飾品做為禮物送給你呢。

題解

做容斥題有一個基本模型,就是有一個限制,我們直接在轉移或者統計複雜度過高,但如果把它放寬一點的話複雜度會降低許多。

然後總的條件數也支援2^n列舉,就可以去考慮容斥。

這個題是說,有一個n個點的無向圖和n個點的一棵樹,問有多少種一一對應的對映使得在樹中有的邊,圖中也有。

看到樹可以聯想樹形dp,因為我們要求一一對應,所以我們可以考慮設dp[i][j][s]表示以i為根的子樹,i對應了圖中的j,i子樹對應了圖中的集合s的方案數。

轉移還是比較簡單的。

我們觀察到這個演算法複雜度瓶頸在於列舉s,所以我們考慮能不能去掉。

去掉之後會出現樹中的多個點對應了圖中的一個點,方案數會算多。

怎麼辦?這個形式其實已經很明顯了,直接套用容斥公式算就好了。

2^n列舉圖中選那些點,然後做二維的樹形dp就好了。

程式碼

#include<iostream>
#include<cstdio>
#define N 18
#define R register
using namespace std;
typedef long long ll;
int n,m,tot,head[N],cou[1<<N];
ll dp[N][N],ans;
bool a[N][N],jin[N];
inline int rd(){
    int
x=0;char c=getchar();bool f=0; while(!isdigit(c)){if(c=='-')f=1;c=getchar();} while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();} return f?-x:x; } struct edge{int n,to;}e[N*N*2]; inline void add(int u,int v){ e[++tot].n=head[u];e[tot].to=v;head[u]=tot; } void dfs(int u,int fa){ for(R int i=1;i<=n;++i)if(!jin[i])dp[u][i]=1;else dp[u][i]=0; for(R int i=head[u];i;i=e[i].n)if(e[i].to!=fa){ int v=e[i].to;dfs(v,u); for(R int j=1;j<=n;++j)if(!jin[j]){ ll num=0; for(R int k=1;k<=n;++k)if(a[j][k]&&!jin[k])num+=dp[v][k]; dp[u][j]*=num; } } } int main(){ n=rd();m=rd();int x,y; for(R int i=1;i<=m;++i){ x=rd();y=rd(); a[x][y]=a[y][x]=1; } for(R int i=1;i<n;++i){x=rd();y=rd();add(x,y);add(y,x);} for(R int i=0;i<(1<<n);++i){ cou[i]=cou[i>>1]+(i&1); for(R int j=1;j<=n;++j)jin[j]=(i&(1<<j-1))!=0; dfs(1,0); ll num=0; for(R int j=1;j<=n;++j)num+=dp[1][j]; if(cou[i]&1)ans-=num;else ans+=num; } cout<<ans; return 0; }
View Code

[SHOI2016]黑暗前的幻想鄉

題目描述

四年一度的幻想鄉大選開始了,最近幻想鄉最大的問題是很多來歷不明的妖怪湧入了幻想鄉,擾亂了幻想鄉昔日的秩序。但是幻想鄉的建制派妖怪(人類)博麗靈夢和八雲紫等人整日高談所有妖怪平等,幻想鄉多元化等等,對於幻想鄉目前面臨的種種大問題卻給不出合理的解決方案。

風見幽香是幻想鄉里少有的意識到了問題嚴重性的大妖怪。她這次勇敢地站了出來參加幻想鄉大選,提出包括在幻想鄉邊境建牆(並讓人類出錢),大力開展基礎設施建設挽回失業率等一系列方案,成為了大選年出人意料的黑馬並順利地當上了幻想鄉的大統領。

幽香上臺以後,第一項措施就是要修建幻想鄉的公路。幻想鄉一共有 nn 個城市,之前原來沒有任何路。幽香向選民承諾要減稅,所以她打算只修 n-1n1條公路將這些城市連線起來。但是幻想鄉有正好 n-1n1 個建築公司,每個建築公司都想在修路地過程中獲得一些好處。雖然這些建築公司在選舉前沒有給幽香錢,幽香還是打算和他們搞好關係,因為她還指望他們幫她建牆。所以她打算讓每個建築公司都負責一條路來修。

每個建築公司都告訴了幽香自己有能力負責修建的路是哪些城市之間的。所以幽香打算 n - 1n1條能夠連線幻想鄉所有城市的邊,然後每條邊都交給一個能夠負責該邊的建築公司修建,並且每個建築公司都恰好修建一條邊。

幽香現在想要知道一共有多少種可能的方案呢?兩個方案不同當且僅當它們要麼修的邊的集合不同,要麼邊的分配方式不同。

題解

這題和上一題相似,有兩個限制。

上一題是要滿足樹中和圖中都要有某條邊。

這題是要滿足我們的生成樹中,既要有n個不同的點,還要滿足每種顏色的邊個出現一次。

如果我們去掉第二個限制,那就變成了生成樹計數問題,套用矩陣樹定理即可。

然後我們發現面前的這個問題還是可以套用容斥公式直接計算的,於是這道題被愉快的解決了。

程式碼

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define N 18
using namespace std;
typedef long long ll;
const int mod=1e9+7;
ll a[N][N];
int n,m,cou[1<<N];
bool tag[N];
inline int rd(){
    int x=0;char c=getchar();bool f=0;
    while(!isdigit(c)){if(c=='-')f=1;c=getchar();}
    while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return f?-x:x;
}
inline ll power(ll x,ll y){
    ll ans=1;
    while(y){if(y&1)ans=ans*x%mod;x=x*x%mod;y>>=1;}
    return ans;
}
struct node{int x,y;};
vector<node>vec[N];
inline ll ni(ll x){return power(x,mod-2);}
inline ll gauss(ll tot){
    ll ans=1;
    for(int i=1;i<=tot;++i)
     for(int j=i+1;j<=tot;++j){
         ll t=a[j][i]*ni(a[i][i])%mod;
         for(int k=i;k<=tot;++k)a[j][k]=((a[j][k]-t*a[i][k])%mod+mod)%mod;
     }
    for(int i=1;i<=tot;++i)ans=(ans*a[i][i]%mod+mod)%mod;
    return ans;
}
inline ll work(){
    memset(a,0,sizeof(a));
    for(int i=1;i<n;++i)if(tag[i]){
        for(int j=0;j<vec[i].size();++j){
            int x=vec[i][j].x,y=vec[i][j].y;
            a[x][x]++;a[y][y]++;a[x][y]--;a[y][x]--;
        } 
    }
    return gauss(n-1);
}
int main(){
    n=rd();int x,y;
    for(int i=1;i<n;++i){
      m=rd();
      for(int j=1;j<=m;++j){
        x=rd();y=rd();vec[i].push_back(node{x,y});
      }
    }
    ll ans=0;
    for(int i=0;i<(1<<n-1);++i){
        cou[i]=cou[i>>1]+(i&1);
        for(int j=1;j<=n-1;++j)tag[j]=(i&(1<<j-1))==0;
        if(cou[i]&1)ans-=work();else ans+=work();
        ans=(ans%mod+mod)%mod;
    }
    cout<<ans;
    return 0;
}
View Code