在大学课程里,大家都学习过“图”这种数据结构,以及与其相关联的各种图遍历算法。譬如:最短路径算法等。
在应用项目中,我们可以用图对以下关系进行建模:
1. web页面以及页面之间的链接关系
2. 一个作者和他发表的文章的关系
3. 城市和城市之间的关系
4. 一个人和他的家庭成员之间的关系
5. 网络中节点之间的关系
…
通常我们会用“邻接表”或“邻接矩阵”对图进行描述。
邻接表表示图:
Map<String, List<String>>
邻接矩阵表示图:
String[][]
用上述数据结构表示图,表达性不强,并且操作复杂。并且上述的数据结构只能表示“无权图”,对于“带权图”以及存在“并行边”的图就无法表示了。笔者曾经审阅过多份用类似“最短路径算法”求解的题目,几乎每份作业的建模方式都不一样,自然表达性和算法的效率也大相径庭。
在Java的世界里,遇到难题,经过自己的思考后,首先应该去找开源社区寻求帮助,因为Java有着强大无比的社区。对于个人是新问题,在社区里可能会发现对于该问题已经“前人之述备矣”。在众多的Java开源项目中,第一个想到的就应该是Guava。Guava是什么,不知道的读者可以借助各种搜索引擎搜索下。
在Guava20.0版本(released on October 28, 2016)中,Guava提供了一个全新的package, common.graph
。按照官方的表述,common.graph
旨在提供一种通用的、可扩展性的语言描述实体以及实体之间的关系。
原文:
common.graph is a library for modeling graph-structured data, that is, entities and the relationships between them. Its purpose is to provide a common and extensible language for working with such data.
关于common.graph
的API介绍读者可以参考:https://github.com/google/guava/wiki/GraphsExplained, 写的非常详细,本文就不再赘述。
以下用两个实际的问题展示common.graph
的强大威力。
- 最短路径问题
- 电信网络中“邻区”问题
最短路径问题
问题定义:
在图G中,给定一个顶点S,找出S到图G中其他顶点的最短路径。
例如:
本文讨论的是单源最短路径问题,并且是带权有向无环图。因为对于无权图,用广度优先搜索算法就可以计算出最短路径了。对于本问题,我们采用经典的Dijkstra算法求解。
import com.google.common.graph.ElementOrder;
import com.google.common.graph.EndpointPair;
import com.google.common.graph.MutableValueGraph;
import com.google.common.graph.ValueGraphBuilder;
import java.util.HashSet;
import java.util.Set;
public class DijkstraSolve {
private final String sourceNode;
private final MutableValueGraph<String, Integer> graph;
public DijkstraSolve(String sourceNode, MutableValueGraph<String, Integer> graph) {
this.sourceNode = sourceNode;
this.graph = graph;
}
public static void main(String[] args) {
MutableValueGraph<String, Integer> graph = buildGraph();
DijkstraSolve dijkstraSolve = new DijkstraSolve("A", graph);
dijkstraSolve.dijkstra();
dijkstraSolve.printResult();
}
private void dijkstra() {
initPathFromSourceNode(sourceNode);
Set<String> nodes = graph.nodes();
if(!nodes.contains(sourceNode)) {
throw new IllegalArgumentException(sourceNode + " is not in this graph!");
}
Set<String> notVisitedNodes = new HashSet<>(graph.nodes());
String currentVisitNode = sourceNode;
while(!notVisitedNodes.isEmpty()) {
String nextVisitNode = findNextNode(currentVisitNode, notVisitedNodes);
if(nextVisitNode.equals("")) {
break;
}
notVisitedNodes.remove(currentVisitNode);
currentVisitNode = nextVisitNode;
}
}
private String findNextNode(String currentVisitNode, Set<String> notVisitedNodes) {
int shortestPath = Integer.MAX_VALUE;
String nextVisitNode = "";
for (String node : graph.nodes()) {
if(currentVisitNode.equals(node) || !notVisitedNodes.contains(node)) {
continue;
}
if(graph.successors(currentVisitNode).contains(node)) {
Integer edgeValue = graph.edgeValue(sourceNode, currentVisitNode) + graph.edgeValue(currentVisitNode, node);
Integer currentPathValue = graph.edgeValue(sourceNode, node);
if(edgeValue > 0) {
graph.putEdgeValue(sourceNode, node, Math.min(edgeValue, currentPathValue));
}
}
if(graph.edgeValue(sourceNode, node) < shortestPath) {
shortestPath = graph.edgeValue(sourceNode, node);
nextVisitNode = node;
}
}
return nextVisitNode;
}
private void initPathFromSourceNode(String sourceNode) {
graph.nodes().stream().filter(
node -> !graph.adjacentNodes(sourceNode).contains(node))
.forEach(node -> graph.putEdgeValue(sourceNode, node, Integer.MAX_VALUE));
graph.putEdgeValue(sourceNode, sourceNode, 0);
}
private void printResult() {
for (String node : graph.nodes()) {
System.out.println(sourceNode + "->" + node + " shortest path is:" + graph.edgeValue(sourceNode, node));
}
}
private static MutableValueGraph<String, Integer> buildGraph() {
MutableValueGraph<String, Integer> graph = ValueGraphBuilder.directed()
.nodeOrder(ElementOrder.<String>natural()).allowsSelfLoops(true).build();
graph.putEdgeValue("A", "B", 10);
graph.putEdgeValue("A", "C", 3);
graph.putEdgeValue("A", "D", 20);
graph.putEdgeValue("B", "D", 5);
graph.putEdgeValue("C", "B", 2);
graph.putEdgeValue("C", "E", 15);
graph.putEdgeValue("D", "E", 11);
return graph;
}
}
电信网络中“邻区”问题
问题定义:
在电信网络中,“服务小区”为用户提供了打电话、上网等服务。为了保证相邻服务小区的参数不冲突,经常会计算某个“服务小区”的一维邻区,甚至二维邻区。
比如:“服务小区”A邻接B,B邻接C,A不邻接C,那么A的一维邻区是B,二维邻区是C。
解决这类问题,用graph建模显然是再合适不过的。如果不关心图中两个节点的距离,应该选择MutableGraph<String>
import com.google.common.graph.*;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class AdjCelSolve {
private MutableGraph<String> graph;
public AdjCelSolve(MutableGraph<String> graph) {
this.graph = graph;
}
public static void main(String[] args) {
MutableGraph<String> graph = buildGraph();
AdjCelSolve adjCelSolve = new AdjCelSolve(graph);
System.out.println(adjCelSolve.adjacentNodes("A", 1));
System.out.println(adjCelSolve.adjacentNodes("A", 2));
System.out.println(adjCelSolve.adjacentNodes("A", 3));
System.out.println(adjCelSolve.adjacentNodes("A", 4));
/*
output:
[B, C, D]
[B, D, E]
[D, E]
[E]
*/
}
private Set<String> adjacentNodes(String sourceNode, int dimension) {
Set<String> adjacentNodes = new HashSet<>();
adjacentNodes.add(sourceNode);
for (int i = 0; i < dimension; i++) {
Set<String> currentDimensionNodes = new HashSet<>(adjacentNodes);
adjacentNodes.clear();
for (String adjacentNode : currentDimensionNodes) {
adjacentNodes.addAll(graph.nodes().stream().filter(
node -> graph.successors(adjacentNode).contains(node)).collect(Collectors.toSet()));
}
}
return adjacentNodes;
}
private static MutableGraph<String> buildGraph() {
MutableGraph<String> graph = GraphBuilder.directed()
.nodeOrder(ElementOrder.<String>natural()).allowsSelfLoops(false).build();
graph.putEdge("A", "B");
graph.putEdge("A", "C");
graph.putEdge("A", "D");
graph.putEdge("B", "D");
graph.putEdge("C", "B");
graph.putEdge("C", "E");
graph.putEdge("D", "E");
return graph;
}
}
以上通过两个实际的问题介绍了Guava graph的冰山一角,如何更好的使用graph解决更多的问题还有待我们大家一起探索。
**最后:
最新的Guava 21.0不兼容JDK1.6,所以JDK1.6版本只能使用Guava20.0。**