-
Notifications
You must be signed in to change notification settings - Fork 824
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
The lca package #597
Changes from 35 commits
1a449c3
4fe4eaf
9e26b03
edf9900
091dc67
176e867
c6b36be
3b842e9
88b67ed
2336275
861b239
b3b3eb0
b1765b0
da2e4ab
0586b4d
6c66f66
1dda979
3b91111
e4b14a0
21aabb1
fefe40e
756c6d2
3a4295f
9a2037b
ad24696
3c36ba6
1fabf8b
72d4292
b00ec07
ac712d4
c238f0e
06d917f
50002cd
736f4cf
71d4dc2
61cb2a4
751066e
8a89b81
7ca0aeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> { | ||
|
||
/** | ||
* 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); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a reference for this technique? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we need more than the current explanation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it looks fine. @jkinable ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it already added? See jgrapht/jgrapht-core/src/main/java/org/jgrapht/alg/lca/BinaryLiftingLCAFinder.java Line 33 in 7ca0aeb
|
||||
* | ||||
* <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.
Sorry, something went wrong. |
||||
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); | ||||
} | ||||
} |
There was a problem hiding this comment.
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.