[NOIP2024] 树的遍历

GitHub跳转原题关系图返回列表

把边遍历看成线图 DFS 树计数,先算单根方案,再用树形 DP 统计关键边对的重复贡献。

OJ: luogu

题目 ID: P11363

难度:提高+/省选-

标签:树形结构动态规划组合计数

日期: 2026-06-22 19:19

题意

给定一棵 n 个点的树。现在把原树中的每条边看成一个新结点,两条边有公共端点就认为它们相邻。

从一条关键边出发,按 DFS 的方式遍历所有边。每次第一次走到一条新边时,就在“上一条边”和“这条新边”对应的新结点之间连边。最终会得到一棵由 n-1 个新结点组成的新树。

问从任意关键边出发,可能得到多少种不同的新树。

思路

先看一个小数据暴力:在线图上直接枚举 DFS 的每一步选择,把得到的新树放进集合去重。

cpp
// 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 条边可以按任意顺序被展开,所以这个点贡献:

text
(deg(u)-1)!

所有点相互独立,因此单个起点的方案数是:

text
base = prod (deg(u)-1)!

如果有 k 条关键边,先看成有 k * base 种。但同一棵新树可能由两条不同关键边作为起点得到,需要减去重复。

两条关键边会重复,取决于它们在原树中之间的路径。路径经过点 u 时,路径两侧的两条边必须在 u 的局部 DFS 顺序中紧挨着。相比任意顺序,这会让局部方案数除以:

text
deg(u)-1

于是定义点权:

text
w(u) = 1 / (deg(u)-1)    deg(u)>1
w(u) = 1                deg(u)=1

一对关键边的重复权值,就是它们之间路径上点权的乘积。设所有关键边对的重复权值和为 pair_bad,答案为:

text
base * (k - pair_bad)

现在问题变成:求所有关键边对之间的路径点权乘积之和。

把原树任意定根,做树形 DP:

  • g[u]:从 u 的子树中某条关键边走到 u 的路径权值和;
  • f[u]u 子树内已经统计出的关键边对重复权值和。

合并 u 的多个孩子时,不同孩子子树中的关键边对会在 u 处相遇,贡献要乘上 w(u)。如果 u 的父边本身是关键边,它也可以和子树中的关键边配对。

用这个 DP 求出 pair_bad = f[root],再代入公式即可。

代码

cpp
#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;
}

复杂度

每组数据只需要遍历原树常数次,时间复杂度为 O(n)O(n)

空间复杂度为 O(n)O(n)

总结

本题的关键是把“边的 DFS”理解成原树线图上的 DFS 树计数。

单个起点的数量是每个原树点局部展开顺序的乘积。多个关键边起点的重复,则由关键边对之间的路径决定,用树形 DP 统计所有路径权值和即可。