NeFut Logo NeFut
EN 管理员登录

高效生成随机树与图的算法与实践

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #Tree #Graph

核心逻辑与数学原理

在测试大规模图论与树形结构算法(如网络流、最短路、树链剖析)时,传统的均匀随机数据生成(Uniform Random Generation)极易产生两类逻辑退化:

  1. 多重边与自环的非法污染:若直接随机离散生成边集 $E = \{(u, v) \mid u, v \in [1, N]\}$,会高概率引入 $u = v$(自环)或重复边。这会导致部分不设防的图论算法(如 Dijkstra 算法在未去重权时、增广路计数等)陷入死循环或计数值呈指数级偏离。
  2. 树结构的形态严重退化:如果仅通过让节点 $i \ (i > 1)$ 随机向 $[1, i-1]$ 连边来生成树,由于期望父节点偏向较小编号,树的期望高度将收敛于 $O(\log N)$。这种过于平衡的树形态无法测试出专门卡树链剖析、栈深度、长链剖析的极极端情况(如退化为 $O(N)$ 复杂度的单链或扇形菊花树)。

为了保证图论测试数据的物理合法性与多变性,必须引入拓扑结构洗牌(Topological Shuffling)与 Prüfer 序列(Prüfer Sequence) 映射技术。

根据凯莱公式(Cayley's Formula),包含 $N$ 个标号节点的完全图,其生成树的数量为:

$$N^{N-2}$$

Prüfer 序列建立了一棵拥有 $N$ 个节点的标号树与一个长度为 $N-2$、值域为 $[1, N]$ 的整数序列之间的一一映射(双射)。通过在 $[1, N]$ 内完全均匀随机地构造一个长度为 $N-2$ 的随机序列,再利用线性时间 $O(N)$ 或 $O(N \log N)$ 逆向重构出边集,即可在数学上绝对均匀地生成全集 $N^{N-2}$ 中任意形态的树,其概率测度完全一致。


树与图的构造机制推导

1. Prüfer 序列逆向重构树

设随机生成的 Prüfer 序列为 $P = \{p_1, p_2, \dots, p_{n-2}\}$。

2. 高效图论去重与打散

生成无向无权简单图时,需保证 $|E| \le \frac{N(N-1)}{2}$ 且无自环、无重边。


C++ 标准源码

考虑到考场机器对 Python 的支持可能存在环境依赖或执行效率问题(例如 Python 生成 $10^6$ 级别的树结构常数极大),以下提供一套在 C++ 生产环境下,完全基于 C++11 <random> 库、智能指针及 Prufer 序列的高性能、硬核测试数据生成器源码。

#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
#include <set>

using std::cin;
using std::cout;
using std::vector;
using std::pair;

// 高性能硬件级随机数引擎封装
std::mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count());

// 生成区间 [l, r] 内的随机整数
long long randint(long long l, long long r) {
    std::uniform_int_distribution<long long> dist(l, r);
    return dist(rng);
}

// 1. 基于 Prufer 序列生成绝对均匀随机的树结构
void generate_random_tree(int n) {
    if (n <= 1) return;

    vector<int> prufer(n - 2);
    vector<int> degree(n + 1, 1); // 初始度数均为 1 (包含隐式的一条边贡献)

    for (int i = 0; i < n - 2; ++i) {
        prufer[i] = randint(1, n);
        degree[prufer[i]]++;
    }

    // 指针 p 指向当前度数为 1 的最小节点
    int p = 1;
    while (degree[p] != 1) p++;

    int leaf = p;
    vector<pair<int, int>> edges;

    for (int i = 0; i < n - 2; ++i) {
        int v = prufer[i];
        edges.push_back({leaf, v});

        degree[leaf]--;
        degree[v]--;

        if (degree[v] == 1 && v < p) {
            leaf = v; // 优化:如果当前父节点度数减为 1 且编号更小,直接作为下一个叶子
        } else {
            p++;
            while (p <= n && degree[p] != 1) p++;
            leaf = p;
        }
    }

    // 连结最后剩下的两个度数为 1 的节点
    int u = -1, v = -1;
    for (int i = 1; i <= n; ++i) {
        if (degree[i] == 1) {
            if (u == -1) u = i;
            else { v = i; break; }
        }
    }
    if (u != -1 && v != -1) edges.push_back({u, v});

    // 节点编号洗牌映射,打破 1 号节点必然度数特殊的规律
    vector<int> mapping(n + 1);
    for (int i = 1; i <= n; ++i) mapping[i] = i;
    std::shuffle(mapping.begin() + 1, mapping.end(), rng);

    // 打散边集的输出顺序
    std::shuffle(edges.begin(), edges.end(), rng);

    cout << n << "\n";
    for (const auto& edge : edges) {
        // 随机颠倒边的左右端点顺序
        if (randint(0, 1)) {
            cout << mapping[edge.first] << " " << mapping[edge.second] << "\n";
        } else {
            cout << mapping[edge.second] << " " << mapping[edge.first] << "\n";
        }
    }
}

// 2. 生成无自环、无重边的随机简单连通图
void generate_simple_graph(int n, int m) {
    // 致命踩坑点:边数 m 必须满足连通图的最低要求且不超过完全图上限
    if (m < n - 1 || m > 1LL * n * (n - 1) / 2) return;

    std::set<pair<int, int>> edge_set;
    vector<pair<int, int>> final_edges;

    // 策略:先用树结构保证图的绝对连通性,防止生成孤立点导致最短路算法出现 Inf
    for (int i = 2; i <= n; ++i) {
        int fa = randint(1, i - 1); // 快捷生成一棵树
        int u = fa, v = i;
        if (u > v) std::swap(u, v);
        edge_set.insert({u, v});
        final_edges.push_back({u, v});
    }

    // 补齐剩余的 m - (n - 1) 条边
    int remaining = m - (n - 1);
    while (remaining > 0) {
        int u = randint(1, n);
        int v = randint(1, n);
        if (u == v) continue; // 强行拦截自环

        if (u > v) std::swap(u, v);
        if (edge_set.find({u, v}) == edge_set.end()) {
            edge_set.insert({u, v});
            final_edges.push_back({u, v});
            remaining--;
        }
    }

    // 洗牌打散
    std::shuffle(final_edges.begin(), final_edges.end(), rng);

    cout << n << " " << m << "\n";
    for (const auto& edge : final_edges) {
        // 附带随机边权输出示例
        long long weight = randint(1, 100000);
        if (randint(0, 1)) cout << edge.first << " " << edge.second << " " << weight << "\n";
        else cout << edge.second << " " << edge.first << " " << weight << "\n";
    }
}

int main() {
    std::ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // 此处可以通过修改参数生成特定类型的数据
    // 示例:生成包含 10 个节点的随机树
    generate_random_tree(10);

    // 示例:生成包含 10 个节点,15 条边的随机图
    // generate_simple_graph(10, 15);

    return 0;
}

NOIP 实战避坑指南

1. 图论边数边界上界溢出

同时注意乘法操作必须强转 1LL,否则 $N \times (N-1)$ 会在 $N \ge 5 \times 10^4$ 时直接触发 int32 符号溢出,变成一个负数,从而导致校验失效。

2. 伪随机数引擎的周期坍塌(rand() 的滥用)


经典 NOIP/洛谷 真题

1. 洛谷 P3379 [模板] 最近公共祖先 (LCA)

2. 洛谷 P4779 [模板] 单源最短路径 (标准版)

原文链接: local://23.5

[h] 返回首页