org.jgrapht.alg.spanning.GreedyMultiplicativeSpanner Maven / Gradle / Ivy
/*
* (C) Copyright 2016-2023, by Dimitrios Michail and Contributors.
*
* JGraphT : a free Java graph-theory library
*
* See the CONTRIBUTORS.md file distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the
* GNU Lesser General Public License v2.1 or later
* which is available at
* http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
*
* SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
*/
package org.jgrapht.alg.spanning;
import org.jgrapht.*;
import org.jgrapht.alg.interfaces.*;
import org.jgrapht.graph.*;
import org.jgrapht.graph.builder.*;
import org.jgrapht.util.*;
import org.jheaps.*;
import org.jheaps.tree.*;
import java.util.*;
/**
* Greedy algorithm for $(2k-1)$-multiplicative spanner construction (for any integer
* {@literal k >= 1}).
*
*
* The spanner is guaranteed to contain $O(n^{1+1/k})$ edges and the shortest path distance between
* any two vertices in the spanner is at most $2k-1$ times the corresponding shortest path distance
* in the original graph. Here n denotes the number of vertices of the graph.
*
*
* The algorithm is described in: Althoefer, Das, Dobkin, Joseph, Soares.
* On Sparse Spanners of Weighted Graphs. Discrete
* Computational Geometry 9(1):81-100, 1993.
*
*
* If the graph is unweighted the algorithm runs in $O(m n^{1+1/k})$ time. Setting $k$ to infinity
* will result in a slow version of Kruskal's algorithm where cycle detection is performed by a BFS
* computation. In such a case use the implementation of Kruskal with union-find. Here n and m are
* the number of vertices and edges of the graph respectively.
*
*
* If the graph is weighted the algorithm runs in $O(m (n^{1+1/k} + n \log n))$ time by using
* Dijkstra's algorithm. Edge weights must be non-negative.
*
* @param the graph vertex type
* @param the graph edge type
*
* @author Dimitrios Michail
*/
public class GreedyMultiplicativeSpanner
implements SpannerAlgorithm
{
private final Graph graph;
private final int k;
private static final int MAX_K = 1 << 29;
/**
* Constructs instance to compute a $(2k-1)$-spanner of an undirected graph.
*
* @param graph an undirected graph
* @param k positive integer.
*
* @throws IllegalArgumentException if the graph is not undirected
* @throws IllegalArgumentException if k is not positive
*/
public GreedyMultiplicativeSpanner(Graph graph, int k)
{
this.graph = Objects.requireNonNull(graph, "Graph cannot be null");
if (!graph.getType().isUndirected()) {
throw new IllegalArgumentException("graph is not undirected");
}
if (k <= 0) {
throw new IllegalArgumentException(
"k should be positive in (2k-1)-spanner construction");
}
this.k = Math.min(k, MAX_K);
}
@Override
public Spanner getSpanner()
{
if (graph.getType().isWeighted()) {
return new WeightedSpannerAlgorithm().run();
} else {
return new UnweightedSpannerAlgorithm().run();
}
}
// base algorithm implementation
private abstract class SpannerAlgorithmBase
{
public abstract boolean isSpannerReachable(V s, V t, double distance);
public abstract void addSpannerEdge(V s, V t, double weight);
public Spanner run()
{
// sort edges
ArrayList allEdges = new ArrayList<>(graph.edgeSet());
allEdges.sort(Comparator.comparingDouble(graph::getEdgeWeight));
// check precondition
double minWeight = graph.getEdgeWeight(allEdges.get(0));
if (minWeight < 0.0) {
throw new IllegalArgumentException("Illegal edge weight: negative");
}
// run main loop
Set edgeList = new LinkedHashSet<>();
double edgeListWeight = 0d;
for (E e : allEdges) {
V s = graph.getEdgeSource(e);
V t = graph.getEdgeTarget(e);
if (!s.equals(t)) { // self-loop?
double eWeight = graph.getEdgeWeight(e);
if (!isSpannerReachable(s, t, (2 * k - 1) * eWeight)) {
edgeList.add(e);
edgeListWeight += eWeight;
addSpannerEdge(s, t, eWeight);
}
}
}
return new SpannerImpl<>(edgeList, edgeListWeight);
}
}
private class UnweightedSpannerAlgorithm
extends SpannerAlgorithmBase
{
protected Graph spanner;
protected Map vertexDistance;
protected Deque queue;
protected Deque touchedVertices;
public UnweightedSpannerAlgorithm()
{
spanner = GraphTypeBuilder
. undirected().allowingMultipleEdges(false).allowingSelfLoops(false)
.edgeSupplier(graph.getEdgeSupplier()).buildGraph();
touchedVertices = new ArrayDeque<>(graph.vertexSet().size());
for (V v : graph.vertexSet()) {
spanner.addVertex(v);
touchedVertices.push(v);
}
vertexDistance = CollectionUtil.newHashMapWithExpectedSize(graph.vertexSet().size());
queue = new ArrayDeque<>();
}
/**
* Check if two vertices are reachable by a BFS in the spanner graph using only a certain
* number of hops.
*
* We execute this procedure repeatedly, therefore we need to keep track of what it touches
* and only clean those before the next execution.
*/
@Override
public boolean isSpannerReachable(V s, V t, double hops)
{
// initialize distances and queue
while (!touchedVertices.isEmpty()) {
V u = touchedVertices.pop();
vertexDistance.put(u, Integer.MAX_VALUE);
}
while (!queue.isEmpty()) {
queue.pop();
}
// do BFS
touchedVertices.push(s);
queue.push(s);
vertexDistance.put(s, 0);
while (!queue.isEmpty()) {
V u = queue.pop();
Integer uDistance = vertexDistance.get(u);
if (u.equals(t)) { // found
return uDistance <= hops;
}
for (E e : spanner.edgesOf(u)) {
V v = Graphs.getOppositeVertex(spanner, e, u);
Integer vDistance = vertexDistance.get(v);
if (vDistance == Integer.MAX_VALUE) {
touchedVertices.push(v);
vertexDistance.put(v, uDistance + 1);
queue.push(v);
}
}
}
return false;
}
@Override
public void addSpannerEdge(V s, V t, double weight)
{
spanner.addEdge(s, t);
}
}
private class WeightedSpannerAlgorithm
extends SpannerAlgorithmBase
{
protected Graph spanner;
protected AddressableHeap heap;
protected Map> nodes;
public WeightedSpannerAlgorithm()
{
this.spanner = new SimpleWeightedGraph<>(DefaultWeightedEdge.class);
for (V v : graph.vertexSet()) {
spanner.addVertex(v);
}
this.heap = new PairingHeap<>();
this.nodes = new LinkedHashMap<>();
}
@Override
public boolean isSpannerReachable(V s, V t, double distance)
{
// init
heap.clear();
nodes.clear();
AddressableHeap.Handle sNode = heap.insert(0d, s);
nodes.put(s, sNode);
while (!heap.isEmpty()) {
AddressableHeap.Handle uNode = heap.deleteMin();
double uDistance = uNode.getKey();
V u = uNode.getValue();
if (uDistance > distance) {
return false;
}
if (u.equals(t)) { // found
return true;
}
for (DefaultWeightedEdge e : spanner.edgesOf(u)) {
V v = Graphs.getOppositeVertex(spanner, e, u);
AddressableHeap.Handle vNode = nodes.get(v);
double vDistance = uDistance + spanner.getEdgeWeight(e);
if (vNode == null) { // no distance
vNode = heap.insert(vDistance, v);
nodes.put(v, vNode);
} else if (vDistance < vNode.getKey()) {
vNode.decreaseKey(vDistance);
}
}
}
return false;
}
@Override
public void addSpannerEdge(V s, V t, double weight)
{
Graphs.addEdge(spanner, s, t, weight);
}
}
}