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.
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(); // 6let first = edges.edges()[0]; // [1, 2]let loops = edges.self_loops_dropped(); // 1let 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
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│23
2│134
3│125
4│25
5│34
matriz de bits
1234512345
CSR
offsets02581012targets231341252534
Representação
Memória
neighbors(v)
has_edge(u, v)
AdjacencyList
O(n + m) palavras, um bloco por vértice
O(deg v)
O(log deg u)
Csr
O(n + m) palavras, dois blocos
O(deg v)
O(log deg u)
AdjacencyMatrix
O(n²) bits
O(n / 64) palavras
O(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); // truelet 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); // 76let matrix = Representation::AdjacencyMatrix.required_bytes(375_000, 0, false); // 17_580_000_000let refused = chosen.build_within(&edges, MemoryBudget::Bytes(64)); // Err(OverBudget { .. })
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 2DFS a partir de 1: o mesmo grafo, um caminho só até a profundidade 4
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.structUntil(Vertex, Option<u32>);
implVisitorforUntil {
fn discover(&mutself, v: Vertex, _parent: Vertex, level: u32) -> Control {
if v != self.0 {
returnControl::Continue;
}
self.1 = Some(level);
Control::Break
}
}
letmut 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.letmut tree = SearchTree::new(graph.vertex_count());
for root in graph.vertices() {
algo::bfs_into(&graph, root, &mut tree, &mut ());
}
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(); // 2let 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 azul07
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-5let 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 é 208
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(); // truelet 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])
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-5let 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 3Dijkstra a partir de 1: o caminho mais leve, 1 → 2 → 3 → 5 pesa 2,5
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.
Fila
Como guarda as estimativas
pop_min
decrease
Uma execução
VectorFrontier
uma posição por vértice, o mínimo achado por varredura
O(n)
O(1)
O(n² + m)
HeapFrontier
um heap binário com a posição de cada vértice: decrease-key
O(log n)
O(log n)
O((n + m) log n)
LazyHeapFrontier
um heap binário que insere uma cópia e pula as velhas
O(log m)
O(log m)
O((n + m) log m)
QuadHeapFrontier
o mesmo heap indexado com quatro filhos por nó: mais raso
O(log n)
O(log n)
O((n + m) log n)
PairingHeapFrontier
uma árvore de ponteiros: decrease corta uma subárvore e a funde com a raiz
O(log n) amortizado
O(log n) amortizado
O((n + m) log n)
RadixHeapFrontier
baldes pelos bits da distância: monótona, sem comparações
O(log C) amortizado
O(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.letmut frontier = HeapFrontier::default();
letmut tree = ShortestPathTree::new(graph.vertex_count());
letmut 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
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 spellinglet 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 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
5
1 2
1 3
2 3
2 4
3 5
4 5
um grafo no formato da disciplina
→
graphman bfs grafo.txt --from 1
um comando
→
grafo.bfs-1.txt
# root 1
# vertex parent level
1 0 0
2 1 1
3 1 1
4 2 2
5 3 2
o arquivo de saída da disciplina
graphman info graphs/grafo_1.txt
o arquivo de resumo: contagens, estatísticas de grau, componentes