Il 0% ha trovato utile questo documento (0 voti)
12 visualizzazioni15 pagine

Ex Graphs

Il documento presenta una serie di problemi e soluzioni riguardanti algoritmi su grafi, inclusi algoritmi per trovare cicli, contare coppie di vertici raggiungibili, dimostrare proprietà sui livelli generati dalla BFS, identificare componenti connesse e verificare se un grafo è bipartito. Ogni problema è accompagnato da un algoritmo specifico e un'analisi della complessità, generalmente Θ(n + m). Inoltre, viene menzionata la teoria dei 6 gradi di separazione, con un'applicazione pratica utilizzando un grafo rappresentante i profili di Facebook.
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Il 0% ha trovato utile questo documento (0 voti)
12 visualizzazioni15 pagine

Ex Graphs

Il documento presenta una serie di problemi e soluzioni riguardanti algoritmi su grafi, inclusi algoritmi per trovare cicli, contare coppie di vertici raggiungibili, dimostrare proprietà sui livelli generati dalla BFS, identificare componenti connesse e verificare se un grafo è bipartito. Ogni problema è accompagnato da un algoritmo specifico e un'analisi della complessità, generalmente Θ(n + m). Inoltre, viene menzionata la teoria dei 6 gradi di separazione, con un'applicazione pratica utilizzando un grafo rappresentante i profili di Facebook.
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Sei sulla pagina 1/ 15

Dati e Algoritmi (Pietracaprina)

Esercizi svolti sui Grafi


Dati e Algoritmi I (Pietracaprina): Esercizi 1

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).

Soluzione. Si ricorda che da un insieme di K oggetti si possono formare esattamente K(K −


1)/2 coppie distinte. L’idea è di visitare tutte le componenti connesse di G usando una
BFS modificata che per ogni componente connessa ne determina il numero di vertici K e
aggiunge il valore K(K − 1)/2 al conteggio delle coppie di vertici connessi da un cammino
(cioè raggiungibili uno dall’altro), mantenuto tramite un contatore count. Supponiamo di
modificare BFS(G, v) definendo un contatore count, inizializzato a 1 (per tenere traccia di v),
che viene incrementata ogni volta in cui si visita un vertice w 6= v impostando w.ID a 1, e il
cui valore viene restituito in output. L’algoritmo richiesto è il seguente.
Algoritmo ReachablePairs(G)
Input: Grafo G = (V, E) non diretto e non connesso
Output: Numero coppie u, v ∈ V , con u =
6 v, collegate da cammini
totcount ← 0;
forall v ∈ V do v.ID ← 0;
forall e ∈ V do e.label ← null;
forall v ∈ V do
if (v.ID = 0) then
K ← BFS(G, v);
totcount ← totcount+K(K − 1)/2
return totcount
La correttezza dell’algoritmo è immediata. 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 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.

Soluzione. Come base dell’induzione consideriamo i = 0 e i = 1. Il livello L0 contiene il


solo vertice s e quindi |L0 | = 1 ≤ c · (c − 1)−1 . Il livello L1 contiene i c vicini di s, e quindi
|L1 | = c = c · (c − 1)0 . Fissiamo un indice i ≥ 1 e supponiamo, come ipotesi induttiva, che
per ogni 0 ≤ j ≤ i si abbia |Lj | ≤ c · (c − 1)j−1 . I vertici del livello Li+1 sono tutti adiacenti
Dati e Algoritmi I (Pietracaprina): Esercizi 3

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

|Li+1 | ≤ (c − 1) · |Li | ≤ (c − 1) · c · (c − 1)i−1 = c · (c − 1)i .

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.

Soluzione. Si noti che da un vertice v ∈ V sono raggiungibili ≥ n/2 altri vertici se e


solo se la componente connessa di v contiene almeno n/2 + 1 vertici. L’idea è quindi quella
di determinare, tramite BFS modificata, la cardinalità di ciascuna componente connessa e,
appena se ne trova una di cardinalità almeno n/2 + 1, restituire un vertice arbitrario di essa
(ad es., quello da cui è iniziata la visita). Supponiamo di modificare BFS(G, v) definendo una
variabile cardinality, inizializzata a 0, che viene incrementata ogni volta in cui si visita un
vertice w e si imposta w.ID a 1, e il cui valore viene restituito in output. L’algoritmo richiesto
è il seguente.
Algoritmo LargeComponent(G)
Input: grafo G = (V, E) non diretto
Output: vertice v ∈ V da cui sono raggiungibili ≥ n/2 vertici, o null
se un tale vertice non esiste
forall v ∈ V do v.ID ← 0;
forall e ∈ V do e.label ← null;
forall v ∈ V do
if (v.ID = 0) then
K ← BFS(G, v);
if (K ≥ n/2 + 1) then return v;
return null
La correttezza dell’algoritmo discende immediatamente dalla osservazione fatta all’inizio. 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 5 Un grafo G = (V, E) si dice bipartito se l’insieme di vertici V può essere


partizionato in due sottoinsiemi X e Y tali che ogni arco di E incide su un vertice di X
e uno di Y . Progettare e analizzare un algoritmo efficiente che determini se un grafo non
diretto G è bipartito.

Soluzione. Si osservi anzitutto che G è bipartito se e solo se ciascuna sua componente


connessa è bipartita. Basterà quindi controllare per ogni componente connessa se essa è
bipartita oppure no. Si ricordi che, nell’esecuzione della BFS, ogni DISCOVERY EDGE collega
due vertici in livelli adiacenti, mentre ogni CROSS EDGE collega due vertici che appartengono
allo stesso livello o a livelli adiacenti. In virtù di questa osservazione, per controllare se una
componente connessa è bipartita sarà sufficiente invocare la BFS a partire da un suo vertice
Dati e Algoritmi I (Pietracaprina): Esercizi 4

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.

a. Trovare l’enunciato di tale teoria.

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

Problema 7 Si consideri l’esecuzione di DFS(G, s), e sia T lo spanning tree di Cs , radicato


in s, costituito dagli archi etichettati come DISCOVERY EDGE da tale esecuzione. Supponiamo
che durante una delle varie chiamate ricorsive DFS(G, v) si etichetti un arco (v, w) come BACK
EDGE. Dimostrare che w è antenato di v in T .

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).

Soluzione. Siano n = |V |, m = |E|. Si denoti con uj un vertice arbitrario dell’j-esima


componente connessa di G, per 1 ≤ j ≤ k. Si aggiungano a E i seguenti k − 1 archi:

{(uj , uj+1 ) : 1 ≤ j < k}.

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;

La correttezza dell’algoritmo è giustificata dalla precedente osservazione. Per quanto riguarda


la complessità, l’algoritmo equivale sostanzialmente a una visita di tutto il grafo e, come visto
a lezione, ha complessità Θ (n + m).

Osservazione: all’algoritmo non serve conoscere il numero k di componenti connesse. 2

Problema 9 Si consideri un labirinto L che ha un unico punto di ingresso s e al cui interno


è nascosto il terribile Minotauro. L’ ing. Teseo deve entrare in L, trovare il Minotauro,
ucciderlo, e uscire da L. Trovare un’opportuna rappresentazione del labirinto come grafo
e far vedere come, sfruttando l’algoritmo DFS, l’ing. Teseo può compiere con successo la
sua missione. Si assuma che il labirinto sia connesso, nel senso che ogni punto di esso sia
raggiungibile da s.

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

• Se trova il Minotauro lo uccide.

• La prima volta che Teseo entra in un vertice v appende un a marca di ingresso vicino
all’arco da cui è entrato.

• Quando Teseo attraversa un arco e = (u, v) da u a v, opera come segue:

– 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

