Grafos Ponderados em Python: TP3 Completo com Evidência, Big-O e Casos de Borda

Guia autodidata exercício por exercício: o que a questão pede, contrato, implementação, evidência interpretada, por que a resposta fecha o enunciado e quais bordas derrubam soluções frágeis.

Por Fábio Linhares • Instituto Infnet

BASE
Como ler o TP3 sem errar a pergunta
Estabelecer o protocolo fixo de prova e o mapa didático do trabalho antes de entrar nos algoritmos.
O que este HTML passa a fazer
A página deixa de ser resumo condensado e passa a seguir o mesmo ciclo didático do arquivo de respostas: pergunta, contrato, implementação núcleo, evidência interpretada, justificativa técnica, Big-O e casos de borda.
Como ler o TP3 sem errar a pergunta
Q1 a Q3 cobram modelagem OO, travessias determinísticas e caminho mínimo com reconstrução.
Q4 separa explicitamente "menor número de arestas" de "menor soma de pesos".
Q5 a Q9 pedem não só resultado, mas leitura correta da saída: AGM, matriz, Mermaid e validação estrutural.
Q10 a Q12 exigem análise madura: custo empírico, integração de algoritmos e decisões de projeto com trade-off real.
Protocolo fixo de evidência
ENTRADA Qual grafo, qual origem/destino, qual parâmetro foi usado.
SAÍDA Lista, dicionário, caminho, matriz, string Mermaid, exceção ou tabela de contadores.
ONDE ESTÁ A RESPOSTA O valor exato do output que responde à pergunta do enunciado.
PROVA MÍNIMA assert ... ou checagem de propriedade. Printar não é provar.
Regra editorial do notebook
Em cada exercício, a resposta só está completa quando o notebook mostra: dado de entrada, resultado produzido, trecho exato do output que responde à pergunta e prova mínima por assert.
EX.01
Representação de grafo ponderado (WeightedGraph)
Construir uma base OO auditável com lista de adjacência, pesos, política explícita para arestas e invariantes claros.
O que a questão está pedindo
  • Classe OO para grafo ponderado.
  • Lista de adjacência com pesos.
  • Métodos básicos de inserção e consulta.
  • Grafo de teste com pelo menos 8 vértices e pesos distintos.
  • Invariantes explícitas.
Contrato / invariantes / pré-condições
add_vertex(v) cria o vértice se ele ainda não existir.
add_edge(u, v, w, directed) espelha a aresta quando directed=False.
neighbors(u) devolve vizinhos em ordem determinística.
weight(u, v) devolve o peso exato da aresta.
Aresta paralela mantém o menor peso observado.
Implementação núcleo
class WeightedGraph:
    def __init__(self):
        self.adj: dict[str, dict[str, float]] = {}

    def add_vertex(self, v: str) -> None:
        if v not in self.adj:
            self.adj[v] = {}

    def add_edge(self, u: str, v: str, w: float, directed: bool = False) -> None:
        self.add_vertex(u)
        self.add_vertex(v)
        self.adj[u][v] = min(self.adj[u].get(v, float("inf")), float(w))
        if not directed:
            self.adj[v][u] = min(self.adj[v].get(u, float("inf")), float(w))

    def neighbors(self, u: str) -> list[str]:
        return sorted(self.adj[u].keys())

    def weight(self, u: str, v: str) -> float:
        return self.adj[u][v]

g = build_demo_graph()
assert g.neighbors("A") == ["B", "C"]
assert g.weight("A", "C") == 2.0

gd = WeightedGraph()
gd.add_edge("X", "Y", 7, directed=True)
assert gd.neighbors("X") == ["Y"]
assert gd.neighbors("Y") == []
Evidência interpretada
ENTRADA: consulta em A e microcaso dirigido X -> Y.
SAÍDA: vizinhos de A, peso de A-C e diferença entre vizinhança de X e Y.
ONDE ESTÁ A RESPOSTA: nos retornos de neighbors("A"), weight("A","C") e nos asserts do caso dirigido.
PROVA MÍNIMA: igualdade exata dos asserts.
Por que isso responde à pergunta
A questão não quer "qualquer código de grafo". Ela quer uma representação consultável, auditável e capaz de provar que dirigido e não direcionado foram realmente implementados.
Big-O
add_edge: O(1) esperado com dict.
weight(u, v): O(1) esperado.
neighbors(u): O(grau(u) log grau(u)) por causa do sorted.
Casos de borda
Inserir aresta com vértice novo.
Repetir aresta com peso maior e verificar que o menor foi preservado.
Exigir assimetria controlada no caso dirigido.
EX.02
BFS e DFS determinísticos
Mostrar que a travessia não apenas visita vértices, mas o faz em ordem reproduzível e explicável.
O que a questão está pedindo
  • BFS com fila.
  • DFS com pilha ou recursão.
  • Ordem determinística de visita.
  • Registro explícito da estrutura usada.
  • Análise de tempo e memória.
Contrato / invariantes / pré-condições
BFS usa deque como fila.
DFS usa list como pilha.
Vizinhos são sempre processados em ordem determinística.
Vértice inexistente deve levantar KeyError.
Implementação núcleo
g = build_demo_graph()
bfs_order = g.bfs("A")
dfs_order = g.dfs("A")

assert bfs_order == ['A','B','C','D','E','F','G','H']
assert dfs_order == ['A','B','C','D','E','F','H','G']
Evidência interpretada
ENTRADA: start = "A".
SAÍDA: listas bfs_order e dfs_order.
ONDE ESTÁ A RESPOSTA: nas duas listas de visita.
PROVA MÍNIMA: igualdade exata com a ordem esperada.
Por que isso responde à pergunta
O enunciado não pede só que a busca alcance os vértices. Ele exige ordem determinística e identificação da estrutura usada. Sem isso, a visita vira resultado casual e difícil de defender.
Big-O
Tempo: O(|V| + |E|).
Memória auxiliar: O(|V|).
Casos de borda
Vértice inexistente.
Componente desconexo: a travessia visita apenas o componente do start.
EX.03
Dijkstra com distâncias e reconstrução de caminho
Fechar a resposta completa para caminho mínimo ponderado: distância, predecessores, reconstrução e prova de custo.
O que a questão está pedindo
  • Executar Dijkstra no grafo ponderado.
  • Calcular distâncias da origem para todos os vértices.
  • Reconstruir pelo menos um caminho mínimo.
  • Demonstrar com mais de uma origem.
Contrato / invariantes / pré-condições
Pesos precisam ser não negativos.
dist[target] e path respondem perguntas diferentes e complementares.
O custo somado do caminho reconstruído precisa ser igual à menor distância registrada.
Implementação núcleo
def path_cost(g: WeightedGraph, path: list[str]) -> float:
    return sum(g.weight(path[i], path[i + 1]) for i in range(len(path) - 1))

g = build_demo_graph()
for source, target, expected_dist, expected_path in [
    ("A", "H", 23.0, ['A','C','B','D','F','H']),
    ("D", "H", 15.0, ['D','F','H']),
    ("F", "H", 9.0, ['F','H']),
]:
    dist, prev = g.dijkstra(source)
    path = g.reconstruct_path(prev, source, target)
    assert dist[target] == expected_dist
    assert path == expected_path
    assert path_cost(g, path) == dist[target]

gneg = WeightedGraph()
gneg.add_edge("A", "B", -1, directed=False)
try:
    gneg.dijkstra("A")
    assert False
except ValueError:
    pass
Evidência interpretada
ENTRADA: pares (source, target) e um microcaso com peso negativo.
SAÍDA: dist, prev e path.
ONDE ESTÁ A RESPOSTA: dist[target] é a menor distância; path é o percurso mínimo.
PROVA MÍNIMA: custo somado do caminho = distância registrada; peso negativo dispara exceção.
Por que isso responde à pergunta
Quem mostra só dist não provou o caminho. Quem mostra só path não provou minimalidade. A resposta correta precisa fechar o triângulo: distância, reconstrução e verificação do custo.
Big-O
Com heap: O((|V| + |E|) log |V|).
Memória: O(|V| + |E|).
Casos de borda
Origem inexistente.
Vértice inalcançável: dist = inf e path = None.
Múltiplos caminhos com mesmo custo: usar tie-break para reprodutibilidade.
EX.04
Menor caminho em grafo não ponderado via BFS
Separar explicitamente "menor número de arestas" de "menor soma de pesos".
O que a questão está pedindo
Retornar o menor caminho entre dois vértices em número de arestas, usando BFS com predecessores e caminho explícito como lista.
Contrato / invariantes / pré-condições
Esta questão ignora peso e trabalha com número de saltos.
O caminho retornado precisa ser válido no grafo.
Se não houver conectividade, o resultado esperado é None.
Implementação núcleo
g = build_demo_graph()
path = g.shortest_path_unweighted("A", "H")

