【题目】
原题地址
给定一棵
个点的树,进行
轮操作,每轮操作随机选择一条边
,将
两点合成一个点,即删去这两个点后新建一个点
,将原来与
或
连边的点连向
,接着
的编号随机为
中的一个。
对于每个编号
~
,求它最终留下的概率。
【题目分析】
我的概率好弱啊。
比赛的时候往直接计算概率上想了很久,然而并没有什么用。
同学用这个思路使劲打,一个我们都觉得很对的方法
过不了样例。
赛后 同学提供了一个将问题转化为排列组合问题的想法,顺着这个思路往下想(顺便看了下大佬代码),方才得到了一个可过的做法。
【解题思路】
观察合并的过程,实际上类似并查集的合并,我们合并一条边
,相当于将
并入
的并查集。设最后留下来的点作为根
,那么对于一条边
,它有
的贡献,当且仅当合并这条边时,
已经在
的并查集里。
现在考虑这样一个状态: 表示以 为根的子树,已经合并了 次,且 仍然还在的概率。特别地,对于叶子节点, 。
考虑状态转移,设当前节点为 ,一堆儿子 。我们枚举 的子树已经被合并了 次,那么接下来要考虑的就是在 下多合并了几个节点以及 这条边是否产生贡献。我们接着枚举 的子树已经被合并了 次,在 下多合并了 个节点。
显然在 时, 中已经合并的点数不到总合并点数(合并得不够多)。也就是说此时 这条边必然会因为合并产生贡献。
因为各棵子树之间是相互独立的,而合并的节点贡献和未合并的节点顺序也是独立的,我们只需要用隔板法就能计算出所有的排列方案,具体可以参见代码。
最后我们再在全局下除以一个阶乘就可以了。
【参考代码】(话说谁知道怎么改代码块啊,好丑啊)
#include<bits/stdc++.h>
using namespace std;
typedef double db;
typedef long double ldb;
const int N=55;
int n,tot,siz[N],head[N];
ldb g[N],f[N][N],C[N][N];
int read()
{
int ret=0;char c=getchar();
while(!isdigit(c)) c=getchar();
while(isdigit(c)) ret=ret*10+(c^48),c=getchar();
return ret;
}
struct Tway{int v,nex;}e[N<<1];
void add(int u,int v)
{
e[++tot]=(Tway){v,head[u]};head[u]=tot;
e[++tot]=(Tway){u,head[v]};head[v]=tot;
}
void init()
{
for(int i=0;i<N;++i)
{
C[i][0]=C[i][i]=1;
for(int j=1;j<i;++j) C[i][j]=C[i-1][j]+C[i-1][j-1];
}
n=read();
for(int i=1;i<n;++i) add(read(),read());
}
void dfs(int x,int fa)
{
siz[x]=f[x][1]=1;
for(int l=head[x];l;l=e[l].nex)
{
int v=e[l].v;
if(v==fa) continue; dfs(v,x);
for(int i=0;i<=siz[x]+siz[v];++i) g[i]=0;
for(int i=1;i<=siz[x];++i) for(int j=1;j<=siz[v];++j) for(int k=0;k<=siz[v];++k)
{
ldb tmp=f[x][i]*f[v][min(j,k+1)]*(j<=k?0.5:1);
g[i+k]+=tmp*C[i+k-1][k]*C[siz[x]-i+siz[v]-k][siz[v]-k];
}
for(int i=0;i<=siz[x]+siz[v];++i) f[x][i]=g[i]; siz[x]+=siz[v];
//for(int i=0;i<=siz[x];++i) printf("%lf ",(db)g[i]);puts("");
}
}
void solve()
{
ldb fc=1; for(int i=1;i<n;++i) fc*=(ldb)i;
for(int i=1;i<=n;++i) dfs(i,0),printf("%.9lf\n",(db)(f[i][n]/fc));
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("CF1060F.in","r",stdin);
freopen("CF1060F.out","w",stdout);
#endif
init();
solve();
return 0;
}