Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The lca package #597

Merged
merged 39 commits into from
Aug 18, 2018
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1a449c3
Add interface
AlexandruValeanu May 23, 2018
4fe4eaf
Add interface
AlexandruValeanu May 23, 2018
9e26b03
Update old implementations
AlexandruValeanu May 23, 2018
edf9900
Add EulerTourRMQLCAFinder and tests for all implementations
AlexandruValeanu May 28, 2018
091dc67
Fix bug with disconnected graphs.
AlexandruValeanu May 28, 2018
176e867
Add more tests
AlexandruValeanu May 28, 2018
c6b36be
Fix Java related issues
AlexandruValeanu Jun 6, 2018
3b842e9
Add support for multiple roots
AlexandruValeanu Jun 6, 2018
88b67ed
Add Heavy Path Decomposition
AlexandruValeanu Jun 6, 2018
2336275
Add forest generator
AlexandruValeanu Jun 7, 2018
861b239
Add performance tests
AlexandruValeanu Jun 9, 2018
b3b3eb0
Small fixes
AlexandruValeanu Jun 9, 2018
b1765b0
docs + opt-heavy-path
AlexandruValeanu Jun 9, 2018
da2e4ab
Update docs
AlexandruValeanu Jun 11, 2018
0586b4d
Update heavy-path tests
AlexandruValeanu Jun 12, 2018
6c66f66
Refactor heavy-path decomposition
AlexandruValeanu Jun 12, 2018
1dda979
Fix checkstyle errors
AlexandruValeanu Jun 12, 2018
3b91111
Fix checkstyle errors
AlexandruValeanu Jun 12, 2018
e4b14a0
Merge branch 'lca' of https://github.com/AlexandruValeanu/jgrapht int…
AlexandruValeanu Jun 12, 2018
21aabb1
Fix checkstyle errors
AlexandruValeanu Jun 12, 2018
fefe40e
Update docs
AlexandruValeanu Jun 12, 2018
756c6d2
Merge remote-tracking branch 'remotes/upstream/master' into lca
AlexandruValeanu Jun 21, 2018
3a4295f
Update docs
AlexandruValeanu Jun 21, 2018
9a2037b
Merge branch 'master' of https://github.com/jgrapht/jgrapht into lca
AlexandruValeanu Jun 21, 2018
ad24696
Merge branch 'master' of https://github.com/jgrapht/jgrapht into lca
AlexandruValeanu Jun 21, 2018
3c36ba6
Fix imports
AlexandruValeanu Jun 21, 2018
1fabf8b
Fix imports
AlexandruValeanu Jun 21, 2018
72d4292
Update docs + tests
AlexandruValeanu Jun 22, 2018
b00ec07
Fix doc
AlexandruValeanu Jun 23, 2018
ac712d4
Merge branch 'master' of https://github.com/jgrapht/jgrapht into lca
AlexandruValeanu Jul 18, 2018
c238f0e
Add docs and reorganize lca package
AlexandruValeanu Jul 21, 2018
06d917f
Merge branch 'master' of https://github.com/jgrapht/jgrapht into lca
AlexandruValeanu Aug 4, 2018
50002cd
Refactor lca package
AlexandruValeanu Aug 4, 2018
736f4cf
Fix broken javadoc
AlexandruValeanu Aug 4, 2018
71d4dc2
Update documentation of BinaryLifting
AlexandruValeanu Aug 4, 2018
61cb2a4
Rename interface to LowestCommonAncestorAlgorithm
AlexandruValeanu Aug 7, 2018
751066e
Update method names
AlexandruValeanu Aug 7, 2018
8a89b81
Small fixes + documentation update
AlexandruValeanu Aug 13, 2018
7ca0aeb
Restore vanished tests
AlexandruValeanu Aug 14, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
*/
package org.jgrapht.alg;

import org.jgrapht.*;
import org.jgrapht.Graph;
import org.jgrapht.GraphTests;

import java.util.*;

Expand Down Expand Up @@ -61,8 +62,10 @@
*
* @param <V> the graph vertex type
* @param <E> the graph edge type
*
*
* @deprecated Replaced by {@link org.jgrapht.alg.lca.NaiveLCAFinder}
*/
@Deprecated
public class NaiveLcaFinder<V, E>
{
private Graph<V, E> graph;
Expand Down Expand Up @@ -259,4 +262,4 @@ private V overlappingMember(Set<V> x, Set<V> y)
}
}

// End NaiveLcaFinder.java
// End NaiveLCAFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
*/
package org.jgrapht.alg;

