题目链接:
https://ac.nowcoder.com/acm/contest/5668/J
这题来自2014北京区域赛的原题,不过数据范围更大了。
官方题解与HDU大佬的原题题解的思路本质上类似,都是树形DP,dp的其中一个维度是比子树中排在当前节点前的数量。不同的是,官方题解中将节点状态分为三种状态:好节点,已于好节点相连的坏节点,未与好节点相连的坏节点。另一种是将这三种情况一起讨论了。
具体转移方程不叙述了,看代码注释更方便。
代码来自北航大佬的AC代码:
#include <bits/stdc++.h>
const int N = 2010;
const int moder = 998244353;
std::vector<int> e[N];
int comb[N][N];
int dp[N][3][N], dp1[N][3][N];
int tmp[3][N], tmp1[3][N];
int sz[N];
// 0 -> good 1 -> bad, adjacent to good 2 -> bad, not adjacent to good
void add(int &u, int v){
u += v;
u -= u >= moder ? moder : 0;
}
void dfs(int u){
sz[u] = 1;
dp[u][0][0] = dp[u][2][0] = 1;
for (auto v : e[u]){
dfs(v);
for (int i = 0; i < 3; ++ i){//更新dp的值为前缀和,便于后续计算。注:此时dp含义已经变化,dp[i][sta][j]变成了最多j个节点比i大时的情况
for (int j = 1; j < sz[v]; ++ j){
add(dp[v][i][j], dp[v][i][j - 1]);
add(dp1[v][i][j], dp1[v][i][j - 1]);
}
}
for (int i = 0; i < sz[u]; ++ i){//枚举v之前的子树中,比x大的方案数
for (int j = 0; j <= sz[v]; ++ j){//枚举v子树中,比x大的方案数
int coe = 1ll * comb[i + j][i] * comb[sz[u] - 1 - i + sz[v] - j][sz[v] - j] % moder;
//恰好共有i+j个节点比u大,且其中j个节点属于v子树的方案数
//=比u大的i+j个点,有i个点是v之前的子树中的方案数 * 比x小的sz[u]-1-i+sz[v]-j个点中,有sz[v]-j个点在v子树中的方案。
for (int type1 = 0; type1 < 3; ++ type1){
for (int type2 = 0; type2 < 3; ++ type2){
// v节点在u之前的情况,即比u大的i+j个节点中,最多有j-1个节点属于v节点的情况
int cnt = j ? dp[v][type2][j - 1] : 0;
//v子树中最多有j-1个点比v大的方案数
int cnt1 = j ? dp1[v][type2][j - 1] : 0;
//v子树中,最多有j-1个点比v大时的v子树的贡献
int coe1 = 1ll * coe * dp[u][type1][i] % moder * cnt % moder;
//v节点比u节大的方案数
int base = coe * (1ll * dp[u][type1][i] * cnt1 % moder + 1ll * dp1[u][type1][i] * cnt % moder) % moder;
//v节点比u节点大时,u节点和v子树的贡献
if (type1 == 0){
if (type2 == 1){//u好和v坏的状态更新到u好
add(tmp[0][i + j], coe1);
add(tmp1[0][i + j], base);
}
}
else if (type1 == 1){
if (type2 == 0 || type2 == 1){//u坏和v好/坏的状态,更新到u坏
add(tmp[1][i + j], coe1);
add(tmp1[1][i + j], base);
}
}
else if (type1 == 2){
if (type2 == 0){//u半坏和v好的状态,更新到u坏
add(tmp[1][i + j], coe1);
add(tmp1[1][i + j], base);
}
else if (type2 == 1){//u半坏和v坏的状态,更新到u半坏
add(tmp[2][i + j], coe1);
add(tmp1[2][i + j], base);
}
}
//v节点在u之后的情况,即比u大的i+j个节点中,至少有j个节点属于v节点的情况,与上一种情况对立
cnt = dp[v][type2][sz[v] - 1] - cnt;
cnt += cnt < 0 ? moder : 0;
//v子树中至少有j个节点比v大的方案数
cnt1 = dp1[v][type2][sz[v] - 1] - cnt1;
cnt1 += cnt1 < 0 ? moder : 0;
//v子树中,至少有j个点比v大时的v子树的贡献
coe1 = 1ll * coe * dp[u][type1][i] % moder * cnt % moder;
//v节点比u节小的方案数
base = coe * (1ll * dp[u][type1][i] * cnt1 % moder + 1ll * dp1[u][type1][i] * cnt % moder) % moder;
//v节点比u节点小时,u节点和v子树的贡献
if (type1 == 0){
if (type2 == 1 || type2 == 2){//u好和v坏/半坏,更新到u好
add(tmp[0][i + j], coe1);
add(tmp1[0][i + j], base);
}
}
else if (type1 == 1){
if (type2 == 0 || type2 == 1){//u坏和v好/坏,更新到u坏
add(tmp[1][i + j], coe1);
add(tmp1[1][i + j], base);
}
}
else if (type1 == 2){
if (type2 == 0 || type2 == 1){
//u半坏和v好/坏的状态,更新到u半坏。
//因为v是在u之后,所以在u半坏是因为u的父亲在u之前导致的,u之后是允许v好的
add(tmp[2][i + j], coe1);
add(tmp1[2][i + j], base);
}
}
}
}
}
}
sz[u] += sz[v];
for (int i = 0; i < sz[u]; ++ i){//拷贝到dp上
for (int j = 0; j < 3; ++ j){
dp[u][j][i] = tmp[j][i];
dp1[u][j][i] = tmp1[j][i];
tmp[j][i] = 0;
tmp1[j][i] = 0;
}
}
}
for (int i = 0; i < sz[u]; ++ i){//加上u自己的贡献
add(dp1[u][0][i], dp[u][0][i]);
}
}
void solve(){
int n;
scanf("%d", &n);
for (int i = 0; i <= n; ++ i){
e[i].clear();
memset(dp[i], 0, sizeof(dp[i]));
memset(dp1[i], 0, sizeof(dp1[i]));
}
for (int i = 2, fa; i <= n; ++ i){
scanf("%d", &fa);
++ fa;
e[fa].emplace_back(i);
}
dfs(1);
int ans = 0;
for (int i = 0; i < n; ++ i){
add(ans, dp1[1][0][i]);//取模加法,由于用减法替代除法取模,因此会算得更快
add(ans, dp1[1][1][i]);
}
printf("%d\n", ans);
}
int main(){
for (int i = 0; i < N; ++ i){
comb[i][0] = 1;
for (int j = 1; j <= i; ++ j){
comb[i][j] = (comb[i - 1][j - 1] + comb[i - 1][j]) % moder;
}
}//杨辉三角算组合数
int test;
scanf("%d", &test);
while (test --){
solve();
}
return 0;
}