1. 程式人生 > 實用技巧 >【題解】小星星 [ZJOI2016] [P3349]

【題解】小星星 [ZJOI2016] [P3349]

【題解】小星星 [ZJOI2016] [P3349]

傳送門:小星星 \(\text{[ZJOI2016] [P3349]}\)

【題目描述】

給出 \(n\) 個點的一張無向圖 \((V,E)\) 和一顆樹,先需要給每個點 \(i\) 安排一個對映 \(a_i\),滿足對於樹上的任意邊 \((x,y)\),均有邊 \((a_x,a_y)\) 存在於給定無向圖。要求 \(a\) 為一個排列(即不存在兩個點的對映相同),求方案數。

【分析】

組合意義天地滅,代數推導保平安。 —— tiger0133

大家似乎都是直接硬找的容斥係數,這裡提供一個不需要費腦子的子集反演做法(最終柿子是一樣的)。

先考慮最 \(\text{naive}\)

的暴力狀壓:設 \(dp(x,j,S)\) 表示點 \(x\) 對映為 \(j\)\(x\) 子樹內所有點已經用完了 \(S\)(二進位制數表示狀態)的方案數。

\(dp\) 陣列第一、三維的作用顯然,第二維 \(j\) 是為了方便判斷是否有連邊。

轉移需要列舉 \(S\) 的子集,複雜度 \(O(n^33^n)\),用多項式科技優化子集卷積可以做到 \(O(n^42^n)\),顯然還是過不了。

複雜度瓶頸在於列舉子集,必須把這個東西去掉。

而優化子集 \(dp\) 顯然要上容斥,但這裡我們用另一個思路:子集反演。

反演的第一步是設計兩個狀態,需滿足其中一個可以方便求得、另一個可以方便得出答案、且兩者之間存在關係式。

設計狀態就用類似二項式反演的套路去思考,如果實在想不到就一個一個列舉限制條件,逐個檢驗是否可用。

而本題的關鍵限制在於:任意兩點的對映不能相同。當存在這一限制時無論怎麼設都不好搞,所以設狀態時需要把這個限制去掉。

\(f(S)\)\(n\) 個點的對映恰好使用了 \(S\) 中的所有點(無需滿足 \(a\) 為排列)。

\(g(S)\)\(n\) 個點的對映至多可使用 \(S\) 中的所有點(無需滿足 \(a\) 為排列)。

易知:

\[g(S)=\sum_{T\subseteq S}f(T) \]

由子集反演可得:

\[f(S)=\sum_{T\subseteq S}(-1)^{|S|-|T|}g(T) \]

注意:在做二項式反演時,我們設計的“至多”和“至少”都是假的,真正含義為:欽定某一部分滿足某條件,其餘部分隨意。因此 \(f,g\) 的關係式中會存在一個二項式係數(一個 \(f(i)\) 會被 \(g(x)\) 統計多次,這個次數可以為 \(C_{i}^{x}\) 或者 \(C_{x}^{i}\))。
但這裡子集反演定義的“至多”是真貨,每個 \(f(T)\) 只會被 \(g(S)\) 統計一次,因此 \(g(S)\) 直接就等於 \(\sum_{T\subseteq S}f(T)\),不會帶有奇怪的係數。

回到這道題,最終答案為 \(f(V)\),其中 \(V\) 為全集(即 \(2^{n}-1\))。

Q:為什喵?前面做定義時不是說的不一定滿足 \(a\) 為排列嗎?
A: 由於全集 \(V\) 中的 \(n\) 個點全都被作為對映使用了,而一個點只能對映一個,所以 \(f(V)\) 統計的方案中每個點的對映必定兩兩不同。

那麼最後的問題就是 在不涉及 \(f\) 的情況下快速計算 \(g(S)\) 了。

修改一下前面的暴力狀壓:

\(dp(x,j,S)\) 表示點 \(x\) 對映為 \(j\)\(x\) 子樹內所有點至多隻能使用 \(S\) 中的點作為對映值 的方案數。

轉移為:

\[dp(x,j,S)=\prod\limits_{to\in\{son(x)\}}\left(\sum\limits_{k\subseteq S,(j,k)\in E}dp(to,k,S)\right) \]

可得 \(g(S)=\sum_{j\subseteq S}dp(rt,j,S)\),其中 \(rt\) 為樹的根。

時間複雜度為:\(O(n^32^n)\),要憑信仰卡常....

完結撒程式碼~

...等等,還沒完!

這題神奇地沒有讓取模!

雖然答案的最大值 \(n!\) 沒有爆 \(\text{long long}\),但 \(\max(g(V))=n^n\) 爆了啊。

emm....手寫高精多半會 \(\text{TLE}\) ....就醬吧awa
(據說用 \(\text{long long}\)\(\text{int_128}\) 算出來的結果是一樣的)

【Code】

#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#define LL long long
#define Re register int
using namespace std;
const int N=18,M=131072+3;
int n,m,x,y,o,V,v[N],cnt[M],head[N],A[N][N];LL ans,g[M],dp[N][N];
struct QAQ{int to,next;}a[N<<1];
inline void add(Re x,Re y){a[++o].to=y,a[o].next=head[x],head[x]=o;}
inline void in(Re &x){
    int f=0;x=0;char ch=getchar();
    while(ch<'0'||ch>'9')f|=ch=='-',ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    x=f?-x:x;
}
inline void dfs(Re x,Re fa,Re S){
    for(Re i=1;i<=v[0];++i)dp[x][i]=1;
    for(Re i=head[x],to;i;i=a[i].next)
        if((to=a[i].to)!=fa){
            dfs(to,x,S);
            for(Re j=1;j<=v[0];++j){
                LL tmp=0;
                for(Re k=1;k<=v[0];++k)if(A[v[j]][v[k]])tmp+=dp[to][k];
                dp[x][j]*=tmp;
            }
        }
}
int main(){
//    freopen("123.txt","r",stdin);
    in(n),in(m),V=(1<<n)-1;
    if(n==1){puts("1");return 0;}
    while(m--)in(x),in(y),A[x][y]=A[y][x]=1;
    m=n-1;
    while(m--)in(x),in(y),add(x,y),add(y,x);
    for(Re s=0;s<=V;++s){
        cnt[s]=cnt[s>>1]+(s&1),v[0]=0;
        for(Re i=1;i<=n;++i)if(s&(1<<i-1))v[++v[0]]=i;
        dfs(1,0,s);LL g=0;
        for(Re i=1;i<=v[0];++i)g+=dp[1][i];
        ans+=(n-cnt[s]&1)?-g:g;
    }
    printf("%lld\n",ans);
}