9.28 二叉树计数
题意
给定一颗\(N\)个节点的二叉树并对其标号,标号方法如下:编号为\(i\)的结点在二叉树的前序遍历中恰好是第\(i\)个出现
定义\(A_i\)表示编号为\(i\)的点在二叉树的中序遍历中出现的位置
现在,给出\(M\)个限制条件,第\(i\)个限制\(<u_i,v_i>\)表示\(A_{u_i}<A_{v_i}\),即中序遍历中\(u_i\)在\(v_i\)之前出现
请计算有多少种不同的带标号二叉树满足上述所有限制条件,取模\(10^9+7\)
\(N\leq 500\)
解法
在不考虑限制条件的情况下,答案显然就是卡特兰数
不考虑卡特兰数,尝试用\(DP\)解决这个问题,首先我们有一个\(O(N^3)\)的\(DP\)
可以发现对于结点\(x\),设其左子树的大小为\(a\),右子树的大小为\(b\)
那么其左子树中的编号集合为\(x+1\to x+a\),右子树为\(x+a+1\to x+a+b+1\)
那么我们设\(f[x][y]\)为以\(x\)为根,大小为\(y\)的子树的答案
那么转移就很简单了:我们枚举子树大小后再枚举左子树大小,用乘法原理进行转移
\[f[x][y]=\sum_{i=0}^y f[x+1][i]\times f[x+i+1][y-i]
\]
我们在这个\(DP\)的基础上加上限制,那么就意味着我们只在满足所有限制的情况下进行转移
观察每一个限制:如果在中序遍历中想要\(u\)出现在\(v\)之前,那么只有三种情况:
- \(u\)在\(v\)的左子树中
- \(v\)在\(u\)的右子树中
- 对于\(u\)与\(v\)的最近公共祖先\(w\),\(u\)在\(w\)的左子树中,\(v\)在\(w\)的右子树中
可以发现子树中的编号都是连续的,这启示我们用类似前缀和的方式查询限制的有无
由于限制是与两颗子树有关的,我们开一个二维数组记录所有限制,那么查询两颗子树内是否有限制实际上就意味着查询这两颗子树对应编号形成的矩形内是否有值
这样我们记录一个二维前缀和就可以\(O(1)\)判断了
代码
#include <cstdio>
#include <cctype>
#include <cstring>
using namespace std;
const int MAX_N = 410;
const int mod = 1e9 + 7;
int read();
int a[MAX_N][MAX_N];
long long f[MAX_N][MAX_N];
int check(int x1, int x2, int y1, int y2) {
return a[x2][y2] - a[x1 - 1][y2] - a[x2][y1 - 1] + a[x1 - 1][y1 - 1];
}
long long DFS(int l, int r) {
if (l >= r) return 1;
if (~f[l][r]) return f[l][r];
long long res = 0;
for (int i = l; i <= r; ++i) {
if (check(l, l, l + 1, i) || check(i + 1, r, l, i)) continue;
res = (res + DFS(l + 1, i) * DFS(i + 1, r) % mod) % mod;
}
return f[l][r] = res;
}
int main() {
int T = read();
while (T--) {
int N = read(), M = read();
memset(a, 0, sizeof a);
memset(f, -1, sizeof f);
for (int i = 1; i <= M; ++i) {
int u = read(), v = read();
a[u][v]++;
}
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= N; ++j)
a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
printf("%lld\n", DFS(1, N));
}
}
int read() {
int x = 0, c = getchar();
while (!isdigit(c)) c = getchar();
while (isdigit(c)) x = x * 10 + c - 48, c = getchar();
return x;
}