assert path == ["A", "B", "D", "F", "H"]
assert all(path[i + 1] in g.neighbors(path[i]) for i in range(len(path) - 1))
Evidência interpretada
ENTRADA: src = "A" e dst = "H".
SAÍDA: lista path.
ONDE ESTÁ A RESPOSTA: na própria lista retornada.
PROVA MÍNIMA: cada par consecutivo é uma aresta real do grafo.
Por que isso responde à pergunta
Em grafo não ponderado, a BFS resolve "menor número de arestas". Dijkstra resolveria outra pergunta: menor soma de pesos. Essa distinção precisa ser escrita, não assumida.
Big-O
Tempo: O(|V| + |E|).
Memória: O(|V|).
Casos de borda
Origem igual ao destino.
Destino fora do componente da origem.
Grafo com pesos altos: os pesos continuam irrelevantes para esta pergunta.
EX.05
Prim para árvore geradora mínima
Provar que a AGM não é só uma lista de arestas bonita, mas uma estrutura coerente em custo, cardinalidade e existência.
O que a questão está pedindo
  • Implementar Prim em grafo ponderado.
  • Devolver as arestas da AGM.
  • Calcular o custo total.
  • Explicar o que acontece em grafo desconexo.
Contrato / invariantes / pré-condições
mst precisa ter |V|-1 arestas no componente alcançável.
Toda aresta da solução precisa existir no grafo original.
total precisa ser igual à soma dos pesos em mst.
Implementação núcleo
g = build_demo_graph()
mst, total = g.prim("A")

assert mst == [
    ('A','C',2.0), ('C','B',1.0), ('B','D',5.0),
    ('D','E',3.0), ('D','F',6.0), ('E','G',7.0),
    ('F','H',9.0)
]
assert total == 33.0
assert total == sum(w for _, _, w in mst)
for u, v, w in mst:
    assert (v in g.adj[u] and g.adj[u][v] == w) or (u in g.adj[v] and g.adj[v][u] == w)
assert len(mst) == len(g.vertices()) - 1
Evidência interpretada
ENTRADA: start = "A".
SAÍDA: mst e total.
ONDE ESTÁ A RESPOSTA: na lista de arestas e no custo acumulado.
PROVA MÍNIMA: cardinalidade |V|-1, soma dos pesos e existência de cada aresta.
Por que isso responde à pergunta
A pergunta quer a árvore e quer o custo. Sem provar cardinalidade, soma dos pesos e existência das arestas, o aluno só mostrou uma coleção qualquer de conexões.
Big-O
Com heap: O((|V| + |E|) log |V|).
Memória: O(|V| + |E|).
Casos de borda
Grafo desconexo: a implementação retorna a AGM do componente alcançável a partir de start.
Arestas com mesmo peso: o tie-break ajuda na reprodutibilidade.
EX.06
Floyd-Warshall com leitura correta de matriz
Ensinar o aluno a localizar e interpretar a célula certa, e não apenas imprimir uma matriz sem saber lê-la.
O que a questão está pedindo
  • Calcular menor caminho entre todos os pares.
  • Devolver matriz final.
  • Explicar o significado de verts, idx e dist.
  • Demonstrar consistência com Dijkstra.
Contrato / invariantes / pré-condições
verts é a lista ordenada de vértices.
idx[v] mapeia cada vértice para o índice correto da matriz.
A célula certa é dist[idx[src]][idx[dst]], e não dist["A"]["H"].
A diagonal precisa ficar em zero.
Implementação núcleo
g = build_demo_graph()
verts, idx, dist = g.all_shortest_paths_matrix()

for v in verts:
    i = idx[v]
    assert dist[i][i] == 0.0

assert dist[idx["A"]][idx["H"]] == 23.0

dA, _ = g.dijkstra("A")
for v in verts:
    assert dist[idx["A"]][idx[v]] == dA[v]
