L'algorithme du plus court chemin à source unique (SSSP) calcule le chemin le plus court entre un sommet source spécifié et tous les autres sommets accessibles dans un graphe. L'algorithme de Dijkstra est une méthode classique pour résoudre le problème SSSP dans un graphe orienté.
Fonctionnement
L'algorithme de Dijkstra utilise des vertices pour mettre à jour les shortest distance values. Chaque vertex conserve sa shortest distance value actuelle par rapport au source vertex. Lorsque cette valeur change, le sommet ajoute le weight d'une edge à la nouvelle valeur et envoie un message pour notifier ses adjacent vertices. Lors de l'itération suivante, les adjacent vertices mettent à jour leurs shortest distance values actuelles en fonction des messages reçus. L'itération prend fin lorsque les shortest distance values actuelles de tous les vertices ne changent plus.
Initialisation : la distance du sommet source
svers lui-même est de 0 (d[s]=0), et la distance de tout autre sommetuverssest infinie (d[u]=∞).Itération : si une arête existe de
uversv, la distance minimale desversvest mise à jour selon la formuled[v]=min(d[v], d[u]+weight(u, v)). Le processus se poursuit jusqu'à ce que les distances desvers tous les autres sommets se stabilisent.
Pour un graphe orienté pondéré G=(V,E), plusieurs chemins peuvent exister entre un sommet source s et un sommet puits v. Le chemin dont la somme des poids des arêtes est minimale constitue le plus court chemin de s vers v.
Cet algorithme s'adapte naturellement au modèle de programmation MaxCompute Graph.
Cas d'utilisation
MaxCompute prend en charge les graphes orientés et non orientés. Étant donné que la disponibilité des chemins dépend des données sources et de la construction du graphe, les résultats SSSP peuvent différer selon le type de graphe. Le graphe orienté constitue le modèle de données fondamental sur lequel reposent tous les calculs.
Exemples de code
Les exemples suivants illustrent des implémentations SSSP pour les graphes orientés et non orientés.
-
Graphe orienté
-
Définissez la classe
BaseLoadingVertexResolver. Cette classe est référencée dans la classe principaleSSSP.import com.aliyun.odps.graph.Edge; import com.aliyun.odps.graph.LoadingVertexResolver; import com.aliyun.odps.graph.Vertex; import com.aliyun.odps.graph.VertexChanges; import com.aliyun.odps.io.Writable; import com.aliyun.odps.io.WritableComparable; import java.io.IOException; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; @SuppressWarnings("rawtypes") public class BaseLoadingVertexResolver<I extends WritableComparable, V extends Writable, E extends Writable, M extends Writable> extends LoadingVertexResolver<I, V, E, M> { @Override public Vertex<I, V, E, M> resolve(I vertexId, VertexChanges<I, V, E, M> vertexChanges) throws IOException { Vertex<I, V, E, M> vertex = addVertexIfDesired(vertexId, vertexChanges); if (vertex != null) { addEdges(vertex, vertexChanges); } else { System.err.println("Ignore all addEdgeRequests for vertex#" + vertexId); } return vertex; } protected Vertex<I, V, E, M> addVertexIfDesired( I vertexId, VertexChanges<I, V, E, M> vertexChanges) { Vertex<I, V, E, M> vertex = null; if (hasVertexAdditions(vertexChanges)) { vertex = vertexChanges.getAddedVertexList().get(0); } return vertex; } protected void addEdges(Vertex<I, V, E, M> vertex, VertexChanges<I, V, E, M> vertexChanges) throws IOException { Set<I> destVertexId = new HashSet<I>(); if (vertex.hasEdges()) { List<Edge<I, E>> edgeList = vertex.getEdges(); for (Iterator<Edge<I, E>> edges = edgeList.iterator(); edges.hasNext(); ) { Edge<I, E> edge = edges.next(); if (destVertexId.contains(edge.getDestVertexId())) { edges.remove(); } else { destVertexId.add(edge.getDestVertexId()); } } } for (Vertex<I, V, E, M> vertex1 : vertexChanges.getAddedVertexList()) { if (vertex1.hasEdges()) { List<Edge<I, E>> edgeList = vertex1.getEdges(); for (Edge<I, E> edge : edgeList) { if (destVertexId.contains(edge.getDestVertexId())) continue; destVertexId.add(edge.getDestVertexId()); vertex.addEdge(edge.getDestVertexId(), edge.getValue()); } } } } protected boolean hasVertexAdditions(VertexChanges<I, V, E, M> changes) { return changes != null && changes.getAddedVertexList() != null && !changes.getAddedVertexList().isEmpty(); } }Explication du code :
Ligne 15 : définit
BaseLoadingVertexResolver. Cette classe gère les conflits survenant lors du chargement des données pour un graphe orienté.Ligne 18 : la méthode
resolvecontient la logique de gestion des conflits. Par exemple, si un sommet est ajouté deux fois via deux opérationsaddVertexRequest, un conflit de chargement se produit. Vous devez résoudre ce conflit avant que le calcul ne puisse se poursuivre.
-
Définissez la classe
SSSP.import java.io.IOException; import com.aliyun.odps.graph.Combiner; import com.aliyun.odps.graph.ComputeContext; import com.aliyun.odps.graph.Edge; import com.aliyun.odps.graph.GraphJob; import com.aliyun.odps.graph.GraphLoader; import com.aliyun.odps.graph.MutationContext; import com.aliyun.odps.graph.Vertex; import com.aliyun.odps.graph.WorkerContext; import com.aliyun.odps.io.WritableRecord; import com.aliyun.odps.io.LongWritable; import com.aliyun.odps.data.TableInfo; public class SSSP { public static final String START_VERTEX = "sssp.start.vertex.id"; public static class SSSPVertex extends Vertex<LongWritable, LongWritable, LongWritable, LongWritable> { private static long startVertexId = -1; public SSSPVertex() { this.setValue(new LongWritable(Long.MAX_VALUE)); } public boolean isStartVertex( ComputeContext<LongWritable, LongWritable, LongWritable, LongWritable> context) { if (startVertexId == -1) { String s = context.getConfiguration().get(START_VERTEX); startVertexId = Long.parseLong(s); } return getId().get() == startVertexId; } @Override public void compute( ComputeContext<LongWritable, LongWritable, LongWritable, LongWritable> context, Iterable<LongWritable> messages) throws IOException { long minDist = isStartVertex(context) ? 0 : Long.MAX_VALUE; for (LongWritable msg : messages) { if (msg.get() < minDist) { minDist = msg.get(); } } if (minDist < this.getValue().get()) { this.setValue(new LongWritable(minDist)); if (hasEdges()) { for (Edge<LongWritable, LongWritable> e : this.getEdges()) { context.sendMessage(e.getDestVertexId(), new LongWritable(minDist + e.getValue().get())); } } } else { voteToHalt(); } } @Override public void cleanup( WorkerContext<LongWritable, LongWritable, LongWritable, LongWritable> context) throws IOException { context.write(getId(), getValue()); } @Override public String toString() { return "Vertex(id=" + this.getId() + ",value=" + this.getValue() + ",#edges=" + this.getEdges() + ")"; } } public static class SSSPGraphLoader extends GraphLoader<LongWritable, LongWritable, LongWritable, LongWritable> { @Override public void load( LongWritable recordNum, WritableRecord record, MutationContext<LongWritable, LongWritable, LongWritable, LongWritable> context) throws IOException { SSSPVertex vertex = new SSSPVertex(); vertex.setId((LongWritable) record.get(0)); String[] edges = record.get(1).toString().split(","); for (String edge : edges) { String[] ss = edge.split(":"); vertex.addEdge(new LongWritable(Long.parseLong(ss[0])), new LongWritable(Long.parseLong(ss[1]))); } context.addVertexRequest(vertex); } } public static class MinLongCombiner extends Combiner<LongWritable, LongWritable> { @Override public void combine(LongWritable vertexId, LongWritable combinedMessage, LongWritable messageToCombine) throws IOException { if (combinedMessage.get() > messageToCombine.get()) { combinedMessage.set(messageToCombine.get()); } } } public static void main(String[] args) throws IOException { if (args.length < 3) { System.out.println("Usage: <startnode> <input> <output>"); System.exit(-1); } GraphJob job = new GraphJob(); job.setGraphLoaderClass(SSSPGraphLoader.class); job.setVertexClass(SSSPVertex.class); job.setCombinerClass(MinLongCombiner.class); job.setLoadingVertexResolver(BaseLoadingVertexResolver.class); job.set(START_VERTEX, args[0]); job.addInput(TableInfo.builder().tableName(args[1]).build()); job.addOutput(TableInfo.builder().tableName(args[2]).build()); long startTime = System.currentTimeMillis(); job.run(); System.out.println("Job Finished in " + (System.currentTimeMillis() - startTime) / 1000.0 + " seconds"); } }Explication du code :
-
Ligne 19 : définit
SSSPVertex. Dans cette classe :La valeur du sommet représente la distance minimale entre ce sommet et le sommet source
startVertexId.La méthode
compute()utilise la formule itératived[v]=min(d[v], d[u]+weight(u, v))pour calculer la distance minimale et mettre à jour la valeur du sommet actuel.La méthode
cleanup()écrit la distance minimale entre le sommet actuel et le sommet source dans la table de sortie.
Ligne 54 : si la Value du sommet actuel (le plus court chemin entre le sommet et le sommet source) ne change pas, appelez la méthode voteToHalt() via le framework pour faire passer le sommet à l'état halt. Lorsque tous les sommets entrent dans l'état halt, le calcul se termine.
Ligne 71 : définit un
GraphLoaderpour charger les données du graphe en tant que graphe orienté. Il analyse les enregistrements de la table en sommets et arêtes de graphe et les charge dans le framework. Dans cet exemple, la méthodeaddVertexRequestcharge les informations des sommets dans le contexte de calcul du graphe.Ligne 90 : définit
MinLongCombiner. Celle-ci combine les messages envoyés au même sommet afin d'optimiser les performances et de réduire la consommation de mémoire.Ligne 101 : la fonction
maindéfinit leGraphJob. Elle configure les implémentations pourVertex,GraphLoader,BaseLoadingVertexResolveretCombiner, ainsi que les tables d'entrée et de sortie.Ligne 110 : définit la classe
BaseLoadingVertexResolverpour gérer les conflits.
-
-
-
Graphe non orienté
import com.aliyun.odps.data.TableInfo; import com.aliyun.odps.graph.*; import com.aliyun.odps.io.DoubleWritable; import com.aliyun.odps.io.LongWritable; import com.aliyun.odps.io.WritableRecord; import java.io.IOException; import java.util.HashSet; import java.util.Set; public class SSSPBenchmark4 { public static final String START_VERTEX = "sssp.start.vertex.id"; public static class SSSPVertex extends Vertex<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> { private static long startVertexId = -1; public SSSPVertex() { this.setValue(new DoubleWritable(Double.MAX_VALUE)); } public boolean isStartVertex( ComputeContext<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> context) { if (startVertexId == -1) { String s = context.getConfiguration().get(START_VERTEX); startVertexId = Long.parseLong(s); } return getId().get() == startVertexId; } @Override public void compute( ComputeContext<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> context, Iterable<DoubleWritable> messages) throws IOException { double minDist = isStartVertex(context) ? 0 : Double.MAX_VALUE; for (DoubleWritable msg : messages) { if (msg.get() < minDist) { minDist = msg.get(); } } if (minDist < this.getValue().get()) { this.setValue(new DoubleWritable(minDist)); if (hasEdges()) { for (Edge<LongWritable, DoubleWritable> e : this.getEdges()) { context.sendMessage(e.getDestVertexId(), new DoubleWritable(minDist + e.getValue().get())); } } } else { voteToHalt(); } } @Override public void cleanup( WorkerContext<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> context) throws IOException { context.write(getId(), getValue()); } } public static class MinLongCombiner extends Combiner<LongWritable, DoubleWritable> { @Override public void combine(LongWritable vertexId, DoubleWritable combinedMessage, DoubleWritable messageToCombine) { if (combinedMessage.get() > messageToCombine.get()) { combinedMessage.set(messageToCombine.get()); } } } public static class SSSPGraphLoader extends GraphLoader<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> { @Override public void load( LongWritable recordNum, WritableRecord record, MutationContext<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> context) throws IOException { LongWritable sourceVertexID = (LongWritable) record.get(0); LongWritable destinationVertexID = (LongWritable) record.get(1); DoubleWritable edgeValue = (DoubleWritable) record.get(2); Edge<LongWritable, DoubleWritable> edge = new Edge<LongWritable, DoubleWritable>(destinationVertexID, edgeValue); context.addEdgeRequest(sourceVertexID, edge); Edge<LongWritable, DoubleWritable> edge2 = new Edge<LongWritable, DoubleWritable>(sourceVertexID, edgeValue); context.addEdgeRequest(destinationVertexID, edge2); } } public static class SSSPLoadingVertexResolver extends LoadingVertexResolver<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> { @Override public Vertex<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> resolve( LongWritable vertexId, VertexChanges<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> vertexChanges) throws IOException { SSSPVertex computeVertex = new SSSPVertex(); computeVertex.setId(vertexId); Set<LongWritable> destinationVertexIDSet = new HashSet<>(); if (hasEdgeAdditions(vertexChanges)) { for (Edge<LongWritable, DoubleWritable> edge : vertexChanges.getAddedEdgeList()) { if (!destinationVertexIDSet.contains(edge.getDestVertexId())) { destinationVertexIDSet.add(edge.getDestVertexId()); computeVertex.addEdge(edge.getDestVertexId(), edge.getValue()); } } } return computeVertex; } protected boolean hasEdgeAdditions(VertexChanges<LongWritable, DoubleWritable, DoubleWritable, DoubleWritable> changes) { return changes != null && changes.getAddedEdgeList() != null && !changes.getAddedEdgeList().isEmpty(); } } public static void main(String[] args) throws IOException { if (args.length < 2) { System.out.println("Usage: <startnode> <input> <output>"); System.exit(-1); } GraphJob job = new GraphJob(); job.setGraphLoaderClass(SSSPGraphLoader.class); job.setLoadingVertexResolver(SSSPLoadingVertexResolver.class); job.setVertexClass(SSSPVertex.class); job.setCombinerClass(MinLongCombiner.class); job.set(START_VERTEX, args[0]); job.addInput(TableInfo.builder().tableName(args[1]).build()); job.addOutput(TableInfo.builder().tableName(args[2]).build()); long startTime = System.currentTimeMillis(); job.run(); System.out.println("Job Finished in " + (System.currentTimeMillis() - startTime) / 1000.0 + " seconds"); } }Explication du code :
-
Ligne 15 : définit
SSSPVertex. Dans cette classe :La valeur du sommet représente la distance minimale entre ce sommet et le sommet source
startVertexId.La méthode
compute()utilise la formule itératived[v]=min(d[v], d[u]+weight(u, v))pour calculer la distance minimale et mettre à jour la valeur du sommet actuel.La méthode
cleanup()écrit la distance minimale entre le sommet actuel et le sommet source dans la table de sortie.
Ligne 54 : si la Value du sommet actuel (le plus court chemin entre ce sommet et le sommet source) ne change pas, le sommet passe à l'état halt en appelant voteToHalt() via le framework. Le calcul se termine lorsque tous les sommets entrent dans l'état halt.
Ligne 61 : définit
MinLongCombiner. Celle-ci combine les messages envoyés au même sommet afin d'optimiser les performances et de réduire la consommation de mémoire.-
Ligne 72 : définit un
GraphLoaderpour charger les données du graphe en tant que graphe non orienté. Il utiliseaddEdgeRequestpour charger l'arête entre deux sommets comme une arête bidirectionnelle, garantissant ainsi que les données de la table sont chargées sous forme de graphe non orienté.Ligne 80 : la première colonne représente l'ID du sommet source.
Ligne 81 : la deuxième colonne représente l'ID du sommet de destination.
Ligne 82 : la troisième colonne représente le poids de l'arête.
Ligne 83 : crée une arête composée de l'ID du sommet de destination et du poids de l'arête.
Ligne 84 : demande l'ajout de l'arête au sommet source.
Lignes 85-87 : chaque
Recordreprésente une arête bidirectionnelle. La logique des lignes 83 et 84 est répétée pour la direction inverse.
Définit
SSSPLoadingVertexResolver. Cette classe gère les conflits survenant lors du chargement des données pour un graphe non orienté. Par exemple, si la même arête est ajoutée deux fois via deux opérationsaddEdgeRequest, un conflit de chargement se produit. Vous devez gérer les arêtes en double pour garantir l'exactitude du calcul.Ligne 101 : la fonction
maindéfinit leGraphJob. Elle configure les implémentations pourVertex,GraphLoader,SSSPLoadingVertexResolveretCombiner, ainsi que les tables d'entrée et de sortie.
-
Résultats d'exécution
La sortie suivante provient de l'exécution de l'exemple de code pour le graphe orienté. Pour plus d'informations, consultez Développer des programmes Graph.
vertex value
1 0
2 2
3 1
4 3
5 2
vertex: le sommet actuel.value: la distance minimale entre le sommet actuel et le sommet source (1).
Pour créer des données pour un graphe non orienté, utilisez l'ID du sommet source, l'ID du sommet de destination et le poids de l'arête, comme illustré dans l'exemple de code précédent.
Tutoriel
Pour plus d'informations sur la mise en œuvre des exemples de code précédents, consultez Développer des programmes Graph.