Ex Graphs
Ex Graphs
Problema 1 Progettare e analizzare un algoritmo efficiente basato sulla BFS che, dato un
grafo G = (V, E) non diretto, restituisca un ciclo, se esiste, o null, se non esiste.
Soluzione. Osserviamo che visitando tutte le componenti connesse di G con la BFS, se nessun
arco viene etichettato come CROSS EDGE non ci possono essere cicli (infatti i DISCOVERY EDGE
formano un albero per ogni componente connessa), mentre un arco (u, v) è etichettato come
CROSS EDGE (u, v), esiste un ciclo, che può essere ottenuto aggiungendo all’arco (u, v) tutti i
DISCOVERY EDGE incontrati risalendo da u e da v al loro primo antentato comune nell’albero
dei DISCOVERY EDGE radicato nella sorgente da cui è iniziata la visita della loro componente
connessa. L’algoritmo è basato su questa osservazione. Assumiamo che per ciascun vertice
v ∈ V siano disponibili i seguenti campi (oltre a v.ID): v.level, in cui verrà memorizzato il
livello di v nella BFS della sua componente connessa, e v.parent, in cui verrà memorizzato il
padre di v, se esiste, nel BFS tree della sua componente connessa. Si supponga di modificare
BFS(G, s) come segue: quando si esamina un vertice v ∈ Li e si inserisce un vicino w nel livello
Li+1 , etichettando l’arco (v, w) come DISCOVERY EDGE, allora, dopo aver impostato w.ID a 1,
si imposta w.level ← i + 1, e w.parent ← v. L’algoritmo è il seguente:
Algoritmo FindCycle(G)
Input: grafo G = (V, E) non diretto
Output: Un ciclo in G (lista di archi), se esiste, o null se non
esiste
forall v ∈ V do
v.ID ← 0;
v.parent ← null;
forall e ∈ V do e.label ← null;
forall v ∈ V do if (v.ID = 0) then BFS(G,v);
C ← lista vuota;
forall e ∈ E do
if (e.label = CROSS EDGE) then
C ← {e};
Siano u e v i vertici su cui e incide;
if (u.level = v.level + 1) then
aggiungi (u, u.parent) a C;
u ← u.parent;
if (v.level = u.level + 1) then
aggiungi (v, v.parent) a C;
v ← v.parent;
while (u 6= v) do
aggiungi (u, u.parent) e (v, v.parent) a C;
u ← u.parent;
v ← v.parent;
return C;
return null;
È facile vedere che le modifiche richieste alla BFS non ne alterano la complessità. Di con-
Dati e Algoritmi I (Pietracaprina): Esercizi 2
seguenza, i primi tre cicli forall eseguono sostanzialmente a una visita di tutto il grafo e, come
visto a lezione, hanno una complessità Θ (n + m), dove n è il numero di vertici ed m il numero
di archi. L’ultimo ciclo forall esegue una scansione di tutti gli archi (ad esempio sfruttando
la lista LE ) e appena trova un arco marcato come CROSS EDGE fa un numero costante di
operazioni e poi un ciclo while con al più n iterazioni, ciascuna di complessità costante. Se ne
deduce che la complssità finale è Θ (n + m). (Si osservi che le BFS potrebbero essere interrotte
appena si marca un arco come CROSS EDGE.) 2
Problema 2 Sia G = (V, E) un grafo con n vertici ed m archi. Progettare un algoritmo che
conti le coppie di vertici u, v ∈ V , tali che u 6= v ed esiste un cammino da u a v, analizzandone
la complessità. Per avere punteggio pieno la complessità deve essere O (n + m).
Problema 3 Sia G = (V, E) un grafo non diretto e connesso in cui ciascun vertice ha grado
esattamente c, con c > 2 costante intera. Si consideri l’esecuzione di BFS(G, s) a partire
da un vertice arbitrario s ∈ V . Dimostrare per induzione su i che il livello Li generato da
BFS(G, s) contiene ≤ c · (c − 1)i−1 vertici, per ogni i ≥ 0.
a vertici del livello i e poiché ciascun vertice v ∈ Li ha c vicini, di cui però almeno uno è nel
livello Li−1 , concludiamo che Li+1 ≤ (c − 1) · |Li |. Applicando l’ipotesi induttiva, otteniamo
Problema 4 Sia G = (V, E) un grafo con n vertici ed m archi. Progettare un algoritmo che
restituisce, se esiste, un vertice v ∈ V da cui sono raggiungibili (con cammini) ≥ n/2 altri
vertici, e analizzarne la complessità. Se un tale vertice non esiste l’algoritmo restituisce null.
arbitrario v e verificare che non ci siano CROSS EDGE tra vertici dello stesso livello. Infatti,
se non ci sono CROSS EDGE tra vertici dello stesso livello, tutti gli archi connettono vertici
in livelli adiacenti (uno di indice pari e uno di indice dispari). In questo caso, definendo Xv
come l’insieme di vertici appartenenti a livelli di indice pari e Yv come l’insieme di vertici
appartenenti a livelli di indice dispari, si ha una corretta bipartizione. Se invece esiste un
CROSS EDGE tra vertici dello stesso livello la componente connessa non può essere bipartita.
Infatti, se lo fosse e i suoi vertici fossero suddivisi in Xv e Yv , con v ∈ Xv , si avrebbe che
tutti i vertici appartenenti a livelli di indice pari dovrebbero essere in Xv e tutti i vertici
appartenenti a livelli di indice dispari dovrebbero essere in Yv , escludendo la possibilità di
CROSS EDGE tra vertici dello stesso livello.
Assumiamo che per ciascun vertice v ∈ V , oltre al campo v.ID, sia disponibile un campo
v.level, in cui verrà memorizzato il livello di v nella BFS della sua componente connessa.
Assumiamo anche di avere la lista LE degli archi (che può essere scansionata tramite un
iteratore) e che, dato un arco e = (u, v), i vertici u e v possano essere ottenuti in tempo O (1).
Si supponga di modificare BFS(G, s) come segue: quando si esamina un vertice v ∈ Li e si
inserisce un vicino w nel livello Li+1 , etichettando l’arco (v, w) come DISCOVERY EDGE, allora
dopo aver impostato w.ID a 1, si imposta w.level ← i + 1. L’algoritmo è il seguente:
Algoritmo isBipartite(G)
Input: Grafo G = (V, E) non diretto
Output: TRUE/FALSE se G è/non è bipartito
forall v ∈ V do v.ID ← 0;
forall e ∈ V do e.label ← null;
forall v ∈ V do
if (v.ID = 0) then
BFS(G, v)
forall e ∈ E do
Siano u e v i vertici su cui e incide;
if (u.level = v.level) then return FALSE;
return TRUE;
Per quanta riguarda la complessità, è facile vedere che le modifiche richieste alla BFS non ne
alterano la complessità. Di conseguenza, l’algoritmo equivale sostanzialmente a una visita di
tutto il grafo e, come visto a lezione, ha complessità Θ (n + m). 2
Problema 6 La teoria dei 6 gradi di separazione fu formulata per la prima volta nel 1929
dallo scrittore ungherese Frigyes Karinthy nel racconto omonimo pubblicato nel volume Catene.
b. Definire un problema computazionale su grafo tale che la sua risoluzione possa servire
a confermare o confutare la teoria, usando Facebook come insieme di dati di input.
c. Progettare e analizzare un algoritmo efficiente per il problema definito nel punto prece-
dente.
Soluzione.
Dati e Algoritmi I (Pietracaprina): Esercizi 5
a. La teoria afferma che ogni persona può essere collegata a qualunque altra persona o
cosa attraverso una catena di conoscenze e relazioni con non più di 5 intermediari.
b. Sia G = (V, E) il grafo (non diretto) di Facebook in cui i vertici rappresentano i profili
e gli archi le amicizie. Assumiamo che G connesso (in realtà non lo è) e definiamo la
separazione tra due profili u, v ∈ V come il numero di archi nel cammino più breve da u
a v in G. Per confermare o confutare la teoria dei 6 gradi di separazione, possiamo per
determinarne la massima separazione tra due profili in G. La teoria sarà confermata se
la massima separazione risulta minore o uguale a 6, e sarà confutata altrimenti.
c. Si noti che la separazione tra due profili u, v ∈ V non è altro che la distanza d(u, v)
tra u e v in G. Definiamo max-sep(v) la massima separazione tra v e un qualsiasi altro
vertice u. (Il valore max-sep(v) è anche chiamato eccentricità di v.) L’esercizio chiede
quindi di determinare il valore
max max-sep(v).
v∈V
Si osservi che per un qualsiasi profilo v ∈ V l’esecuzione di BFS(G, v) partiziona gli altri
profili in base alla loro separazione da v. In particolare, il livello Li , con i > 0, conterrà
tutti e soli i profili con separazione i da v. Se BFS(G, v) ha generato k+1 livelli non vuoti
(L0 , L1 , . . . , Lk ) significa che max-sep(v) = k. Supponiamo di modificare BFS(G, v) in
modo che alla fine restituisca l’indice dell’ultimo livello non vuoto generato. Assumiamo
anche di avere la lista LE degli archi, che può essere scansionata tramite un iteratore.
Per trovare la massima separazione tra due profili si può usare il seguente algoritmo.
Algoritmo MaxSeparation(G)
Input: grafo G = (V, E) non diretto e connesso
Output: maxv∈V max-sep(v)
max-sep → 0;
forall v ∈ V do
forall u ∈ V do u.ID ← 0;
forall e ∈ E do e.label ← null;
max-sep → max{max-sep, BFS(G, v)}
return max-sep
Si noti che l’inizializzazione dei campi ID e label fatta all’inizio di ciascuna iterazione
del forall esterno assicura che ciascuna invocazione della BFS visiti tutto il grafo. Di
conseguenza, la viene invocata esattamente una volta a partire dal ciascun vertice.
Poichè G è connesso, ogni invocazione costa Θ (n + m) = Θ (m), dato che m ≥ n − 1.
Quindi, la complessità di tutto l’algoritmo è Θ (nm).
2
Soluzione. Quando DFS(G, v) esamina w come vicino di v, il fatto che etichetti l’arco (v, w)
come BACK EDGE, implica che l’arco è stato trovato non ancora etichettato e w.ID = 1. Questo
Dati e Algoritmi I (Pietracaprina): Esercizi 6
significa che DFS(G, w) deve essere stata già invocata ma non conclusa, altrimenti (v, w)
avrebbe già una etichetta diversa da null quando DFS(G, v) lo esamina. Ne consegue che
esiste una sequenza di vertici w = w1 , w2 , . . . , wk = v tali che DFS(G, wi+1 ) è invocata
direttamente da DFS(G, wi ), per 1 ≤ i < k, e quindi (w1 , w2 )(w2 , w3 ), · · · , (wk−1 , wk ) è un
cammino di discovery edge da w a v, ovvero w è antenato di v. 2
Problema 8 Sia G = (V, E) un grafo non diretto con k > 1 componenti connesse. Pro-
gettare un algoritmo basato sulla DFS che aggiunga k − 1 archi a G per renderlo connesso, e
analizzarne la complessità. Si assuma di poter aggiungere a E un arco (u, v) 6∈ E in tempo
costante invocando il metodo G.addArc(u,v).
Si vede facilmente che con tale aggiunta il grafo diventa connesso. L’algoritmo richiesto è il
seguente.
Algoritmo MakeConnected(G)
Input: Grafo G = (V, E) non diretto con k componenti connesse
Output: Grafo G0 = (V, E ∪ E 0 ) connesso con |E 0 | = k − 1
forall v ∈ V do v.ID ← 0;
forall e ∈ V do e.label ← null;
u ← vertice arbitrario di V ;
DFS(G, u);
forall v ∈ V do
if (v.ID = 0) then
G.addArc(u, v);
DFS(G, v);
u ← v;
Soluzione. Rappresentiamo L come grafo non diretto nel modo seguente. I vertici del
grafo sono: il punto di ingresso s, gli incroci di L in cui si possono fare diverse scelte, e i
Dati e Algoritmi I (Pietracaprina): Esercizi 7
punti terminali di strade senza uscita in cui si è costretti a tornare indietro. Gli archi del
grafo sono tutte le strade che collegano direttamente coppie di incroci. Realisticamente l’ing.
Teseo può percorrere il labirinto a partire da s soltanto spostandosi lungo gli archi, e quindi
muovendosi da un vertice a un vertice adiacente. Assumiamo che ogni vertice u sia una sorta
di rotonda stradale in cui gli archi incidenti vengono esaminati in senso antiorario. Con questa
assunzione, è sufficiente che Teseo esegua una DFS sul grafo con i seguenti adattamenti
• La prima volta che Teseo entra in un vertice v appende un a marca di ingresso vicino
all’arco da cui è entrato.
– Se non trova alcuna marca di ingresso in v (quindi v non è stato ancora visitato)
appende una marca di ingresso in v vicino a e, che equivale a etichettare e come
DISCOVERY EDGE e a invocare DFS(L, v).
– Se trova una marca di ingresso in v ed era arrivato a u per la prima volta da v
(perchè in u c’è la marca di ingresso vicino all’arco e), allora significa che ha finito
l’esame di tutti gli archi incidenti su u. Questo equivale alla fine della chiamata
DFS(L, u), e Teseo prosegue a esaminare il prossimo arco incidente su v.
– Se trova una marca di ingresso in v ma non era arrivato a u per la prima volta da
v, allora Teseo torna su u e prosegue a esaminare il prossimo arco incidente su u.
Questo equivale a trovare e già etichettato o etichettarlo come BACK EDGE.
– Appena ritorna a s esce.
Teseo avrà successo nella sua missione dato che tutti i vertici vengono visitati e tutti gli archi
vengono attraversati. (In effetti la DFS è stata inventata nel 19-esimo secolo dal matematico
Francese Trémaux proprio come metodo di risoluzione di labirinti.) 2
Soluzione.
Problema 11 Un grafo non diretto G si dice biconnected se non esiste alcun vertice la cui
rimozione, unitamente alla rimozione degli archi incidenti, disconnette il grafo. Far vedere
che aggiungendo al più n archi a un grafo G = (V, E) non diretto, connesso, e con n = |V | ≥ 3
vertici, lo si può rendere biconnected (se non lo è inizialmente).
Soluzione. Sia V = {v0 , v1 , . . . , vn−1 }, dove i vertici sono indicizzati in base alla loro po-
sizione nella lista LV . Si supponga di aggiungere a E l’arco (vi , vi+1 mod n ), se non già presente,
per ogni 0 ≤ i ≤ n − 1. Con l’aggiunta di tali archi, che sono al più n dato che alcuni potreb-
bero già essere parte di E, si crea un ciclo che tocca tutti i vertici. Chiaramente, la rimozione
di un qualsiasi vertice vi , e degli archi in esso incidenti, non disconnette il grafo in quanto gli
altri vertici rimangono connessi almeno dal cammino identificato dai seguenti archi:
(vi+1 , vi+2 ) · · · (vn−1 , v0 )(v0 , v1 ) · · · (vi−2 , vi−1 ).
2
Dati e Algoritmi I (Pietracaprina): Esercizi 9
Problema 12 Sia G = (V, E) un grafo non diretto che rappresenta una rete sociale con
n vertici ed m archi. Ogni vertice u ∈ V ha un campo u.influencer che vale 1 se u è
un influencer e 0 altrimenti. Un vertice x non influencer si dice influenzabile se esiste un
influencer y e un cammino tra x e y. Progettare in pseudocodice un algoritmo Influenzabili
che conti il numero di vertici influenzabili in G, e analizzarne la complessità. Per avere
punteggio pieno la complessità deve essere O (n + m).
Soluzione. Si osservi che se in una componente connessa è presente un influencer, allora tutti
i vertici non influencer di quella componente connessa sono influenzabili. In base a questa
osservazione, possiamo risolvere il problema come segue. Si modifica BFS(G, s) in modo che
determini il numero di influencer e il numero di non influencer nella componente connessa di
s, restituendoli in output. A tale fine è sufficiente utilizzare due variabili n1 e n2 inizializzate
a 0, e, quando si visita un vertice u, incrementare n1 o n2 di 1, a seconda che u sia influencer
oppure no. Adesso, su ciascuna componente connessa, si invoca la BFS modificata e, se essa
contiene almeno un influencer, allora il numero di non influencer della componente connessa
viene aggiunto al conteggio degli influenzabili. Lo pseudocodice è il seguente.
Algoritmo Influenzabili(G)
Input: Grafo G = (V, E) non diretto con vertici etichettati come
influencer/non influencer
Output: Numero di vertici di V influenzabili
count ← 0;
forall v ∈ V do v.ID ← 0;
forall e ∈ V do e.label ← null;
forall v ∈ V do
if (v.ID = 0) then
(r1 , r2 ) ← BFS(G, v);
if (r1 > 0) then count ← count+r2 ;
return count
È immediato vedere che le modifiche richieste alla BFS non ne alterano la complessità. Dato
che la BFS viene invocata al più una volta per ogni componente connessa (l’algoritmo ha la
stessa struttura del pattern di visita completa di un grafo) la complessità è Θ (n + m). 2
Soluzione.
a. Modifichiamo BFS(G, s) in modo che accumuli in una variabile num-k, inizializzata a 0,
il numero di vertici che durante la visita sono inseriti nei livelli Li , con 0 ≤ i ≤ k. Questi
sono tutti e soli i vertici che hanno distanza ≤ k da s. Al termine della vista BFS(G, s)
imposterà la variabile s.numNeighbors al valore accumulato in num-k. A questo punto,
è sufficiente invocare la BFS modificata da ciascun vertice di v ∈ V , resettando a 0 i
campi ID di tutti i vertici, e a null i campi label di tutti gli archi, prima di ogni
Dati e Algoritmi I (Pietracaprina): Esercizi 10
b. È immediato vedere che le modifiche richieste alla BFS non ne alterano la complessità.
Quindi, ciascuna iterazione del ciclo forall esterno richiede O (n + m) operazioni. Di
conseguenza, la complessità dell’algoritmo è O (n(n + m)) = O n2 + nm .
Soluzione. L’inizializzazione assicura che l’invariante sia vero all’inizio del ciclo. Si osservi
che la prima proprietà è assicurata dal fatto che v.parent può essere impostato a un valore
diverso da null solo se v.D diventa < +∞. Per quanto riguarda la seconda proprietà, essa
viene mantenuta in ciascuna iterazione in base alla seguente osservazione. Se in una iterazione,
v.D viene impostato a u.D + w(u, v) e v.parent viene impostato a u, la proprietà continua
a valere in quanto esiste l’arco (u, v) e dall’iterazione precedente sappiamo che il cammino
ottenuto risalendo da u di “parent in parent” è un cammino da s a u di lunghezza u.D. 2
Problema 15 Sia G = (V, E, w) un grafo non diretto con n vertici ed m archi e con pesi
non negativi sugli archi.
b. Dire se l’algoritmo modificato restituisce comunque le distanze corrette anche per i ver-
tici non in Cs .
Soluzione.
a. È facile vedere che nella versione di ShortestPaths(G, s) presentata a lezione, nel mo-
mento in cui si estrae dalla Priority Queue Q una entry (u.D, u) con u.D = +∞, ovvero
Dati e Algoritmi I (Pietracaprina): Esercizi 11
b. L’algoritmo modificato restituisce comunque le distanze corrette anche per i vertici non
in Cs , in quanto esse sono impostate a +∞ all’inizio dell’algoritmo, e non sono mai
modificate.
2
Dati e Algoritmi I (Pietracaprina): Esercizi 12
Soluzione. L’idea è di visitare tutti gli archi e, per ciascun arco (v, u), incrementare di 1
v.outdeg e u.indeg, inizialmente impostati a 0. Lo pseudocodice è il seguente.
Algoritmo SetInOut(G)
Input: Grafo G = (V, E) diretto con n vertici ed m archi
Output: ∀v ∈ V , v.indeg = indegree(v) e v.outdeg = outdegree(v)
forall v ∈ V do
v.indeg ← 0;
v.outdeg ← 0;
forall v ∈ V do
forall e ∈ G.incidentEdges(v) do
v.outdeg ← v.outdeg + 1;
u ← G.opposite(v, e);
u.indeg ← u.indeg + 1;
2
Soluzione. Sia T lo spanning tree definito dai DISCOVERY EDGE. È facile osservare che
i discendenti di un vertice v di T sono tutti e soli i vertici scoperti durante l’esecuzione di
DFS(G, v). Quindi, per sapere se v è un antenato di u è sufficiente verificare se la chiamata
DFS(G, v) ha terminato la sua esecuzione oppure no. A questo scopo, modofichiamo la DFS
come segue. Supponiamo che ciascun vertice v ∈ V abbia un flag v.ON che viene inizializzato
a 0 prima della prima chiamata DFS(G, s). Tale flag sarà impostato a 1 appena DFS(G, v)
inizia la sua esecuzione, e resettato a 0 quando DFS(G, v) termina la sua esecuzione. In questo
modo, durante l’esecuzione di DFS(G, u) e l’esame dell’arco (u, v), se v è già stato visitato e
(u, v) non ancora etichettato, l’arco sarà etichettato BACK EDGE se v.ON = 1, e ALTRO se
v.ON = 0. È immediato vedere che la modifica alla DFS non ne altera la complessità. 2
Problema 18 Sia G = (V, E) un grafo diretto con n vertici e m archi. Dato un vertice s ∈ V
si definisca Es l’insieme di archi uscenti da reachable(s). È noto che esiste un ciclo diretto
in Es se e solo se DFS(s) etichetta un arco come BACK EDGE. Usando questa proprietà,
progettare e analizzare un algoritmo che dato s ∈ V restituisce una lista L che contiene gli
archi di un ciclo in Es , se ne esiste uno, altrimenti è vuota.
Soluzione. Dal Problema 17 sappiamo che la DFS può essere facilmente modificata in modo
che identifichi i BACK EDGE. La modifichiamo ulteriormente come segue. Si supponga che
ciascun vertice v abbia un campo v.parent inizializzato a null.
È facile vedere che le modifiche possono essere implementate senza alterare la complessità di
DFS. A questo punto, grazie alla proprietà enunciata nel testo del problema, sappiamo che
se DFS(G, s) non identifica alcun BACK EDGE, l’insieme di archi Es non contiene cicli e si
restituisce una lista L vuota. Altrimenti se DFS(G, s) restituisce un BACK EDGE (u, v) il
ciclo da restiutire è ottenuto inserendo nella lista L l’arco (u, v) e tutti DISCOVERY EDGE
incontrati risalendo da u a v di parent in parent. La scrittura dettagliata dello pseudocodice è
lasciata come esercizio. Se si assume che i campi dei vertici e degli archi richiesti dall’algoritmo
siano già opportunamente inizializzati, l’invocazione della DFS modificata e la costruzione del
ciclo richiede Θ (|Es |) operazioni. 2
Problema 19 Sia G = (V, E) un grafo diretto con n vertici e m archi. Progettare un algo-
ritmo che dato un vertice s ∈ V , renda l’insieme reachable(s) di nodi raggiungibili a partire
da s uguale a V , aggiungendo, se necessario, nuovi archi a E, ma non più di un arco uscente
per ogni vertice. Si assuma di poter aggiungere a E un arco (u, v) 6∈ E in tempo costante
invocando il metodo G.addArc(u, v). L’algoritmo avere complessità O (n + m).
È facile vedere che ogni vertice o è parte del cammino che parte da s (formato dagli archi
aggiunti), oppure è raggiungibile da uno dei vertici di tale cammino, e quindi alla fine tutti
i vertici sono raggiungibili da V . Inoltre, viene aggiunto al più un arco uscente per ogni
vertice. Per quanto riguarda la complessità, si vede che un vertice viene visitato una sola
volta, e ogni arco viene etichettato una sola volta. Quindi, ragionando come nell’analisi della
vista completa di un grafo, si dimostra che complessità è O (n + m), come richiesto. 2
profilo u che è meno influenzato da i, ovvero per cui la distanza d(u, i) del cammino minimo
da u a i è massima. Progettare e analizzare un algoritmo per determinarne un tale vertice u
in tempo O(n + m).
b. Dato un grafo semplice non diretto G = (V, E) è possibile dare un orientamento ai suoi
archi in modo che esso diventi un DAG.
Soluzione.