给定一棵n个节点的有根树,节点编号范围为1∼n,其中根节点编号为1。现在你需要给树上每个节点赋值左括号(或者右括号),问你有多少种赋值方案,使得赋值完毕后从根节点到达每一个叶子节点的唯一简单路径所经过的点构成的括号串是一个合法括号串(求出方案数对998244353取模的结果)?
我们递归的定义合法括号串:
- 空括号串是合法的。
- 若
A合法,则(A)合法。 - 若
A和B合法,则AB合法。
格式
输入格式:
第一行一个整数T(1≤T≤500),表示测试数据组数,对于每组测试数据:
第一行一个整数n(1≤n≤3000)。
接下来n−1n−1行,每行两个整数u,v(1≤u,v≤n),表示点uu与点vv之间有一条边。
保证单个测试点的所有组测试数据的nn之和不超过3000。
输出格式:
对于每组测试数据:
输出一行一个整数,表示答案。
样例 1
输入:
1
4
1 2
2 3
3 4
复制
输出:
2
复制
样例 2
输入:
1
11
1 2
1 3
1 4
2 5
2 8
3 10
5 6
5 7
8 9
10 11
复制
输出:
4
复制
备注
对于样例11,这是一条链,合法的括号串有()()和(())两种,答案为2。
#include <iostream>
#include <vector>
using namespace std;
const int MOD = 998244353;
vector<int> adj[3005];
long long dp[3005][3005];//来到u节点前,欠j个左括号,到根后能形成的合法方案数
int n;
void dfs(int u, int p) {
// 1. 先递归处理子节点
vector<int> children;
for (int v : adj[u]) {
if (v != p) {
children.push_back(v);
dfs(v, u);
}
}
// 2. 计算当前节点在不同初始余额 j 下的方案数
// j 是"进入节点 u 之前"的余额
for (int j = 0; j <= n; ++j) {
long long ways_as_L = 0;
long long ways_as_R = 0;
// 情况 A: 节点 u 填左括号 '('
// 填完后余额变为 j + 1
if (j + 1 <= n) {
if (children.empty()) {
// 如果 u 是叶子,填 '(' 后余额为 j+1
// 路径结束要求余额必须为 0,所以 j+1 == 0 (不可能)
ways_as_L = 0;
} else {
ways_as_L = 1;
for (int v : children) {
ways_as_L = (ways_as_L * dp[v][j + 1]) % MOD;
}
}
}
// 情况 B: 节点 u 填右括号 ')'
// 填完后余额变为 j - 1
if (j - 1 >= 0) {
if (children.empty()) {
// 如果 u 是叶子,填 ')' 后余额为 j-1
// 路径结束要求余额必须为 0,即 j 必须是 1
ways_as_R = (j - 1 == 0 ? 1 : 0);
} else {
ways_as_R = 1;
for (int v : children) {
ways_as_R = (ways_as_R * dp[v][j - 1]) % MOD;
}
}
}
dp[u][j] = (ways_as_L + ways_as_R) % MOD;
}
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) {
adj[i].clear();
for (int j = 0; j <= n; j++) dp[i][j] = 0;
}
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// 题目要求根节点到叶子的路径。根节点编号为 1。
dfs(1, 0);
// 最终答案是:进入根节点 1 之前,余额为 0 的方案数
cout << dp[1][0] << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--) {
solve();
}
return 0;
}