使用Guava-Graph建模Java世界的图

在大学课程里,大家都学习过“图”这种数据结构,以及与其相关联的各种图遍历算法。譬如:最短路径算法等。
在应用项目中,我们可以用图对以下关系进行建模:
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的强大威力。

  1. 最短路径问题
  2. 电信网络中“邻区”问题

最短路径问题

问题定义:
在图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。**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值