给定\(N\)、\(K\),求由\(N\)个节点、构造出的\(K\)层完全二叉树的方案数。
题意:
题目描述 农民约翰准备购买一群新奶牛。 在这个新的奶牛群中, 每一个母亲奶牛都生两个小奶牛。这些奶牛间的关系可以用二叉树来表示。这些二叉树总共有\(N\)个节点($ 3 N < 200 $)。这些二叉树有如下性质: 每一个节点的度是\(0\)或\(2\)。度是这个节点的孩子的数目。 树的高度等于\(K\)(\(1 < K < 100\))。高度是从根到最远的那个叶子所需要经过的结点数; 叶子是指没有孩子的节点。 有多少不同的家谱结构? 如果一个家谱的树结构不同于另一个的, 那么这两个家谱就是不同的。输出可能的家谱数的个数除以\(9901\)的余数。
输入格式: 两个空格分开的整数, N和K。 输出格式: 一个整数,表示可能的家谱树的个数除以9901的余数。 输入输出样例 输入样例#1: 5 3 输出样例#1: 2 说明 翻译来自NOCOW USACO 2.3
做法:
暴搜(TLE)
对于已知\(1到i\)层二叉树的方案数,想要在此之上构建出第\(i+1\)层的方案数。我们的方法有: 1. 左子树是\(i\)层,右子树是\(1到i-1\)层。(在这种情况下,上面加一个根节点) 2. 左子树是\(1到i-1\)层,右子树是\(i\)层。(在这种情况下,上面加一个根节点) 3. 左右子树都是\(i\)层。(在这种情况下,上面加一个根节点)
按照这个方法,可以递归去生成这么一颗树。但是我们并不知道左右子树的节点个数,所以我们还要枚举节点个数。
因此,我们可以定义函数dfs( remain_points , now_floor , floor )
表示使用remain_points
个节点,构建floor
的方案数。按照上面的方法去生成即可。
代码:
1 |
|
这样的代码毫无疑问是会T的。时间复杂度\(O( n * 2^k )\)
然后我想用我无往不利的将dfs
改成记忆化搜索,我想那样的话就肯定可以过。 结果用和dfs
参数类似的数组dp[maxn][maxk][maxk]
改记忆化搜索后,\(MLE\)了(说来搞ACM,好久没遇到MLE了2333。感觉一直对空间限制都不是很严格。没想到USACO限制挺严格的。)。
压状态爆搜(TLE)
因为我们dp[maxn][maxk][maxk]
爆空间了。毫无疑问是我们的状态太多了。因此,我们不得不去压缩一下状态。 考虑到对于floor
这个参数来说,其实完全没有必要记录\(1到k\)每一个的状态。对于我们来说,只关心是等于k还是小于k即可。 所以我们修改dfs
为dfs( remain_points , now_floor , flag )
,用一个二维状态表示即可(不过这个修改,我还改了挺长时间的。改起来挺麻烦的)。
代码:
1 |
|
这个代码的时间复杂度和上面的方法没有本质的区别。因此毫无疑问,依然是一个\(TLE\)的代码。
所以需要加上记忆化搜索。
压状态记忆化搜索(AC)
我们用dp[maxn][maxk][flag]
表示一个状态,加上记忆化搜索后。 代码如下:
1 |
|
成功AC。
四次方DP(AC)
考虑改写成DP形式。 用dp[i][j]
表示使用\(i\)个节点,恰好构成\(j\)层的方案数。 则dp的转移方程,和上文所说的三种情况一样。枚举两维,二维转移即可。
代码:
1 |
|
三次方DP(AC)
注意到四次方DP,每次都重复计算了\(1到i-2\)层的情况。因此我们考虑可以使用数组sum[i][j]
表示使用\(i\)个节点,构成的少于\(j\)层的方案数。
代码:
1 |
|
这个sum
数组用了前缀和的思想。一开始我把sum
数组放到了第三层循环里面,变成了四次方的DP。结果答案是对的,但是\(TLE\)了。(上面那个四次方DP能过大概是因为枚举的状态少一些吧。每次不一定把循环跑满了。)
因为我们把dp方程写成加和的形式,会发现:
\[sum[i][nowfloor-2]=\\sum_{j=1}^{nowfloor-2}{sum[i][j]}\]
也就是说,每次更新了dp[now_point][now_floor]
数组后,sum
数组显然需要一同更新。 因为每次更新的时候dp
的时候,now_point
都是新出现的,所以这一维不需要去变化。而层数因为sum[now_point][now_floor]
表示的是小于now_floor
的情况,所以所有大于等于now_floor
等都应该遍历去更新。这里可以利用构造前缀和的思想去操作。 (关于转移而来的状态,由代码很容易看出都是已经被计算过的)
三次方DP(AC)
看到有题解使用的方法是,用dp[i][j]
表示用i个节点,构造出的小于等于j
个节点的情况。然后最终答案是dp[n][k] - dp[n-1][k]
。 这个方法也很巧妙。代码量极少。我认为这个方法是把我上一个方法的dp数组和sum数组结合起来而产生的dp方法。 因为复杂度和我的代码区别不是很大,加上别人的这个想法我目前理解的不是很透彻,也并非我自己独立思考得来,这道题我也想了五六天了。所以这个方法暂时没有去花时间思考和实现。有机会可能会补上。
一些优化
这道题还有一些优化部分。 如 - 注意到每层字数分配的节点肯定都是奇数个,所以可以以2递增。 - 对于每层子树,很容易计算出最大节点数。借此剪枝。