All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.googlecode.blaisemath.graph.GraphUtils Maven / Gradle / Ivy

There is a newer version: 4.0.8
Show newest version
package com.googlecode.blaisemath.graph;

/*
 * #%L
 * BlaiseGraphTheory
 * --
 * Copyright (C) 2009 - 2019 Elisha Peterson
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.google.common.collect.*;
import com.google.common.graph.*;
import com.googlecode.blaisemath.graph.metrics.Degree;
import com.googlecode.blaisemath.graph.util.Matrices;

import java.lang.reflect.Array;
import java.util.*;
import java.util.Map.Entry;

import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;

/**
 * Utility methods for creating and analyzing graphs.
 *
 * @author Elisha Peterson
 */
@SuppressWarnings("UnstableApiUsage")
public class GraphUtils {

    //region COMPARATORS

    /** Used to sort graphs in descending order by size */
    public static final Comparator GRAPH_SIZE_DESCENDING = (o1, o2) -> {
        int size1 = o1.nodes().size();
        int size2 = o2.nodes().size();
        int edges1 = o1.edges().size();
        int edges2 = o2.edges().size();
        return size1 == size2 && edges1 == edges2 ? o2.nodes().toString().compareTo(o1.nodes().toString())
                : size1 == size2 ? edges2 - edges1
                : size2 - size1;
    };

    //endregion

    // utility class
    private GraphUtils() {
    }

    //region CREATORS
    
    /**
     * Create an empty graph for nodes of appropriate type.
     * @param  node type
     * @param directed whether result should be a directed graph
     * @return new empty graph
     */
    public static  Graph emptyGraph(boolean directed) {
        return ImmutableGraph.copyOf(directed ? GraphBuilder.directed().build() : GraphBuilder.undirected().build());
    }

    /**
     * Create a graph using a given list of nodes and edges.
     * @param  node type
     * @param directed whether result should be a directed graph
     * @param nodes nodes
     * @param edges edges
     * @return new graph
     */
    public static  Graph createFromEdges(boolean directed, Iterable nodes, Iterable> edges) {
        MutableGraph res = directed ? GraphBuilder.directed().allowsSelfLoops(true).build()
                : GraphBuilder.undirected().allowsSelfLoops(true).build();
        nodes.forEach(res::addNode);
        edges.forEach(e -> res.putEdge(e.nodeU(), e.nodeV()));
        return res;
    }

    /**
     * Create a graph using a given list of nodes and edges
     * @param  node type
     * @param directed whether result should be a directed graph
     * @param nodes nodes
     * @param edges edges
     * @return new empty graph
     */
    public static  Graph createFromArrayEdges(boolean directed, Iterable nodes, Iterable edges) {
        MutableGraph res = directed ? GraphBuilder.directed().allowsSelfLoops(true).build()
                : GraphBuilder.undirected().allowsSelfLoops(true).build();
        nodes.forEach(res::addNode);
        edges.forEach(e -> res.putEdge(e[0], e[1]));
        return res;
    }

    /**
     * Creates an undirected copy of the specified graph.
     * @param  graph node type
     * @param graph a graph
     * @return undirected copy with the same collection of edges
     */
    public static  Graph copyUndirected(Graph graph) {
        return createFromEdges(false, graph.nodes(), graph.edges());
    }

    //endregion

    //region PRINTING

    /**
     * Returns string representation of specified graph
     * @param graph the graph to print
     * @return string representation of graph
     */
    public static String printGraph(Graph graph) {
        return printGraph(graph, true, true);
    }

    /**
     * Returns string representation of specified graph
     * @param  graph node type
     * @param graph the graph to print
     * @param printNodes whether to print nodes or not
     * @param printEdges whether to print edges or not
     * @return string representation of graph
     */
    public static  String printGraph(Graph graph, boolean printNodes, boolean printEdges) {
        if (!printEdges && !printNodes) {
            return "GRAPH";
        }
        
        Set nodes = graph.nodes();
        boolean sortable = nodes.stream().allMatch(n -> n instanceof Comparable);
        if (sortable) {
            nodes = new TreeSet<>(nodes);
        }
        StringBuilder result = new StringBuilder();
        if (printNodes) {
            result.append("NODES: ").append(nodes).append("  ");
        }
        
        if (printEdges) {
            result.append("EDGES:");
            for (N n : nodes) {
                result.append(" ").append(n).append(": ")
                        .append(sortable ? new TreeSet(graph.successors(n)) : graph.successors(n));
            }
        }
        return result.toString().trim();
    }

