usaco2.3 nocows dp + 失败的递归

题目链接


题目大意:

给出树的节点个数和深度,并且对树的限定是,每个节点的孩子数为 2 或为 0,问有多少种形态不同的数。


思路:


最开始的时候是这样想找规律的

举个例子吧,比如节点数 11, 深度 5

很显然前两层都是满的,剩下节点 11 - 3 = 8,这 8 个要分到 3 层去.

可以是 2 2 4, 2 4 2, 4 2 2.

要求是后一个数 <= 前一个数 * 2 (最前面两个数固定的是 1 2)

因为下一层的两个节点是作为一个整体接到上一层的一个节点上的,上一层有 m 个节点,这一层有 n 个节点,那么就有 C m (n/2)

上面那个例子就是 C21 * C21 * C22 + C21 * C22 * C41 + C22 * C41 * C21 = 20.

是的方法就是这样的,可是并没有什么用,毕竟测试数据里层数多达 100 层,这样递归的找符合情况的组数显然会爆。


于是后来就换了个想法,dp 啊,我们要计算 K 层 N 个节点,只要考虑它的 左子树 和 右子数,然后再递归不就可以了吗

(我严重怀疑当时是怎么想的,显然是递归就也会爆啊。)

借着这个思路,既然是左右等价,那么只需要找到 

左子树节点数 >= 右子树的深度,并且左子树深度 = h - 1,右子数深度 <= h - 1,

满足这个条件的,对于所有左子树可能的节点数,对 左子树深度 = h - 1 的情况总数 * 右子数所有可能的深度的情况总数 * 2 求个和,不就好了吗

(其中对于 左子树节点个数 == 右子树节点个数 且 左子树深度 == 右子树深度的,不需要 *2)

是的,感觉完全没问题。

然而就是有错。

为什么呢?


我苦思冥想了很久,知道了问题出在哪里。

满足 左子树节点数 >= 右子树节点数 时,我条件反射地只算了 左子树深度 = h - 1 的情况,事实上如果右子树深度可以达到 h - 1,那么左子树深度  <= h - 1 也是可以的

想通这点的时候,真的是拨开云雾见青天啊(误

事实上显然还是爆了。

但是对于深度 <= 32 的,正确性还是能保证的,就放一下代码吧(毕竟我辛辛苦苦((。


/*
PROB: nocows
LANG: C++
ID: fan_0111
*/
#include <iostream>
#include <cmath>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
int rec[210][110];
int f(int n, int h) {
	if (rec[n][h]) return rec[n][h];
	int Min = 2 * (h-1) - 1, tot = n-1, ans = 0;
	int le = Min, ri = tot - le;
	while (le < ri) { le += 2; ri -= 2; }
	for (; le <= (2 << (h-2)) -1 && tot - le >= 1; le += 2) {
		int ri = tot - le;
		int loR = ceil(log(ri+1)/log(2)), hiR = min((ri+1) / 2, h-1);
		for (int j = loR; j <= hiR; ++j) {
			int x = ((LL)f(le, h-1) * f(ri, j)) % 9901;
			if (le == ri && j == h-1) ans = (ans + (LL)x) % 9901;
			else					  ans = (ans + 2*(LL)x) % 9901;
		}
		if (le != ri && hiR == h-1) {
			int loL = ceil(log(le+1)/log(2)), hiL = h-1;
			for (int i = loL; i < hiL; ++i) {
				int x = ((LL)f(le, i) * f(ri, hiR)) % 9901;
				ans = (ans + 2*(LL)x) % 9901;
			}
		}
	}
	rec[n][h] = ans;
	return ans;
}
int main() {
//	freopen("nocows.in", "r", stdin);
//	freopen("nocows.out", "w", stdout);
	int n, k;
	rec[1][1] = 1; rec[3][2] = 1;
	cin >> n >> k;
	if (!(n & 1)) cout << 0 << endl;
//	for (int k = 1; k <= 65; ++k) {
//		for (int n = 2*k-1; n <= (1 << k)-1; ++n) cout << f(n, k) << " ";
//		cout << endl;
//	}
	else cout << f(n, k) << endl;
	fclose(stdin);
	fclose(stdout);
	return 0;
}


在这道题上已经花了很久了。

后来......黔驴技穷去百度了。

看到一个十分简单的 dp (叹

其实一开始并不需要考虑深度是不是这么多,只需要用深度为 h 的减去深度为 h-1,得到的情况就都只剩下深度为 h 的了。

这样写起来也十分的方便简洁。


AC代码如下:

/*
PROB: nocows
LANG: C++
ID: fan_0111
*/
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
int ans[110][210];
int main() {
	freopen("nocows.in", "r", stdin);
	freopen("nocows.out", "w", stdout);
	int n, k;
	cin >> n >> k;
	for (int i = 1; i <= k; ++i) ans[i][1] = 1;
	for (int i = 2; i <= k; ++i) {
		for (int j = 3; j <= n; j += 2) {
			for (int k = 1; k <= j-2; k += 2) {
				ans[i][j] = (ans[i][j] + ((LL)ans[i-1][k] * ans[i-1][j-1-k]) % 9901) % 9901;
			}
		}
	}
	cout << (ans[k][n] - ans[k-1][n]+9901) % 9901 << endl;	
	
	fclose(stdin);
	fclose(stdout);
	return 0;
}


要反思。

一开始总是往复杂的方向去想了,平白浪费了很多时间。

没有好好想想可行性就开始写,也是浪费。

还有对情况的考虑不周全,自以为没问题。

还有最根本的 dp 太差劲...。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值