2020牛客多校#3 J - Operating on the Tree 树形DP+组合数学

题目链接:
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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值