Gerador Probabilístico de Grafos Cacto

Definição e Propriedades Estruturais

Um grafo cacto é formalmente definido como um grafo não direcionado e conexo no qual cada aresta está contida em, no máximo, um ciclo simples. Esta restrição topológica impede a sobreposição de arestas entre diferentes ciclos, permitindo apenas o compartilhamento de vértices isolados.

Estratégia de Construção Algorítmica

Para sintetizar um grafo válido com n vértices, o procedimento inicia-se pela criação de uma árvore aleatória que servirá como esqueleto de busca em profundidade (DFS). Esta fase assegura a conectividade global. Em seguida, arestas extras (back-edges) são introduzidas de forma controlada, mantendo a invariantes fundamental da classe cacto.

Durante o percorrer a árvore a partir de um nó u, o algoritmo rastreia um vértice candidato c que poderia servir como destino de uma nova ligação. A condição crítica exige que o caminho na árvore entre u e c esteja completamente livre de arestas já alocadas a outros ciclos. Ao finalizar a chamada recursiva para um filho v, obtém-se um valor de retorno t. Com base em uma distribuição probabilística, o sistema pode optar por propagar tpara o nó atual ou, alternativamente, materializar a aresta(u, t), desde que atendidos os critérios de paridade, limite superior de arestas e permissão de multiedges.

Complexidade e Distribuição

A execução completa opera em tempo linear O(n + m). É importante observar que o método não gera grafos cacto com probabilidade uniforme sobre o espaço amostral de todos os grafos válidos de tamanho n. Contudo, a variação topológica produzida é amplamente suficiente para validação cruzada (stress testing) e testes de robustez.

Implementação do Gerador

O código abaixo, desenvolvido em C++17, expõe parâmetros configuráveis via constantes. A representação do grafo é impressa no fluxo padrão, enquanto métricas operacionais e diagnósticos são direcionados a stderr.

// Configurações operacionais const unsigned int SEMENTE = chrono::system_clock::now().time_since_epoch().count(); bool permitir_arestas_duplas = false; bool forcar_ciclos_impares = false; int total_nos = 500000; int limite_arestas = -1; // -1 indica ausência de restrição

mt19937 gerador(SEMENTE); vector<pair<int, int>> lista_arcos; vector<vector> filhos; vector profundidade;

inline int sortear_intervalo(int inf, int sup) { return uniform_int_distribution(inf, sup)(gerador); }

int processar_no(int atual) { int candidato = atual; for (int prox : filhos[atual]) { profundidade[prox] = profundidade[atual] + 1; int retorno_filho = processar_no(prox);

    // Escolha probabilística: propagar retorno ou criar aresta extra
    if (sortear_intervalo(0, (int)filhos[atual].size()) == 0) {
        candidato = retorno_filho;
    } else {
        bool elegibilidade = (retorno_filho != prox || permitir_arestas_duplas) &&
                             (!forcar_ciclos_impares || (profundidade[retorno_filho] - profundidade[atual] + 1) % 2 != 0) &&
                             (limite_arestas == -1 || (int)lista_arcos.size() < limite_arestas);
        if (elegibilidade) lista_arcos.emplace_back(atual, retorno_filho);
    }
}
return candidato;

}

int main() { if (total_nos < 1) { cerr << "Falha: quantidade invalida de vertices" << endl; return 1; } if (limite_arestas != -1 && limite_arestas < total_nos - 1) { cerr << "Falha: limite inferior incompativel com arvore" << endl; return 1; }

filhos.assign(total_nos, vector<int>());
profundidade.assign(total_nos, 0);

// Geracao da arvore base aleatoria (0-indexed interno)
for (int i = 1; i < total_nos; ++i) {
    int pai = sortear_intervalo(0, i - 1);
    lista_arcos.emplace_back(pai, i);
    filhos[pai].push_back(i);
}

processar_no(0);

// Embaralhamento da ordem das arestas
for (size_t i = 1; i < lista_arcos.size(); ++i)
    swap(lista_arcos[i], lista_arcos[sortear_intervalo(0, (int)i)]);

cout << total_nos << " " << lista_arcos.size() << "\n";
for (auto& par : lista_arcos) {
    if (sortear_intervalo(0, 1)) swap(par.first, par.second);
    cout << par.first + 1 << " " << par.second + 1 << "\n";
}

cerr << "Execucao finalizada. Vertices: " << total_nos
     << " | Arestas: " << lista_arcos.size()
     << " | Ciclos adicionados: " << lista_arcos.size() - (total_nos - 1) << "\n";
return 0;

}


</details>Lógica do Validador (Checker)
-----------------------------

A verificação da integridade cacto utiliza uma estrutura de união e busca (DSU) para separar arestas da árvore geradora das arestas formadoras de ciclos. Para assegurar que nenhuma aresta arbórea participe de múltiplos ciclos, aplica-se um acumulador de diferenças na estrutura hierárquica, combinado com consultas de menor ancestral comum (LCA) por esparsa binária. Caso o modo de validação restrita esteja ativo, o comprimento de cada ciclo é verificado explicitamente.

<details><summary>Código Fonte do Validador</summary>```
#include <bits/stdc++.h>
using namespace std;

const int LIMITE_NOS = 1000010;
bool validar_somente_impares = false;

namespace conjuntos {
    int representante[LIMITE_NOS];
    int buscar(int x) { return representante[x] == x ? x : representante[x] = buscar(representante[x]); }
    void unir(int a, int b) { representante[buscar(a)] = buscar(b); }
}

int n, m;
vector<int> adj[LIMITE_NOS];
vector<pair<int, int>> arestas_extras;
int pai[LIMITE_NOS], nivel[LIMITE_NOS], tempo_entrada[LIMITE_NOS];
int ancestral[LIMITE_NOS][21];
int contador[LIMITE_NOS];
int cronometro = 0;

void preparar_lca(int u, int p, int h) {
    pai[u] = p;
    nivel[u] = h;
    tempo_entrada[u] = ++cronometro;
    ancestral[u][0] = p;
    for (int k = 1; k <= 20; ++k)
        ancestral[u][k] = ancestral[ancestral[u][k-1]][k-1];
    for (int v : adj[u]) if (v != p) preparar_lca(v, u, h + 1);
}

int consultar_lca(int u, int v) {
    if (nivel[u] < nivel[v]) swap(u, v);
    for (int k = 20; k >= 0; --k)
        if (nivel[u] - (1 << k) >= nivel[v]) u = ancestral[u][k];
    if (u == v) return u;
    for (int k = 20; k >= 0; --k)
        if (ancestral[u][k] != ancestral[v][k]) { u = ancestral[u][k]; v = ancestral[v][k]; }
    return ancestral[u][0];
}

void propagar(int u) {
    for (int v : adj[u]) {
        if (v != pai[u]) {
            propagar(v);
            contador[u] += contador[v];
        }
    }
    if (contador[u] > 1) { cerr << "Invalido: aresta compartilhada por multiplos ciclos" << endl; exit(1); }
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    if (!(cin >> n >> m)) return 1;
    if (n < 1) { cerr << "Falha: parametro n invalido" << endl; return 1; }

    iota(conjuntos::representante, conjuntos::representante + n + 1, 0);
    for (int i = 0; i < m; ++i) {
        int x, y; cin >> x >> y;
        if (x < 1 || x > n || y < 1 || y > n) { cerr << "Falha: vertice fora do dominio" << endl; return 1; }
        int ra = conjuntos::buscar(x), rb = conjuntos::buscar(y);
        if (ra == rb) {
            arestas_extras.emplace_back(x, y);
        } else {
            conjuntos::unir(ra, rb);
            adj[x].push_back(y);
            adj[y].push_back(x);
        }
    }

    for (int i = 2; i <= n; ++i)
        if (conjuntos::buscar(i) != conjuntos::buscar(1)) {
            cerr << "Falha: estrutura desconexa" << endl; return 1;
        }

    preparar_lca(1, 1, 0);
    for (auto& par : arestas_extras) {
        int a = par.first, b = par.second, lca = consultar_lca(a, b);
        contador[a]++, contador[b]++;
        contador[lca] -= 2;
        int comprimento = nivel[a] + nivel[b] - 2 * nivel[lca] + 1;
        if (validar_somente_impares && comprimento % 2 == 0) {
            cerr << "Falha: ciclo par detectado na verificacao" << endl; return 1;
        }
    }
    propagar(1);
    cout << "Validacao bem-sucedida. Total de ciclos: " << arestas_extras.size() << "\n";
    return 0;
}

A aleatoriedade inerente ao algoritmo base tende a produzir anéis de dimensão reduzida. Para cenários que demandam estruturas com ciclos maiores, recomenda-se uma abordagem composta: inicializar um ciclo raiz com o comprimento desejado e, subsequentemente, aplicar o procedimento de geração cacto recursivamente sobre cada vértice pertencente a esse anel principal, preservando a invariantes de não-sobreposição de arestas em todas as ramificações.

Tags: grafos-cactus geracao-probabilistica menor-ancestral-comum uniao-e-busca validacao-de-grafos

Publicado em 8-13 08:18