import org.jgrapht.*;
import org.jgrapht.alg.util.*;
import org.jgrapht.Graph;
import org.jgrapht.alg.util.UnionFind;

import java.util.*;

Expand All @@ -29,8 +29,10 @@
* @param <E> the graph edge type
*
* @author Leo Crawford
*
* @deprecated Replaced by {@link org.jgrapht.alg.lca.TarjanLCAFinder}
*/
public class TarjanLowestCommonAncestor<V, E>
@Deprecated public class TarjanLowestCommonAncestor<V, E>
{
private Graph<V, E> g;

Expand Down Expand Up @@ -232,4 +234,4 @@ public Set<LcaRequestResponse<V>> getOrCreate(V key)
}
}

// End TarjanLowestCommonAncestor.java
// End TarjanLCAFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
* into a set of disjoint paths.
*
* <p>
* The techniques was first introduced in <i>Sleator, D. D.; Tarjan, R. E. (1983).
* "A Data Structure for Dynamic Trees". Proceedings of the thirteenth annual ACM symposium on Theory of computing
* - STOC '81 doi:10.1145/800076.802464 </i>
* </p>
*
* <p>
* In a heavy path decomposition, the edges set is partitioned into two sets, a set of heavy edges and a
* set of light ones according to the relative number of nodes in the vertex's subtree.
*
Expand Down Expand Up @@ -80,7 +86,7 @@ public class HeavyPathDecomposition<V, E> implements TreeToPathDecompositionAlgo
/**
* Create an instance with a reference to the tree that we will decompose and to the root of the tree.
*
* Note: The constructor will NOT check if the input tree is a valid tree.
* Note: The constructor will NOT check if the input graph is a valid tree.
*
* @param tree the input tree
* @param root the root of the tree
Expand All @@ -94,7 +100,7 @@ public HeavyPathDecomposition(Graph<V, E> tree, V root) {
* forest (one root per tree).
*
* Note: If two roots appear in the same tree, an error will be thrown.
* Note: The constructor will NOT check if the input forest is a valid forest.
* Note: The constructor will NOT check if the input graph is a valid forest.
*
* @param forest the input forest
* @param roots the set of roots of the graph
Expand Down Expand Up @@ -512,5 +518,16 @@ public int[] getPositionInPathArray(){
public int[] getFirstNodeInPathArray(){
return firstNodeInPath;
}

/**
* Return the internal parent array.
* For each vertex $v \in V$, $parentArray[normalizeVertex(v)] = normalizeVertex(u)$ if $getParent(v) = u$ or
* $-1$ if $getParent(v) = null$.
*
* @return internal parent array
*/
public int[] getParentArray(){
return parent;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* (C) Copyright 2018-2018, by Alexandru Valeanu and Contributors.
*
* JGraphT : a free Java graph-theory library
*
* This program and the accompanying materials are dual-licensed under
* either
*
* (a) the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation, or (at your option) any
* later version.
*
* or (per the licensee's choosing)
*
* (b) the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation.
*/
package org.jgrapht.alg.interfaces;

import org.jgrapht.alg.util.Pair;

import java.util.List;
import java.util.stream.Collectors;

/**
* Algorithm to compute a <a href="https://en.wikipedia.org/wiki/Lowest_common_ancestor">lowest common ancestor</a>
* in a tree, forest or DAG.
*
* @param <V> vertex the graph vertex type
*
* @author Alexandru Valeanu
*/
public interface LCAAlgorithm<V> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the main interface name, let's spell it out as LowestCommonAncestorAlgorithm. Elsewhere (e.g. in the method names and in the implementation classes) we can keep the acronym.


/**
* Return the LCA of a and b
*
* @param a the first element to find LCA for
* @param b the other element to find the LCA for
*
* @return the LCA of a and b, or null if there is no LCA.
*/
V getLCA(V a, V b);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we expose getAllLCAs at the interface level as well? Algorithms are free to throw UnsupportedOperationException if they don't support it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. That method is only added to classes that can implement the function more efficiently than the default one (only Tarjan so far) and to make it easier for the user to get LCAs for a batch of queries.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm referring to the getAllLCAs in NaiveLCAFinder, which finds all the lowest common ancestors (in the case where there are multiple) instead of just returning one chosen arbitrarily.

Copy link
Collaborator Author

@AlexandruValeanu AlexandruValeanu Aug 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I completely misread it. We can expose it and make all implementations except NaiveLCAFinder return the result of getLCA. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works, but then it shouldn't have "all" in the name since it's not guaranteed to return a comprehensive set (and as with isomorphism inspectors, each alg doc should specify its own behavior). How about

getLCA(V, V) : returns one
getLCASet(V, V) : returns many
getBatchLCA(List): batch version, each query returns one
getBatchLCASet(List): batch version, each query returns many

Copy link
Collaborator Author

@AlexandruValeanu AlexandruValeanu Aug 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I assume that the batch version is implemented in terms of the non-batch one. And in this case, getLCASet is not supported for all tree-based implementations.

/**
* Return a list of LCA for a batch of queries
*
* @param queries a list of pairs of vertices
* @return a list L of LCAs where L(i) is the LCA of pair queries(i)
*/
default List<V> getLCAs(List<Pair<V, V>> queries){
return queries.stream().map(p -> getLCA(p.getFirst(), p.getSecond())).collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* (C) Copyright 2018-2018, by Alexandru Valeanu and Contributors.
*
* JGraphT : a free Java graph-theory library
*
* This program and the accompanying materials are dual-licensed under
* either
*
* (a) the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation, or (at your option) any
* later version.
*
* or (per the licensee's choosing)
*
* (b) the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation.
*/
package org.jgrapht.alg.lca;

import org.jgrapht.Graph;
import org.jgrapht.Graphs;
import org.jgrapht.alg.interfaces.LCAAlgorithm;
import org.jgrapht.util.VertexToIntegerMapping;

import java.util.*;

import static org.jgrapht.util.MathUtil.log2;

/**
* Algorithm for computing lowest common ancestors in rooted trees and forests using the binary lifting method.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a reference for this technique?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure a formal reference (i.e. academic paper) exists. I've added a link to the original article on Topcoder. That's the best I can think of.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually looks like a "brute-force" approach, using the lookup table.

This reference might be related, but it needs further investigation.

Bender, Michael A., and Martın Farach-Colton. "The level ancestor problem simplified." Theoretical Computer Science 321.1 (2004): 5-12.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is indeed a technique from that paper (or some older one). See Demaine's notes page 7, Algorithm B.

"The basic idea is to use jump pointers. These are pointers at a node which reference one of the
node’s ancestors. For each node create jump pointers to ancestors at levels 1, 2, 4, . . . , 2k. Queries are answered by repeatedly jumping from node to node, each time jumping more than half of the remaining levels between the current ancestor and goal ancestor. So worst-case number of jumps is O(log n). Preprocessing is done by filling in jump pointers using dynamic programming."

No one is actually using the term "binary lifting". I think this was coined up by people in topcoder or some other programming competition.

While keeping the name is fine, I think the Javadocs need a reference to an academic paper using this technique. Moreover, they need at least a paragraph explaining the lookup table with the jump pointers, and how you answer the LCA query in O(logn) time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we need more than the current explanation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it looks fine. @jkinable ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine if we add the reference (+quoted text) pointed out by @d-michail

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it already added? See

* The method appears in <i>Bender, Michael A., and Martın Farach-Colton. "The level ancestor problem

*
* <p>
* The method appears in <i>Bender, Michael A., and Martın Farach-Colton. "The level ancestor problem
* simplified." Theoretical Computer Science 321.1 (2004): 5-12</i> and it is also nicely presented in
* the following article on
* <a href="https://www.topcoder.com/community/data-science/data-science-tutorials/range-minimum-query-and-lowest-common-ancestor/#Another%20easy%20solution%20in%20O(N%20logN,%20O(logN)">Topcoder</a>
* for more details about the algorithm.
* </p>
*
* <p>
* Algorithm idea:<br>
* We improve on the naive approach by using jump pointers. These are pointers at a node which reference one of the
* node’s ancestors. Each node stores jump pointers to ancestors at levels 1, 2, 4, . . . , 2^k. <br>
* Queries are answered by repeatedly jumping from node to node, each time jumping more than half of the
* remaining levels between the current ancestor and the goal ancestor (i.e. the lca).
* The worst-case number of jumps is $O(log(|V|))$.
* </p>
*
* <p>
* Preprocessing Time complexity: $O(|V| log(|V|))$<br>
* Preprocessing Space complexity: $O(|V| log(|V|))$<br>
* Query Time complexity: $O(log(|V|))$<br>
* Query Space complexity: $O(1)$<br>
* </p>
*
* @param <V> the graph vertex type
* @param <E> the graph edge type
*
* @author Alexandru Valeanu
*/
public class BinaryLiftingLCAFinder<V, E> implements LCAAlgorithm<V> {

private final Graph<V, E> graph;
private final Set<V> roots;
private final int maxLevel;

private Map<V, Integer> vertexMap;
private List<V> indexList;

// ancestors[u][i] = the 2^i ancestor of u (e.g ancestors[u][0] = father(u))
private int[][] ancestors;

private int[] timeIn, timeOut;
private int clock = 0;

private int numberComponent;
private int[] component;

/**
* Construct a new instance of the algorithm.
*
* Note: The constructor will NOT check if the input graph is a valid tree.
*
* @param graph the input graph
* @param root the root of the graph
*/
public BinaryLiftingLCAFinder(Graph<V, E> graph, V root){
this(graph, Collections.singleton(Objects.requireNonNull(root, "root cannot be null")));
}

/**
* Construct a new instance of the algorithm.
*
* Note: If two roots appear in the same tree, an error will be thrown.
* Note: The constructor will NOT check if the input graph is a valid forest.
*
* @param graph the input graph
* @param roots the set of roots of the graph
*/
public BinaryLiftingLCAFinder(Graph<V, E> graph, Set<V> roots){
this.graph = Objects.requireNonNull(graph, "graph cannot be null");
this.roots = Objects.requireNonNull(roots, "roots cannot be null");
this.maxLevel = log2(graph.vertexSet().size());

if (this.roots.isEmpty())
throw new IllegalArgumentException("roots cannot be empty");

if (!graph.vertexSet().containsAll(roots))
throw new IllegalArgumentException("at least one root is not a valid vertex");

computeAncestorMatrix();
}

private void normalizeGraph(){
VertexToIntegerMapping<V> vertexToIntegerMapping = Graphs.getVertexToIntegerMapping(graph);
vertexMap = vertexToIntegerMapping.getVertexMap();
indexList = vertexToIntegerMapping.getIndexList();
}

private void dfs(int u, int parent){
component[u] = numberComponent;
timeIn[u] = ++clock;

ancestors[0][u] = parent;
for (int l = 1; l < maxLevel; l++) {
if (ancestors[l - 1][u] != -1)
ancestors[l][u] = ancestors[l - 1][ancestors[l - 1][u]];
}

V vertexU = indexList.get(u);
for (E edge: graph.edgesOf(vertexU)){

This comment was marked as resolved.

int v = vertexMap.get(Graphs.getOppositeVertex(graph, edge, vertexU));

if (v != parent){
dfs(v, u);
}
}

timeOut[u] = ++clock;
}

private void computeAncestorMatrix(){
ancestors = new int[maxLevel + 1][graph.vertexSet().size()];

for (int l = 0; l < maxLevel; l++) {
Arrays.fill(ancestors[l], -1);
}

timeIn = new int[graph.vertexSet().size()];
timeOut = new int[graph.vertexSet().size()];

// Ensure that isAncestor(x, y) == false if either x and y hasn't been explored yet
for (int i = 0; i < graph.vertexSet().size(); i++) {
timeIn[i] = timeOut[i] = -(i + 1);
}

numberComponent = 0;
component = new int[graph.vertexSet().size()];

normalizeGraph();

for (V root: roots) {
if (component[vertexMap.get(root)] == 0) {
numberComponent++;
dfs(vertexMap.get(root), -1);
} else {
throw new IllegalArgumentException("multiple roots in the same tree");
}
}
}

private boolean isAncestor(int ancestor, int descendant) {
return timeIn[ancestor] <= timeIn[descendant] && timeOut[descendant] <= timeOut[ancestor];
}

/**
* {@inheritDoc}
*/
@Override
public V getLCA(V a, V b) {
int indexA = vertexMap.getOrDefault(a, -1);
if (indexA == -1)
throw new IllegalArgumentException("invalid vertex: " + a);

int indexB = vertexMap.getOrDefault(b, -1);
if (indexB == -1)
throw new IllegalArgumentException("invalid vertex: " + b);

// Check if a == b because lca(a, a) == a
if (a.equals(b))
return a;

// if a and b are in different components then they do not have a lca
if (component[indexA] != component[indexB] || component[indexA] == 0)
return null;

if (isAncestor(indexA, indexB))
return a;

if (isAncestor(indexB, indexA))
return b;

for (int l = maxLevel - 1; l >= 0; l--)
if (ancestors[l][indexA] != -1 && !isAncestor(ancestors[l][indexA], indexB))
indexA = ancestors[l][indexA];

int lca = ancestors[0][indexA];

// if lca is null
if (lca == -1)
return null;
else
return indexList.get(lca);
}
}
Loading