[CF1060F] Shrinking Tree 题目 分析 代码

点这里看题目。

分析

美妙的思维题目!反正我是做不来了。

显然我们可以对于每一个点计算它作为根的答案,这个答案又可以通过 DP 的方式求出来。

它难道还能不是个 DP ?

直接求解概率比较复杂,而操作序列的总方案数比较好求,是 ((n-1)!) 。不过,由于同一个操作序列的成功概率会因为每条边的操作而不同,因此我们实际上 DP 的不应该是方案数,而是所有操作序列的成功概率的和。最后将答案再除以 ((n-1)!)

(P(p)) 为操作序列 (p) 成功概率。 DP 的东西可以理解为:

[sum_{p是操作序列} P(p) ]

第一眼不难想到这样一个状态:

(f(u,i)):以 (u) 为根的子树内,剩余的边有 (i) 条,最后操作到只有一个 (u) 的概率和。

然后发现,如果仅仅局限性地要求根必须保留,就会忽略那些 " 先随意合并,再让根节点过来吞掉 " 的情况。

因此,我们可以对状态加上一些限制:

(f(u,i)):以 (u) 为根的子树内,剩余的边有 (i) 条,且当前的 (u) 标号已经变成了根的,继续操作留下根的概率和。

这样起始状态就是 (f(u,0)=1) ,目标状态就是 (f(x,n-1)) (假设我们计算 (x) 为根的情况)。

顺便再规定一下 (s_u)(u) 的子树大小。

首先我们考虑一个比较简单的情况——一棵树 (u) ,有 (x) 条剩余边;另一棵树 (v) ,有 (y) 条剩余边。现在我们要把它们 " " 起来。这就意味着边的数量不会变

根据定义,我们实际上是考虑序列概率和的变化。也就自然分成两个部分:

(mathcal{1.})序列贡献的变化。因为序列的成功概率是可以合并的,所以我们可以得到新的贡献为 (f(u,x) imes f(v,y))

(mathcal{2.})序列形态的变化。考虑未操作边,合并的方案是 (inom{x+y}{x}) ;已操作边,合并的方案是 (inom{s_u-x+s_v-y-2}{s_u-x-1}) 。两种边的顺序不可调换,因而一种组合序列对应的方案数是 (inom{x+y}{x} imes inom{s_u-x+s_v-y-2}{s_u-x-1})

可以发现,每种新序列的新形态的方案都相同,因而有新贡献为 (f(u,x) imes f(v,y) imes inom{x+y}{x} imes inom{s_u-x+s_v-y-2}{s_u-x-1})

再考虑节点 (u) 的转移。我们尝试合并上来 (v) 。我们只需要想办法把 ((u,v)) 合并到 (f(v)) 的操作序列中,得到新的贡献 (g(v))(g(v,i)) 表示合并过后有 (i) 条剩余边的概率和 ),就可以套用之前的计算方法合并 (u)(v) 了。

不妨枚举一下我们需要的 (g(v,i)) ,再枚举一下 (f(v,j)) ,考虑 (f(v,j))(g(v,i)) 的贡献。

(mathcal{1.} j>i) ,显然这种情况下 (f(v,j)) 不可能贡献到 (g(v,i))

(mathcal{2.} j<i) ,考虑此时的合法合并:首先,当 (v) 的子树内剩余 (i-1) 条边的时候, (u) 被替换为了根;接着, (v) 继续合并直到剩余 (j) 条边;然后, ((u,v)) 合并并且保留 (u) 的标号;最后进入到 (f(v,j)) 。注意到 (u) 必须保留,因此贡献为 (frac 1 2f(v,j))

(mathcal{3.} j=i) ,这意味着 (g(v,i)) 要操作的边就是 (f(v,j)) ,那么 ((u,v)) 就应该随着 (v) 子树中那些被删除的边一起删除。显然那些边总共有 (s_v-1-j) 条,对应就有 (s_v-j) 个空位。注意到 (u) 并不一定要保留,因此贡献就是 ((s_v-j)f(v,j))

所以我们可以首先处理出来 (g(v)) ,然后按照上述方法进行合并,就可以得到 (f(x,n-1)) ,除以 ((n-1)!) 就得到了答案。

可以发现这个转移本质上类似于树形背包,因此单次时间应该是 (O(n^2)) 。总时间就是 (O(n^3))

有价值的点:

(mathcal{1.}) DP 的状态设计,有一定的难度。

(mathcal{2.}) DP 的转移,也有一定的难度。

没错,这道题就是一道 DP 毒瘤思维题。

代码

#include <cstdio>

typedef long long LL;

const int MAXN = 55;

template<typename _T>
void read( _T &x )
{
	x = 0;char s = getchar();int f = 1;
	while( s > '9' || s < '0' ){if( s == '-' ) f = -1; s = getchar();}
	while( s >= '0' && s <= '9' ){x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar();}
	x *= f;
}

template<typename _T>
void write( _T x )
{
	if( x < 0 ){ putchar( '-' ); x = ( ~ x ) + 1; }
	if( 9 < x ){ write( x / 10 ); }
	putchar( x % 10 + '0' );
}

struct edge
{
	int to, nxt;
}Graph[MAXN << 1];

double fac[MAXN];
double f[MAXN][MAXN], g[MAXN], tmp[MAXN];
int head[MAXN], siz[MAXN];
int N, cnt;

void addEdge( const int from, const int to )
{
	Graph[++ cnt].to = to, Graph[cnt].nxt = head[from];
	head[from] = cnt;
}

void init()
{
	fac[0] = 1;
	for( int i = 1 ; i <= N ; i ++ )
		fac[i] = fac[i - 1] * i;
}

double C( const int n, const int m ) { return fac[n] / fac[m] / fac[n - m]; }

void DFS( const int u, const int fa )
{
	siz[u] = 1;
	f[u][0] = 1; for( int i = 1 ; i <= N ; i ++ ) f[u][i] = 0;
	for( int i = head[u], v ; i ; i = Graph[i].nxt )
		if( ( v = Graph[i].to ) ^ fa )
		{
			DFS( v, u );
			for( int k = 0 ; k <= N ; k ++ ) tmp[k] = g[k] = 0;
			for( int k = 0 ; k <= siz[v] ; k ++ )
				for( int j = 0 ; j <= k ; j ++ )
				{
					if( k == j ) g[k] += ( siz[v] - k ) * f[v][j];
					else g[k] += 0.5 * f[v][j];
				}
			for( int j = 0 ; j < siz[u] ; j ++ )
				for( int k = 0 ; k <= siz[v] ; k ++ )
					tmp[j + k] += f[u][j] * g[k] * C( j + k, j ) * C( siz[u] - j - 1 + siz[v] - k, siz[v] - k );
			siz[u] += siz[v];
			for( int k = 0 ; k < siz[u] ; k ++ ) f[u][k] = tmp[k];
		}
}

int main()
{
	read( N );
	for( int i = 1, a, b ; i < N ; i ++ )
		read( a ), read( b ), addEdge( a, b ), addEdge( b, a );
	init();
	for( int i = 1 ; i <= N ; i ++ ) 
	{
		DFS( i, 0 );
		printf( "%.10lf
", f[i][N - 1] / fac[N - 1] );
	}
	return 0;
}