Problema 10 (Esercizio C-14.56 in [GTG14]) Sia G = (V, E) un grafo non diretto. Un


insieme di vertici I ⊆ V è un Independent Set (IS) se E non contiene archi (u, v) con
u, v ∈ I, e si dice Maximal se appena si aggiunge a esso un vertice w ∈ V − I, si perde la
proprietà di IS. In altre parole, I è un Maximal Independent Set (MIS) se per ogni w ∈ V − I
esiste v ∈ I tale che (v, w) ∈ E.

a. Dimostrare che in ogni grafo G esiste sempre un MIS.

b. Progettare e analizzare un algoritmo efficiente per trovare un MIS in un grafo G.

Soluzione.

a. Si consideri un algoritmo che parte da un IS I vuoto, ed esegue un ciclo in cui ogni


iterazione aggiunge a I un vertice v che non è ancora in I e che non è adiacente ad
alcun vertice di I, se un tale v esiste. L’algoritmo termina quando non si trovano più
vertici da aggiungere a I, ovvero quando I = V , oppure qualsiasi vertice non in I è
adiacente a qualche vertice in I. In entrambi i casi, I è un MIS dato per costruzione
non ci sono archi tra i soi vertici, e nessuno dei vertici in V − I può essere aggiunto a I
senza violare la proprietà di IS. L’algoritmo dimostra che esiste sempre un MIS.
Dati e Algoritmi I (Pietracaprina): Esercizi 8

b. Un’implementazione banale dell’algoritmo del punto precedente sembrerebbe richiedere,


al caso pessimo, un’intera scansione di tutto V per ogni iterazione del ciclo. Tuttavia si
può fare meglio eseguendo la scansione dei vertici una sola volta e aggiungendo ciascun
vertice v all’IS I corrente se, nel momento in cui v viene esaminato, esso non ha vicini
in I. Assumiamo di avere a disposizione, per ogni v ∈ V , un campo binario v.IS che
alla fine varrà 1 per tutti e soli i vertici del MIS. L’algoritmo è il seguente:
Algoritmo MIS(G)
Input: Grafo G = (V, E) non diretto
Output: MIS I per G
forall v ∈ V do v.IS ← 0;
I ←− ∅;
forall v ∈ V do
v.IS ← 1;
forall e ∈ G.incidentEdges(v) do
u ← G.opposite(v, e);
if (u.IS = 1) then v.IS ← 0;
if (v.IS = 1) then aggiungi v a I;
return I
La correttezza discende facilmente dal seguente invariante che vale alla fine di ogni
iterazione del secondo ciclo forall:
• L’insieme I è un IS
• Nessuno dei vertici già esaminati e non inseriti in I può essere aggiunto a I senza
violare la proprietà di IS.
La verifica dell’invariante è lasciata come esercizio. Per quanto riguarda la complessità,
sia n il numero di vertici ed m il numero di archi di G. Il primo ciclo for richiede
O (n) operazioni, mentre ogni iterazione v del secondo ciclo for richiede un numero di
operazioni proporzionale al grado del vertice v. Dato che la somma dei gradi di tutti i
vertici è O (m) si deduce che la complessità dell’algoritmo è O (n + m).
2

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

Problema 13 Sia G = (V, E) un grafo non diretto con n vertici ed m archi.


a. Progettare un algoritmo che dato G ed un intero k > 0, per ogni vertice v ∈ V salva in
una variabile v.numNeighbors il numero di vertici a distanza ≤ k da v.

b. Analizzare la complessità dell’algoritmo.

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

invocazione (come nell’algoritmo sviluppato per il Problema 6) Lo pseudocodice è il


seguente.
Algoritmo CountNeighbors(G, k)
Input: Grafo G = (V, E) non diretto con n vertici ed m archi, intero
k ∈ [0, n)
Output: ∀v ∈ V , v.numNeighbors = numero vertici a distanza ≤ k da v
forall v ∈ V do
forall u ∈ V do u.ID ← 0;
forall e ∈ E do e.label ← null;
BFS(G, v) /* BFS modificata */

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 .


