単一始点最短経路 (SSSP) アルゴリズムは、グラフ内の指定された始点から到達可能なすべての他の頂点への最短経路を計算します。ダイクストラ法は、有向グラフにおける SSSP 問題を解くための古典的な手法です。
仕組み
ダイクストラ法では、頂点を使用して最短距離値を更新します。各頂点は、始点からの現在の最短距離値を保持します。この値が変更されると、頂点はエッジの重みを新しい値に加算し、その結果を隣接頂点に通知するメッセージを送信します。次の反復では、隣接頂点が受信したメッセージに基づいて現在の最短距離値を更新します。すべての頂点の現在の最短距離値が変化しなくなった時点で、反復は終了します。
-
初期化:始点
sから自身への距離は 0 (d[s]=0) であり、他の任意の頂点uからsへの距離は無限大 (d[u]=∞) です。 -
反復:頂点
uからvへのエッジが存在する場合、sからvへの最短距離はd[v]=min(d[v], d[u]+weight(u, v))として更新されます。この処理は、sから他のすべての頂点への距離が安定するまで継続されます。
重み付き有向グラフ G=(V,E) において、始点 s からシンク頂点 v への複数のパスが存在する可能性があります。s から v への最短パスとは、エッジの重みの合計が最小となるパスです。
このアルゴリズムは MaxCompute Graph プログラミングモデルに自然にマップされます。
適用範囲
MaxCompute は有向グラフと無向グラフの両方をサポートしています。パスの可用性はソースデータおよびグラフ構築方法に依存するため、グラフの種類によって SSSP の結果が異なる場合があります。有向グラフは、すべての計算の基本となるデータモデルです。
コード例
以下の例では、有向グラフおよび無向グラフ向けの SSSP 実装を示します。
-
有向グラフ
-
BaseLoadingVertexResolverクラスを定義します。このクラスは、メインのSSSPクラスから参照されます。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(); } }コードの説明:
-
15 行目:
BaseLoadingVertexResolverを定義します。このクラスは、有向グラフのデータロード時に発生する競合を処理します。 -
18 行目:
resolveメソッドには、競合を処理するロジックが含まれます。たとえば、2 つのaddVertexRequest操作を通じて同じ頂点が 2 回追加された場合、ロード競合が発生します。計算を続行する前に、この競合を解決する必要があります。
-
-
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"); } }コードの説明:
-
19 行目:
SSSPVertexを定義します。このクラスでは、以下を行います。-
頂点の値は、この頂点から始点
startVertexIdまでの最短距離を表します。 -
compute()メソッドは、反復式d[v]=min(d[v], d[u]+weight(u, v))を使用して最短距離を計算し、現在の頂点の値を更新します。 -
cleanup()メソッドは、現在の頂点から始点までの最短距離を出力テーブルに書き込みます。
-
-
54 行目:現在の頂点の値(この頂点から始点までの最短パス)が変化しない場合、フレームワークを通じて voteToHalt() メソッドを呼び出して、頂点をhalt 状態に遷移させます。すべての頂点がhalt 状態になると、計算は終了します。
-
71 行目:
GraphLoaderを定義して、グラフデータを有向グラフとしてロードします。このクラスはテーブルレコードをグラフの頂点およびエッジに解析し、フレームワークにロードします。この例では、addVertexRequestメソッドを使用して頂点情報をグラフの計算コンテキストにロードします。 -
90 行目:
MinLongCombinerを定義します。これにより、同じ頂点に送信されるメッセージを結合して、パフォーマンスを最適化し、メモリ消費を削減します。 -
101 行目:
main関数でGraphJobを定義します。Vertex、GraphLoader、BaseLoadingVertexResolver、およびCombinerの実装を設定し、入力および出力テーブルを構成します。 -
110 行目:
BaseLoadingVertexResolverクラスを設定して競合を処理します。
-
-
-
無向グラフ
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"); } }コードの説明:
-
15 行目:
SSSPVertexを定義します。このクラスでは、以下を行います。-
頂点の値は、この頂点から始点
startVertexIdまでの最短距離を表します。 -
compute()メソッドは、反復式d[v]=min(d[v], d[u]+weight(u, v))を使用して最短距離を計算し、現在の頂点の値を更新します。 -
cleanup()メソッドは、現在の頂点から始点までの最短距離を出力テーブルに書き込みます。
-
-
54 行目:現在の頂点の値(この頂点から始点までの最短パス)が変化しない場合、フレームワークを通じて voteToHalt() を呼び出して頂点をhalt 状態に遷移させます。すべての頂点がhalt 状態になると、計算は終了します。
-
61 行目:
MinLongCombinerを定義します。これにより、同じ頂点に送信されるメッセージを結合して、パフォーマンスを最適化し、メモリ消費を削減します。 -
72 行目:
GraphLoaderを定義して、グラフデータを無向グラフとしてロードします。addEdgeRequestを使用して、2 つの頂点間のエッジを双方向エッジとしてロードし、テーブルデータが無向グラフとしてロードされることを保証します。-
80 行目:第 1 列は始点の頂点 ID を表します。
-
81 行目:第 2 列は送信先の頂点 ID を表します。
-
82 行目:第 3 列はエッジの重みを表します。
-
83 行目:送信先の頂点 ID とエッジの重みで構成されるエッジを作成します。
-
84 行目:始点の頂点にエッジを追加するリクエストを行います。
-
85~87 行目:各
Recordは双方向エッジを表します。83 行目および 84 行目のロジックを逆方向についても繰り返します。
-
-
SSSPLoadingVertexResolverを定義します。このクラスは、無向グラフのデータロード時に発生する競合を処理します。たとえば、2 つのaddEdgeRequest操作を通じて同じエッジが 2 回追加された場合、ロード競合が発生します。正しい計算を行うには、重複するエッジを適切に処理する必要があります。 -
101 行目:
main関数でGraphJobを定義します。Vertex、GraphLoader、SSSPLoadingVertexResolver、およびCombinerの実装を設定し、入力および出力テーブルを構成します。
-
実行結果
以下の出力は、有向グラフのコード例を実行した結果です。詳細については、「Graph プログラムの開発」をご参照ください。
vertex value
1 0
2 2
3 1
4 3
5 2
-
vertex:現在の頂点。 -
value:現在の頂点から始点 (1) までの最短距離。
無向グラフ用のデータを作成するには、前述のコード例に示されているように、始点の頂点 ID、送信先の頂点 ID、およびエッジの重みを使用してください。
チュートリアル
上記のコード例の実装方法の詳細については、「Graph プログラムの開発」をご参照ください。