Loj#6495. 「雅礼集训 2018 Day1」树题解

题目链接

前言

本题解无法解决本题,但是可能会对这道题的DP式子的推导产生帮助

对于本题解所解决的题目,实际上是一个类似多测的情况,具体的可以参考代码,但是对于推导不产生影响

题目大意

题目已经很精简了,难以概括,下面是原题题面

有一棵 \(n\)个点的有根树,点编号为\(1\)\(n\),其中\(1\)号点为根,除\(1\)号点外,\(i\)号点的父亲在\(1\)\(i - 1\)内均匀随机。

定义一棵树的深度为所有节点到根路径上节点数的最大值,求这棵树的期望深度。

而本题解所解决的题目与原题不同的一点是,原体面\(N\le24\),而在本题解中,\(N \le 500\),并且,最终的答案是总的深度,而不用再去考虑期望

以下的内容都是建立在这“全新”的题目内容上

解题思路

由于在新的数据范围里,\(N\)得到了巨大加强,因此,只能选择去想\(O(n^4)\)甚至\(O(n^3)\)的算法

赛时思路

首先打了表,尝试寻找规律,无果后,考虑推式子。但很遗憾,因为我太弱了,所以没有推出来。

正确思路

实际上,这道题在推式子过后,就会理所当然的想到DP

解决方法

在想到DP后,接下来便是经典的状态设计,状态转移

状态设计

首先设\(f_i\)表示选择\(i\)个点的答案,但是,这很明显是不够的,因为当手玩过样例后,就会发现关于深度的设计不可避免

因此,我们只能设计\(f_{i,d}\),表示选择\(i\)个点,深度为\(d\)的方案数

状态转移

注意:这是本题最难的部分

首先我们可以注意到,\(2\)号节点必定是与\(1\)号节点相连的,因此,本题的点可以分为两类(\(1,2\)号节点都不再考虑):

1.在\(2\)的子树上

2.不在\(2\)的子树上

这个分类看似是没用的,但是这会在之后的状态转移上发挥重要作用

具体的状态转移如下,由于笔者叙述能力有限,只能先列出式子,再进行阐述

\[f[i][d] = f[i-1][d-1] + \sum_{j=1}^{i-2}(f[j][d-1] \times f[i-j][\le d-1]+f[i-j][d] \times f[j][\le d-1]) \times C_{i-2}^{j-1} \]

解释如下(以Q&A的形式呈现):

\(i\)表示节点,\(d\)表示最大深度

Q1.为什么可以直接加上\(f[i-1][d-1]\)

A1.这是因为这种情况表示的是全部节点都在\(2\)的子树上,那么我们就可以看作删除了\(1\)号节点,而二号节点也就成了\(1\)号节点,那么也就自然可以继承

Q2.后面的一堆式子的意思?

A2.\(j\)表示的是以\(2\)为根的子树大小,那么接下来是一大串的分讨

​ 1.是2的子树节点创下了\(d\)的最大深度,那么其式子就是\(f[j][d-1] \times f[i-j][\le d-1]\),因为在\(2\)号点子树上的点已经自带1个深度,因此需要为\(d-1\),而另一部分(也就是没连在2的子树上的点)的深度可以为\(1,2... d-1\),而之所以不去到\(d\),是因为还有后面的分讨

​ 2.不是2的子树上的节点创下了\(d\)的最大深度,那么其式子就是\(f[i-j][d] \times f[j][\le d-1]\),其实际意义和上面的类似,读者容易发现,在此不过多赘述。而上面只能取到\(d-1\)的原因也就显而易见了,是因为这样可以避免重复的情况(即两类节点都创造了\(d\)的深度,此时只能取一个)

Q3.为什么\(j\)不可以取到\(i-1\)

A3.这实际上就是Q1的情况,只不过拆开来了

Q4.为什么要乘上组合数?

A4.因为我们要从\(i-2\)个点中选出\(j-1\)个点,以此组成\(2\)的子树

代码实现

既然式子都已经推出来了, 那么代码应该并不难写

建议在思考过后,不看参考代码,直接编写代码,锻炼码力

代码如下:

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long 
#define File(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout)
#define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0)
const int N = 510;
int n;
ull p;
ull f[N][N],s[N][N],C[N][N];
void init()
{
    for(int i=0;i<=n;i++){
        C[i][0] = 1;
        for(int j=1;j<=i;j++){
            C[i][j] = C[i-1][j-1] + C[i-1][j];
            C[i][j] %= p;
        }
    }
    return ;
}
int main()
{
    IOS;
    File("tree");
    cin >> n >> p;
    f[1][1] = 1;
    init();
    for(int i=2;i<=n;i++){
        for(int j=1;j<=n;j++){
            s[i-1][j] = s[i-1][j-1] + f[i-1][j];
            s[i-1][j] %= p;
        }
            
        for(int d=1;d<=i;d++){
            f[i][d] = f[i-1][d-1];
            for(int j=1;j<i-1;j++){
                f[i][d] = f[i][d] + (1ull * s[i-j][d-1]* f[j][d-1] + 1ull * f[i-j][d] * s[j][d-1])  % p * C[i-2][j-1] % p;
                f[i][d] %= p;
            }  
        }
    }
    for(int i=1;i<=n;i++){
        ull ans = 0;
        for(ull d=1;d<=i;d++){
			ans += f[i][d] * d;
			ans %= p;
		}
           
        cout << ans <<"\n";
    }
    return 0;
}

后记

本题是一道不错的DP练习题,实际上笔者学会这道题的写法也花了数个小时,或许是因为笔者太菜了。要彻底想透此题,需要积极模拟,仔细思考。

后后记

最后提醒一下:此代码无法通过原题,只能通过笔者在题面大意中描述的新题面

posted @ 2025-07-23 20:23  WinterXorSnow  阅读(34)  评论(0)    收藏  举报