NC20811 蓝魔法师

题目链接

题目

题目描述

“你,你认错人了。我真的,真的不是食人魔。”--蓝魔法师

给出一棵树,求有多少种删边方案,使得删后的图每个连通块大小小于等于k,两种方案不同当且仅当存在一条边在一个方案中被删除,而在另一个方案中未被删除,答案对998244353取模

输入描述

第一行两个整数n,k, 表示点数和限制
2 <= n <= 2000, 1 <= k <= 2000
接下来n-1行,每行包括两个整数u,v,表示u,v两点之间有一条无向边
保证初始图联通且合法

输出描述

共一行,一个整数表示方案数对998244353取模的结果

示例1

输入

5 2
1 2
1 3
2 4
2 5

输出

7

题解

知识点:树形dp,背包dp,计数dp。

显然是个树上背包,但涉及组合计数。

\(dp[u][i]\) 表示以 \(u\) 为根的子树划分成大小不大于 \(k\) 连通块,且 \(u\) 所在连通块大小为 \(i\) 的方案数。这道题要剪枝,转移方程我用了刷表法。下面代码有打表和刷表的对比,明显刷表法更容易写出最佳的循环边界。转移方程为:

\[\left \{ \begin{array}{l} dp[u][i+j] &= dp[u][i + j] + dp[u][i] \cdot dp[v][j]\\ dp[u][i] &= dp[u][i] + \sum dp[v][j] \end{array} \right . \]

  1. 连接 \((u,v)\)\(dp[u][i]\)\(dp[v][j]\) 用乘法原理合并,最后得到 \(dp[u][i+j]\) ,显然 \(i+j\) 要小于等于 \(k\) 。 $i \in [1,\min(sz[u],k)] $ , \(j \in [1,\min (sz[v],k)]\) ,两者都要倒序遍历保证 \(i+j\) 是倒序的。
  2. 断开 \((u,v)\)\(dp[u][i]\) 的方案加上 \(dp[v][j]\) 的所有方案数。为了防止 \(dp[u][i]\) 的更新影响 \(i+j\) ,因此这个更新要在一次 \(i\) 的更新最后执行。

时间复杂度 \(O(nk^2)\)

空间复杂度 \(O(nk)\)

代码

#include <bits/stdc++.h>

using namespace std;

const int mod = 998244353;
int n, k;
vector<int> g[2007];
int dp[2007][2007], sz[2007];

/* void dfs(int u, int fa) {
    dp[u][1] = 1;
    sz[u] = 1;
    for (auto v : g[u]) {
        if (v == fa) continue;
        dfs(v, u);
        sz[u] += sz[v];
        int sumv = 0;
        for (int i = 1;i <= sz[v];i++) sumv = (sumv + dp[v][i]) % mod;
        for (int i = min(sz[u], k);i >= 1;i--) {
            dp[u][i] = 1LL * dp[u][i] * sumv % mod;///断开
            for (int j = max(1, sz[v] + i - sz[u]);j <= min({ i, k,sz[v] });j++) ///链接
                dp[u][i] = (dp[u][i] + 1LL * dp[u][i - j] * dp[v][j]) % mod;
        }
    }
} */

///复杂度和常数比上面一种方式少一点,甚至比上面边界好写很多
void dfs(int u, int fa) {
    dp[u][1] = 1;
    sz[u] = 1;
    for (auto v : g[u]) {
        if (v == fa) continue;
        dfs(v, u);
        int sumv = 0;
        for (int i = 1;i <= sz[v];i++) sumv = (sumv + dp[v][i]) % mod;
        for (int i = min(sz[u], k);i >= 1;i--) {
            for (int j = min(sz[v], k);j >= 1;j--) {
                if (i + j > k) continue;///也可以用tmp暂存的方法,然后顺序更新,就可以直接break,否则只能倒叙
                dp[u][i + j] = (dp[u][i + j] + 1LL * dp[u][i] * dp[v][j]) % mod;
            }
            dp[u][i] = 1LL * dp[u][i] * sumv % mod;
        }
        sz[u] += sz[v];
    }
}///复杂度 O(nk^2),剪枝以后复杂度不变但常数会少巨多,因为少遍历了很多不可能的状态

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> n >> k;
    for (int i = 1;i < n;i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);
    int ans = 0;
    for (int i = 1;i <= k;i++) ans = (ans + dp[1][i]) % mod;
    cout << ans << '\n';
    return 0;
}
posted @ 2022-08-24 14:58  空白菌  阅读(25)  评论(0编辑  收藏  举报