    //endregion
    
    //region ADJACENCY MATRIX METHODS

    /**
     * Compute adjacency matrix of a graph.
     * @param  graph node type
     * @param graph the input graph
     * @param order if empty, will be filled with order of nodes; if non-empty, will be used to order nodes in graph
     * @return matrix of integers describing adjacencies... contains 0's and 1's;  it is symmetric when the graph is
     * undirected, otherwise it may not be symmetric
     */
    public static  boolean[][] adjacencyMatrix(Graph graph, List order) {
        requireNonNull(order);
        if (order.isEmpty()) {
            order.addAll(graph.nodes());
        }
        int n = order.size();
        boolean[][] result = new boolean[n][n];
        for (int i1 = 0; i1 < n; i1++) {
            for (int i2 = 0; i2 < n; i2++) {
                result[i1][i2] = graph.isDirected() ? graph.successors(order.get(i1)).contains(order.get(i2))
                        : graph.hasEdgeConnecting(order.get(i1), order.get(i2));
            }
        }
        return result;
    }

    /**
     * Computes the adjacency matrix and several of its powers.
     * @param  graph node type
     * @param graph the input graph
     * @param order if empty, will be filled with order of nodes; if non-empty, will be used to order nodes in graph
     * @param maxPower maximum power of the adjacency matrix to include in result
     * @return matrix of integers describing adjacencies... contains 0's and 1's... it is symmetric when the graph is
     *         undirected, otherwise it may not be symmetric
     */
    public static  int[][][] adjacencyMatrixPowers(Graph graph, List order, int maxPower) {
        boolean[][] adj0 = adjacencyMatrix(graph, order);
        int[][] adj1 = new int[adj0.length][adj0.length];
        for (int i = 0; i < adj1.length; i++) {
            for (int j = 0; j < adj1.length; j++) {
                adj1[i][j] = adj0[i][j] ? 1 : 0;
            }
        }
        int[][][] result = new int[maxPower][adj1.length][adj1[0].length];
        result[0] = adj1;
        int cur = 2;
        while (cur <= maxPower) {
            result[cur - 1] = Matrices.matrixProduct(result[cur - 2], adj1);
            cur++;
        }
        return result;
    }

    //endregion

    //region DEGREE

    /**
     * Computes and returns degree distribution.
     * @param  graph node type
     * @param graph the graph
     * @return map associating degree #s with counts, sorted by degree
     */
    public static  Multiset degreeDistribution(Graph graph) {
        return GraphMetrics.distribution(graph, (g, n) -> graph.degree(n));
    }

    //endregion

    //region GEODESIC & SPANNING TREE METHODS

    /**
     * Computes and creates a tree describing geodesic distances from a specified node, traversing the graph in the direction
     * of the edges. When there are multiple paths with the same minimum length, the resulting path is unspecified.
     * @param  graph node type
     * @param graph the starting graph
     * @param node the starting node
     * @return map with node distance lengths
     */
    public static  Map geodesicTree(Graph graph, N node) {
        return geodesicTree(graph, node, Integer.MAX_VALUE);
    }

    /**
     * Computes and creates a tree describing geodesic distances from a specified node, up through a distance
     * specified by the max parameter. Choice of geodesic when multiple are possible is unspecified. The graph
     * only contains the nodes that are in the same component as the starting node (forward component if directed).
     * @param  graph node type
     * @param graph the starting graph
     * @param node the starting node
     * @param max the maximum distance to proceed from the starting node
     * @return graph with objects associated to each node that describe the distance from the main node.
     */
    public static  Map geodesicTree(Graph graph, N node, int max) {
        // nodes left to add
        Set remaining = Sets.newHashSet(graph.nodes());
        // nodes added already, by distance
        List> added = Lists.newArrayList();
        // stores size of remaining nodes
        int sRemaining = -1;
        
        int max2 = max == Integer.MAX_VALUE ? max-1 : max;

        remaining.remove(node);
        added.add(new HashSet<>(singletonList(node)));
        while (sRemaining != remaining.size() && added.size() < max2+1) {
            sRemaining = remaining.size();
            added.add(new HashSet<>());
            for (N n1 : added.get(added.size() - 2)) {
                Set toRemove = Sets.newHashSet();
                for (N n2 : remaining) {
                    if (graph.hasEdgeConnecting(n1, n2)) {
                        toRemove.add(n2);
                        added.get(added.size() - 1).add(n2);
                        N[] arr = (N[]) Array.newInstance(n1.getClass(), 2);
                        arr[0] = n1;
                        arr[1] = n2;
                    }
                }
                remaining.removeAll(toRemove);
            }
        }

        Map result = new HashMap<>();
        for (int i = 0; i < added.size(); i++) {
            for (N n : added.get(i)) {
                result.put(n, i);
            }
        }
        return result;
    }

