1 条题解
-
0
题解:树中点覆盖的关键点组合计数问题
一、题目核心分析
1. 问题本质
给定一棵 个节点的树和一个函数 (),要求统计满足以下条件的关键点组合 的数量:
- 对于每个节点 , 是距离 最近的关键点的最小索引(若多个关键点距离相同,取索引最小的)。
- 关键点组合中 是树的节点(可重复?不,因每个 是独立关键点,且需满足覆盖条件,实际隐含 需对应其支配区域)。
2. 关键约束
(1)支配区域的连通性**
对于每个索引 ,令 ( 的支配区域),则 必须是连通的。因为若 不连通,中间会被其他 ()分隔,导致分隔区域的节点距离 更近,与 矛盾。
(2)关键点的支配优先级
索引越小的关键点优先级越高:对于 , 中的所有节点到 的距离必须严格小于到任何 的距离;若距离相等,会优先选择索引更小的 ,与 矛盾。
(3)关键点的连通性校验
所有关键点对应的支配区域必须构成树的一个覆盖,且通过并查集可验证:将每个节点 与其相邻且 的节点 合并,最终必须形成一个包含所有 个关键点类型的连通分量(代码中
check函数实现)。二、完整代码
#include <bits/stdc++.h> using namespace std; namespace QYB { using ll = long long; const int P = 998244353; int n, m, c[3005], fa[3005], f[3005][3005], g[3005][3005]; vector<int> t[3005]; // 并查集查找(路径压缩) int get(int x) { return fa[x] != x ? fa[x] = get(fa[x]) : x; } // 快速读入(适配大数据量) char getchar() { static char buf[1 << 25], *p1 = buf, *p2 = buf; return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 25, stdin)) == buf ? EOF : *p1++; } int read() { int res = 0; char ch = getchar(); while (ch < 48 || ch > 57) ch = getchar(); while (ch >= 48 && ch <= 57) res = res * 10 + ch - 48, ch = getchar(); return res; } // 并查集合并 bool merge(int x, int y) { x = get(x); y = get(y); if (x == y) return false; fa[x] = y; return true; } // 校验是否满足基本连通性条件 bool check() { int tmp = m; // m 即 k(关键点个数) // 初始化并查集 for (int i = 1; i <= n; i++) fa[i] = i; // 合并每个节点与其相邻且 f(u) <= f(v) 的节点 for (int i = 1; i <= n; i++) { for (int j : t[i]) { if (c[i] >= c[j]) { // c[i] 即 f(i),合并优先级不低于自身的相邻节点 tmp -= merge(c[i], c[j]); } } } // 最终需形成 1 个连通分量(所有关键点类型连通) return tmp == 1; } // 树形DP:计算合法关键点组合数 void dfs(int cur, int fr) { // f[cur][d]:以 cur 为根的子树中,cur 到其关键点 a_{c[cur]} 的距离为 d 的方案数 f[cur][0] = 1; for (int i = 1; i <= n; i++) f[cur][i] = 0; // g[cur][d]:辅助DP,用于合并子树时的过渡计算 for (int i = 0; i <= n; i++) g[cur][i] = 1; for (int to : t[cur]) { if (to == fr) continue; dfs(to, cur); // 递归处理子树 if (c[cur] == c[to]) { // 子节点与当前节点属于同一支配区域(f(to) = f(cur)) // 更新 f[cur]:同一区域内,cur 和 to 到 a_{c[cur]} 的距离需满足 d_cur = d_to ± 1 或 d_cur = d_to for (int i = 0; i <= n; i++) { // 方案1:cur 的距离为 i,to 的距离为 i(通过 g[to][i] 累积) f[cur][i] = (ll)f[cur][i] * g[to][i] % P; // 方案2:cur 的距离为 i,to 的距离为 i-1(i>0 时) if (i > 0) { (f[cur][i] += (ll)g[cur][i-1] * f[to][i-1] % P) %= P; } } // 更新 g[cur]:用于后续子树合并 for (int i = 0; i <= n; i++) { g[cur][i] = (i < n) ? (ll)g[cur][i] * g[to][i+1] % P : 0; } } else { // 子节点与当前节点属于不同支配区域(f(to) ≠ f(cur)) if (c[cur] < c[to]) { // 当前节点优先级更高(f(cur) < f(to)) // 更新 f[cur]:to 到 a_{c[cur]} 的距离必须 < 到 a_{c[to]} 的距离 for (int i = 0; i <= n; i++) { // tmp:to 中满足 d_to < d_{to, a_{c[to]}} 的方案数(d_{to, a_{c[cur]}} = i+1) int tmp = f[to][i]; // d_{to, a_{c[to]}} > i+1 → 取 f[to][i] if (i > 0) { (tmp += f[to][i-1]) %= P; // d_{to, a_{c[to]}} == i+1 → 但 cur 优先级更高,仍合法 } f[cur][i] = (ll)f[cur][i] * tmp % P; } // 更新 g[cur] for (int i = 0; i <= n; i++) { int tmp = (i < n) ? f[to][i+1] : 0; (tmp += f[to][i]) %= P; g[cur][i] = (ll)g[cur][i] * tmp % P; } } else { // 子节点优先级更高(f(cur) > f(to)) // 更新 f[cur]:cur 到 a_{c[to]} 的距离必须 > to 到 a_{c[to]} 的距离 for (int i = 0; i <= n; i++) { // tmp:to 中满足 d_{cur, a_{c[to]}} = i+1 > d_to 的方案数 int tmp = f[to][i]; // d_to < i+1 → 取 f[to][i] if (i < n) { (tmp += f[to][i+1]) %= P; // d_to == i+1 → 但 to 优先级更高,仍合法 } f[cur][i] = (ll)f[cur][i] * tmp % P; } // 更新 g[cur] for (int i = 0; i <= n; i++) { int tmp = (i < n) ? f[to][i+1] : 0; if (i < n - 1) { (tmp += f[to][i+2]) %= P; } g[cur][i] = (ll)g[cur][i] * tmp % P; } } } } } int main() { for (int T = read(); T; T--) { // 初始化 for (int i = 1; i <= n; i++) t[i].clear(); n = read(); m = read(); // m 存储 k(关键点个数) // 读入树的边 for (int i = 2; i <= n; i++) { int u = read(), v = read(); t[u].push_back(v); t[v].push_back(u); } // 读入 f 数组(c[i] = f(i)) for (int i = 1; i <= n; i++) { c[i] = read(); } // 校验基本条件,不满足则输出 0 if (!check()) { printf("0\n"); continue; } // 树形DP计算方案数 dfs(1, 0); // 答案为根节点(1)所有可能距离的方案数之和 int ans = 0; for (int i = 0; i <= n; i++) { (ans += f[1][i]) %= P; } printf("%d\n", ans); } return 0; } } int main() { return QYB::main(); }三、关键逻辑详解
1. 预处理:连通性校验(
check函数)该函数用于快速排除明显不合法的情况,核心思想是:
- 每个节点 的支配区域 必须与优先级更高()的区域连通,否则无法满足“最近关键点”的条件。
- 利用并查集合并节点与其相邻且 的节点,最终需形成一个连通分量(所有关键点类型连通),否则输出 。
2. 树形DP设计
(1)DP状态定义
- :以 为根的子树中, 到其支配区域的关键点 ()的距离为 时,合法的关键点组合数。
- :辅助DP数组,用于合并子树时的过渡计算,存储子树中“兼容当前节点距离 ”的方案数累积。
(2)DP转移逻辑
DP转移分两种情况:当前节点与子节点的支配区域相同()或不同()。
情况1:(同一支配区域)
- 同一区域内的节点到关键点 的距离必须满足连通性(相邻节点距离差 ≤ 1)。
- 转移方程:
- $f[cur][i] = f[cur][i] \times g[to][i] + g[cur][i-1] \times f[to][i-1]$():
- 第一项: 距离为 , 距离为 (兼容);
- 第二项: 距离为 , 距离为 (相邻节点距离差 1)。
- :更新辅助数组,为后续子树合并做准备。
- $f[cur][i] = f[cur][i] \times g[to][i] + g[cur][i-1] \times f[to][i-1]$():
情况2:(不同支配区域)
根据优先级( 或 )分别处理:
- 若 (当前节点优先级更高):
- 到 的距离()必须 ≤ 到 的距离(否则 应等于 )。
- 转移时累积 中满足该条件的方案数。
- 若 (子节点优先级更高):
- 到 的距离()必须 > 到 的距离(否则 应等于 )。
- 转移时累积 中满足该条件的方案数。
3. 答案计算
根节点(任选 1 作为根,树的无根性不影响结果)的所有可能距离 对应的方案数之和,即为合法关键点组合的总数。
四、样例解析
样例 1 第三组输入
3 2 1 2 2 3 2 1 1- ,,树为链 ,。
- 支配区域:(连通),(连通)。
- 连通性校验:合并后形成 1 个连通分量,满足条件。
- DP计算:
- 根节点为 1,,需计算 的和。
- 合法关键点组合:
- ,: 到 距离 0, 到 距离 0, 到 距离 1,满足条件。
- ,: 到 距离 0, 到 距离 1, 到 距离 0,满足条件。
- 总方案数为 2,与样例输出一致。
五、复杂度分析
- 时间复杂度:。每组测试数据的树形DP需遍历树的所有节点,每个节点的DP转移需遍历 个距离值,适配 的数据范围( 的平方为 ,乘以 后仍可接受)。
- 空间复杂度:。DP数组 和 均为 大小,适配数据范围。
总结
本题的核心是将“关键点组合计数”转化为“树形DP问题”,通过以下步骤解决:
- 预处理校验支配区域的连通性和优先级约束;
- 设计双DP数组( 和 ),覆盖同一/不同支配区域的转移场景;
- 累积根节点的所有合法方案数,得到最终答案。
- 1
信息
- ID
- 5462
- 时间
- 3000ms
- 内存
- 1024MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者