1. 程式人生 > >Codeforces 1060 F. Shrinking Tree

Codeforces 1060 F. Shrinking Tree

發現 感覺 劃分 一起 signed 基本功 turn register tro

題目鏈接

一道思維好題啊...感覺這種類型的題很檢驗基本功是否紮實(像我這樣的就掛了)。

題意:你有一棵\(n\)個點的樹,每次隨機選擇一條邊,將這條邊的兩個端點合並,並隨機繼承兩個點標號中的一個,問對於每一個點來說,最終剩下的那個點標號等於它的標號的概率。\(n\leq 50\),用浮點數方式輸出。

碰到浮點數輸出的題就很怕卡精,不過這道題似乎不卡,擔心卡精可以開\(long \ double\)(還要吐槽一句cf的\(C++11\)\(long\ double\)的輸出好像不是很資瓷...還要轉\(double\)輸出)。

好了現在開始講做法吧。我們的大體思想是每一個點分別求解答案。對於每一個點,用某種方法算出它最終被留下的方案數,那麽再除以\((n-1)!\)

顯然就是答案。不過要註意的一點是因為標號的繼承是隨機的,因此對於同一種刪邊順序,得到的結果可能不同,因此我們算出的其實是所有順序下這個點保留的概率的總和(可能是浮點數),但是為了接下來表達的簡便,不妨不嚴謹的稱其為方案數。

現在來關心怎麽求出每一個點被留下的方案數,我們將要求答案的點\(x\)當作樹的根,並用\(size_i\)表示以\(i\)為根的子樹的大小。考慮樹形\(dp\),我們用\(f_{i,j}\)表示當根節點的標號繼承到\(i\)點時,如果\(i\)的子樹還剩下\(j\)條邊,根節點的標號最終被保留下來的方案數。那麽\(f_{x,n-1}\)就是我們想要的答案。

我們先來解決一個小問題:

假設我們將當前節點\(u\)的子樹劃分為兩部分,並且已經知道了左半部分還剩\(i\)條邊時的方案數\(a\)和右半部分還剩\(j\)條邊時的方案數\(b\),如何求解它們對整棵子樹還剩\(i+j\)條邊的方案數的貢獻?

顯然左右兩部分的子樹對對方是沒有影響的,因此我們可以將左右的方案合並。只要剩下的左邊的\(i\)條邊和右邊的\(j\)條邊在之後刪除的相對順序不變,那麽一定會得到同一種結果,因此這部分合並的方案數就是\({{i+j}\choose i}\)種(即在刪除序列的\(i+j\)個空位種選\(i\)個給左邊的邊)。

同時我們還要註意已經刪除的邊,在真實的操作序列中它們也同樣需要合在一起。因此和上面相似,我們假設左邊原來一共有\(x\)

條邊,右邊原來一共有\(y\)條邊,那麽這部分合並的方案數就是\({{x+y-i-j}\choose x-i}\)

綜上所述,它們的貢獻應該是\(a*b*{{i+j}\choose i}*{{x+y-i-j}\choose x-i}\)

那麽沿著剛剛的想法繼續思考,我們或許可以采取如下策略\(dp\):對於某一棵以\(u\)為根的子樹,不考慮任何子樹時有\(f_{u,0}=1\)。假如我們有一種方法,可以計算出一個單點在只考慮一棵子樹時的答案,那麽我們的問題就做完了,因為我們在新考慮一棵子樹的時候,我們可以先計算只考慮它時的答案而將其視為我們剛剛所講的“右半部分”,將之前已經計算完的部分視為“左半部分”,就可以直接按照之前所講的方法合並。

現在我們只要解決如何計算只考慮\(u\)的某一棵子樹時的答案,設其根為\(v\)。顯然我們可以枚舉\(i\),表示我們想要求其還剩下\(i\)條邊時的答案,設其為\(g_i\),接著再枚舉\(j\),考慮\(f_{v,j}\)\(g_i\)的貢獻。分兩類情況討論:

\(1\)、假設\(j<i\),顯然合法的過程應該是這樣的:\(v\)的子樹中合並到還剩\(i-1\)條邊時,根的標號繼承到了\(u\)上,接著\(v\)的子樹中的邊繼續合並到只剩\(j\)條邊,接著根的標號再從\(u\)繼承到了\(v\)上。註意到\(u\)的標號繼承到\(v\)上發生的概率是\(\frac{1}{2}\),因此此時\(f_{v,j}\)\(g_i\)的貢獻是\(\frac{1}{2}f_{v,j}\)

\(2\)、假設\(j=i\),顯然合法的過程應該是這樣的:\(v\)的子樹原來共有\(size_v-1\)條邊,如果要剩下\(i\)條邊,應該刪除\(size_v-1-i\)條邊,而\(u\)\(v\)的連邊也應該隨著這些邊的刪除一起被刪除,考慮被刪除的\(size_v-1-i\)條邊組成的序列,\(u\)\(v\)的連邊可以插入到\(size_v-i\)個空位(因為兩端也是可以的)中的任何一個。同時我們可以發現如此一來,當根節點的標號繼承到\(u\)時,\(u\)\(v\)的連邊已經消失,因此就不需要考慮那\(\frac{1}{2}\)的概率了,貢獻是\((size_v-i)*f_{v,j}\)

\(3\)、假設\(j>i\),畫圖考慮一下就發現這是沒有合法方案的,貢獻是\(0\)

於是我們終於完成了最後一塊拼圖,得到了可行的解法。最後總結一下做法,我們分別計算每一個答案,接著進行樹形\(dp\)。對於每一個新考慮的兒子,我們先計算只考慮這個子樹的情況,接著將其與原有答案進行合並。粗略計算一下復雜度,在每一個點更新它對父親的貢獻時至多是\(O(n^2)\)的,因此計算一個點的答案復雜度是\(O(n^3)\),計算所有的點就是\(O(n^4)\)的。因為是粗略計算因此上界其實是很松的,常數也比較小,於是就可以通過了。

我的代碼:

#include<cstdio>
#include<vector>
using std::vector;
typedef long double ldb;
const int N=55;
int n;
vector<int> G[N];
int size[N];
ldb fact[N];
ldb dp[N][N],tmp[N],g[N];
inline ldb choose(int n,int m)
{
    return fact[n]/(fact[m]*fact[n-m]);
}
void dfs(int now,int father)
{
    register int i,j;
    dp[now][0]=1;size[now]=1;
    for(auto x:G[now])
    {
        if(x==father)
            continue;
        dfs(x,now);
        for(i=0;i<=size[x];i++)
        {
            g[i]=0;
            for(j=1;j<=size[x];j++)
                if(j<=i)
                    g[i]+=0.5*dp[x][j-1];
                else
                    g[i]+=dp[x][i];
        }
        for(i=0;i<size[now]+size[x];i++)
            tmp[i]=0;
        for(i=0;i<size[now];i++)
            for(j=0;j<=size[x];j++)
                tmp[i+j]+=dp[now][i]*g[j]*choose(i+j,i)*choose(size[now]-1-i+size[x]-j,size[now]-1-i);
        for(i=0;i<size[now]+size[x];i++)
            dp[now][i]=tmp[i];
        size[now]+=size[x];
    }
    return;
}
signed main()
{
    int x,y;
    register int i;
    scanf("%d",&n);
    fact[0]=1;
    for(i=1;i<=n-1;i++)
        fact[i]=fact[i-1]*i;
    for(i=1;i<=n-1;i++)
    {
        scanf("%d%d",&x,&y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    for(i=1;i<=n;i++)
    {
        dfs(i,0);
        printf("%.9lf\n",(double)(dp[i][n-1]/fact[n-1]));
    }
    return 0;
}

Codeforces 1060 F. Shrinking Tree