    /**
     * Finds geodesic distance between two nodes in a graph. For directed graphs, the path must traverse the graph in
     * the direction of the edges.
     * @param  graph node type
     * @param graph the graph
     * @param start first node
     * @param end second node
     * @return geodesic distance between the nodes, or 0 if they are the same node, or -1 if they are not connected
     */
    public static  int geodesicDistance(Graph graph, N start, N end) {
        if (start.equals(end)) {
            return 0;
        }
        if (!(graph.nodes().contains(start) && graph.nodes().contains(end))) {
            return -1;
        }

        List nodesToAdd = Lists.newArrayList(graph.nodes());
        List> nodesAdded = Lists.newArrayList();
        int nodesToAddCount;

        nodesToAdd.remove(start);
        nodesAdded.add(Sets.newHashSet(start));
        do {
            nodesToAddCount = nodesToAdd.size();
            nodesAdded.add(new HashSet<>());
            for (N n1 : nodesAdded.get(nodesAdded.size() - 2)) {
                Set toRemove = new HashSet<>();
                for (N n2 : nodesToAdd) {
                    if (graph.hasEdgeConnecting(n1, n2)) {
                        if (n2.equals(end)) {
                            return nodesAdded.size() - 1;
                        }
                        toRemove.add(n2);
                        nodesAdded.get(nodesAdded.size() - 1).add(n2);
                    }
                }
                nodesToAdd.removeAll(toRemove);
            }
        } while (nodesToAddCount != nodesToAdd.size());

        return -1;
    }

    //endregion
    
    //region NEIGHBORHOOD & COMPONENT METHODS

    /**
     * Generate ordered set of nodes from an adjacency map.
     * @param  graph node type
     * @param adj an adjacency map
     * @return list of nodes
     */
    public static  Set nodes(Multimap adj) {
        Set result = Sets.newLinkedHashSet();
        result.addAll(adj.keySet());
        result.addAll(adj.values());
        return result;
    }

    /**
     * Compute neighborhood about provided node up to a given radius, as a set of nodes. The result always includes
     * the node itself. For directed graphs, this only traverses the graph in the direction of the edges.
     * @param  graph node type
     * @param graph the graph
     * @param node the starting node
     * @param radius the maximum distance to consider
     * @return a list containing the nodes within the neighborhood
     */
    public static  Set neighborhood(Graph graph, N node, int radius) {
        return geodesicTree(graph, node, radius).keySet();
    }

    /**
     * Generate connected components from an adjacency map.
     * @param  graph node type
     * @param dirAdj an adjacency map (may be directed)
     * @return set of components, as a set of sets
     */
    public static  Collection> components(Multimap dirAdj) {
        // ensure symmetry
        Multimap adj = HashMultimap.create();
        for (Entry en : dirAdj.entries()) {
            adj.put(en.getKey(), en.getValue());
            adj.put(en.getValue(), en.getKey());
        }
        
        List> res = Lists.newArrayList();
        Set toAdd = Sets.newHashSet(adj.keySet());
        while (!toAdd.isEmpty()) {
            N next = toAdd.iterator().next();
            Set nComp = component(adj, next);
            res.add(nComp);
            toAdd.removeAll(nComp);
        }
        return res;
    }
    
    private static  Set component(Multimap adj, N v0) {
        Set toSearch = Sets.newHashSet(v0);
        Set res = Sets.newHashSet();
        while (!toSearch.isEmpty()) {
            Set next = Sets.newHashSet();
            for (N n : toSearch) {
                next.addAll(adj.get(n));
            }
            res.addAll(toSearch);
            next.removeAll(res);
            toSearch = next;
        }
        return res;
    }

    /**
     * Generate connected components from a graph.
     * @param  graph node type
     * @param graph the graph
     * @return set of connected components
     */
    public static  Collection> components(Graph graph) {
        return components(adjacencies(graph, graph.nodes()));
    }

    /**
     * Generate connected components from a subset of nodes in a graph.
     * @param  graph node type
     * @param graph the graph
     * @param nodes subset of nodes
     * @return set of connected components
     */
    public static  Collection> components(Graph graph, Set nodes) {
        return components(adjacencies(graph, nodes));
    }

