My undergraduate thesis work focuses on a particular class of graphs, namely temporal graphs.
A temporal graph $G=(V,E)$ is a graph where each arc $(u,v)\in E$ is associated with a list of timestamps, which indicate the instants of time in which the arc is active.
My work was to find an efficient algorithm that solves the problem of Temporal Connectivity, that is, to determine whether a given temporal graph with a tree topology is temporally connected or not. Temporally connected means that for each pair of nodes, there must be a valid temporal path.
The goal of this thesis was to find an algorithm much more efficient than Näive, which has a complexity of $$O(nM^2)$$ with $M$=total number of timestamps, in order to solve this problem.
My algorithm exploits the ideas of Earliest-Arrival and Latest-Departure, and its complexity turns out to be $$O(n\log(L))$$ with $L=\max_{v}|L_v|$ and $L_v$=timestamp on the arc $(v,p(v))$
Code
The following is the code of my algorithm, which has poly-log complexity.
defvisit_dfs(tree,node,EA_max,LD_max):"""
Visita DFS unificata che, per ogni nodo v:
- Se v è foglia, imposta EA_max[v] = min(L_v) e LD_max[v] = max(L_v)
- Altrimenti, visita ricorsivamente i figli, raccoglie le coppie (EA_max, LD_max)
e controlla la consistenza degli intervalli secondo lo pseudocodice.
- Infine, calcola EA = max_{u in child(v)} EA_max[u] e LD = min_{u in child(v)} LD_max[u],
e determina NextEA e NextLD in L_v (lista dei pesi associati a v) tramite funzioni di
ricerca (binary_search e binary_search_leq).
Se per qualche ragione il calcolo fallisce (NextEA o NextLD == -1), imposta EA_max[v] e
LD_max[v] a infinito e ritorna False, altrimenti ritorna True.
Parametri:
- tree: grafo orientato rappresentante l'albero.
- node: nodo corrente.
- EA_max: dizionario per memorizzare i valori EA_max per ciascun nodo.
- LD_max: dizionario per memorizzare i valori LD_max per ciascun nodo.
Ritorna:
- True se la connettività temporale risulta consistente lungo il ramo, False altrimenti.
"""# L_v: vettore dei pesi associato al nodo correnteweights=tree.nodes[node].get("weight",[])children=list(tree.successors(node))# Caso base: fogliaifnotchildren:EA_max[node]=min(weights)LD_max[node]=max(weights)returnTrue# Inizializza il vettore D_v per raccogliere le coppie dai figliD_v=[]forchildinchildren:ifnotvisit_dfs(tree,child,EA_max,LD_max):returnFalse# Se uno dei figli ha valori infiniti, la connettività non è rispettataifEA_max[child]==float("inf")orLD_max[child]==float("inf"):returnFalseD_v.append((EA_max[child],LD_max[child]))# Se sono presenti almeno due figli, eseguo il controllo di consistenzaiflen(D_v)>1:# Trovo i due minimi rispetto a EA_max:# (min1, ld1) è la coppia con EA_min più piccolo e (min2, ld2) la secondasorted_intervals=sorted(D_v,key=lambdax:x[0])minEA,ld1=sorted_intervals[0]secondEA,ld2=sorted_intervals[1]# Per ogni figlio, effettuo il controllo:forchildinchildren:ifEA_max[child]==minEA:# Se il figlio con EA_min ha un EA_max maggiore di ld2, la condizione non è soddisfattaifEA_max[child]>ld2:returnFalseelse:# Gli altri devono rispettare EA_max[u] <= ld1ifEA_max[child]>ld1:returnFalse# Se c'è un solo figlio non serve ulteriore controlloifnode==1:returnTrue# Calcolo EA e LD aggregati dai figliEA=max(EA_max[child]forchildinchildren)LD=min(LD_max[child]forchildinchildren)# Calcola NextEA e NextLD usando la lista dei pesi L_v e le funzioni di ricerca# Si assume che 'binary_search' trovi il successore di EA in weights,# e 'binary_search_leq' trovi il predecessore (o il più vicino minore uguale) di LD.NextEA=binary_search(weights,EA)ifweightselse-1NextLD=binary_search_leq(weights,LD)ifweightselse-1if(NextEA==-1orNextLD==-1)andnode!=1:EA_max[node]=float("inf")LD_max[node]=float("inf")returnFalseelifNextEA!=-1andNextLD!=-1:EA_max[node]=NextEALD_max[node]=NextLDreturnTruedefalgoritmo_unificato(T):"""
Algoritmo unificato per la verifica della connettività temporale di un albero.
T: grafo orientato rappresentante l'albero.
"""n=T.number_of_nodes()EA_max=[0]*(n+1)LD_max=[0]*(n+1)ifvisit_dfs(T,1,EA_max,LD_max):return"L'albero è temporalmente connesso"else:return"L'albero non è temporalmente connesso"
As you can see, my algorithm is nothing more than a modified DFS visit (we can call it without loss of generality Temporal DFS visit) that exploits the principles of EA and LD.
It also exploits a particular heuristic, that is, taking $N(v)=$neighborhood of $v$ if $$EA_{max}[v]\leq\min_{w\in N(v)}LD_{max}[w]$$Then I can say that the subtree rooted in $v$ is temporally connected.
This check is propagated upwards until it reaches the root. It is there that True or False will be returned, depending on the result of the check seen previously.
Experiments
Here are some tests conducted on randomly generated trees.
They compare the average execution times of my algorithm and Näive.