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

org.jgrapht.alg.matching.PathGrowingWeightedMatching Maven / Gradle / Ivy

The newest version!
/*
 * (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.matching;

import org.jgrapht.*;
import org.jgrapht.alg.interfaces.*;
import org.jgrapht.alg.util.*;

import java.util.*;

/**
 * A linear time $\frac{1}{2}$-approximation algorithm for finding a maximum weight matching in an
 * arbitrary graph. Linear time here means $O(m)$ where m is the cardinality of the edge set, even
 * if the graph contains isolated vertices. $\frac{1}{2}$-approximation means that for any graph
 * instance, the algorithm computes a matching whose weight is at least half of the weight of a
 * maximum weight matching. The implementation accepts directed and undirected graphs which may
 * contain self-loops and multiple edges. There is no assumption on the edge weights, i.e. they can
 * also be negative or zero.
 * 
 * 

* The algorithm is due to Drake and Hougardy, described in detail in the following paper: *

    *
  • D.E. Drake, S. Hougardy, A Simple Approximation Algorithm for the Weighted Matching Problem, * Information Processing Letters 85, 211-213, 2003.
  • *
* *

* This particular implementation uses by default two additional heuristics discussed by the authors * which also take linear time but improve the quality of the matchings. These heuristics can be * disabled by calling the constructor {@link #PathGrowingWeightedMatching(Graph, boolean)}. * Disabling the heuristics has the effect of fewer passes over the edge set of the input graph, * probably at the expense of the total weight of the matching. * *

* For a discussion on engineering approximate weighted matching algorithms see the following paper: *

    *
  • Jens Maue and Peter Sanders. Engineering algorithms for approximate weighted matching. * International Workshop on Experimental and Efficient Algorithms, Springer, 2007.
  • *
* * @see GreedyWeightedMatching * * @param the graph vertex type * @param the graph edge type * * @author Dimitrios Michail */ public class PathGrowingWeightedMatching implements MatchingAlgorithm { /** * Default value on whether to use extra heuristics to improve the result. */ public static final boolean DEFAULT_USE_HEURISTICS = true; private final Graph graph; private final Comparator comparator; private final boolean useHeuristics; /** * Construct a new instance of the path growing algorithm. Floating point values are compared * using {@link #DEFAULT_EPSILON} tolerance. By default two additional linear time heuristics * are used in order to improve the quality of the matchings. * * @param graph the input graph */ public PathGrowingWeightedMatching(Graph graph) { this(graph, DEFAULT_USE_HEURISTICS, DEFAULT_EPSILON); } /** * Construct a new instance of the path growing algorithm. Floating point values are compared * using {@link #DEFAULT_EPSILON} tolerance. * * @param graph the input graph * @param useHeuristics if true an improved version with additional heuristics is executed. The * running time remains linear but performs a few more passes over the input. While the * approximation factor remains $\frac{1}{2}$, in most cases the heuristics produce * matchings of higher quality. */ public PathGrowingWeightedMatching(Graph graph, boolean useHeuristics) { this(graph, useHeuristics, DEFAULT_EPSILON); } /** * Construct a new instance of the path growing algorithm. * * @param graph the input graph * @param useHeuristics if true an improved version with additional heuristics is executed. The * running time remains linear but performs a few more passes over the input. While the * approximation factor remains $\frac{1}{2}$, in most cases the heuristics produce * matchings of higher quality. * @param epsilon tolerance when comparing floating point values */ public PathGrowingWeightedMatching(Graph graph, boolean useHeuristics, double epsilon) { if (graph == null) { throw new IllegalArgumentException("Input graph cannot be null"); } this.graph = graph; this.comparator = new ToleranceDoubleComparator(epsilon); this.useHeuristics = useHeuristics; } /** * Get a matching that is a $\frac{1}{2}$-approximation of the maximum weighted matching. * * @return a matching */ @Override public Matching getMatching() { if (useHeuristics) { return runWithHeuristics(); } else { return run(); } } /** * Compute all vertices that have positive degree by iterating over the edges on purpose. This * keeps the complexity to $O(m)$ where $m$ is the number of edges. * * @return set of vertices with positive degree */ private Set initVisibleVertices() { Set visibleVertex = new HashSet<>(); for (E e : graph.edgeSet()) { V s = graph.getEdgeSource(e); V t = graph.getEdgeTarget(e); if (!s.equals(t)) { visibleVertex.add(s); visibleVertex.add(t); } } return visibleVertex; } // the algorithm (no heuristics) private Matching run() { // lookup all relevant vertices Set visibleVertex = initVisibleVertices(); // run algorithm Set m1 = new HashSet<>(); Set m2 = new HashSet<>(); double m1Weight = 0d, m2Weight = 0d; int i = 1; while (!visibleVertex.isEmpty()) { // find vertex arbitrarily V x = visibleVertex.stream().findAny().get(); // grow path from x while (x != null) { // first heaviest edge incident to vertex x (among visible neighbors) double maxWeight = 0d; E maxWeightedEdge = null; V maxWeightedNeighbor = null; for (E e : graph.edgesOf(x)) { V other = Graphs.getOppositeVertex(graph, e, x); if (visibleVertex.contains(other) && !other.equals(x)) { double curWeight = graph.getEdgeWeight(e); if (comparator.compare(curWeight, 0d) > 0 && (maxWeightedEdge == null || comparator.compare(curWeight, maxWeight) > 0)) { maxWeight = curWeight; maxWeightedEdge = e; maxWeightedNeighbor = other; } } } // add it to either m1 or m2, alternating between them if (maxWeightedEdge != null) { switch (i) { case 1: m1.add(maxWeightedEdge); m1Weight += maxWeight; break; case 2: m2.add(maxWeightedEdge); m2Weight += maxWeight; break; default: throw new RuntimeException( "Failed to figure out matching, seems to be a bug"); } i = 3 - i; } // remove x and incident edges visibleVertex.remove(x); // go to next vertex x = maxWeightedNeighbor; } } // return best matching if (comparator.compare(m1Weight, m2Weight) > 0) { return new MatchingImpl<>(graph, m1, m1Weight); } else { return new MatchingImpl<>(graph, m2, m2Weight); } } // the algorithm (improved with additional heuristics) private Matching runWithHeuristics() { // lookup all relevant vertices Set visibleVertex = initVisibleVertices(); // create solver for paths DynamicProgrammingPathSolver pathSolver = new DynamicProgrammingPathSolver(); Set matching = new HashSet<>(); double matchingWeight = 0d; Set matchedVertices = new HashSet<>(); // run algorithm while (!visibleVertex.isEmpty()) { // find vertex arbitrarily V x = visibleVertex.stream().findAny().get(); // grow path from x LinkedList path = new LinkedList<>(); while (x != null) { // first heaviest edge incident to vertex x (among visible neighbors) double maxWeight = 0d; E maxWeightedEdge = null; V maxWeightedNeighbor = null; for (E e : graph.edgesOf(x)) { V other = Graphs.getOppositeVertex(graph, e, x); if (visibleVertex.contains(other) && !other.equals(x)) { double curWeight = graph.getEdgeWeight(e); if (comparator.compare(curWeight, 0d) > 0 && (maxWeightedEdge == null || comparator.compare(curWeight, maxWeight) > 0)) { maxWeight = curWeight; maxWeightedEdge = e; maxWeightedNeighbor = other; } } } // add edge to path and remove x if (maxWeightedEdge != null) { path.add(maxWeightedEdge); } visibleVertex.remove(x); // go to next vertex x = maxWeightedNeighbor; } // find maximum weight matching of path using dynamic programming Pair> pathMatching = pathSolver.getMaximumWeightMatching(graph, path); // add it to result while keeping track of matched vertices matchingWeight += pathMatching.getFirst(); for (E e : pathMatching.getSecond()) { V s = graph.getEdgeSource(e); V t = graph.getEdgeTarget(e); if (!matchedVertices.add(s)) { throw new RuntimeException( "Set is not a valid matching, please submit a bug report"); } if (!matchedVertices.add(t)) { throw new RuntimeException( "Set is not a valid matching, please submit a bug report"); } matching.add(e); } } // extend matching to maximal cardinality (out of edges with positive weight) for (E e : graph.edgeSet()) { double edgeWeight = graph.getEdgeWeight(e); if (comparator.compare(edgeWeight, 0d) <= 0) { // ignore zero or negative weight continue; } V s = graph.getEdgeSource(e); if (matchedVertices.contains(s)) { // matched vertex, ignore continue; } V t = graph.getEdgeTarget(e); if (matchedVertices.contains(t)) { // matched vertex, ignore continue; } // add edge to matching matching.add(e); matchingWeight += edgeWeight; } // return extended matching return new MatchingImpl<>(graph, matching, matchingWeight); } /** * Helper class for repeatedly solving the maximum weight matching on paths. * * The work array used in the dynamic programming algorithm is reused between invocations. In * case its size is smaller than the path provided, its length is increased. This class is not * thread-safe. */ class DynamicProgrammingPathSolver { private static final int WORK_ARRAY_INITIAL_SIZE = 256; // work array private double[] a = new double[WORK_ARRAY_INITIAL_SIZE]; /** * Find the maximum weight matching of a path using dynamic programming. * * @param path a list of edges. The code assumes that the list of edges is a valid simple * path, and that is not a cycle. * @return a maximum weight matching of the path */ public Pair> getMaximumWeightMatching(Graph g, LinkedList path) { int pathLength = path.size(); // special cases switch (pathLength) { case 0: // special case, empty path return Pair.of(0d, Collections.emptySet()); case 1: // special case, one edge E e = path.getFirst(); double eWeight = g.getEdgeWeight(e); if (comparator.compare(eWeight, 0d) > 0) { return Pair.of(eWeight, Collections.singleton(e)); } else { return Pair.of(0d, Collections.emptySet()); } } // make sure work array has enough space if (a.length < pathLength + 1) { a = new double[pathLength + 1]; } // first pass to find solution Iterator it = path.iterator(); E e = it.next(); double eWeight = g.getEdgeWeight(e); a[0] = 0d; a[1] = (comparator.compare(eWeight, 0d) > 0) ? eWeight : 0d; for (int i = 2; i <= pathLength; i++) { e = it.next(); eWeight = g.getEdgeWeight(e); if (comparator.compare(a[i - 1], a[i - 2] + eWeight) > 0) { a[i] = a[i - 1]; } else { a[i] = a[i - 2] + eWeight; } } // reverse second pass to build solution Set matching = new HashSet<>(); it = path.descendingIterator(); int i = pathLength; while (i >= 1) { e = it.next(); if (comparator.compare(a[i], a[i - 1]) > 0) { matching.add(e); // skip next edge if (i > 1) { e = it.next(); } i--; } i--; } // return solution return Pair.of(a[pathLength], matching); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy