org.jgrapht.alg.lca.TarjanLCAFinder Maven / Gradle / Ivy
/*
* (C) Copyright 2018-2023, by Alexandru Valeanu 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.lca;
import org.jgrapht.*;
import org.jgrapht.alg.interfaces.*;
import org.jgrapht.alg.util.*;
import java.util.*;
/**
* Tarjan's offline algorithm for computing lowest common ancestors in rooted trees and forests.
*
*
* See the article on wikipedia
* for more information on the algorithm.
*
*
*
*
* The original algorithm can be found in Gabow, H. N.; Tarjan, R. E. (1983), "A linear-time
* algorithm for a special case of disjoint set union", Proceedings of the 15th ACM Symposium on
* Theory of Computing (STOC), pp. 246–251, doi:10.1145/800061.808753
*
*
*
* Preprocessing Time complexity: $O(1)$
* Preprocessing Space complexity: $O(1)$
* Query Time complexity: $O(|V| log^{*}(|V|) + |Q|)$ where $|Q|$ is the number of queries
* Query Space complexity: $O(|V| + |Q|)$ where $|Q|$ is the number of queries
*
*
*
* For small (i.e. less than 100 vertices) trees or forests, all implementations behave similarly.
* For larger trees/forests with less than 50,000 queries you can use either
* {@link BinaryLiftingLCAFinder}, {@link HeavyPathLCAFinder} or {@link EulerTourRMQLCAFinder}. Fo
* more than that use {@link EulerTourRMQLCAFinder} since it provides $O(1)$ per query.
* Space-wise, {@link HeavyPathLCAFinder} and {@link TarjanLCAFinder} only use a linear amount while
* {@link BinaryLiftingLCAFinder} and {@link EulerTourRMQLCAFinder} require linearithmic space.
* For DAGs, use {@link NaiveLCAFinder}.
*
*
* @param the graph vertex type
* @param the graph edge type
*
* @author Alexandru Valeanu
*/
public class TarjanLCAFinder
implements LowestCommonAncestorAlgorithm
{
private Graph graph;
private Set roots;
private UnionFind unionFind;
private Map ancestors;
private Set blackNodes;
private HashMap> queryOccurs;
private List lowestCommonAncestors;
private List> queries;
/**
* 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 TarjanLCAFinder(Graph 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 TarjanLCAFinder(Graph graph, Set roots)
{
this.graph = Objects.requireNonNull(graph, "graph cannot be null");
this.roots = Objects.requireNonNull(roots, "roots cannot be null");
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");
}
/**
* {@inheritDoc}
*/
@Override
public V getLCA(V a, V b)
{
return getBatchLCA(Collections.singletonList(Pair.of(a, b))).get(0);
}
/**
* {@inheritDoc}
*/
@Override
public List getBatchLCA(List> queries)
{
return computeTarjan(queries);
}
private void initialize()
{
unionFind = new UnionFind<>(Collections.emptySet());
ancestors = new HashMap<>();
blackNodes = new HashSet<>();
}
private void clear()
{
unionFind = null;
ancestors = null;
blackNodes = null;
queryOccurs = null;
queries = null;
lowestCommonAncestors = null;
}
private List computeTarjan(List> queries)
{
initialize();
this.queries = queries;
this.lowestCommonAncestors = new ArrayList<>(queries.size());
this.queryOccurs = new HashMap<>();
for (int i = 0; i < queries.size(); i++) {
V a = this.queries.get(i).getFirst();
V b = this.queries.get(i).getSecond();
if (!graph.containsVertex(a))
throw new IllegalArgumentException("invalid vertex: " + a);
if (!graph.containsVertex(b))
throw new IllegalArgumentException("invalid vertex: " + b);
if (a.equals(b))
this.lowestCommonAncestors.add(a);
else {
queryOccurs.computeIfAbsent(a, x -> new HashSet<>()).add(i);
queryOccurs.computeIfAbsent(b, x -> new HashSet<>()).add(i);
this.lowestCommonAncestors.add(null);
}
}
Set visited = new HashSet<>();
for (V root : roots) {
if (visited.contains(root))
throw new IllegalArgumentException("multiple roots in the same tree");
blackNodes.clear();
computeTarjanOLCA(root, null, visited);
}
List tmpRef = lowestCommonAncestors;
clear();
return tmpRef;
}
private void computeTarjanOLCA(V u, V p, Set visited)
{
visited.add(u);
unionFind.addElement(u);
ancestors.put(u, u);
for (E edge : graph.outgoingEdgesOf(u)) {
V v = Graphs.getOppositeVertex(graph, edge, u);
if (!v.equals(p)) {
computeTarjanOLCA(v, u, visited);
unionFind.union(u, v);
ancestors.put(unionFind.find(u), u);
}
}
blackNodes.add(u);
for (int index : queryOccurs.computeIfAbsent(u, x -> new HashSet<>())) {
Pair query = queries.get(index);
V v;
if (query.getFirst().equals(u))
v = query.getSecond();
else
v = query.getFirst();
if (blackNodes.contains(v)) {
lowestCommonAncestors.set(index, ancestors.get(unionFind.find(v)));
}
}
}
/**
* Note: This operation is not supported.
*
* {@inheritDoc}
*
* @throws UnsupportedOperationException if the method is called
*/
@Override
public Set getLCASet(V a, V b)
{
throw new UnsupportedOperationException();
}
}