Evidência interpretada
ENTRADA: grafo de teste completo.
SAÍDA: verts, idx e dist.
ONDE ESTÁ A RESPOSTA: na célula dist[idx["A"]][idx["H"]] e na linha cruzada com Dijkstra.
PROVA MÍNIMA: diagonal zero e igualdade entre a linha de A na matriz e dijkstra("A").
Por que isso responde à pergunta
A questão não quer só "uma matriz impressa". Ela quer leitura correta da estrutura. Explicar verts e idx é parte da resposta, não um detalhe cosmético.
Big-O
Tempo: O(|V|^3).
Memória: O(|V|^2).
Casos de borda
Grafo desconexo: células inalcançáveis permanecem inf.
Grafo não direcionado: a matriz tende a ser simétrica.
EX.07
Comparação entre Dijkstra e Floyd-Warshall
Transformar comparação solta em decisão técnica objetiva, com tempo, memória e cenário recomendado.
O que a questão está pedindo
Comparar complexidade, memória e caso de uso. Aqui a resposta boa não é parágrafo genérico; é decisão técnica auditável.
Contrato / invariantes / pré-condições
Separar custo por origem de custo para todos os pares.
Separar memória auxiliar de cenário recomendado.
Justificar a escolha pelo problema, não só pela fórmula.
Implementação núcleo / resposta auditável
Critério Dijkstra (heap) Floyd-Warshall
Problema que resolve melhor Uma origem para todos os demais Todos os pares
Tempo O((|V| + |E|) log |V|) por origem O(|V|^3)
Memória O(|V| + |E|) além do grafo O(|V|^2)
Melhor cenário Poucas origens, grafo esparso Grafo pequeno ou médio com análise global
Ponto fraco Repetir para muitas origens pode custar caro Tempo e memória explodem quando |V| cresce
Evidência interpretada
ENTRADA: os dois algoritmos e seus cenários de uso.
SAÍDA: tabela comparativa.
ONDE ESTÁ A RESPOSTA: nas colunas de tempo, memória e melhor cenário.
PROVA MÍNIMA: a tabela força a separar custo, memória e contexto, impedindo explicação vaga.
Por que isso responde à pergunta
A comparação correta não é "qual é melhor?". É "qual é melhor para este cenário?". A tabela fecha exatamente essa decisão.
Big-O
Dijkstra: O((|V| + |E|) log |V|) por origem.
Floyd-Warshall: O(|V|^3).
Casos de borda
Se precisar de todos os pares, repetir Dijkstra pode se tornar desvantajoso.
Se o grafo crescer muito, Floyd-Warshall pode deixar de ser viável por memória.
EX.08
Exportação em Mermaid com regra anti-duplicação
Transformar validação visual em evidência auditável por contagem de arestas e checagem de pesos-chave.
O que a questão está pedindo
Gerar uma visualização reproduzível do grafo ponderado e provar que ela representa corretamente o grafo sem duplicar arestas não direcionadas.
Contrato / invariantes / pré-condições
Toda aresta não direcionada precisa ser emitida uma única vez.
O peso precisa sair junto da aresta.
A validação não pode ser subjetiva.
Implementação núcleo
g = build_demo_graph()
m = g.to_mermaid(directed=False)
print(m)

edge_lines = [ln for ln in m.splitlines() if "---|" in ln]
assert len(edge_lines) == 12
assert "A ---|2.0| C" in m or "C ---|2.0| A" in m
assert "B ---|5.0| D" in m or "D ---|5.0| B" in m
assert "G ---|12.0| H" in m or "H ---|12.0| G" in m
Evidência interpretada
ENTRADA: grafo de teste completo.
SAÍDA: string Mermaid.
ONDE ESTÁ A RESPOSTA: nas linhas u ---|w| v.
PROVA MÍNIMA: contagem de arestas e presença de arestas-chave com peso correto.
Por que isso responde à pergunta
"Validar visualmente" não pode significar "olhei e gostei". O mínimo técnico é provar que a quantidade de arestas bate com o grafo e que a regra anti-duplicação foi respeitada.
Big-O
O(|V| + |E|) para percorrer a estrutura e emitir as linhas.
Casos de borda
Duplicação acidental de u-v e v-u.
Aresta importante omitida ou com peso incorreto.
EX.09
Validação estrutural com casos válidos e inválidos
Mostrar engenharia defensiva: o método aceita o grafo correto e reprova exatamente os cenários quebrados.
O que a questão está pedindo
  • Detectar pesos inválidos.
  • Detectar vértices inexistentes.
  • Detectar arestas inconsistentes em simetria exigida.
Contrato / invariantes / pré-condições
Grafo válido passa sem exceção.
Grafo inválido precisa falhar com ValueError.
Quando a exigência é não direcionada, a simetria precisa existir nos dois sentidos.
Implementação núcleo
g = build_demo_graph()
g.validate_structure(require_undirected_symmetry=True)

g_bad = build_demo_graph()
g_bad.adj["A"]["Z"] = 1.0
try:
    g_bad.validate_structure()
    assert False
except ValueError:
    pass

g_bad2 = WeightedGraph()
g_bad2.add_edge("A", "B", 1, directed=True)
try:
    g_bad2.validate_structure(require_undirected_symmetry=True)
    assert False
except ValueError:
    pass
Evidência interpretada
ENTRADA: um grafo válido e dois grafos deliberadamente inválidos.
SAÍDA: execução normal ou exceção.
ONDE ESTÁ A RESPOSTA: na exceção disparada e no cenário que a causou.
PROVA MÍNIMA: o try/except confirma que falhou onde deveria falhar.
Por que isso responde à pergunta
A questão pede robustez estrutural. Não basta dizer que o grafo está correto; é preciso provar que o método detecta estrutura quebrada e reage do modo esperado.
Big-O
O(|V| + |E|), porque a validação percorre vértices e arestas relevantes.
Casos de borda
Aresta apontando para vértice inexistente.
Assimetria em grafo exigido como não direcionado.
Peso não numérico ou não finito.
EX.10
Instrumentação e custo empírico
Ligar teoria assintótica a observação concreta usando contadores reais de execução.
O que a questão está pedindo
Instrumentar um algoritmo e comparar o crescimento observado com a teoria, sem fingir que o experimento prova a fórmula exata.
Contrato / invariantes / pré-condições
Registrar contadores reais: heap_pop, heap_push, relax_attempts e relax_success.
Comparar cenários de tamanhos crescentes.
Interpretar tendência, não exigir duplicação perfeita.
Implementação núcleo
sizes = [20, 40, 80]
rows = []
for n in sizes:
    gg = gen_sparse_graph(n, extra_edges_per_vertex=3, seed=123)
    _, _, c = gg.dijkstra_instrumented("0")
    rows.append((n, c["heap_pop"], c["heap_push"], c["relax_attempts"], c["relax_success"]))
    print(rows[-1])

assert rows[0][3] < rows[1][3] < rows[2][3]
Evidência interpretada
ENTRADA: grafos de tamanhos 20, 40 e 80.
SAÍDA: tabela rows.
ONDE ESTÁ A RESPOSTA: nas colunas de contagem, especialmente relax_attempts.
PROVA MÍNIMA: relax_attempts cresce monotonicamente.
Por que isso responde à pergunta
Comparar empiricamente com Big-O não significa provar a fórmula exata. Significa mostrar que, quando o tamanho cresce, as contagens relevantes crescem de modo compatível com a teoria.
Big-O
A análise teórica segue O((|V| + |E|) log |V|), mas o experimento observa contagens e tendência de crescimento, não o limite formal.
Casos de borda
Python tem overhead e constantes influenciam muito em tamanhos pequenos.
Grafos aleatórios diferentes podem alterar a contagem absoluta.
EX.11
Integração BFS → componente → Dijkstra
Mostrar fluxo de decisão real: usar informação estrutural da BFS para decidir se vale a pena rodar Dijkstra.
O que a questão está pedindo
Usar BFS para validar conectividade e só então executar Dijkstra em componentes conectados.
Contrato / invariantes / pré-condições
Se target não estiver no componente de source, a função devolve None.
Dijkstra roda apenas quando a conectividade foi comprovada.
Implementação núcleo
def safe_shortest_path_if_connected(g: WeightedGraph, source: str, target: str) -> list[str] | None:
    comp = g.connected_component(source)
    if target not in comp:
        return None
    dist, prev = g.dijkstra(source)
    return g.reconstruct_path(prev, source, target)

g2 = WeightedGraph()
g2.add_edge("A", "B", 1, False)
g2.add_edge("B", "C", 2, False)
g2.add_edge("X", "Y", 1, False)

assert g2.connected_component("A") == {"A", "B", "C"}
assert safe_shortest_path_if_connected(g2, "A", "Y") is None

g = build_demo_graph()
assert safe_shortest_path_if_connected(g, "A", "H") == ['A','C','B','D','F','H']
Evidência interpretada
ENTRADA: um grafo desconexo e um grafo conectado.
SAÍDA: componente conectado e caminho final, ou None.
ONDE ESTÁ A RESPOSTA: no conjunto retornado pela BFS e na decisão de retornar None ou caminho.
PROVA MÍNIMA: se target não está no componente, não há por que rodar Dijkstra.
Por que isso responde à pergunta
Isto não é só encadear chamadas. A BFS fornece a informação estrutural que evita rodar Dijkstra sem sentido em alvos inalcançáveis. Isso é integração real de algoritmos.
Big-O
BFS: O(|V| + |E|).
Se o alvo estiver no componente, soma-se o custo do Dijkstra.
Casos de borda
Origem ou destino inexistentes.
Grafo totalmente desconexo.
Alvo no mesmo componente, mas com vários caminhos de custo equivalente.
EX.12
Decisões de projeto e complexidade
Fechar o TP3 com maturidade de engenharia: custo, memória, justificativa estrutural e trade-off real.
O que a questão está pedindo
Uma seção final madura, com tabela de custos, justificativa das escolhas estruturais e relação entre decisão e trade-off concreto.
Contrato / invariantes / pré-condições
A tabela precisa separar tempo, memória auxiliar e observação prática.
As decisões precisam mostrar ganho e custo pago.
Implementação núcleo / tabela pronta
Algoritmo / operação Tempo Memória auxiliar Observação
neighbors(u) com ordenação O(grau(u) log grau(u)) O(grau(u)) Custo pago para garantir determinismo.
BFS / DFS O(|V| + |E|) O(|V|) Base para conectividade e inspeção.
Dijkstra (heap) O((|V| + |E|) log |V|) O(|V| + |E|) Melhor para poucas origens em grafos esparsos.
BFS shortest path O(|V| + |E|) O(|V|) Correto apenas para não ponderado.
Prim (heap) O((|V| + |E|) log |V|) O(|V| + |E|) AGM do componente de start.
Floyd-Warshall O(|V|^3) O(|V|^2) Melhor para todos os pares em grafos menores.
to_mermaid() O(|V| + |E|) O(|E|) Com regra anti-duplicação.
  • adj como dict[str, dict[str, float]]: ganha consulta O(1) e integração simples com Dijkstra, Prim e validação; paga memória maior que uma estrutura mais compacta.
  • Determinismo por sorted(neighbors) + tie-break no heap: ganha auditoria e reprodutibilidade; paga ordenação e um contador extra.
Evidência interpretada
ENTRADA: todas as operações usadas no TP3.
SAÍDA: tabela de custos e duas decisões justificadas.
ONDE ESTÁ A RESPOSTA: nas colunas de custo e nos pares ganho/custo das decisões.
PROVA MÍNIMA: cada decisão conecta benefício técnico e preço pago.
Por que isso responde à pergunta
A seção final do TP não quer só recitar Big-O. Ela quer mostrar maturidade de engenharia: estrutura de dados, algoritmo e cenário de uso precisam aparecer conectados.
Big-O
Já está consolidado na tabela para facilitar transcrição direta no notebook.
Casos de borda
Pagar determinismo onde reprodutibilidade importa e abrir mão dele onde custo seria injustificável.
Evitar estrutura "bonita" mas ruim para consulta de peso e integração com os algoritmos do TP.
CHECK
Checklist final de prova auditável
Fechar a página como material de respostas-autodidata completo, garantindo que cada exercício mostre o que provar e onde a resposta aparece.
Checklist de prova
  • WeightedGraph: consulta de vizinhos e pesos funciona; caso dirigido demonstrado.
  • BFS/DFS: ordem determinística demonstrada e estrutura de dados declarada.
  • Dijkstra: distância e caminho reconstruído provados; peso negativo bloqueado.
  • BFS shortest path: caminho válido em não ponderado e distinção explícita para Dijkstra.
  • Prim: arestas, custo, cardinalidade e existência das arestas provados.
  • Floyd: diagonal zero, leitura correta via idx e consistência com Dijkstra.
  • Comparação: tabela objetiva preenchida com tempo, memória e cenário recomendado.
  • Mermaid: contagem sem duplicação e arestas-chave presentes.
  • Validação estrutural: caso válido e casos inválidos com exceções esperadas.
  • Instrumentação: contadores reais e interpretação do crescimento empírico.
  • Integração: Dijkstra não roda quando target está fora do componente.
  • Cada seção contém: o que a questão pede, contrato, implementação, evidência interpretada, por que isso responde, Big-O e casos de borda.
Fecho didático
Se o aluno consegue explicar cada exercício neste formato, ele não está apenas mostrando que o código rodou. Está mostrando que entendeu a estrutura do problema, a lógica do algoritmo e o significado técnico da saída.