O algoritmo de caminho mais curto de fonte única (SSSP) calcula o menor caminho entre um vértice de origem especificado e todos os outros vértices alcançáveis em um grafo. O algoritmo de Dijkstra é um método clássico para resolver o problema SSSP em grafos direcionados.
Funcionamento
O algoritmo de Dijkstra utiliza vertices para atualizar os shortest distance values. Cada vertex mantém seu shortest distance value atual em relação ao source vertex. Quando esse valor muda, o vértice soma o weight de uma edge ao novo valor e envia uma mensagem para notificar seus adjacent vertices. Na iteração seguinte, os adjacent vertices atualizam seus shortest distance values com base nas mensagens recebidas. O processo iterativo termina quando os shortest distance values de todos os vertices não sofrem mais alterações.
Inicialização: A distância do vértice de origem
saté ele mesmo é 0 (d[s]=0), enquanto a distância de qualquer outro vérticeuatésé infinita (d[u]=∞).Iteração: Caso exista uma aresta de
uparav, a distância mínima desatévé atualizada conforme a expressãod[v]=min(d[v], d[u]+weight(u, v)). Esse processo continua até que as distâncias despara todos os demais vértices se estabilizem.
Em um grafo direcionado ponderado G=(V,E), podem existir múltiplos caminhos entre um vértice de origem s e um vértice de destino v. O caminho cuja soma dos pesos das arestas seja a menor representa o caminho mais curto de s até v.
Esse algoritmo se encaixa naturalmente no modelo de programação MaxCompute Graph.
Casos de uso
O MaxCompute oferece suporte tanto a grafos direcionados quanto não direcionados. Como a disponibilidade de caminhos depende dos dados de origem e da construção do grafo, os resultados do SSSP podem variar conforme o tipo de grafo utilizado. O grafo direcionado constitui o modelo de dados fundamental no qual todas as computações se baseiam.
Exemplos de código
Os exemplos a seguir demonstram implementações do SSSP para grafos direcionados e não direcionados.
-
Grafo direcionado
-
Defina a classe
BaseLoadingVertexResolver. Essa classe é referenciada na classe principalSSSP.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(); } }Explicação do código:
Linha 15: Define
BaseLoadingVertexResolver. Esta classe gerencia conflitos durante o carregamento de dados para um grafo direcionado.Linha 18: O método
resolvecontém a lógica para tratar conflitos. Por exemplo, se um vértice for adicionado duas vezes por meio de duas operaçõesaddVertexRequest, ocorre um conflito de carregamento. Resolva esse conflito antes que a computação prossiga.
-
Defina a 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"); } }Explicação do código:
-
Linha 19: Define
SSSPVertex. Nesta classe:O valor do vértice representa a distância mínima deste vértice até o vértice de origem
startVertexId.O método
compute()aplica a fórmula iterativad[v]=min(d[v], d[u]+weight(u, v))para calcular a distância mínima e atualizar o valor do vértice atual.O método
cleanup()grava a distância mínima do vértice atual até o vértice de origem na tabela de saída.
Linha 54: Se o Value do vértice atual (o caminho mais curto do vértice até a origem) não mudar, chame o método voteToHalt() por meio do framework para colocar o vértice no estado halt. A computação termina quando todos os vértices entram no estado halt.
Linha 71: Define um
GraphLoaderpara carregar os dados do grafo como um grafo direcionado. Ele analisa os registros da tabela, transformando-os em vértices e arestas do grafo, e os carrega no framework. Neste exemplo, o métodoaddVertexRequestcarrega as informações dos vértices no contexto de computação do grafo.Linha 90: Define
MinLongCombiner. Este componente combina mensagens enviadas para o mesmo vértice, otimizando o desempenho e reduzindo o consumo de memória.Linha 101: A função
maindefine oGraphJob. Ela configura as implementações deVertex,GraphLoader,BaseLoadingVertexResolvereCombiner, além de definir as tabelas de entrada e saída.Linha 110: Configura a classe
BaseLoadingVertexResolverpara gerenciar conflitos.
-
-
-
Grafo não direcionado
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"); } }Explicação do código:
-
Linha 15: Define
SSSPVertex. Nesta classe:O valor do vértice representa a distância mínima deste vértice até o vértice de origem
startVertexId.O método
compute()aplica a fórmula iterativad[v]=min(d[v], d[u]+weight(u, v))para calcular a distância mínima e atualizar o valor do vértice atual.O método
cleanup()grava a distância mínima do vértice atual até o vértice de origem na tabela de saída.
Linha 54: Se o Value do vértice atual (o caminho mais curto deste vértice até a origem) não mudar, o vértice entra no estado halt ao chamar voteToHalt() por meio do framework. A computação termina quando todos os vértices entram no estado halt.
Linha 61: Define
MinLongCombiner. Este componente combina mensagens enviadas para o mesmo vértice, otimizando o desempenho e reduzindo o consumo de memória.-
Linha 72: Define um
GraphLoaderpara carregar os dados do grafo como um grafo não direcionado. Ele utilizaaddEdgeRequestpara carregar a aresta entre dois vértices como uma aresta bidirecional, garantindo que os dados da tabela sejam interpretados como um grafo não direcionado.Linha 80: A primeira coluna representa o ID do vértice de origem.
Linha 81: A segunda coluna representa o ID do vértice de destino.
Linha 82: A terceira coluna representa o peso da aresta.
Linha 83: Crie uma aresta composta pelo ID do vértice de destino e pelo peso da aresta.
Linha 84: Faz uma solicitação para adicionar a aresta ao vértice de origem.
Linhas 85-87: Cada
Recordrepresenta uma aresta bidirecional. A lógica das linhas 83 e 84 se repete para a direção inversa.
Define
SSSPLoadingVertexResolver. Esta classe gerencia conflitos durante o carregamento de dados para um grafo não direcionado. Por exemplo, se a mesma aresta for adicionada duas vezes por meio de duas operaçõesaddEdgeRequest, ocorre um conflito de carregamento. Trate arestas duplicadas para garantir a correção da computação.Linha 101: A função
maindefine oGraphJob. Ela configura as implementações deVertex,GraphLoader,SSSPLoadingVertexResolvereCombiner, além de definir as tabelas de entrada e saída.
-
Resultados da execução
A saída abaixo resulta da execução do exemplo de código para grafo direcionado. Para mais informações, consulte Develop Graph programs.
vertex value
1 0
2 2
3 1
4 3
5 2
vertex: O vértice atual.value: A distância mínima do vértice atual até o vértice de origem (1).
Para criar dados para um grafo não direcionado, utilize o ID do vértice de origem, o ID do vértice de destino e o peso da aresta, conforme demonstrado no exemplo de código anterior.
Tutorial
Para obter mais detalhes sobre como implementar os exemplos de código anteriores, consulte Develop Graph programs.