Rappresentazioni dei grafi e configurazione dei percorsi
Costruisca grafi diretti e non diretti con liste di adiacenza, inizializzi BFS con una deque e DFS con uno stack o la ricorsione, gestendo il tracciamento dei nodi visitati
Rappresentazioni dei grafi e configurazione dei percorsi è una lezione Coding Interview Prep gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Coding Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Coding Interview Prep include 4 lezioni in totale.
Che cos'è un grafo
Un grafo è un insieme di nodi (vertici) collegati da archi. A differenza degli alberi, i grafi possono contenere cicli, più percorsi tra i nodi e componenti disconnesse. I grafi modellano sistemi reali come social network, mappe stradali, alberi delle dipendenze e collegamenti tra pagine web. Quasi ogni colloquio di system design o sugli algoritmi che affronti sistemi non banali include i grafi: padroneggiarne la rappresentazione e le visite è essenziale.
# Graph terminology:
# - V: set of vertices (nodes)
# - E: set of edges
# - Directed graph: edges have direction (A -> B but not B -> A)
# - Undirected graph: edges are bidirectional
# - Weighted graph: edges have costs/weights
# - Cyclic: contains at least one cycle
# - Acyclic: no cycles (DAG = Directed Acyclic Graph)
# - Connected: every node reachable from every other
# - Disconnected: multiple isolated components
print('Graph: nodes + edges, directed/undirected, weighted/unweighted')Rappresentazione con lista di adiacenza
Una lista di adiacenza memorizza la lista dei nodi adiacenti a ciascun nodo. In Python, utilizzi un dict che associa ogni nodo a una lista di nodi adiacenti. È la rappresentazione più comune nei problemi da colloquio tecnico: spazio O(V + E) (efficiente per i grafi sparsi), O(grado) per iterare sui nodi adiacenti e O(1) in media per verificare se due nodi sono adiacenti con una variante basata su un hash set. La maggior parte dei problemi sui grafi di LeetCode utilizza questo formato.
from collections import defaultdict
# Build an undirected graph
def build_undirected(edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # both directions
return graph
edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
graph = build_undirected(edges)
print(dict(graph))
# {0:[1,2], 1:[0,3], 2:[0,3], 3:[1,2,4], 4:[3]}
# Directed graph: only one direction
def build_directed(edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v) # only u -> v
return graphRappresentazione con matrice di adiacenza
Una matrice di adiacenza è un array bidimensionale V×V in cui matrix[i][j] = 1 (o il peso dell'arco) se esiste un arco da i a j e 0 altrimenti. Offre un accesso agli archi in O(1), ma utilizza spazio O(V²) indipendentemente dal numero di archi: uno spreco per i grafi sparsi. È preferibile quando il grafo è denso (con molti archi) o quando sono essenziali verifiche rapide dell'esistenza degli archi, ad esempio nel calcolo dei cammini minimi tra tutte le coppie di Floyd-Warshall.
# Adjacency matrix for 5 nodes
V = 5
matrix = [[0] * V for _ in range(V)]
edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
for u, v in edges:
matrix[u][v] = 1
matrix[v][u] = 1 # undirected
# Print the matrix:
for row in matrix:
print(row)
# Neighbour check: O(1)
print('Edge 0-2:', bool(matrix[0][2])) # True
print('Edge 0-4:', bool(matrix[0][4])) # False
# Space: O(V^2) vs adjacency list O(V+E)
# Dense graph: matrix often better; sparse: list betterRappresentazione con lista degli archi
Una lista degli archi è la rappresentazione più semplice: consiste solo in una lista di tuple (sorgente, destinazione), eventualmente con i pesi. Utilizza spazio O(E) ed è facile da scorrere per esaminare tutti gli archi. Tuttavia, per trovare i nodi adiacenti a un nodo è necessario analizzare tutti gli archi: O(E). Le liste degli archi vengono utilizzate negli algoritmi sui grafi che esaminano esattamente tutti gli archi, come Bellman-Ford (rilassamento di tutti gli archi n-1 volte) e l'algoritmo di Kruskal per l'albero ricoprente minimo.
# Weighted edge list: (source, destination, weight)
edge_list = [
(0, 1, 4),
(0, 2, 1),
(1, 3, 1),
(2, 3, 5),
(3, 4, 3)
]
# Useful for:
# Bellman-Ford: iterate all edges n-1 times
# Kruskal's MST: sort by weight then union-find
# Sort by weight for Kruskal:
edge_list_sorted = sorted(edge_list, key=lambda e: e[2])
print('Sorted by weight:', edge_list_sorted)
# Finding neighbours: O(E) scan -- inefficient for traversal
node_0_neighbors = [v for u, v, w in edge_list if u == 0]
print('Node 0 neighbors:', node_0_neighbors)Configurazione BFS: coda e insieme dei visitati
BFS (ricerca in ampiezza) esplora un grafo livello per livello utilizzando una coda. Il componente fondamentale è un insieme dei visitati per evitare di visitare nuovamente i nodi nei grafi ciclici. Senza questo insieme, BFS su un grafo ciclico entrerebbe in un ciclo infinito. La configurazione standard consiste nell'inizializzare la coda con il nodo sorgente, contrassegnarlo come visitato e quindi estrarre ripetutamente un nodo dalla coda, elaborarlo e inserire in coda i nodi adiacenti non visitati.
from collections import deque
def bfs(graph, start):
visited = {start} # mark source as visited
queue = deque([start]) # initialise queue
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbour in graph[node]:
if neighbour not in visited:
visited.add(neighbour) # mark BEFORE enqueue
queue.append(neighbour)
return order
from collections import defaultdict
graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,3),(2,3),(3,4)]:
graph[u].append(v); graph[v].append(u)
print(bfs(graph, 0)) # [0, 1, 2, 3, 4]Configurazione DFS: stack o ricorsione
DFS (ricerca in profondità) esplora ogni ramo il più possibile prima di tornare indietro. Può essere implementata in modo ricorsivo (utilizzando lo stack delle chiamate) oppure iterativo (utilizzando uno stack esplicito). Entrambe le implementazioni richiedono un insieme dei visitati nei grafi ciclici. La versione iterativa inserisce i nodi adiacenti in ordine inverso per riprodurre l'ordine di visita della DFS ricorsiva, anche se l'ordine di esplorazione può variare tra le due implementazioni.
def dfs_recursive(graph, node, visited=None, order=None):
if visited is None: visited = set(); order = []
visited.add(node)
order.append(node)
for neighbour in graph[node]:
if neighbour not in visited:
dfs_recursive(graph, neighbour, visited, order)
return order
def dfs_iterative(graph, start):
visited = set()
stack = [start]
order = []
while stack:
node = stack.pop()
if node in visited: continue
visited.add(node)
order.append(node)
for neighbour in reversed(graph[node]): # reverse for same order as recursive
if neighbour not in visited:
stack.append(neighbour)
return order
print('Recursive DFS:', dfs_recursive(graph, 0))
print('Iterative DFS:', dfs_iterative(graph, 0))Quando usare BFS o DFS
Scelga BFS quando ha bisogno del cammino minimo (con il minor numero di archi) in un grafo non pesato o quando deve elaborare i nodi livello per livello. Scelga DFS quando deve esplorare tutti i nodi raggiungibili, rilevare cicli, trovare componenti connesse, eseguire un ordinamento topologico o enumerare tutti i cammini. In pratica: BFS per «cammino minimo/numero minimo di passaggi», DFS per «esistenza/raggiungibilità/enumerazione».
# BFS use cases:
# - Shortest path in unweighted graph (fewest edges)
# - Level-order traversal
# - Word ladder (minimum transformations)
# - Clone graph
# DFS use cases:
# - Connected components (flood fill)
# - Cycle detection
# - Topological sort
# - All paths between two nodes
# - Maze solving (any path)
# - N-queens, Sudoku (backtracking)
# Both: O(V + E) time, O(V) space for visited
print('BFS: shortest hops | DFS: existence and enumeration')Grafi nei formati di input di LeetCode
I problemi sui grafi di LeetCode possono presentare diversi formati di input. Lista degli archi: [[0,1],[0,2]] — costruisca una lista di adiacenza. Lista di adiacenza indicizzata: graph[i] è la lista dei nodi adiacenti a i. Griglia/matrice: un array bidimensionale m×n in cui le celle sono nodi e le celle adiacenti (sopra/sotto/sinistra/destra) sono nodi adiacenti. Nodo con figli: classi personalizzate come Node(val, neighbors). Riconosca questi formati e li converta in una lista di adiacenza come primo passaggio.
# Format 1: edge list -> adjacency list
def edges_to_adj(n, edges):
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
return graph
# Format 2: 2D grid -> adjacency (implicit)
# Neighbours of (r, c): (r-1,c), (r+1,c), (r,c-1), (r,c+1)
DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
def grid_neighbours(grid, r, c):
rows, cols = len(grid), len(grid[0])
return [(r+dr, c+dc) for dr, dc in DIRS
if 0 <= r+dr < rows and 0 <= c+dc < cols]
grid = [[1,1,0],[0,1,1],[1,0,0]]
print('Neighbours of (0,0):', grid_neighbours(grid, 0, 0))
print('Neighbours of (1,1):', grid_neighbours(grid, 1, 1))Contrassegnare le celle visitate nelle griglie
Nei problemi sulle griglie, esistono due modi per tenere traccia delle celle visitate. Opzione A: utilizzi un insieme visited separato di tuple (row, col) — spazio aggiuntivo O(m*n). Opzione B: modifichi la griglia direttamente contrassegnando le celle visitate con un valore sentinella (ad es., '#' o 2) e ripristinandole in seguito, se necessario. L'approccio diretto utilizza spazio aggiuntivo O(1) ed è comune nei problemi di flood fill e Number of Islands.
def num_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols:
return
if grid[r][c] != '1':
return
grid[r][c] = '#' # mark as visited (in-place)
dfs(r+1, c); dfs(r-1, c)
dfs(r, c+1); dfs(r, c-1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
dfs(r, c)
count += 1
return count
grid = [['1','1','0','0'],
['1','1','0','0'],
['0','0','1','0'],
['0','0','0','1']]
print(num_islands(grid)) # 3Inizializzare BFS con più sorgenti
La BFS multi-sorgente parte simultaneamente da più nodi, inizializzando la coda con tutti i nodi sorgente già contrassegnati come visitati. Viene utilizzata in problemi come «distance from nearest 0», «rotting oranges» e «walls and gates», nei quali si desidera la distanza minima da uno qualsiasi dei nodi sorgente. La BFS multi-sorgente ha complessità O(V + E), come quella a sorgente singola, perché ogni nodo viene comunque visitato al massimo una volta.
from collections import deque
def rotting_oranges(grid):
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh = 0
# Multi-source: all rotten oranges start at time=0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c, 0)) # (row, col, time)
elif grid[r][c] == 1:
fresh += 1
dirs = [(0,1),(0,-1),(1,0),(-1,0)]
time = 0
while queue:
r, c, t = queue.popleft()
for dr, dc in dirs:
nr, nc = r+dr, c+dc
if 0<=nr<rows and 0<=nc<cols and grid[nr][nc]==1:
grid[nr][nc] = 2 # mark rotten
fresh -= 1
queue.append((nr, nc, t+1))
time = t + 1
return time if fresh == 0 else -1
print(rotting_oranges([[2,1,1],[1,1,0],[0,1,1]])) # 4Densità del grafo e scelta della rappresentazione
La scelta tra lista e matrice di adiacenza dipende dalla densità del grafo, cioè dal rapporto E/V². Un grafo sparso (E << V²) trae vantaggio dalle liste di adiacenza: spazio O(V+E) rispetto a O(V²) per una matrice. Un grafo denso (E ≈ V²) trae vantaggio dalle matrici di adiacenza: accesso agli archi in O(1) rispetto a O(grado) per le liste. Nei problemi da colloquio tecnico, le liste di adiacenza sono quasi sempre la scelta corretta, poiché la maggior parte dei problemi riguarda grafi sparsi.
# Graph density comparison:
# Sparse: social network (V=1B users, avg 200 friends)
# E = 200 * 1B = 200B << V^2 = 10^18 -> adjacency list
# Dense: complete graph (every node connected to every other)
# E = V*(V-1)/2 ≈ V^2 -> adjacency matrix
# Interview rule of thumb:
# - Default to adjacency list (defaultdict(list))
# - Use matrix only when asked about dense graph or O(1) edge lookup
# - Grid problems: use implicit adjacency (4-directional neighbours)
print('Sparse graph (E << V^2): use adjacency list')
print('Dense graph (E ~ V^2): consider adjacency matrix')Controllo rapido
Metta alla prova la sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.
Riepilogo della lezione
In questa lezione ha appreso: tre rappresentazioni dei grafi (lista di adiacenza, matrice e lista degli archi) e quando scegliere ciascuna, la configurazione di BFS e DFS con insiemi dei visitati per evitare cicli infiniti nei grafi ciclici e schemi pratici come il contrassegno diretto delle celle nelle griglie e la BFS multi-sorgente. Nel prossimo argomento applicheremo BFS per trovare cammini minimi ed effettuare visite per livelli.
Impara Coding Interview Prep con un tutor IA — gratis
Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.
- Corsi
- 90
- Lezioni
- 360
Domande Frequenti
La lezione «Rappresentazioni dei grafi e configurazione dei percorsi» è gratuita?
Sì — il testo completo di «Rappresentazioni dei grafi e configurazione dei percorsi» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Coding Interview Prep, passa a CoddyKit PRO. Il corso Coding Interview Prep include 4 lezioni in totale.
Cosa imparerò in «Rappresentazioni dei grafi e configurazione dei percorsi»?
Costruisca grafi diretti e non diretti con liste di adiacenza, inizializzi BFS con una deque e DFS con uno stack o la ricorsione, gestendo il tracciamento dei nodi visitati Eserciti Coding Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Coding Interview Prep?
Non è richiesta alcuna esperienza precedente. Coding Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.
Quanto tempo richiede la lezione «Rappresentazioni dei grafi e configurazione dei percorsi»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Coding Interview Prep?
Sì. Ogni lezione Coding Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Rappresentazioni dei grafi e configurazione dei percorsi
- BFS: percorso più breve e visita per livelli
- DFS: componenti connesse e flood fill
- Rilevamento dei cicli nei grafi diretti e non diretti