A pressão do relógio na OBI. 16 implementações, 10 problemas, 3 edições, e a lição dolorosa de que O(N³) te custa uma medalha.
Vitor Neuenschwander
CS Student & Developer
Na Olimpíada Brasileira de Informática (OBI), 3 horas separam o ouro do nada. Cada segundo de execução importa. Cada decisão de estrutura de dados pode ser a diferença entre "Accepted" e o temido TLE — Time Limit Exceeded.
Este post documenta os 16 arquivos e 10 problemas que resolvi nas edições de 2019, 2021 e 2023 da OBI — e os algoritmos de grafos que construí para treinar. O resultado? Medalha de bronze na OBI 2024.
Quando precisas de caminho mínimo mas o grafo tem pesos negativos, Dijkstra não serve. Bellman-Ford relaxa todas as arestas V-1 vezes — bruto, mas correto.
Complexidade: O(V × E) tempo, O(V) espaço.A implementação suporta 3 representações de grafo (classe OOP completa com Node, parent, distance):
class Node:
def __init__(self, tag):
self.tag = tag
self.parent = None
self.distance = float('inf')
def __lt__(self, other):
return self.distance < other.distance
class BellmanFord:
def __init__(self, vertices, edges):
self.V = vertices
self.edges = edges # dict: (u,v) -> weight
self.nodes = [Node(i) for i in range(vertices)]
def relax(self, u, v, weight):
if self.nodes[u].distance + weight < self.nodes[v].distance:
self.nodes[v].distance = self.nodes[u].distance + weight
self.nodes[v].parent = u
return True
return False
def run(self, source):
self.nodes[source].distance = 0
# V-1 relaxamentos
for _ in range(self.V - 1):
for (u, v), w in self.edges.items():
self.relax(u, v, w)
# Detecção de ciclo negativo (iteração extra)
for (u, v), w in self.edges.items():
if self.nodes[u].distance + w < self.nodes[v].distance:
raise Exception("Ciclo negativo detectado!")
return self.nodes
A dor de implementar Dijkstra três vezes em formatos diferentes me ensinou como cada representação de grafo afeta a performance:
# O(V²) — mais lento, mas simples
def dijkstra_matrix(graph, start, V):
dist = [float('inf')] * V
dist[start] = 0
visited = [False] * V
parents = [None] * V
for _ in range(V):
# Seleciona vértice não-visitado com menor distância
u = min_distance(dist, visited, V)
visited[u] = True
for v in range(V):
if graph[u][v] > 0 and not visited[v]:
if dist[u] + graph[u][v] < dist[v]:
dist[v] = dist[u] + graph[u][v]
parents[v] = u
return dist, parents
# Mais eficiente em grafos esparsos
def dijkstra_adj_list(graph, start):
dist = {node: float('inf') for node in graph}
dist[start] = 0
parents = {start: None}
visited = set()
while len(visited) < len(graph):
u = min((d, n) for n, d in dist.items()
if n not in visited)[1]
visited.add(u)
for v, w in graph[u]:
if v not in visited and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
parents[v] = u
return dist, parents
heapqimport heapq
def dijkstra_heap(graph, start):
dist = {node: float('inf') for node in graph}
dist[start] = 0
parents = {start: None}
pq = [(0, start)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue
for v, w in graph[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
parents[v] = u
heapq.heappush(pq, (nd, v))
return dist, parents
A reconstrução do caminho usa o array parents[]:
def show_path(parents, target):
path = []
node = target
while node is not None:
path.append(node)
node = parents.get(node)
return list(reversed(path))
O problema clássico: encontrar pares de números que somam a um valor alvo. A primeira intuição? Dois loops.
# O(N³) — TLE garantido para N > 1.000
for i in range(n):
for j in range(i+1, n):
if sum(arr[i:j+1]) == target: # sum() é O(N) extra!
count += 1
A solução passava por eliminar o sum() interno e usar acumuladores — ou melhor, sets para busca O(1). Lição: quando vês N ≤ 10⁵ no enunciado, não penses em O(N²).
Simulação de propagação de água numa matriz 2D. A sacada: em vez de BFS/DFS (que são O(N²) em matrizes), usar varreduras direcionais — uma passagem esquerda→direita e outra direita→esquerda. Complexidade real: O(N × M) por passagem.
Simulação de um sistema de controle temporal com eventos R (registrar), E (encerrar), T (total). A estrutura core: um dicionário mapeando nomes para pares [acumulador, estado]:
registros = {} # nome -> [tempo_acumulado, estado_ativo]
for evento in eventos:
tipo, nome, tempo = parse(evento)
if tipo == 'R':
if nome not in registros:
registros[nome] = [0, True]
registros[nome][1] = True
elif tipo == 'E':
registros[nome][0] += tempo
registros[nome][1] = False
elif tipo == 'T':
for k in sorted(registros):
print(k, registros[k][0])
Cifra de substituição onde cada consoante é mapeada pela vogal mais próxima no alfabeto. Implementação pura de indexação e distância absoluta — sem regex, sem bibliotecas.
Maximizar o número de contas pagas com orçamento limitado. A solução gulosa clássica: ordena por valor crescente e paga as baratas primeiro.
# O(N log N) — sort domina
contas.sort()
total = 0
pagas = 0
for conta in contas:
if total + conta <= orcamento:
total += conta
pagas += 1
else:
break
Encontrar o maior lance num leilão. Uso de sort(key=lambda, reverse=True) em lista de tuplas (nome, valor):
lances = [(nome, valor) for _ in range(n)]
lances.sort(key=lambda x: x[1], reverse=True)
print(lances[0][0]) # Nome do vencedor
O arquivo exec.py existe para provar um ponto sobre otimização. Usando o módulo timeit:
import timeit
# list.sort() — ordena in-place
list_time = timeit.timeit(
'data.sort()',
setup='data = list(range(10000, 0, -1))',
number=1000
)
# sorted(dict) com lambda — cria nova lista
dict_time = timeit.timeit(
'sorted(d.items(), key=lambda x: x[1])',
setup='d = {i: 10000-i for i in range(10000)}',
number=1000
)
print(f"List: {list_time:.4f}s")
print(f"Dict: {dict_time:.4f}s")
# Spoiler: list.sort() é MUITO mais rápido
Na OBI, milissegundos decidem entre Accepted e TLE. A estrutura de dados que escolhes dita quem ganha a medalha.
| Problema | Edição | Técnica | Complexidade |
|---|---|---|---|
| Soma | 2019 | Força bruta → Otimização | O(N³) → O(N) |
| Soma FODA | 2019 | Enumeração combinatória | O(N³) |
| Chuva | 2019 | Propagação em grade 2D | O(N×M) |
| Idade | 2019 | Aritmética simples | O(1) |
| Cifra | 2021 | Substituição por proximidade | O(N) |
| Tempo | 2021 | Simulação com dicionários | O(N×K) |
| Contas | 2023 | Algoritmo guloso | O(N log N) |
| Estoque | 2023 | Simulação linear | O(L+C) |
| Leilão | 2023 | Ordenação com lambda | O(N log N) |
timeit