把边遍历看成线图 DFS 树计数,先算单根方案,再用树形 DP 统计关键边对的重复贡献。
OJ: luogu
题目 ID: P11363
难度:提高+/省选-
标签:树形结构动态规划组合计数
日期: 2026-06-22 19:19
题意
给定一棵 n 个点的树。现在把原树中的每条边看成一个新结点,两条边有公共端点就认为它们相邻。
从一条关键边出发,按 DFS 的方式遍历所有边。每次第一次走到一条新边时,就在“上一条边”和“这条新边”对应的新结点之间连边。最终会得到一棵由 n-1 个新结点组成的新树。
问从任意关键边出发,可能得到多少种不同的新树。
思路
先看一个小数据暴力:在线图上直接枚举 DFS 的每一步选择,把得到的新树放进集合去重。
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 12;
int case_id, T;
int n, k;
pair<int, int> edge_list[MAXN];
int key_edge[MAXN];
bool adjacent_edge[MAXN][MAXN];
set<vector<pair<int, int> > > all_trees;
void dfs_line_graph(int root, int current, int visited_mask, vector<int> parent_stack, vector<pair<int, int> > tree_edges) {
bool moved = false;
for (int i = 1; i < n; i++) {
if ((visited_mask & (1 << (i - 1))) || !adjacent_edge[current][i]) {
continue;
}
moved = true;
vector<pair<int, int> > next_edges = tree_edges;
int a = current, b = i;
if (a > b) {
swap(a, b);
}
next_edges.push_back(make_pair(a, b));
vector<int> next_stack = parent_stack;
next_stack.push_back(current);
dfs_line_graph(root, i, visited_mask | (1 << (i - 1)), next_stack, next_edges);
}
if (moved) {
return;
}
if (current == root) {
if (visited_mask == (1 << (n - 1)) - 1) {
sort(tree_edges.begin(), tree_edges.end());
all_trees.insert(tree_edges);
}
} else {
int parent = parent_stack.back();
parent_stack.pop_back();
dfs_line_graph(root, parent, visited_mask, parent_stack, tree_edges);
}
}
bool share_endpoint(pair<int, int> a, pair<int, int> b) {
return a.first == b.first || a.first == b.second || a.second == b.first || a.second == b.second;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> case_id >> T;
while (T--) {
cin >> n >> k;
for (int i = 1; i < n; i++) {
cin >> edge_list[i].first >> edge_list[i].second;
}
for (int i = 1; i <= k; i++) {
cin >> key_edge[i];
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < n; j++) {
adjacent_edge[i][j] = (i != j && share_endpoint(edge_list[i], edge_list[j]));
}
}
all_trees.clear();
for (int i = 1; i <= k; i++) {
vector<int> empty_stack;
vector<pair<int, int> > empty_edges;
int root = key_edge[i];
dfs_line_graph(root, root, 1 << (root - 1), empty_stack, empty_edges);
}
cout << all_trees.size() << '\n';
}
return 0;
}暴力会枚举大量 DFS 顺序,无法处理 n=10^5。
先考虑只有一条关键边作为起点。原树中某个点 u 连接着 deg(u) 条边。DFS 从其中一条边来到这个点附近后,剩下 deg(u)-1 条边可以按任意顺序被展开,所以这个点贡献:
(deg(u)-1)!所有点相互独立,因此单个起点的方案数是:
base = prod (deg(u)-1)!如果有 k 条关键边,先看成有 k * base 种。但同一棵新树可能由两条不同关键边作为起点得到,需要减去重复。
两条关键边会重复,取决于它们在原树中之间的路径。路径经过点 u 时,路径两侧的两条边必须在 u 的局部 DFS 顺序中紧挨着。相比任意顺序,这会让局部方案数除以:
deg(u)-1于是定义点权:
w(u) = 1 / (deg(u)-1) deg(u)>1
w(u) = 1 deg(u)=1一对关键边的重复权值,就是它们之间路径上点权的乘积。设所有关键边对的重复权值和为 pair_bad,答案为:
base * (k - pair_bad)现在问题变成:求所有关键边对之间的路径点权乘积之和。
把原树任意定根,做树形 DP:
g[u]:从u的子树中某条关键边走到u的路径权值和;f[u]:u子树内已经统计出的关键边对重复权值和。
合并 u 的多个孩子时,不同孩子子树中的关键边对会在 u 处相遇,贡献要乘上 w(u)。如果 u 的父边本身是关键边,它也可以和子树中的关键边配对。
用这个 DP 求出 pair_bad = f[root],再代入公式即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const long long MOD = 1000000007LL;
struct Edge {
int to;
int id;
};
int case_id, T;
int n, k;
int degree_cnt[MAXN];
bool is_key[MAXN];
long long fact[MAXN], inv_num[MAXN];
long long weight_node[MAXN];
long long f[MAXN], g[MAXN];
int parent_node[MAXN], parent_edge[MAXN];
vector<Edge> tree[MAXN];
vector<int> order_list;
long long power_mod(long long a, long long b) {
long long res = 1;
a %= MOD;
while (b > 0) {
if (b & 1LL) {
res = res * a % MOD;
}
a = a * a % MOD;
b >>= 1LL;
}
return res;
}
void prepare_factorials() {
fact[0] = 1;
for (int i = 1; i < MAXN; i++) {
fact[i] = fact[i - 1] * i % MOD;
}
for (int i = 1; i < MAXN; i++) {
inv_num[i] = power_mod(i, MOD - 2);
}
}
void clear_case() {
for (int i = 1; i <= n; i++) {
tree[i].clear();
degree_cnt[i] = 0;
f[i] = g[i] = 0;
parent_node[i] = parent_edge[i] = 0;
}
for (int i = 1; i < n; i++) {
is_key[i] = false;
}
order_list.clear();
}
void build_rooted_tree() {
order_list.clear();
order_list.push_back(1);
parent_node[1] = 0;
parent_edge[1] = 0;
for (int i = 0; i < (int)order_list.size(); i++) {
int u = order_list[i];
for (int j = 0; j < (int)tree[u].size(); j++) {
int v = tree[u][j].to;
int id = tree[u][j].id;
if (v == parent_node[u]) {
continue;
}
parent_node[v] = u;
parent_edge[v] = id;
order_list.push_back(v);
}
}
}
long long solve_one() {
long long base = 1;
for (int i = 1; i <= n; i++) {
base = base * fact[degree_cnt[i] - 1] % MOD;
if (degree_cnt[i] <= 1) {
weight_node[i] = 1;
} else {
weight_node[i] = inv_num[degree_cnt[i] - 1];
}
}
build_rooted_tree();
for (int idx = (int)order_list.size() - 1; idx >= 0; idx--) {
int u = order_list[idx];
long long sum_g = 0;
long long sum_square = 0;
f[u] = 0;
for (int j = 0; j < (int)tree[u].size(); j++) {
int v = tree[u][j].to;
if (parent_node[v] != u) {
continue;
}
f[u] += f[v];
if (f[u] >= MOD) {
f[u] -= MOD;
}
sum_g += g[v];
if (sum_g >= MOD) {
sum_g -= MOD;
}
sum_square = (sum_square + g[v] * g[v]) % MOD;
}
long long pair_sum = (sum_g * sum_g % MOD - sum_square + MOD) % MOD;
pair_sum = pair_sum * inv_num[2] % MOD * weight_node[u] % MOD;
f[u] = (f[u] + pair_sum) % MOD;
if (is_key[parent_edge[u]]) {
f[u] = (f[u] + sum_g * weight_node[u]) % MOD;
g[u] = 1;
} else {
g[u] = sum_g * weight_node[u] % MOD;
}
}
long long choose_one = k % MOD;
long long multiplier = (choose_one - f[1] + MOD) % MOD;
return base * multiplier % MOD;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
prepare_factorials();
cin >> case_id >> T;
while (T--) {
cin >> n >> k;
clear_case();
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
tree[u].push_back((Edge){v, i});
tree[v].push_back((Edge){u, i});
degree_cnt[u]++;
degree_cnt[v]++;
}
for (int i = 1; i <= k; i++) {
int id;
cin >> id;
is_key[id] = true;
}
cout << solve_one() << '\n';
}
return 0;
}复杂度
每组数据只需要遍历原树常数次,时间复杂度为
空间复杂度为
总结
本题的关键是把“边的 DFS”理解成原树线图上的 DFS 树计数。
单个起点的数量是每个原树点局部展开顺序的乘积。多个关键边起点的重复,则由关键边对之间的路径决定,用树形 DP 统计所有路径权值和即可。

