仙人掌生成器

仙人掌生成器

背景

网上找不到正确生成仙人掌的 gen,唯一的一篇似乎是错误的,所以手写一篇。

仙人掌的定义

仙人掌:任意一条边最多出现在一个简单环中的无向连通图。

生成算法

给定 \(n\) 作为仙人掌结点数。先随机生成一棵树,作为目标仙人掌的一棵 dfs 树,这保证了图连通这一条件,接下来要做的就是不断往这棵树上加非树边,同时满足「任意一条边最多出现在一个简单环中」这一限制条件。可选地,我们可以限制边数上界。

考虑 dfs 这棵树,当前在 \(u\),尝试添加一条以 \(u\) 为上端点的非树边,那么下端点 \(v\) 需要满足:\(u\rightarrow v\) 路径上的树边均未被环包含。于是我们可以在 dfs 的时候 return 出一个 \(v\),作为未来可能作为下端点的点。

\(u\) 的初始返回值就是其本身。考虑 dfs 完 \(u\) 的一个孩子 \(v\),返回值为 \(t\),我们有一定概率用 \(t\) 替换 \(u\) 的返回值,如果不作为范围值,有一定概率尝试将 \(u,t\) 之间加一条非树边。

另外,我们可以在此基础上进行适当扩展,例如,要求环必须全都是奇环,要求不能有重边。注意,在允许重边的前提下,重边的个数不能超过 \(2\) 条。

算法分析

该算法的时间复杂度为:\(\mathcal{O}(n+m)\)。

虽然该算法不能保证等概率地,在所有 \(n\) 个结点的仙人掌中,随机选择一棵仙人掌,但是作为对拍的 gen 已经足够了。

代码实现

使用 C++ 实现,请使用 C++14 及以上版本编译。

支持修改随机数种子(默认为当前时间戳)、是否允许重边、是否只允许奇环、点数、边数上界(默认为 \(-1\) 即不限制)。

将生成的仙人掌输出到标准输出流,额外的信息、错误信息输出到标准错误流。程序返回值为 \(0\) 表示生成成功。

输出格式:第一行两个以空格分隔的整数 \(n,m\),分别表示仙人掌的结点数、边数;接下来 \(m\) 行表示仙人掌的边。

#include <cstdio>

#include <iostream>

#include <vector>

#include <random>

#include <chrono>

using namespace std;

/* =========== Parameter =========== */

const int SEED = chrono::system_clock::now().time_since_epoch().count();

bool multiedge = false;

bool onlyOddCircle = false;

int n = 1000000;

int m_limit = -1; // -1 for not limit

/* =========== Parameter =========== */

void _err(const char* msg, int lineNum) {

    fprintf(stderr, "Error at line #%d: %s\n", lineNum, msg);

    exit(1);

}

#define err(msg) _err(msg, __LINE__)

inline int rand(int l, int r) {

    static mt19937 rnd(SEED);

    if (l > r) err("invalid range");

    return l + rnd() % (r - l + 1);

}

vector<pair<int, int>> edges;

vector<vector<int>> son(n, vector<int>());

vector<int> dpt(n);

int dfs(int u) {

    int res = u;

 int cnt = 0;

    for (size_t i = 0; i < son[u].size(); ++i) {

        int v = son[u][i];

  dpt[v] = dpt[u] + 1;

        int t = dfs(v);

        if (rand(0, son[u].size()) == 0)

            res = t;

        else if ((t != v || (multiedge && cnt < 2))

                && ((dpt[t] - dpt[u] + 1) % 2 == 1 || !onlyOddCircle)

                && (m_limit == -1 || (int)edges.size() < m_limit))

            edges.emplace_back(u, t), cnt += t == v;

    }

    return res;

}

signed main() {

    // freopen("yzh", "w", stdout);

    

    if (n < 1) err("n shouldn't be less than 1");

    if (m_limit != -1 && m_limit < n - 1)

        err("m_limit shouldn't less than n-1");

    

    for (int i = 1; i < n; ++i) {

        int fa = rand(0, i - 1);

        edges.emplace_back(fa, i);

        son[fa].emplace_back(i);

    }

    dfs(0);

    

    for (size_t i = 1; i < edges.size(); ++i)

        swap(edges[i], edges[rand(0, i)]);

    

    printf("%d %d\n", n, (int)edges.size());

    for (size_t i = 0; i < edges.size(); ++i) {

        int u = edges[i].first;

        int v = edges[i].second;

        if (rand(0, 1)) swap(u, v);

        printf("%d %d\n", u + 1, v + 1);

    }

    

    fprintf(stderr, "Success!\n");

    fprintf(stderr, "n = %d, m = %d\n", n, (int)edges.size());

    fprintf(stderr, "circle = %d\n", (int)edges.size() - (n - 1));

    return 0;

}

Checker

写了一个程序用来验证 gen 的正确性。使用并查集判断连通性、dfs 序求 \(\operatorname{lca}\)、差分完成树链覆盖。时间复杂度是 \(\mathcal{O}(m+n\log n)\) 的,尽管可以优化到线性 \(\mathcal{O}(n+m)\)。

支持修改是否检查仅允许奇环。

从标准输入流读入图,程序返回值为 \(0\) 表示图为仙人掌。

#include <cstdio>

#include <iostream>

#include <vector>

using namespace std;

/* =========== Parameter =========== */

bool checkOnlyOddCircle = false;

const int N = 1e6 + 10;

/* =========== Parameter =========== */

void _err(const char* msg, int lineNum) {

    fprintf(stderr, "Error at line #%d: %s\n", lineNum, msg);

    exit(1);

}

#define err(msg) _err(msg, __LINE__)

const int lgN = __lg(N) + 1;

int n, m;

namespace $dsu {

int fa[N];

int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }

}

vector<int> son[N];

vector<pair<int, int>> edges;

int fa[N], dpt[N];

int st[lgN][N], idx[N], timer;

void dfs(int u) {

    st[0][idx[u] = ++timer] = u;

    for (int v : son[u]) {

        if (v == fa[u]) continue;

        fa[v] = u, dpt[v] = dpt[u] + 1;

        dfs(v);

    }

}

inline int Min(int u, int v) {

    return dpt[u] < dpt[v] ? u : v;

}

inline int lca(int u, int v) {

    if (u == v) return u;

    if ((u = idx[u]) > (v = idx[v]))

        swap(u, v);

    int p = __lg(v - u++);

    return fa[Min(st[p][u], st[p][v - (1 << p) + 1])];

}

int sum[N];

void redfs(int u) {

    for (int v : son[u]) {

        if (v == fa[u]) continue;

        redfs(v);

        sum[u] += sum[v];

    }

    if (sum[u] > 2) err("an edge appears in more than one simple circle");

}

signed main() {

    scanf("%d%d", &n, &m);

    if (n < 1) err("n shouldn't be less than 1");

    if (n > 1000000) err("n is too big that input can't be determined");

    for (int i = 1; i <= n; ++i) $dsu::fa[i] = i;

    for (int i = 1, u, v; i <= m; ++i) {

        scanf("%d%d", &u, &v);

        if (u == v) err("self-loop exists");

        if (u < 1 || u > n) err("node number out of range");

        if (v < 1 || v > n) err("node number out of range");

        int tu = $dsu::get(u), tv = $dsu::get(v);

        if (tu == tv) {

            edges.emplace_back(u, v);

        } else {

            $dsu::fa[tu] = tv;

            son[u].emplace_back(v);

            son[v].emplace_back(u);

        }

    }

    for (int i = 2; i <= n; ++i)

        if ($dsu::get(i) != $dsu::get(1))

            err("graph not connected");

    dfs(1);

    for (int k = 1; k < lgN; ++k)

        for (int i = 1; i + (1 << k) - 1 <= n; ++i)

            st[k][i] = Min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);

    for (size_t i = 0; i < edges.size(); ++i) {

        int u = edges[i].first;

        int v = edges[i].second;

        int p = lca(u, v);

        ++sum[u], +

+sum[v];

        sum[p] -= 2;

        

        int len = dpt[u] + dpt[v] - 2 * dpt[p] + 1;

        if (len % 2 == 0 && checkOnlyOddCircle)

            err("odd circle exists");

    }

    redfs(1);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

牛马程序员2025

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值