Skip to content

Commit

Permalink
jgrapht#953 lca bug fix (jgrapht#963)
Browse files Browse the repository at this point in the history
  • Loading branch information
Toptachamann authored Jul 3, 2020
1 parent a6bd6b0 commit 6d2e8f8
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 83 deletions.
131 changes: 48 additions & 83 deletions jgrapht-core/src/main/java/org/jgrapht/alg/lca/NaiveLCAFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,52 +19,35 @@

import org.jgrapht.*;
import org.jgrapht.alg.interfaces.*;
import org.jgrapht.alg.util.Pair;
import org.jgrapht.graph.EdgeReversedGraph;
import org.jgrapht.traverse.BreadthFirstIterator;

import java.util.*;

/**
* Find the Lowest Common Ancestor of a directed graph.
*
* <p>
* Find the LCA, defined as <i>Let $G = (V, E)$ be a DAG, and let $x, y \in V$ . Let $G x,y$ be the
* Find the LCA, defined as <i>"Let $G = (V, E)$ be a DAG, and let $x, y \in V$. Let $G_{x,y}$ be the
* subgraph of $G$ induced by the set of all common ancestors of $x$ and $y$. Define SLCA (x, y) to
* be the set of out-degree 0 nodes (leafs) in $G x,y$. The lowest common ancestors of $x$ and $y$
* are the elements of SLCA (x, y). This naive algorithm simply starts at $a$ and $b$, recursing
* upwards to the root(s) of the DAG. Wherever the recursion paths cross we have found our LCA.</i>
* be the set of out-degree 0 nodes (leafs) in $G_{x,y}$. The lowest common ancestors of $x$ and $y$
* are the elements of SLCA (x, y). "</it>
* from <i> Michael A. Bender, Martín Farach-Colton, Giridhar Pemmasani, Steven Skiena, Pavel
* Sumazin, Lowest common ancestors in trees and directed acyclic graphs, Journal of Algorithms,
* Volume 57, Issue 2, 2005, Pages 75-94, ISSN 0196-6774,
* https://doi.org/10.1016/j.jalgor.2005.08.001. </i>
* https://doi.org/10.1016/j.jalgor.2005.08.001.</i>
*
* <p>
* The algorithm:
*
* <pre>
* 1. Start at each of nodes you wish to find the lca for (a and b)
* 2. Create sets aSet containing a, and bSet containing b
* 3. If either set intersects with the union of the other sets previous values (i.e. the set of notes visited) then
* that intersection is LCA. if there are multiple intersections then the earliest one added is the LCA.
* 4. Repeat from step 3, with aSet now the parents of everything in aSet, and bSet the parents of everything in bSet
* 5. If there are no more parents to descend to then there is no LCA
* </pre>
* <ol>
* <li>Find ancestor sets for nodes $a$ and $b$.</li>
* <li>Find their intersection.</li>
* <li>Extract leaf nodes from the intersection set.</li>
* </ol>
*
* The rationale for this working is that in each iteration of the loop we are considering all the
* ancestors of a that have a path of length n back to a, where n is the depth of the recursion. The
* same is true of b.
*
* <p>
* We start by checking if a == b.<br>
* if not we look to see if there is any intersection between parents(a) and (parents(b) union b)
* (and the same with a and b swapped)<br>
* if not we look to see if there is any intersection between parents(parents(a)) and
* (parents(parents(b)) union parents(b) union b) (and the same with a and b swapped)<br>
* continues
*
* <p>
* This means at the end of recursion n, we know if there is an LCA that has a path of &lt;=n to a
* and b. Of course we may have to wait longer if the path to a is of length n, but the path to
* b&gt;n. at the first loop we have a path of 0 length from the nodes we are considering as LCA to
* their respective children which we wish to find the LCA for.
* The algorithm is straightforward in the way it finds the LCA set by definition.
*
* <p>
* Preprocessing Time complexity: $O(1)$<br>
Expand All @@ -88,16 +71,16 @@ public class NaiveLCAFinder<V, E>
implements
LowestCommonAncestorAlgorithm<V>
{
private Graph<V, E> graph;
private final Graph<V, E> graph;

/**
* Create a new instance of the naive LCA finder.
*
*
* @param graph the input graph
*/
public NaiveLCAFinder(Graph<V, E> graph)
{
this.graph = Objects.requireNonNull(graph, "Graph cannot be null");
this.graph = GraphTests.requireDirected(graph);
}

