CF1060F Shrinking Tree

CF1060F Shrinking Tree 

DP好题

%%ywy

肯定考虑在压缩中找到不变的关系。

n很小,不妨枚举每个点成为最后编号的情况,并且把这个点rt作为根

其实概率是:p/(n-1)!*(1/2)^k这里的p是所有缩边的排列中,和rt合并有k次的方案数。

我们只计算p*(1/2)^k部分,最后统一除以(n-1)!

 

基础的想法:

f[x][k]x为根的子树,和x合并k次的方案数,但是由于不知道和父亲的边什么时候缩,无法转移

直接记录到父亲?g[x][k],和x的fa合并k次方案数?其实还不能转移。

问题就是在于和父亲的边e不知道什么时候缩

再考虑到,可能当父亲是rt的时候特殊考虑,

 

如下定义状态:

dp[x][k]x为根的子树的边序列,当x变成rt的时候,还有k条边没有合并,最终合并成一个rt的大点的概率和

g[x][k]x为根的子树+x到fa的e的边的序列g,当fa变成rt的时候,g总共还有k条边没有合并,最终合并成一个rt的大点的概率和

 

这种奇怪的设法,有点对未来承诺的意思,而且是直接加入了状态的定义里,并没有用数组某一维记录。

并且有一个明显的划分点:x或者fa变成rt,这样便于统计1/2概率的额外限制

至于e的合并,我们可以在g中考虑到。

我们把之前的合并k次,直接在dp值中进行计算即可。

 

e在合并的时候,x有没有变成rt,再利用之前的dp[son],两种情况讨论,可以求出g[son]

再用g[son]们,背包进行合并。

只要保证分配的新序列中,x变成rt之前的操作都在x变成rt之前。分别组合数分配即可。

ans[rt]=dp[rt][n-1]

O(n^4)

 

#include<bits/stdc++.h>
#define reg register int
#define il inline
#define fi first
#define se second
#define mk(a,b) make_pair(a,b)
#define numb (ch^'0')
#define pb push_back
#define solid const auto &
#define enter cout<<endl
#define pii pair<int,int>
using namespace std;
typedef long long ll;
template<class T>il void rd(T &x){
    char ch;x=0;bool fl=false;while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true);
    for(x=numb;isdigit(ch=getchar());x=x*10+numb);(fl==true)&&(x=-x);}
template<class T>il void output(T x){if(x/10)output(x/10);putchar(x%10+'0');}
template<class T>il void ot(T x){if(x<0) putchar('-'),x=-x;output(x);putchar(' ');}
template<class T>il void prt(T a[],int st,int nd){for(reg i=st;i<=nd;++i) ot(a[i]);putchar('\n');}
namespace Modulo{
const int mod=998244353;
int ad(int x,int y){return (x+y)>=mod?x+y-mod:x+y;}
void inc(int &x,int y){x=ad(x,y);}
int mul(int x,int y){return (ll)x*y%mod;}
void inc2(int &x,int y){x=mul(x,y);}
int qm(int x,int y=mod-2){int ret=1;while(y){if(y&1) ret=mul(x,ret);x=mul(x,x);y>>=1;}return ret;}
}
//using namespace Modulo;
namespace Miracle{
const int N=55;
double dp[N][N],g[N][N],tmp[N];
double C[N][N];
int n;
double ans[N];
struct node{
    int nxt,to;
}e[2*N];
int hd[N],cnt;
void add(int x,int y){
    e[++cnt].nxt=hd[x];
    e[cnt].to=y;
    hd[x]=cnt;
}
int sz[N];
void dfs(int x,int fa){
    sz[x]=1;
    dp[x][0]=1.00;
    for(reg i=hd[x];i;i=e[i].nxt){
        int y=e[i].to;
        if(y==fa) continue;
        dfs(y,x);
        for(reg j=0;j<=sz[y];++j){
            g[y][j]=(double)(sz[y]-j)*dp[y][j];
            for(reg k=0;k<j;++k){
                g[y][j]+=(double)0.5*dp[y][k];
            }
        }
        memset(tmp,0,sizeof tmp);
        for(reg k=0;k<sz[x];++k){
            for(reg j=0;j<=sz[y];++j){
                tmp[k+j]+=C[sz[x]+sz[y]-k-j-1][sz[x]-1-k]*C[k+j][k]*dp[x][k]*g[y][j];
            }
        }
        sz[x]+=sz[y];
        for(reg k=0;k<sz[x];++k) dp[x][k]=tmp[k];
    }
}
int main(){
    rd(n);int x,y;
    for(reg i=1;i<n;++i){
        rd(x);rd(y);add(x,y);add(y,x);
    }
    C[0][0]=1;
    for(reg i=1;i<=n;++i){
        C[i][0]=1;
        for(reg j=1;j<=i;++j){
            C[i][j]=C[i-1][j-1]+C[i-1][j];
        }
    }
    for(reg i=1;i<=n;++i){
        memset(dp,0,sizeof dp);
        memset(g,0,sizeof g);
        dfs(i,0);
        ans[i]=dp[i][n-1];
    }
    double jie=1;
    for(reg i=2;i<n;++i) jie*=i;
    for(reg i=1;i<=n;++i){
        printf("%.10lf\n",ans[i]/jie);
    }
    return 0;
}

}
signed main(){
    Miracle::main();
    return 0;
}

/*
   Author: *Miracle*
*/

考虑x是不是rt,要进行讨论的。考虑e什么时候合并,也是要讨论的。

x是不是rt就成了分界点。e什么时候合并也是关键点,这样才对son的子树内序列做出了限制。也才能用上dp[son]转移。

所以状态就直接记录还剩下多少个边没有合并,这些边都要注意是否有1/2的概率限制。而且还可知道之前放入了多少边,有助于组合数分配转移。

 

posted @ 2019-05-28 21:52  *Miracle*  阅读(261)  评论(0编辑  收藏  举报