01

Começo

src/lib.rs

Adicione o crate, leia uma lista de arestas, construa uma representação e chame os algoritmos. Os vértices são numerados a partir de 1, exatamente como nos arquivos de entrada.

[dependencies]
graphman = { git = "https://github.com/lkzppm/GraphMan" }
use graphman::{AdjacencyList, Build, EdgeList, Graph, algo};

// The sample graph: five vertices, six edges.
let edges = EdgeList::parse(b"5\n1 2\n1 3\n2 3\n2 4\n3 5\n4 5\n")?;
let graph = AdjacencyList::build(&edges)?;
let n = graph.vertex_count(); // 5

let tree = algo::bfs(&graph, 1);
let parent = tree.parent(4); // Some(2)
let level = tree.level(4); // Some(2)

let diameter = algo::diameter(&graph, algo::DiameterMethod::IFub);
let value = diameter.value; // 2
o arquivo
  1. 5
  2. 1 2
  3. 1 3
  4. 2 3
  5. 2 4
  6. 3 5
  7. 4 5
o grafo
02

O formato

src/io/edge_list.rs

A primeira linha é o número de vértices, depois uma aresta por linha. A leitura normaliza uma única vez, e assim toda representação concorda sobre qual é o grafo: laços são descartados, cada aresta não direcionada é guardada uma vez como [min, max], duplicatas saem, a lista é ordenada.

use graphman::EdgeList;

// The parser normalises once: self-loops go, every edge is kept once as
// [min, max], the list is sorted. Every representation is built from this.
let text = b"5\n2 1\n1 2\n3 3\n1 3\n2 3\n2 4\n3 5\n4 5\n";
let edges = EdgeList::parse(text).unwrap();
let kept = edges.edge_count(); // 6
let first = edges.edges()[0]; // [1, 2]
let loops = edges.self_loops_dropped(); // 1
let duplicates = edges.duplicates_dropped(); // 1

// From a file, or from memory.
// let edges = EdgeList::from_path("graphs/grafo_1.txt")?;
let pairs = [(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)];
let same = EdgeList::from_edges(5, pairs).unwrap(); // == edges
como escrito
2 11 2duplicata3 3laço1 32 32 43 54 5
depois da leitura
[1, 2][1, 3][2, 3][2, 4][3, 5][4, 5]
03

Três representações

src/graph/mod.rs

AdjacencyList (uma linha por vértice), AdjacencyMatrix (uma matriz de bits compacta) e Csr (dois arrays contíguos) implementam o mesmo trait Graph, pequeno, e todas mantêm as linhas de vizinhos em ordem crescente. Os algoritmos são escritos uma vez e monomorfizados por representação: trocar o armazenamento muda o custo, nunca a resposta.

o grafo
lista de adjacência
  1. 1│23
  2. 2│134
  3. 3│125
  4. 4│25
  5. 5│34
matriz de bits
1234512345
CSR
offsets02581012targets231341252534
RepresentaçãoMemórianeighbors(v)has_edge(u, v)
AdjacencyListO(n + m) palavras, um bloco por vérticeO(deg v)O(log deg u)
CsrO(n + m) palavras, dois blocosO(deg v)O(log deg u)
AdjacencyMatrixO(n²) bitsO(n / 64) palavrasO(1)
use graphman::{AdjacencyList, AdjacencyMatrix, Build, Csr, EdgeList, Graph, algo};

let edges = EdgeList::parse(b"5\n1 2\n1 3\n2 3\n2 4\n3 5\n4 5\n").unwrap();
let list = AdjacencyList::build(&edges).unwrap();
let matrix = AdjacencyMatrix::build(&edges).unwrap();
let csr = Csr::build(&edges).unwrap();

// One graph, three layouts in memory, the same answers: neighbours come
// out ascending from all three, so the search trees are identical.
let around_2: Vec<_> = list.neighbors(2).collect(); // [1, 3, 4]
let adjacent = matrix.has_edge(1, 3); // true
let same_tree = algo::bfs(&list, 1) == algo::bfs(&csr, 1); // true

Quando a representação é escolhida em tempo de execução (a CLI recebe como flag), AnyGraph guarda a que foi construída e dispatch! roda uma expressão genérica sobre ela. O algoritmo continua especializado por representação; a única decisão dinâmica é um match. Todo builder também sabe seu custo antes de alocar (required_bytes) e o confere contra um MemoryBudget, a memória da máquina por padrão: a matriz de bits de 375 000 vértices tem 17,6 GB, e pedi-la devolve um BuildError::OverBudget tipado, não um processo morrendo em swap.

use graphman::{BuildError, EdgeList, MemoryBudget, Representation, algo, dispatch};

// Chosen at runtime (the CLI's --repr flag): AnyGraph holds any of the
// three, and dispatch! runs a generic expression on whichever it is.
let edges = EdgeList::parse(b"5\n1 2\n1 3\n2 3\n2 4\n3 5\n4 5\n").unwrap();
let chosen: Representation = "csr".parse().unwrap();
let any = chosen.build(&edges).unwrap();
let depth = dispatch!(&any, g => algo::bfs(g, 1).depth()); // 2

// What each one costs is known before anything is allocated, and every
// builder checks it against a MemoryBudget (the machine's memory by
// default): over it is a typed error, not a process dying in swap.
let bytes = Representation::Csr.required_bytes(5, 6, false); // 76
let matrix = Representation::AdjacencyMatrix.required_bytes(375_000, 0, false); // 17_580_000_000
let refused = chosen.build_within(&edges, MemoryBudget::Bytes(64)); // Err(OverBudget { .. })
04

Buscas

src/algo/traversal.rs

bfs e dfs devolvem uma SearchTree: pai e nível de cada vértice alcançado, mais a ordem de descoberta. A BFS é síncrona por nível (a ordem serve de fila); a DFS é iterativa sobre iteradores de vizinhos, então usa memória O(profundidade) e produz exatamente a árvore que a versão recursiva produziria.

use graphman::{Build, Csr, EdgeList, algo};

let edges = EdgeList::parse(b"5\n1 2\n1 3\n2 3\n2 4\n3 5\n4 5\n").unwrap();
let graph = Csr::build(&edges).unwrap();

// A search tree: parent and level of every reached vertex, in discovery order.
let bfs = algo::bfs(&graph, 1);
let order = bfs.order(); // [1, 2, 3, 4, 5]
let parent_of_4 = bfs.parent(4); // Some(2)
let depth = bfs.depth(); // 2

// DFS takes neighbours in ascending order, iteratively, in O(depth) memory,
// and produces exactly the tree the recursive version would.
let dfs = algo::dfs(&graph, 1);
let dfs_order = dfs.order(); // [1, 2, 3, 5, 4]
let dfs_parent_of_4 = dfs.parent(4); // Some(5)
let dfs_depth = dfs.depth(); // 4
BFS a partir de 1: vértices por nível, arestas da árvore em azul, profundidade 2
DFS a partir de 1: o mesmo grafo, um caminho só até a profundidade 4
05

Visitors

src/algo/traversal.rs

Uma busca reporta a um Visitor: discover, level_complete (BFS) e finish (DFS), cada um com um padrão vazio. Devolver Control::Break interrompe a busca; distância é uma BFS com um visitor que para no destino. As árvores são reutilizáveis pelas variantes _into: um reset só toca o que a busca anterior alcançou.

use graphman::{Build, Control, Csr, EdgeList, Graph, SearchTree, Vertex, Visitor, algo};

let edges = EdgeList::parse(b"5\n1 2\n1 3\n2 3\n2 4\n3 5\n4 5\n").unwrap();
let graph = Csr::build(&edges).unwrap();

// A visitor watches a traversal and may stop it. Distance is just this.
struct Until(Vertex, Option<u32>);
impl Visitor for Until {
    fn discover(&mut self, v: Vertex, _parent: Vertex, level: u32) -> Control {
        if v != self.0 {
            return Control::Continue;
        }
        self.1 = Some(level);
        Control::Break
    }
}
let mut until = Until(4, None);
algo::bfs_with(&graph, 1, &mut until);
let found_at = until.1; // Some(2), after discovering 1, 2, 3 and 4

// A tree is reusable: each search resets only what the previous one
// reached, so thousands of them cost no allocation and no O(n) clear.
let mut tree = SearchTree::new(graph.vertex_count());
for root in graph.vertices() {
    algo::bfs_into(&graph, root, &mut tree, &mut ());
}
a busca quando para
O que Until(4) ouve na BFS a partir de 1
  1. 1discover(1, 0, 0)Continue
  2. 2discover(2, 1, 1)Continue
  3. 3discover(3, 1, 1)Continue
  4. 4discover(4, 2, 2)Break
06

Distância e componentes

src/algo/distance.rs

distance roda uma BFS que para no destino e devolve None entre componentes; eccentricity é o vértice mais distante de um dado. Components::compute rotula cada vértice com uma BFS por componente, numeradas da maior para a menor, empate pelo menor vértice.

use graphman::{Build, Components, Csr, EdgeList, algo};

// The sample plus a second component, {6, 7}.
let edges = EdgeList::parse(b"7\n1 2\n1 3\n2 3\n2 4\n3 5\n4 5\n6 7\n").unwrap();
let graph = Csr::build(&edges).unwrap();

// A BFS that stops at the target; None across components.
let near = algo::distance(&graph, 4, 3); // Some(2)
let apart = algo::distance(&graph, 1, 7); // None
// The farthest vertex from 1, and how far.
let farthest = algo::eccentricity(&graph, 1); // (2, 5)

// Components, numbered from the largest, each listed ascending.
let components = Components::compute(&graph);
let count = components.count(); // 2
let largest = components.members(1); // [1, 2, 3, 4, 5]
let of_7 = components.component_of(7); // 2
duas componentes, o caminho mais curto de 4 a 3 em azul
07

Diâmetro

src/algo/diameter.rs

Quatro estratégias dividem um driver que percorre as componentes da maior para a menor e pula as pequenas demais para superar o melhor valor encontrado. Exact é uma BFS por vértice; iFUB (Crescenzi et al., 2013) e os limites de Takes-Kosters (2011) são exatos com muito menos buscas em grafos reais; o 4-sweep é um limite inferior barato. Toda execução pode ser cancelada por um callback de progresso, e então reporta o melhor limite, marcado como não exato.

use graphman::{Build, Control, Csr, DiameterMethod, EdgeList, algo};

let edges = EdgeList::parse(b"5\n1 2\n1 3\n2 3\n2 4\n3 5\n4 5\n").unwrap();
let graph = Csr::build(&edges).unwrap();

// Four methods, one driver: brute force, iFUB, Takes-Kosters bounds and
// the 4-sweep lower bound. All but the sweep certify their answer.
let exact = algo::diameter(&graph, DiameterMethod::IFub);
let (value, certified) = (exact.value, exact.is_exact); // (2, true)
let endpoints = exact.endpoints; // (1, 5): a longest shortest path, 1-3-5
let sweep = algo::diameter(&graph, DiameterMethod::Sweep);
let lower_bound = (sweep.value, sweep.is_exact); // (2, false)

// Every method takes a budget: the callback sees the BFS count after each
// search and may stop the run, which then reports its best lower bound.
let stop_after_two = |bfs_count: usize| match bfs_count < 2 {
    true => Control::Continue,
    false => Control::Break,
};
let bounded = algo::diameter_with_progress(&graph, DiameterMethod::Exact, &stop_after_two);
let gave_up = bounded.cancelled; // true
as pontas 1 e 5 em destaque, um caminho mais curto mais longo em azul: o diâmetro é 2
08

Pesos

src/graph/mod.rs

Uma terceira coluna torna o grafo ponderado: o parser decide na primeira linha de aresta e exige o mesmo de todas as outras. Um peso é qualquer real finito, negativos inclusive. Os pesos ficam numa coluna ao lado da adjacência, nunca dentro dela: a CSR guarda um vetor paralelo aos alvos, a lista uma linha paralela por vértice, a matriz uma tabela de pesos ao lado do bitset. Graph::neighbors nunca lê a coluna, então BFS, DFS, componentes e diâmetro custam o mesmo de antes, e um arquivo sem pesos não guarda nenhum e responde 1 por aresta. Uma aresta repetida fica com o menor peso, o único que um caminho mínimo usaria.

use graphman::{Build, Csr, EdgeList, Weighted};

// A third column makes a weighted graph: the sample, each edge weighed.
let text = b"5\n1 2 0.5\n1 3 2\n2 3 1\n2 4 2.5\n3 5 1\n4 5 0.75\n";
let edges = EdgeList::parse(text)?;
let graph = Csr::build(&edges)?;
let weighted = graph.is_weighted(); // true
let around_2: Vec<_> = graph.weighted_neighbors(2).collect(); // [(1, 0.5), (3, 1.0), (4, 2.5)]
let w = graph.weight(4, 5); // Some(0.75)

// The weights are a column beside the adjacency: BFS never reads it, and
// a graph without one weighs 1 per edge, so hops are the unit-weight case.
let plain = Csr::build(&EdgeList::parse(b"5\n1 2\n1 3\n2 3\n2 4\n3 5\n4 5\n")?)?;
let unit = plain.weight(4, 5); // Some(1.0)

// A repeated edge keeps its smallest weight, the only one a path would use.
let repeated = EdgeList::parse(b"2\n1 2 4\n2 1 0.5\n")?;
let kept = repeated.weights(); // Some([0.5])
o arquivo, com pesos
  1. 5
  2. 1 2 0.5
  3. 1 3 2
  4. 2 3 1
  5. 2 4 2.5
  6. 3 5 1
  7. 4 5 0.75
o grafo, cada aresta com seu peso
09

Dijkstra

src/algo/shortest_path.rs

dijkstra devolve uma ShortestPathTree: a distância e o pai de cada vértice alcançado, a ordem em que foram fixados (por distância) e path_to. O algoritmo é escrito uma vez, como dijkstra_into sobre uma Frontier, a estrutura que guarda as estimativas de distância, escolhida em tempo de execução como a representação. O relaxamento é estrito e toda fila desempata pelo menor vértice, então todas constroem a mesma árvore. Um grafo com aresta negativa é recusado com NegativeWeight, cuja mensagem termina em "shortest paths with negative weights are not implemented yet": num grafo não direcionado, ir e voltar por uma aresta negativa deixa um caminho tão curto quanto se queira, então não há resposta a dar.

use graphman::{Build, Csr, EdgeList, FrontierKind, algo};

let text = b"5\n1 2 0.5\n1 3 2\n2 3 1\n2 4 2.5\n3 5 1\n4 5 0.75\n";
let graph = Csr::build(&EdgeList::parse(text)?)?;

// One Dijkstra; the frontier (how estimates are kept) is the strategy.
let tree = algo::dijkstra(&graph, 1, FrontierKind::Heap)?;
let to_5 = tree.distance(5); // Some(2.5)
let path = tree.path_to(5); // Some([1, 2, 3, 5]): three edges, lighter than 1-3-5
let order = tree.order(); // [1, 2, 3, 5, 4], by distance

// The vector scan (O(n²)) and the heap (O((n + m) log n)) agree exactly:
// ties go to the smaller vertex in both, so even the tree is the same.
let vector = algo::dijkstra(&graph, 1, FrontierKind::Vector)?;
let same = vector == tree; // true

// A negative weight is refused, not answered wrongly.
let negative = Csr::build(&EdgeList::parse(b"3\n1 2 1\n2 3 -1\n")?)?;
let refused = algo::dijkstra(&negative, 1, FrontierKind::Heap); // Err(NegativeWeight { .. })
BFS a partir de 1: o menor número de arestas, 1 → 3 → 5 pesa 3
Dijkstra a partir de 1: o caminho mais leve, 1 → 2 → 3 → 5 pesa 2,5
10

Filas de prioridade

src/algo/frontier.rs

Uma Frontier guarda as estimativas dos vértices alcançados e ainda não fixados: decrease insere um vértice ou baixa sua estimativa, pop_min devolve o mais próximo, o menor vértice primeiro num empate. A disciplina pede duas, um vetor e um heap cujas chaves mudam de forma eficiente, sem escrever o Dijkstra duas vezes; a biblioteca tem seis, as três que a disciplina discute e três mais rápidas na prática, e um novo jeito de guardar estimativas é uma nova Frontier, nunca um segundo Dijkstra. Para k execuções, como no estudo de caso, dijkstra_into reaproveita uma fila e uma árvore, e cada execução limpa só o que a anterior tocou.

FilaComo guarda as estimativaspop_mindecreaseUma execução
VectorFrontieruma posição por vértice, o mínimo achado por varreduraO(n)O(1)O(n² + m)
HeapFrontierum heap binário com a posição de cada vértice: decrease-keyO(log n)O(log n)O((n + m) log n)
LazyHeapFrontierum heap binário que insere uma cópia e pula as velhasO(log m)O(log m)O((n + m) log m)
QuadHeapFrontiero mesmo heap indexado com quatro filhos por nó: mais rasoO(log n)O(log n)O((n + m) log n)
PairingHeapFrontieruma árvore de ponteiros: decrease corta uma subárvore e a funde com a raizO(log n) amortizadoO(log n) amortizadoO((n + m) log n)
RadixHeapFrontierbaldes pelos bits da distância: monótona, sem comparaçõesO(log C) amortizadoO(1)O(m log C)
use graphman::algo::{self, Frontier, HeapFrontier, ShortestPathTree};
use graphman::{Build, Csr, EdgeList, Graph};

let text = b"5\n1 2 0.5\n1 3 2\n2 3 1\n2 4 2.5\n3 5 1\n4 5 0.75\n";
let graph = Csr::build(&EdgeList::parse(text)?)?;

// A pair query stops as soon as the target is settled.
let path = algo::shortest_path(&graph, 1, 4)?.unwrap();
let (distance, via) = (path.distance, path.vertices); // (3.0, [1, 2, 4])

// k sources (the case study): one frontier and one tree for every run,
// each run resetting only what the previous one touched.
let mut frontier = HeapFrontier::default();
let mut tree = ShortestPathTree::new(graph.vertex_count());
let mut farthest = Vec::new();
for root in graph.vertices() {
    algo::dijkstra_into(&graph, root, &mut frontier, &mut tree, &mut ())?;
    farthest.push(tree.last_settled()); // (vertex, distance) farthest from root
}
let kind = frontier.kind(); // FrontierKind::Heap
Dijkstra a partir de 1, o vértice mais distante em destaque
O que toda fila fixa, em ordem
  1. 1settle(1, 0, 0)
  2. 2settle(2, 1, 0.5)
  3. 3settle(3, 2, 1.5)
  4. 5settle(5, 3, 2.5)
  5. 4settle(4, 2, 3)
11

Nomes

src/io/names.rs

A rede de colaboração dá nome aos vértices num segundo arquivo, um índice e um nome por linha (vírgula, ponto e vírgula, tab ou espaços; uma linha de contagem ou cabeçalho é pulada, e Latin-1 é lido quando o arquivo não é UTF-8). VertexNames guarda todos os nomes numa só string com um índice ordenado por nome: vertex acha a grafia exata que o enunciado pede por busca binária, name volta de um índice para o nome, e search ignora maiúsculas e acentos para o "você quis dizer" que a linha de comando mostra quando quase acerta.

use graphman::VertexNames;

// Names live beside the graph, in their own file (`index,name` per line).
let names =
    VertexNames::parse("1,Edsger W. Dijkstra\n2,Alan M. Turing\n3,Éva Tardos\n".as_bytes())?;
let dijkstra = names.vertex("Edsger W. Dijkstra"); // Some(1), exact spelling
let third = names.name(3); // Some("Éva Tardos")
let near_miss = names.search("eva tardos", 5); // [3]: ignoring case and accents
// let names = VertexNames::from_path("graphs/rede_colaboracao_vertices.txt")?;
o arquivo de nomes
  1. 1,Edsger W. Dijkstra
  2. 2,Alan M. Turing
  3. 3,Éva Tardos
o que ele responde
vertex("Edsger W. Dijkstra")
Some(1)
name(3)
Some("Éva Tardos")
search("eva tardos", 5)
[3]
vertex("Eva Tardos")
None
12

A linha de comando

cli/src/main.rs

O binário graphman embrulha a biblioteca: todo comando recebe um arquivo de grafo, um --repr (list, matrix ou csr) e escreve os arquivos de saída da disciplina. dijkstra escreve a árvore de caminhos mínimos e mostra os caminhos até os alvos. study roda o estudo de caso inteiro, das duas partes, e escreve JSON mais o RESULTS.md.

grafo.txt
  1. 5
  2. 1 2
  3. 1 3
  4. 2 3
  5. 2 4
  6. 3 5
  7. 4 5
um grafo no formato da disciplina
graphman bfs grafo.txt --from 1
um comando
grafo.bfs-1.txt
  1. # root 1
  2. # vertex parent level
  3. 1 0 0
  4. 2 1 1
  5. 3 1 1
  6. 4 2 2
  7. 5 3 2
o arquivo de saída da disciplina
  1. graphman info graphs/grafo_1.txt
    o arquivo de resumo: contagens, estatísticas de grau, componentes
  2. graphman bfs graphs/grafo_1.txt --from 1 --repr csr
    uma árvore de busca a partir do vértice 1, guardada como CSR
  3. graphman distance graphs/grafo_1.txt --pair 10 20 --pair 10 30
    distâncias entre pares de vértices
  4. graphman diameter graphs/grafo_1.txt --method bounds --budget 60
    o diâmetro pelos limites de Takes-Kosters, desistindo após 60 s
  5. graphman dijkstra samples/weighted_100.txt --from 10 --to 20 30 --frontier heap
    uma árvore de caminhos mínimos a partir de 10 com o heap, e os caminhos até 20 e 30
  6. graphman study graphs/grafo_1.txt --out studies
    o estudo de caso de um grafo, em studies/