/**
Expand All @@ -123,75 +106,57 @@ public Set<V> getLCASet(V a, V b)
{
checkNodes(a, b);

List<Set<V>> visitedSets = doubleBfs(a, b);
// all common ancestors of both a and b
Set<V> intersection;
Graph<V, E> edgeReversed = new EdgeReversedGraph<>(graph);
Set<V> aAncestors = getAncestors(edgeReversed, a);
Set<V> bAncestors = getAncestors(edgeReversed, b);
Set<V> commonAncestors;

// optimization trick: save the intersection using the smaller set
if (visitedSets.get(0).size() < visitedSets.get(1).size()) {
visitedSets.get(0).retainAll(visitedSets.get(1));
intersection = visitedSets.get(0);
if (aAncestors.size() < bAncestors.size()) {
aAncestors.retainAll(bAncestors);
commonAncestors = aAncestors;
} else {
visitedSets.get(1).retainAll(visitedSets.get(0));
intersection = visitedSets.get(1);
bAncestors.retainAll(aAncestors);
commonAncestors = bAncestors;
}

/*
* Find the set of all non-leaves by iterating through the set of common ancestors. When we
* encounter a node which is still part of the SLCA(a, b) we remove its parent(s).
*/
Set<V> nonLeaves = new LinkedHashSet<>();
for (V node : intersection) {
for (E edge : graph.incomingEdgesOf(node)) {
if (graph.getEdgeTarget(edge).equals(node)) {
V source = graph.getEdgeSource(edge);

if (intersection.contains(source))
nonLeaves.add(source);
Set<V> leaves = new HashSet<>();
for (V ancestor : commonAncestors) {
boolean isLeaf = true;
for (E edge : graph.outgoingEdgesOf(ancestor)) {
V target = graph.getEdgeTarget(edge);
if (commonAncestors.contains(target)){
isLeaf = false;
break;
}
}
if(isLeaf){
leaves.add(ancestor);
}
}

// perform the actual removal of non-leaves
intersection.removeAll(nonLeaves);
return intersection;
return leaves;
}

/**
* Perform a simultaneous bottom-up bfs from a and b, saving all visited nodes. Once a a node
* has been visited from both a and b, it is no longer expanded in our search (we know that its
* ancestors won't be part of the SLCA(x, y) set).
* Returns a set of nodes reachable from the {@code start}.
*
* @param graph a graph
* @param start a node to start from.
* @return returns a set of nodes reachable from the {@code start}.
*/
private List<Set<V>> doubleBfs(V a, V b)
private Set<V> getAncestors(Graph<V, E> graph, V start)
{
List<Queue<V>> queues =
new ArrayList<>(Arrays.asList(new ArrayDeque<>(), new ArrayDeque<>()));
List<Set<V>> visitedSets = new ArrayList<>(Arrays.asList(new HashSet<>(), new HashSet<>()));

queues.get(0).add(a);
queues.get(1).add(b);

visitedSets.get(0).add(a);
visitedSets.get(1).add(b);

for (int ind = 0; !queues.get(0).isEmpty() || !queues.get(1).isEmpty(); ind ^= 1) {
if (!queues.get(ind).isEmpty()) {
V node = queues.get(ind).poll();

if (!visitedSets.get(0).contains(node) || !visitedSets.get(1).contains(node))
for (E edge : graph.incomingEdgesOf(node)) {
if (graph.getEdgeTarget(edge).equals(node)) {
V source = graph.getEdgeSource(edge);

if (!visitedSets.get(ind).contains(source)) {
queues.get(ind).add(source);
visitedSets.get(ind).add(source);
}
}
}
}
Set<V> ancestors = new HashSet<>();
BreadthFirstIterator<V, E> bfs = new BreadthFirstIterator<>(graph, start);
while (bfs.hasNext()) {
ancestors.add(bfs.next());
}
return visitedSets;
return ancestors;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import org.jgrapht.*;
import org.jgrapht.graph.*;
import org.jgrapht.graph.builder.GraphTypeBuilder;
import org.jgrapht.util.SupplierUtil;
import org.junit.*;

import java.util.*;
Expand Down Expand Up @@ -210,4 +212,30 @@ public void testLcaIsOneOfTheNodes()
checkLcas(finder, 1, 10, Collections.singleton(1));
}

/**
* See issue #953
*/
@Test
public void testLca(){
Graph<String, DefaultEdge> g = new DefaultDirectedGraph<>(DefaultEdge.class);

/*
* a-->b-->c
* | ^
* V |
* d-->e-->f
*
*/
Graphs.addEdgeWithVertices(g, "a", "b");
Graphs.addEdgeWithVertices(g, "b", "c");
Graphs.addEdgeWithVertices(g, "a", "d");
Graphs.addEdgeWithVertices(g, "d", "e");
Graphs.addEdgeWithVertices(g, "e", "f");
Graphs.addEdgeWithVertices(g, "f", "c");

NaiveLCAFinder<String, DefaultEdge> finder = new NaiveLCAFinder<>(g);

checkLcas(finder, "c", "e", Collections.singleton("e"));
}

}

0 comments on commit 6d2e8f8

Please sign in to comment.