Problema 14 Sia G = (V, E, w) un grafo no diretto e pesato, e sia s un vertice di V .


Dimistrare che alla fine di ciascuna iterazione del ciclo while di ShortestPaths(G, s), vale
il seguente invariante. Per ogni v ∈ V :

• se v.D = +∞, allora v.parent = null.

• se v.D < +∞, il cammino ottenuto risalendo da v di “ parent in parent” è un cammino


da s a v di lunghezza v.D.

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.

a. Progettare una modifica di ShortestPaths(G, s) in modo che la complessità sia


O (n + ms log ns ), dove ns è il numero di vertici e ms il numero di archi nella com-
ponente connessa di s (Cs ).

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

associata a un vertice u 6∈ Cs , da quel momento in poi tutte le altre entry estratte


avranno chiave uguale a +∞, ovvero saranno relative a vertici che non appartengono
a Cs , e non ci saranno altri aggiornamenti di Q. In altre parole, le entry associate ai
vertici di Cs sono estratte prima di quelle associate a vertici non in Cs . Allora si può
modificare ShortestPaths(G, s) in modo che Q sia inizializzata solo con la entry (0, s),
e che, la prima volta che per un vertice v si aggiorna il campo v.D a un valore < +∞, la
entry (v.D, v) venga inserita in Q. In questo modo, le entry relative a vertici di Cs sono
estratte da Q nello stesso ordine con cui vengono estratte nell’algoritmo originale e gli
aggiornamenti dei campi D sono gli stessi (e nello stesso ordine) di quelli dell’algoritmo
originale. Lo pseudocodice dell’algoritmo modificato è il seguente
s.D ← 0; s.parent ← null;
forall v ∈ V − {s} do
v.D ← +∞;
v.parent ← null
Q ← Priority Queue contenente solo la entry (0, s);
while (!Q.isEmpty()) do
(u.D, u) ← Q.removeMin();
forall (u, v) ∈ E do
if (u.D + w(u, v) < v.D) then
v.D ← u.D + w(u, v);
v.parent ← u;
if (Q.has(v)) then (Q.decreaseKey(v.D, v));
else (Q.insert(v.D, v));

Riguardo alla complessità, il primo ciclo forall richiede O (n) operazioni e


l’inizializzazione di Q richiede Θ (1) operazioni. Il ciclo while esegue esattamente ns
iterazioni, in ciascuna delle quali viene estratta una entry relativa a un vertice distinto
di Cs . Si noti che Q conterrà solo entry relative a vertici di Cs , (quindi |Q| ≤ ns ), e
che per ogni vertice di Cs , la relativa entry viene inserita/estratta in/da Q solo una
volta. Ne consegue che il contributo del ciclo while alla complessità è quello relativo a
ns estrazioni da Q, ns − 1 inserimenti in Q, e ms edge-relaxation (una per ogni arco di
Cs ), ciascuna delle quali richiede tempo al più proporzionale a quello di una invocazione
al metodo decreaseKey. Si supponga di implementare Q tramite Heap. Come visto
nell’analisi di ShortestPaths, il metodo has può essere implementato con un numero
costante di operazioni (usando opportuni link tra vertici e entry di Q), e il metodo
decreaseKey può essere implementato con un numero di operazioni logaritmico nel nu-
mero di entry in Q. Dato che in ogni momento |Q| ≤ ns , il costo complessivo del ciclo
while è O ((ns + ms ) log ns ) = O (ms log ns ), e la complessità dell’algoritmo modificato
è O (n + ms log ns ), come richiesto.

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

Problema 16 Sia G un grafo diretto rappresentato tramite liste di adiacenza. Progettare un


algoritmo che salva in ogni nodo v ∈ V due campi v.outdeg e v.indeg uguali a, rispettiva-
mente, outdegree(v) e indegree(v), e analizzarne la complessità.

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

Problema 17 Sia G = (V, E) un grafo diretto. Modificare lo pseudocodice DFS(G, s) in modo


che un arco (u, v), esaminato dalla chiamata DFS(G, u), sia etichettato come BACK EDGE
se v è antenato di u nello spanning tree definito dai DISCOVERY EDGE, e sia etichettato
ALTRO negli altri casi.

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.

• Se un arco (v, w) è etichettato come DISCOVERY EDGE, si imposta w.parent a v.


Dati e Algoritmi I (Pietracaprina): Esercizi 13

• Se durante l’esecuzione di DFS(G, v) (e delle chiamate ricorsive al suo interno) almeno


arco e viene etichettato come BACK EDGE, allora DFS(G, v) restituisce un tale arco,
altrimenti restituisce 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).

Soluzione. L’idea è di invocare la BFS a partire da s e, successivamente, a partire da vertici


non ancora visitati, e collegare con un cammino tutti i vertici da cui si inizia la BFS. Lo
pseudocodice è il seguente.
Algoritmo MakeReachable(G, s)
Input: Grafo diretto G = (V, E) vertice s ∈ V
Output: Grafo diretto G0 = (V, E ∪ E 0 ) in cui reachable(s) = V e E 0 ha
al più un arco uscente da ciascun vertice
forall v ∈ V do v.ID ← 0;
forall e ∈ V do e.label ← null;
BFS(G, s);
u ← s;
forall v ∈ V do
if (v.ID = 0) then
BFS(G, v);
G.addArc(u, v);
u ← v;

È 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

Problema 20 Sia G = (V, E) il grafo diretto di Instagram in cui i vertici rappresentano i


profili e un arco (u, v) il fatto che il profilo u segue il profilo v. Si assuma che G sia fortemente
connesso (in realtà non lo è). Dato il profilo di un influencer i ∈ V , si vuole identificare un
Dati e Algoritmi I (Pietracaprina): Esercizi 14

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).

Soluzione. L’idea è la seguente. Sia GT = (V, E T ) il grafo ottenuto da G invertendo


l’orientamento di ciascun arco. Chiaramente, se G è fortemente connesso anche GT lo è.
Inoltre, è immediato vedere che dato i ∈ V , un vertice u per cui la distanza d(u, i) del
cammino minimo da u a i è massima, è un qualsiasi vertice che nella BFS eseguita su GT a
partire da i, è inserito nell’ultimo livello non vuoto. L’algoritmo richiesto deve quindi creare
GT (a questo fine è sufficiente visitare tutto il grafo come nella soluzione del Problema 16 e
creare le liste di adiacenza per GT ), eseguire la BFS(GT , i) modificata in modo che restituisca
un vertice dell’ultimo livello non vuoto, e restituire tale vertice in output. La scrittura dello
pseudocodice e l’analisi sono lasciati come esercizio. 2

Problema 21 Dimostrare le seguenti proprietà:

a. Se un grafo diretto G = (V, E) ha un ciclo diretto, non può avere un ordinamento


topologico.

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.

a. Sia v0 → v1 → · · · → vk−1 → vk = v0 un ciclo diretto in G di lunghezza k, per un


qualche k ≥ 2. Se per assurdo esistesse un ordinamento topologico di G, avremmo che
vi dovrebbe occorrere prima di vj , per ogni 0 ≤ i < j ≤ k, ma questo è impossibile che
si verifichi per i = 0 e j = k, dato che v0 = vk .

b. Sia V = v1 , v2 , . . . , vn , dove i vertici sono indicizzati in modo arbitrario. È sufficiente


orientare ciascun arco dal vertice di indice minore a quello di indice maggiore. In questo
modo non ci possono essere cicli, dato che un ciclo richiederebbe almeno un arco da un
vertice di indice maggiore a uno di indice minore.

Potrebbero piacerti anche