DSA Interview Prep · Lezione

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

Lezione 1 di 413 passaggi

Rappresentazioni dei grafi e configurazione dei percorsi è una lezione DSA 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 DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA 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 graph

Rappresentazione 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 better

Rappresentazione 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))  # 3

Inizializzare 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]]))  # 4

Densità 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.

Gratis per iniziare

Impara Python 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
30
Lezioni
120

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 DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA 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 DSA 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 DSA Interview Prep?

Non è richiesta alcuna esperienza precedente. DSA 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 DSA Interview Prep?

Sì. Ogni lezione DSA 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

  1. Rappresentazioni dei grafi e configurazione dei percorsi
  2. BFS: percorso più breve e visita per livelli
  3. DFS: componenti connesse e flood fill
  4. Rilevamento dei cicli nei grafi diretti e non diretti
← Torna a DSA Interview Prep