    /**
     * Generate connected components as a list of subgraphs.
     * @param  graph node type
     * @param graph the graph of interest
     * @return set of connected component subgraphs
     */
    public static  Set> componentGraphs(Graph graph) {
        return new GraphComponents<>(graph, components(graph)).componentGraphs();
    }

    /**
     * Generate adjacency map from a subgraph.
     * @param  graph node type
     * @param graph the graph
     * @param nodes subset of nodes
     * @return adjacency map restricted to the given subset
     */
    public static  Multimap adjacencies(Graph graph, Set nodes) {
        Multimap res = LinkedHashMultimap.create();
        for (N n : nodes) {
            res.putAll(n, Sets.intersection(graph.adjacentNodes(n), nodes));
        }
        return res;
    }

    /**
     * Performs breadth-first search algorithm to enumerate the nodes in a graph, starting from the specified start node.
     * @param  graph node type
     * @param graph the graph under consideration
     * @param start the starting node.
     * @param numShortest a map that will be filled with info on the # of shortest paths
     * @param lengths a map that will be filled with info on the lengths of shortest paths
     * @param deque a stack (LIFO) that will be filled with elements in non-increasing order of distance
     * @param adjacencies a map that will be filled with adjacency information for the shortest paths
     */
    public static  void breadthFirstSearch(Graph graph, N start, Multiset numShortest, Map lengths,
                                              Deque deque, Multimap adjacencies) {
        Set nodes = graph.nodes();
        numShortest.add(start);
        for (N n : nodes) {
            lengths.put(n, -1);
        }
        lengths.put(start, 0);

        // breadth-first search algorithm
        Deque queue = Queues.newArrayDeque();
        queue.add(start);
        while (!queue.isEmpty()) {
            N n1 = queue.remove();
            deque.add(n1);
            for (N n2 : graph.adjacentNodes(n1)) {
                // if n2 is found for the first time in the tree, add it to the queue, and adjust the length
                if (lengths.get(n2) == -1) {
                    queue.add(n2);
                    lengths.put(n2, lengths.get(n1) + 1);
                }
                // adjust the number of shortest paths to n2 if shortest path goes through n1
                if (lengths.get(n2) == lengths.get(n1) + 1) {
                    numShortest.add(n2, numShortest.count(n1));
                    adjacencies.get(n2).add(n1);
                }
            }
        }
    }

    //endregion
    
    //region CONTRACTED ELEMENTS

    /**
     * Creates a contracted graph from a parent graph, where all of a specified
     * subset of nodes are contracted to a single node
     * @param  graph node type
     * @param graph parent graph
     * @param contract nodes to contract
     * @param replace node to replace all contracted nodes
     * @return graph where the specified nodes have been contracted
     */
    public static  Graph contractedGraph(Graph graph, Collection contract, N replace) {
        List> edges = Lists.newArrayList();
        for (EndpointPair e : graph.edges()) {
            N node1 = contract.contains(e.nodeU()) ? replace : e.nodeU();
            N node2 = contract.contains(e.nodeV()) ? replace : e.nodeV();
            edges.add(e.isOrdered() ? EndpointPair.ordered(node1, node2) : EndpointPair.unordered(node1, node2));
        }
        return createFromEdges(graph.isDirected(), contractedNodeSet(graph.nodes(), contract, replace), edges);
    }
    
    /**
     * Contracts list of nodes, replacing all the "contract" nodes with
     * "replace".
     * @param  graph node type
     * @param nodes collection of nodes
     * @param contract nodes to contract
     * @param replace node to replace all contracted nodes
     * @return node set
     */
    public static  Set contractedNodeSet(Collection nodes, Collection contract, N replace) {
        Set result = Sets.newHashSet(nodes);
        result.removeAll(contract);
        result.add(replace);
        return result;
    }

    /**
     * Contracts list of components, combining all components with nodes in subset.
     * @param  graph node type
     * @param components list of components to contract
     * @param subset subset to contract
     * @param node what to replace contracted subset with
     * @return contracted components
     */
    public static  Set> contractedComponents(Collection> components, Collection subset, N node) {
        Set> result = Sets.newHashSet();
        Set contracted = Sets.newHashSet();
        contracted.add(node);
        result.add(contracted);
        for (Set c : components) {
            if (Collections.disjoint(c, subset)) {
                result.add(Sets.newHashSet(c));
            } else {
                contracted.addAll(c);
            }
        }
        return result;
    }

    //endregion
    
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy