diff --git a/.idea/artifacts/algs4benchmark_jar.xml b/.idea/artifacts/algs4benchmark_jar.xml new file mode 100644 index 000000000..c56fbf56a --- /dev/null +++ b/.idea/artifacts/algs4benchmark_jar.xml @@ -0,0 +1,42 @@ + + + $PROJECT_DIR$/bin/artifacts/algs4benchmark_jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/artifacts/find_new_to_old_benchmark_jar.xml b/.idea/artifacts/find_new_to_old_benchmark_jar.xml new file mode 100644 index 000000000..caa3efd97 --- /dev/null +++ b/.idea/artifacts/find_new_to_old_benchmark_jar.xml @@ -0,0 +1,42 @@ + + + $PROJECT_DIR$/bin/artifacts/find_new_to_old_benchmark_jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/artifacts/find_old_to_new_benchmark_jar.xml b/.idea/artifacts/find_old_to_new_benchmark_jar.xml new file mode 100644 index 000000000..5b80e9790 --- /dev/null +++ b/.idea/artifacts/find_old_to_new_benchmark_jar.xml @@ -0,0 +1,42 @@ + + + $PROJECT_DIR$/bin/artifacts/find_old_to_new_benchmark_jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/artifacts/union_single_elements_benchmark_jar.xml b/.idea/artifacts/union_single_elements_benchmark_jar.xml new file mode 100644 index 000000000..93fc475c6 --- /dev/null +++ b/.idea/artifacts/union_single_elements_benchmark_jar.xml @@ -0,0 +1,42 @@ + + + $PROJECT_DIR$/bin/artifacts/union_single_elements_benchmark_jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/manifests/algs4benchmark/META-INF/MANIFEST.MF b/manifests/algs4benchmark/META-INF/MANIFEST.MF new file mode 100644 index 000000000..691b0a36e --- /dev/null +++ b/manifests/algs4benchmark/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Main-Class: org.sosy_lab.common.collect.union_find.benchmarking.Algs4Dat + asetBenchmark + diff --git a/manifests/find_new_to_old_benchmark_jar/META-INF/MANIFEST.MF b/manifests/find_new_to_old_benchmark_jar/META-INF/MANIFEST.MF new file mode 100644 index 000000000..57e627205 --- /dev/null +++ b/manifests/find_new_to_old_benchmark_jar/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Main-Class: org.sosy_lab.common.collect.union_find.benchmarking.FindNewT + oOldSingleSetBenchmark + diff --git a/manifests/find_old_to_new_benchmark/META-INF/MANIFEST.MF b/manifests/find_old_to_new_benchmark/META-INF/MANIFEST.MF new file mode 100644 index 000000000..c65b8dbfe --- /dev/null +++ b/manifests/find_old_to_new_benchmark/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Main-Class: org.sosy_lab.common.collect.union_find.benchmarking.FindOldT + oNewSingleSetBenchmark + diff --git a/manifests/union_single_elements_benchmark/META-INF/MANIFEST.MF b/manifests/union_single_elements_benchmark/META-INF/MANIFEST.MF new file mode 100644 index 000000000..66ac2bbd2 --- /dev/null +++ b/manifests/union_single_elements_benchmark/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Main-Class: org.sosy_lab.common.collect.union_find.benchmarking.UnionSin + gleElementsIntoExistingSetBenchmark + diff --git a/src/org/sosy_lab/common/collect/PackageSanityTest.java b/src/org/sosy_lab/common/collect/PackageSanityTest.java index d349efcf9..eda379bd9 100644 --- a/src/org/sosy_lab/common/collect/PackageSanityTest.java +++ b/src/org/sosy_lab/common/collect/PackageSanityTest.java @@ -9,7 +9,10 @@ package org.sosy_lab.common.collect; import com.google.common.testing.AbstractPackageSanityTests; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import org.sosy_lab.common.Classes; +import org.sosy_lab.common.collect.union_find.SortedTreeSetUnionFind; public class PackageSanityTest extends AbstractPackageSanityTests { @@ -24,4 +27,19 @@ public class PackageSanityTest extends AbstractPackageSanityTests { OurSortedMap.class, OurSortedMap.EmptyImmutableOurSortedMap.of(), singletonMap); ignoreClasses(Classes.IS_GENERATED); } + + { + setDefault(SortedTreeSetUnionFind.class, new SortedTreeSetUnionFind<>()); + // ignoreClasses(Classes.IS_GENERATED); + + try { + setDefault(Constructor.class, PackageSanityTest.class.getConstructor()); + setDefault(Method.class, PackageSanityTest.class.getDeclaredMethod("defaultMethod")); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + @SuppressWarnings("unused") + private static void defaultMethod() {} } diff --git a/src/org/sosy_lab/common/collect/union_find/AbstractGenericUnionFind.java b/src/org/sosy_lab/common/collect/union_find/AbstractGenericUnionFind.java new file mode 100644 index 000000000..3408c1f5e --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/AbstractGenericUnionFind.java @@ -0,0 +1,179 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.common.base.Preconditions; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * An abstract, generic implementation of {@link UnionFind} using a {@link Map} of {@link Set}s. In + * order to represent subsets by canonical elements, each one is mapped to its representative + * canonical element. This is always the first element added to the subset, unless it has changed + * due to union operations. The union is implemented as union by size. + * + * @param type of elements added to the Union-Find. + */ +public abstract class AbstractGenericUnionFind, M extends Map> + implements UnionFind { + + protected final M mapOfSets; + + /** + * Takes an empty map of the desired kind and allocates it to the variable mapOfSets. This enables + * child classes to simply pass an object of the desired kind without having to modify the + * constructor and methods. + */ + @SuppressWarnings("unchecked") + public AbstractGenericUnionFind() { + mapOfSets = (M) getEmptyMap(); + } + + /** + * Returns the canonical element of the set containing the provided element. + * + * @param pE element for which set is to be found + * @return canonical element of the found set + * @throws IllegalArgumentException if element is not contained in any subset + */ + @Override + public T find(T pE) { + + Preconditions.checkNotNull(pE); + + for (Entry mapping : mapOfSets.entrySet()) { + if (mapping.getValue().contains(pE)) { + return mapping.getKey(); + } + } + + throw new IllegalArgumentException("Element not contained"); + } + + /** + * Merges the sets represented by the two input values according to standard Union-Find behaviour. + * + *

USES: Add new element as new set: pass it as both pE1 and pE2. Add new element to existing + * set: one input value is the new element, the other the canonical element of the set to be added + * to. Merge two existing sets: pE1, pE2 canonical elements of sets to be merged. + * + * @param pE1 first element + * @param pE2 second element + */ + @Override + public void union(T pE1, T pE2) { + + Preconditions.checkNotNull(pE1); + Preconditions.checkNotNull(pE2); + + if (pE1.equals(pE2)) { + addElementAsNewSet(pE1); + } else { + Set canonicalElements = mapOfSets.keySet(); + + if (canonicalElements.contains(pE1)) { + if (canonicalElements.contains(pE2)) { + mergeExistingSets(pE1, pE2); + } else { + addElementToExistingSet(pE2, pE1); + } + } else if (canonicalElements.contains(pE2)) { + addElementToExistingSet(pE1, pE2); + } else { + + if (contains(pE1)) { + if (contains(pE2)) { + mergeExistingSets(find(pE1), find(pE2)); + } else { + addElementToExistingSet(pE2, find(pE1)); + } + } else { + addElementAsNewSet(pE1); + addElementToExistingSet(pE2, pE1); + } + } + } + } + + @SuppressWarnings("unchecked") + private void addElementAsNewSet(T pE) { + + if (!contains(pE)) { + S newSet = (S) getEmptySet(); + newSet.add(pE); + mapOfSets.put(pE, newSet); + } + } + + private void addElementToExistingSet(T pE, T pCanon) { + + if (!contains(pE)) { + mapOfSets.get(pCanon).add(pE); + } else { + mergeExistingSets(find(pE), pCanon); + } + } + + // pE1 will be new canonical element only if its set is actually bigger, otherwise pE2 new canon + private void mergeExistingSets(T pE1, T pE2) { + + S set1 = mapOfSets.get(pE1); + S set2 = mapOfSets.get(pE2); + + assert set1 != null; + assert set2 != null; + + int size1 = set1.size(); + int size2 = set2.size(); + + if (size1 > size2) { + set1.addAll(set2); + assert mapOfSets.remove(pE2, set2); + } else { + set2.addAll(set1); + assert mapOfSets.remove(pE1, set1); + } + } + + /** + * Provides a {@link Collection} containing all current subsets. + * + * @return {@link Collection} containing all current subsets + */ + @Override + public Collection getAllSubsets() { + return mapOfSets.values(); + } + + /** + * Checks whether the provided element is contained in any current subset and returns true or + * false accordingly. + * + * @param pE element to be searched for + * @return true if contained, false if not + */ + @Override + public boolean contains(T pE) { + + Preconditions.checkNotNull(pE); + + for (S current : mapOfSets.values()) { + if (current.contains(pE)) { + return true; + } + } + return false; + } + + protected abstract Set getEmptySet(); + + protected abstract Map> getEmptyMap(); +} diff --git a/src/org/sosy_lab/common/collect/union_find/AbstractImmutableParentPointerTreeBuilder.java b/src/org/sosy_lab/common/collect/union_find/AbstractImmutableParentPointerTreeBuilder.java new file mode 100644 index 000000000..c9ba77f8e --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/AbstractImmutableParentPointerTreeBuilder.java @@ -0,0 +1,80 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Immutable; +import java.util.Collection; +import java.util.Set; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; + +/** + * Abstract builder class which first collects the data in a mutable Union-Find and converts it to + * an immutable Union-Find when {@code build()} is called. From then onward, previously modifying + * methods will not cause further modifications to the Union-Find instance inside. See documentation + * in {@link ParentPointerTreeUnionFind} for explanations on {@code union()}, {@code add()} and + * {@code addAll()} as the methods in this builder simply pass to their aforementioned namesakes. + * + * @param type of elements added to the Union-Find + */ +@Immutable(containerOf = "T") +public abstract class AbstractImmutableParentPointerTreeBuilder { + + // union-find not immutable but only used internally and never mutated passed outward + // build() returns an immutable union-find that contains a copy of this union-find's map + @SuppressWarnings("Immutable") + final ParentPointerTreeUnionFind unionFind; + + // prevents further modifications after build() + // is never modified once having been switched to false + @SuppressWarnings("Immutable") + boolean modificationsAllowed; + + protected AbstractImmutableParentPointerTreeBuilder(UnionType pUnionType) { + unionFind = new ParentPointerTreeUnionFind<>(pUnionType); + modificationsAllowed = true; + } + + @CanIgnoreReturnValue + public AbstractImmutableParentPointerTreeBuilder union(T pE1, T pE2) { + + if (modificationsAllowed) { + + unionFind.union(pE1, pE2); + } + + return this; + } + + @CanIgnoreReturnValue + public AbstractImmutableParentPointerTreeBuilder add(Set pSet) { + + if (modificationsAllowed) { + + unionFind.add(pSet); + } + + return this; + } + + @CanIgnoreReturnValue + public AbstractImmutableParentPointerTreeBuilder addAll(Collection> pSets) { + + if (modificationsAllowed) { + + unionFind.addAll(pSets); + } + + return this; + } + + // get map from mutable Union-Find instance and convert to immutable map, then pass to constructor + // set modificationsAllowed to false!! + public abstract AbstractImmutableUnionFind build(); +} diff --git a/src/org/sosy_lab/common/collect/union_find/AbstractImmutableSortedUnionFind.java b/src/org/sosy_lab/common/collect/union_find/AbstractImmutableSortedUnionFind.java new file mode 100644 index 000000000..dab872d08 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/AbstractImmutableSortedUnionFind.java @@ -0,0 +1,20 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.errorprone.annotations.Immutable; + +/** + * An abstract class for sorted immutable Union-Find implementations. + * + * @param type of elements added to the Union-Find. Must be comparable. + */ +@Immutable(containerOf = "T") +public abstract class AbstractImmutableSortedUnionFind> + extends AbstractImmutableUnionFind implements SortedUnionFind {} diff --git a/src/org/sosy_lab/common/collect/union_find/AbstractImmutableUnionFind.java b/src/org/sosy_lab/common/collect/union_find/AbstractImmutableUnionFind.java new file mode 100644 index 000000000..940b1903c --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/AbstractImmutableUnionFind.java @@ -0,0 +1,28 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.errorprone.annotations.DoNotCall; +import com.google.errorprone.annotations.Immutable; + +/** + * An abstract class for immutable Union-Find implementations. + * + * @param type of elements added to the Union-Find + */ +@Immutable(containerOf = "T") +public abstract class AbstractImmutableUnionFind implements UnionFind { + + @Deprecated + @Override + @DoNotCall + public final void union(T pE1, T pE2) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/AbstractTreeNode.java b/src/org/sosy_lab/common/collect/union_find/AbstractTreeNode.java new file mode 100644 index 000000000..b8351c0dc --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/AbstractTreeNode.java @@ -0,0 +1,55 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +/** + * An abstract class of nodes from which a simple parent pointer tree can be built. + * + * @param type of elements each node holds as value + */ +public abstract class AbstractTreeNode { + + private AbstractTreeNode parent; + private final T value; + + /** + * Constructor for a root node. The parent variable points to itself, thus indicating this is a + * root node. If appended to another tree, parent can be reallocated to the new parent node, while + * the current node simply functions as a non-root node from then on. + * + * @param pValue element to be stored in the node + */ + protected AbstractTreeNode(T pValue) { + parent = this; + value = pValue; + } + + /** + * Constructor for a non-root node. + * + * @param pParent pParent node (can be root or non-root) + * @param pValue element to be stored in the node + */ + protected AbstractTreeNode(AbstractTreeNode pParent, T pValue) { + parent = pParent; + value = pValue; + } + + public AbstractTreeNode getParent() { + return parent; + } + + public void setParent(AbstractTreeNode pParent) { + parent = pParent; + } + + public T getValue() { + return value; + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/ImmutableParentPointerTreeUnionFind.java b/src/org/sosy_lab/common/collect/union_find/ImmutableParentPointerTreeUnionFind.java new file mode 100644 index 000000000..32c367466 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/ImmutableParentPointerTreeUnionFind.java @@ -0,0 +1,143 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.annotations.Var; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; + +/** + * An implementation of {@link UnionFind} using a {@link ImmutableMap} of each element to its {@link + * AbstractTreeNode}. Each node contains a reference to its respective parent node, thus resulting + * in a parent pointer tree structure for each subset. These are each represented by canonical + * elements which are the root of each tree. This is always the first element added to the subset, + * unless it has changed due to union operations. The union can be performed either by size or by + * rank, * determined by a constructor parameter. + * + * @param type of elements added to the Union-Find. + */ +@Immutable(containerOf = "T") +public class ImmutableParentPointerTreeUnionFind extends AbstractImmutableUnionFind { + + // tree nodes are not immutable but only used internally and never mutated after creation + // immutable tree nodes would make conversion during build() difficult and time-consuming + @SuppressWarnings("Immutable") + private final ImmutableMap> allNodes; + + /** + * Only for internal use by the builder. + * + * @param pAllNodes finished immutable map storing all nodes contained in this Union-Find + */ + protected ImmutableParentPointerTreeUnionFind(ImmutableMap> pAllNodes) { + allNodes = pAllNodes; + } + + /** + * Returns the canonical element of the set containing the provided element. + * + * @param pE element for which set is to be found + * @return canonical element of the found set + * @throws IllegalArgumentException if element is not contained in any subset + */ + @Override + public T find(T pE) { + + Preconditions.checkNotNull(pE); + + @Var AbstractTreeNode node = allNodes.get(pE); + + if (node != null) { + @Var AbstractTreeNode parent = node.getParent(); + + while (!node.equals(parent)) { + node = parent; + parent = node.getParent(); + } + + return parent.getValue(); + } + + throw new IllegalArgumentException("Element not contained."); + } + + /** + * Provides a {@link Collection} containing all current subsets. + * + * @return {@link Collection} containing all current subsets + */ + @Override + public Collection> getAllSubsets() { + + Map> allSubsets = new HashMap<>(); + + for (AbstractTreeNode node : allNodes.values()) { + + T canon = find(node.getValue()); + + if (allSubsets.containsKey(canon)) { + allSubsets.get(canon).add(node.getValue()); + } else { + Set set = new HashSet<>(); + set.add(node.getValue()); + allSubsets.put(canon, set); + } + } + + return allSubsets.values(); + } + + /** + * Checks whether the provided element is contained in any current subset and returns true or + * false accordingly. + * + * @param pE element to be searched for + * @return true if contained, false if not + */ + @Override + public boolean contains(T pE) { + + return allNodes.containsKey(pE); + } + + /** + * Builder class which first collects the data in a mutable Union-Find and converts it to an + * immutable Union-Find when {@code build()} is called. See documentation in {@link + * ParentPointerTreeUnionFind} for explanations on {@code union()}, {@code add()} and {@code + * addAll()} as the methods in this builder simply pass to their aforementioned namesakes. + * + * @param type of elements added to the Union-Find + */ + public static final class Builder extends AbstractImmutableParentPointerTreeBuilder { + + private Builder(UnionType pUnionType) { + super(pUnionType); + } + + public static AbstractImmutableParentPointerTreeBuilder getBuilder( + UnionType pUnionType) { + return new Builder<>(pUnionType); + } + + @Override + public ImmutableParentPointerTreeUnionFind build() { + + modificationsAllowed = false; + + return new ImmutableParentPointerTreeUnionFind<>(ImmutableMap.copyOf(unionFind.allNodes)); + } + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/ImmutableSortedParentPointerTreeUnionFind.java b/src/org/sosy_lab/common/collect/union_find/ImmutableSortedParentPointerTreeUnionFind.java new file mode 100644 index 000000000..06e3050fa --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/ImmutableSortedParentPointerTreeUnionFind.java @@ -0,0 +1,149 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.annotations.Var; +import java.util.Collection; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.TreeMap; +import java.util.TreeSet; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; + +/** + * A sorted implementation of {@link UnionFind} using a {@link ImmutableMap} of each element to its + * {@link AbstractTreeNode}. Each node contains a reference to its respective parent node, thus + * resulting in a parent pointer tree structure for each subset. These are each represented by + * canonical elements which are the root of each tree. This is always the first element added to the + * subset, unless it has changed due to union operations. The union can be performed either by size + * or by rank, * determined by a constructor parameter. The elements are stored in unsorted + * structures, but {@code getAllSubsets()} returns a sorted view. + * + * @param type of elements added to the Union-Find. Must be comparable. + */ +@Immutable(containerOf = "T") +public class ImmutableSortedParentPointerTreeUnionFind> + extends AbstractImmutableSortedUnionFind { + + // tree nodes are not immutable but only used internally and never mutated after creation + // immutable tree nodes would make conversion during build() difficult and time-consuming + @SuppressWarnings("Immutable") + private final ImmutableMap> allNodes; + + /** + * Only for internal use by the builder. + * + * @param pAllNodes finished immutable map storing all nodes contained in this Union-Find + */ + protected ImmutableSortedParentPointerTreeUnionFind( + ImmutableMap> pAllNodes) { + allNodes = pAllNodes; + } + + /** + * Returns the canonical element of the set containing the provided element. + * + * @param pE element for which set is to be found + * @return canonical element of the found set + * @throws IllegalArgumentException if element is not contained in any subset + */ + @Override + public T find(T pE) { + + Preconditions.checkNotNull(pE); + + @Var AbstractTreeNode node = allNodes.get(pE); + + if (node != null) { + @Var AbstractTreeNode parent = node.getParent(); + + while (!node.equals(parent)) { + node = parent; + parent = node.getParent(); + } + + return parent.getValue(); + } + + throw new IllegalArgumentException("Element not contained."); + } + + /** + * Provides a {@link Collection} containing all current subsets. The subsets are sorted by their + * canonical elements in ascending order. The contents of each subset are equally sorted in + * ascending order. + * + * @return {@link Collection} containing all current subsets + */ + @Override + public Collection> getAllSubsets() { + + NavigableMap> allSubsets = new TreeMap<>(); + + for (AbstractTreeNode node : allNodes.values()) { + + T canon = find(node.getValue()); + + if (allSubsets.containsKey(canon)) { + allSubsets.get(canon).add(node.getValue()); + } else { + NavigableSet set = new TreeSet<>(); + set.add(node.getValue()); + allSubsets.put(canon, set); + } + } + + return allSubsets.values(); + } + + /** + * Checks whether the provided element is contained in any current subset and returns true or + * false accordingly. + * + * @param pE element to be searched for + * @return true if contained, false if not + */ + @Override + public boolean contains(T pE) { + + return allNodes.containsKey(pE); + } + + /** + * Builder class which first collects the data in a mutable sorted Union-Find and converts it to + * an immutable Union-Find when {@code build()} is called. See documentation in {@link + * ParentPointerTreeUnionFind} for explanations on {@code union()}, {@code add()} and {@code + * addAll()} as the methods in this builder simply pass to their aforementioned namesakes. + * + * @param type of elements added to the Union-Find + */ + public static final class Builder> + extends AbstractImmutableParentPointerTreeBuilder { + + private Builder(UnionType pUnionType) { + super(pUnionType); + } + + public static > Builder getBuilder(UnionType pUnionType) { + return new Builder<>(pUnionType); + } + + @Override + public ImmutableSortedParentPointerTreeUnionFind build() { + + modificationsAllowed = false; + + return new ImmutableSortedParentPointerTreeUnionFind<>( + ImmutableMap.copyOf(unionFind.allNodes)); + } + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/NonRootNode.java b/src/org/sosy_lab/common/collect/union_find/NonRootNode.java new file mode 100644 index 000000000..e318015b3 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/NonRootNode.java @@ -0,0 +1,28 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +/** + * An implementation of {@link AbstractTreeNode} resulting in nodes that can only be used as + * non-root nodes but not as root nodes. + * + * @param type of elements each node holds as value + */ +public final class NonRootNode extends AbstractTreeNode { + + /** + * Constructor for a non-root node. + * + * @param pParent parent node (can be root or non-root) + * @param pValue element to be stored in the node + */ + public NonRootNode(AbstractTreeNode pParent, T pValue) { + super(pParent, pValue); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/ParentPointerTreeUnionFind.java b/src/org/sosy_lab/common/collect/union_find/ParentPointerTreeUnionFind.java new file mode 100644 index 000000000..74ef081d7 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/ParentPointerTreeUnionFind.java @@ -0,0 +1,302 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.Var; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * An implementation of {@link UnionFind} using a {@link Map} of {@link AbstractTreeNode}s. In order + * to represent subsets by canonical elements, each one is mapped to its representative canonical + * element. This is always the first element added to the subset, unless it has changed due to union + * operations. Each subset is stored as a parent pointer tree comprised of {@link NonRootNode}s with + * exactly one {@link RootNode} as the root. The union can be performed either by size or by rank, + * determined by a constructor parameter. + * + * @param type of elements added to the Union-Find. + */ +public class ParentPointerTreeUnionFind implements UnionFind { + + public enum UnionType { + UNION_BY_RANK, + UNION_BY_SIZE + } + + protected final Map> allNodes; + private final UnionType unionType; + + /** + * Creates an empty instance. + * + * @param pUnionType type of union to be performed for all unions on this instance + */ + public ParentPointerTreeUnionFind(UnionType pUnionType) { + allNodes = new HashMap<>(); + unionType = pUnionType; + } + + /** + * Returns the canonical element of the set containing the provided element. Applies path + * compression where possible. + * + * @param pE element for which set is to be found + * @return canonical element of the found set + * @throws IllegalArgumentException if element is not contained in any subset + */ + @Override + public T find(T pE) { + + Preconditions.checkNotNull(pE); + + List> toBeCompressed = new ArrayList<>(); + @Var AbstractTreeNode node = allNodes.get(pE); + + if (node != null) { + @Var AbstractTreeNode parent = node.getParent(); + + while (!node.equals(parent)) { + toBeCompressed.add(node); + node = parent; + parent = node.getParent(); + } + + for (AbstractTreeNode current : toBeCompressed) { + + current.setParent(parent); + } + + return parent.getValue(); + } + + throw new IllegalArgumentException("Element not contained."); + } + + /** + * Merges the sets represented by the two input values according to standard Union-Find behaviour. + * + *

USES: Add new element as new set: pass it as both pE1 and pE2. Add new element to existing + * set: one input value is the new element, the other the canonical element of the set to be added + * to. Merge two existing sets: pE1, pE2 canonical elements of sets to be merged. + * + * @param pE1 first element + * @param pE2 second element + */ + @Override + public void union(T pE1, T pE2) { + + Preconditions.checkNotNull(pE1); + Preconditions.checkNotNull(pE2); + + if (pE1.equals(pE2)) { + addElementAsNewSet(pE1); + } else { + if (contains(pE1)) { + if (contains(pE2)) { + T canon1 = find(pE1); + T canon2 = find(pE2); + + if (!canon1.equals(canon2)) { + mergeExistingSets(findNode(canon1), findNode(canon2)); + } + } else { + addElementToExistingSet(pE2, findNode(pE1)); + } + } else if (contains(pE2)) { + addElementToExistingSet(pE1, findNode(pE2)); + } else { + addElementAsNewSet(pE1); + addElementToExistingSet(pE2, findNode(pE1)); + } + } + } + + /** + * Provides a {@link Collection} containing all current subsets. + * + * @return {@link Collection} containing all current subsets + */ + @Override + public Collection> getAllSubsets() { + + Map> allSubsets = new HashMap<>(); + + for (AbstractTreeNode node : allNodes.values()) { + + T canon = find(node.getValue()); + + if (allSubsets.containsKey(canon)) { + allSubsets.get(canon).add(node.getValue()); + } else { + Set set = new HashSet<>(); + set.add(canon); + set.add(node.getValue()); + allSubsets.put(canon, set); + } + } + + return allSubsets.values(); + } + + /** + * Checks whether the provided element is contained in any current subset and returns true or + * false accordingly. + * + * @param pE element to be searched for + * @return true if contained, false if not + */ + @Override + public boolean contains(T pE) { + + return allNodes.containsKey(pE); + } + + /** + * Adds the contents of a set of elements to the Union-Find. A random element in the set (the + * first one accessed) will be used as the canonical element. If the set contains elements already + * found elsewhere in the Union-Find, these sets will be merged accordingly. + * + * @param pSet set to be added to the Union-Find + */ + public void add(Set pSet) { + + Preconditions.checkNotNull(pSet); + + @Var T canon = null; + + for (T current : pSet) { + + if (canon == null) { + canon = current; + addElementAsNewSet(canon); + } + + addElementToExistingSet(current, findNode(canon)); + } + } + + /** + * Adds multiple sets of elements to the Union-Find. For each set, a random element in the set + * (the first one accessed) will be used as the canonical element. If a set contains elements + * already found elsewhere in the Union-Find, these sets will be merged accordingly. + * + * @param pSets sets to be added to the Union-Find + */ + public void addAll(Collection> pSets) { + + Preconditions.checkNotNull(pSets); + + for (Set set : pSets) { + add(set); + } + } + + private void addElementAsNewSet(T pE) { + + if (!contains(pE)) { + RootNode root = new RootNode<>(pE); + allNodes.put(pE, root); + } + } + + // only call with elements that are definitely canonical! + private void mergeExistingSets(RootNode pCanon1, RootNode pCanon2) { + + Preconditions.checkNotNull(pCanon1); + Preconditions.checkNotNull(pCanon2); + + if (unionType == UnionType.UNION_BY_SIZE) { + unionBySize(pCanon1, pCanon2); + } else { + unionByRank(pCanon1, pCanon2); + } + } + + private void addElementToExistingSet(T pE, RootNode pCanon) { + + NonRootNode newNode = new NonRootNode<>(pCanon, pE); + pCanon.incrementSizeByOne(); + + if (pCanon.getRank() == 0) { + pCanon.incrementRankByOne(); + } + + allNodes.put(pE, newNode); + } + + // pCanon1 will be new canonical element only if its set is actually bigger, otherwise pCanon2 new + // canon + private void unionBySize(RootNode pCanon1, RootNode pCanon2) { + + int size1 = pCanon1.getSize(); + int size2 = pCanon2.getSize(); + + if (size1 > size2) { + pCanon2.setParent(pCanon1); + pCanon1.incrementSizeBy(pCanon2.getSize()); + } else { + pCanon1.setParent(pCanon2); + pCanon2.incrementSizeBy(pCanon1.getSize()); + } + } + + // pCanon1 will be new canonical element only if its rank is actually greater, otherwise pCanon2 + // new + // canon + private void unionByRank(RootNode pCanon1, RootNode pCanon2) { + + int rank1 = pCanon1.getRank(); + int rank2 = pCanon2.getRank(); + + if (rank1 > rank2) { + pCanon2.setParent(pCanon1); + } else { + pCanon1.setParent(pCanon2); + + // as rank only changes if both ranks are the same + if (rank1 == rank2) { + pCanon2.incrementRankByOne(); + } + } + } + + // like find; returns node of canonical element of the set pE belongs to, not value + private RootNode findNode(T pE) { + + Preconditions.checkNotNull(pE); + + List> toBeCompressed = new ArrayList<>(); + @Var AbstractTreeNode node = allNodes.get(pE); + + if (node != null) { + @Var AbstractTreeNode parent = node.getParent(); + + while (!node.equals(parent)) { + toBeCompressed.add(node); + node = parent; + parent = node.getParent(); + } + + for (AbstractTreeNode current : toBeCompressed) { + + current.setParent(parent); + } + + return (RootNode) parent; + } + + throw new IllegalArgumentException("Element not contained."); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/PersistentParentPointerTreeUnionFind.java b/src/org/sosy_lab/common/collect/union_find/PersistentParentPointerTreeUnionFind.java new file mode 100644 index 000000000..d645e31ee --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/PersistentParentPointerTreeUnionFind.java @@ -0,0 +1,306 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.annotations.Var; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; + +/** + * Implementation of a persistent union-find. A persistent data structure is immutable, but provides + * cheap copy-and-write operations. Thus, all write operations ({@link #union(Object, Object)}) will + * not modify the current instance, but return a new instance instead. The union can be performed + * either by size or by rank, determined by a constructor parameter. + * + *

All modifying operations inherited from {@link UnionFind} are not supported and will always + * throw {@link UnsupportedOperationException}. + * + * @param The type of elements added to the Union-Find. + */ +@Immutable(containerOf = "T") +public final class PersistentParentPointerTreeUnionFind extends AbstractImmutableUnionFind + implements PersistentUnionFind { + + // map only used internally and never mutated after creation + // TODO convert to ImmutableMap once Guava upgraded to 31.1 and buildKeepingLast() available + @SuppressWarnings("Immutable") + private final Map mapOfNodesToParents; + + // map only used internally and never mutated after creation + // TODO convert to ImmutableMap once Guava upgraded to 31.1 and buildKeepingLast() available + @SuppressWarnings("Immutable") + private final Map mapOfRootsToWeights; + + private final UnionType unionType; + + private PersistentParentPointerTreeUnionFind(UnionType pUnionType) { + mapOfNodesToParents = new HashMap<>(); + mapOfRootsToWeights = new HashMap<>(); + unionType = pUnionType; + } + + private PersistentParentPointerTreeUnionFind( + Map pMapOfNodesToParents, Map pMapOfRootsToWeights, UnionType pUnionType) { + mapOfNodesToParents = pMapOfNodesToParents; + mapOfRootsToWeights = pMapOfRootsToWeights; + unionType = pUnionType; + } + + /** + * Returns a fresh, empty Union-Find instance of the given union type. + * + * @param pUnionType specifies whether the union is performed by rank or by size + * @return empty instance + * @param type of elements added to the Union-Find. + */ + public static PersistentUnionFind of(UnionType pUnionType) { + return new PersistentParentPointerTreeUnionFind<>(pUnionType); + } + + /** + * Provides a {@link Collection} containing all current subsets. + * + * @return {@link Collection} containing all current subsets + */ + @Override + public Collection> getAllSubsets() { + + Map> allSubsets = new HashMap<>(); + + for (T current : mapOfNodesToParents.keySet()) { + + T root = find(current); + + if (allSubsets.containsKey(root)) { + allSubsets.get(root).add(current); + } else { + Set set = new HashSet<>(); + set.add(root); + set.add(current); + allSubsets.put(root, set); + } + } + + return allSubsets.values(); + } + + /** + * Checks whether the provided element is contained in any current subset and returns true or + * false accordingly. + * + * @param pE element to be searched for + * @return true if contained, false if not + */ + @Override + public boolean contains(T pE) { + + Preconditions.checkNotNull(pE); + + return mapOfNodesToParents.containsKey(pE); + } + + /** + * Returns the canonical element of the set containing the provided element. + * + * @param pE element for which set is to be found + * @return canonical element of the found set + * @throws IllegalArgumentException if element is not contained in any subset + */ + @Override + public T find(T pE) { + + Preconditions.checkNotNull(pE); + + @Var T currentNode = pE; + @Var T parent = mapOfNodesToParents.get(pE); + + if (parent != null) { + while (!currentNode.equals(parent)) { + currentNode = parent; + parent = mapOfNodesToParents.get(currentNode); + } + + return parent; + } + + throw new IllegalArgumentException("Element not contained."); + } + + /** + * Merges the sets represented by the two input values according to standard Union-Find behaviour. + * This operation does not mutate the existing object, but returns a fresh instance to which the + * changes in question have been applied. + * + *

USES: Add new element as new set: pass it as both pE1 and pE2. Add new element to existing + * set: one input value is the new element, the other the canonical element of the set to be added + * to. Merge two existing sets: pE1, pE2 canonical elements of sets to be merged. + * + * @param pE1 first element + * @param pE2 second element + */ + @CheckReturnValue + @Override + public PersistentUnionFind unionAndCopy(T pE1, T pE2) { + + Preconditions.checkNotNull(pE1); + Preconditions.checkNotNull(pE2); + + if (pE1.equals(pE2)) { + return addElementAsNewSetAndCopy(pE1); + } else { + if (contains(pE1)) { + if (contains(pE2)) { + T canon1 = find(pE1); + T canon2 = find(pE2); + + if (!canon1.equals(canon2)) { + return mergeExistingSetsAndCopy(canon1, canon2); + } + } else { + return addElementToExistingSetAndCopy(pE2, find(pE1)); + } + } else if (contains(pE2)) { + return addElementToExistingSetAndCopy(pE1, find(pE2)); + } else { + return addTwoElementsAsSetAndCopy(pE1, pE2); + } + } + + return this; + } + + private PersistentUnionFind addElementAsNewSetAndCopy(T pE) { + + if (!contains(pE)) { + Map updatedNodesToParents = new HashMap<>(mapOfNodesToParents); + updatedNodesToParents.put(pE, pE); + + Map updatedRootsToWeights = new HashMap<>(mapOfRootsToWeights); + if (unionType == UnionType.UNION_BY_RANK) { + updatedRootsToWeights.put(pE, 0); // rank + } else { + updatedRootsToWeights.put(pE, 1); // size + } + + return new PersistentParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } + + return this; + } + + // only call with elements that are definitely canonical! + private PersistentUnionFind mergeExistingSetsAndCopy(T pCanon1, T pCanon2) { + + Preconditions.checkNotNull(pCanon1); + Preconditions.checkNotNull(pCanon2); + + if (unionType == UnionType.UNION_BY_SIZE) { + return unionBySize(pCanon1, pCanon2); + } else { + return unionByRank(pCanon1, pCanon2); + } + } + + private PersistentUnionFind addElementToExistingSetAndCopy(T pE, T pCanon) { + + Preconditions.checkNotNull(pCanon); + + Map updatedNodesToParents = new HashMap<>(mapOfNodesToParents); + updatedNodesToParents.put(pE, pCanon); + + Map updatedRootsToWeights = new HashMap<>(mapOfRootsToWeights); + if (unionType == UnionType.UNION_BY_RANK) { + @Var int rank = mapOfRootsToWeights.get(pCanon); + + if (rank == 0) { + updatedRootsToWeights.put(pCanon, ++rank); + } + } else { + @Var int size = mapOfRootsToWeights.get(pCanon); + updatedRootsToWeights.put(pCanon, ++size); + } + + return new PersistentParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } + + private PersistentUnionFind addTwoElementsAsSetAndCopy(T pE1, T pE2) { + + Map updatedNodesToParents = new HashMap<>(mapOfNodesToParents); + updatedNodesToParents.put(pE1, pE1); + updatedNodesToParents.put(pE2, pE1); + + Map updatedRootsToWeights = new HashMap<>(mapOfRootsToWeights); + if (unionType == UnionType.UNION_BY_RANK) { + updatedRootsToWeights.put(pE1, 1); // rank + } else { + updatedRootsToWeights.put(pE1, 2); // size + } + + return new PersistentParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } + + // pCanon1 will be new canonical element only if its set is actually bigger, otherwise pCanon2 new + // canon + private PersistentUnionFind unionBySize(T pCanon1, T pCanon2) { + + int size1 = mapOfRootsToWeights.get(pCanon1); + int size2 = mapOfRootsToWeights.get(pCanon2); + + Map updatedNodesToParents = new HashMap<>(mapOfNodesToParents); + Map updatedRootsToWeights = new HashMap<>(mapOfRootsToWeights); + + if (size1 > size2) { + updatedNodesToParents.put(pCanon2, pCanon1); + + updatedRootsToWeights.put(pCanon1, size1 + size2); + } else { + updatedNodesToParents.put(pCanon1, pCanon2); + + updatedRootsToWeights.put(pCanon2, size2 + size1); + } + + return new PersistentParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } + + // pCanon1 will be new canonical element only if its rank is actually greater, otherwise pCanon2 + // new + // canon + private PersistentUnionFind unionByRank(T pCanon1, T pCanon2) { + + int rank1 = mapOfRootsToWeights.get(pCanon1); + @Var int rank2 = mapOfRootsToWeights.get(pCanon2); + + Map updatedNodesToParents = new HashMap<>(mapOfNodesToParents); + Map updatedRootsToWeights = new HashMap<>(mapOfRootsToWeights); + + if (rank1 > rank2) { + updatedNodesToParents.put(pCanon2, pCanon1); + } else { + updatedNodesToParents.put(pCanon1, pCanon2); + + if (rank1 == rank2) { + updatedRootsToWeights.put(pCanon2, ++rank2); + } + } + + return new PersistentParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/PersistentSortedParentPointerTreeUnionFind.java b/src/org/sosy_lab/common/collect/union_find/PersistentSortedParentPointerTreeUnionFind.java new file mode 100644 index 000000000..e22051447 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/PersistentSortedParentPointerTreeUnionFind.java @@ -0,0 +1,309 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.annotations.Var; +import java.util.Collection; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.TreeMap; +import java.util.TreeSet; +import org.sosy_lab.common.collect.PathCopyingPersistentTreeMap; +import org.sosy_lab.common.collect.PersistentSortedMap; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; + +/** + * Implementation of a persistent and sorted union-find. A persistent data structure is immutable, + * but provides cheap copy-and-write operations. Thus, all write operations ({@link + * #union(Comparable, Comparable)}) will not modify the current instance, but return a new instance + * instead. The union can be performed either by size or by rank, determined by a constructor + * parameter. + * + *

All modifying operations inherited from {@link SortedUnionFind} are not supported and will + * always throw {@link UnsupportedOperationException}. + * + * @param The type of elements added to the Union-Find. Must be comparable. + */ +@Immutable(containerOf = "T") +public final class PersistentSortedParentPointerTreeUnionFind> + extends AbstractImmutableSortedUnionFind implements PersistentSortedUnionFind { + + private final PersistentSortedMap mapOfNodesToParents; + private final PersistentSortedMap mapOfRootsToWeights; + private final UnionType unionType; + + private PersistentSortedParentPointerTreeUnionFind(UnionType pUnionType) { + mapOfNodesToParents = PathCopyingPersistentTreeMap.of(); + mapOfRootsToWeights = PathCopyingPersistentTreeMap.of(); + unionType = pUnionType; + } + + private PersistentSortedParentPointerTreeUnionFind( + PersistentSortedMap pMapOfNodesToParents, + PersistentSortedMap pMapOfRootsToWeights, + UnionType pUnionType) { + mapOfNodesToParents = pMapOfNodesToParents; + mapOfRootsToWeights = pMapOfRootsToWeights; + unionType = pUnionType; + } + + /** + * Returns a fresh, empty Union-Find instance of the given union type. + * + * @param pUnionType specifies whether the union is performed by rank or by size + * @return empty instance + * @param type of elements added to the Union-Find. Must be comparable. + */ + public static > PersistentSortedUnionFind of(UnionType pUnionType) { + return new PersistentSortedParentPointerTreeUnionFind<>(pUnionType); + } + + /** + * Provides a {@link Collection} containing all current subsets. The subsets are sorted by their + * canonical elements in ascending order. The contents of each subset are equally sorted in + * ascending order. + * + * @return {@link Collection} containing all current subsets + */ + @Override + public Collection> getAllSubsets() { + + NavigableMap> allSubsets = new TreeMap<>(); + + for (T current : mapOfNodesToParents.keySet()) { + + T root = find(current); + + if (allSubsets.containsKey(root)) { + allSubsets.get(root).add(current); + } else { + NavigableSet set = new TreeSet<>(); + set.add(root); + set.add(current); + allSubsets.put(root, set); + } + } + + return allSubsets.values(); + } + + /** + * Checks whether the provided element is contained in any current subset and returns true or + * false accordingly. + * + * @param pE element to be searched for + * @return true if contained, false if not + */ + @Override + public boolean contains(T pE) { + + Preconditions.checkNotNull(pE); + + return mapOfNodesToParents.containsKey(pE); + } + + /** + * Returns the canonical element of the set containing the provided element. + * + * @param pE element for which set is to be found + * @return canonical element of the found set + * @throws IllegalArgumentException if element is not contained in any subset + */ + @Override + public T find(T pE) { + + Preconditions.checkNotNull(pE); + + @Var T currentNode = pE; + @Var T parent = mapOfNodesToParents.get(pE); + + if (parent != null) { + while (!currentNode.equals(parent)) { + currentNode = parent; + parent = mapOfNodesToParents.get(currentNode); + } + + return parent; + } + + throw new IllegalArgumentException("Element not contained."); + } + + /** + * Merges the sets represented by the two input values according to standard Union-Find behaviour. + * This operation does not mutate the existing object, but returns a fresh instance to which the + * changes in question have been applied. + * + *

USES: Add new element as new set: pass it as both pE1 and pE2. Add new element to existing + * set: one input value is the new element, the other the canonical element of the set to be added + * to. Merge two existing sets: pE1, pE2 canonical elements of sets to be merged. + * + * @param pE1 first element + * @param pE2 second element + */ + @CheckReturnValue + @Override + public PersistentSortedUnionFind unionAndCopy(T pE1, T pE2) { + + Preconditions.checkNotNull(pE1); + Preconditions.checkNotNull(pE2); + + if (pE1.equals(pE2)) { + return addElementAsNewSetAndCopy(pE1); + } else { + if (contains(pE1)) { + if (contains(pE2)) { + T canon1 = find(pE1); + T canon2 = find(pE2); + + if (!canon1.equals(canon2)) { + return mergeExistingSetsAndCopy(canon1, canon2); + } + } else { + return addElementToExistingSetAndCopy(pE2, find(pE1)); + } + } else if (contains(pE2)) { + return addElementToExistingSetAndCopy(pE1, find(pE2)); + } else { + return addTwoElementsAsSetAndCopy(pE1, pE2); + } + } + + return this; + } + + private PersistentSortedUnionFind addElementAsNewSetAndCopy(T pE) { + + if (!contains(pE)) { + PersistentSortedMap updatedNodesToParents = mapOfNodesToParents.putAndCopy(pE, pE); + + PersistentSortedMap updatedRootsToWeights; + if (unionType == UnionType.UNION_BY_RANK) { + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pE, 0); // rank + } else { + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pE, 1); // size + } + + return new PersistentSortedParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } + + return this; + } + + // only call with elements that are definitely canonical! + private PersistentSortedUnionFind mergeExistingSetsAndCopy(T pCanon1, T pCanon2) { + + Preconditions.checkNotNull(pCanon1); + Preconditions.checkNotNull(pCanon2); + + if (unionType == UnionType.UNION_BY_SIZE) { + return unionBySize(pCanon1, pCanon2); + } else { + return unionByRank(pCanon1, pCanon2); + } + } + + private PersistentSortedUnionFind addElementToExistingSetAndCopy(T pE, T pCanon) { + + Preconditions.checkNotNull(pCanon); + + PersistentSortedMap updatedNodesToParents = mapOfNodesToParents.putAndCopy(pE, pCanon); + + PersistentSortedMap updatedRootsToWeights; + if (unionType == UnionType.UNION_BY_RANK) { + @Var int rank = mapOfRootsToWeights.get(pCanon); + + if (rank == 0) { + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pCanon, ++rank); + } else { + updatedRootsToWeights = mapOfRootsToWeights; + } + } else { + @Var int size = mapOfRootsToWeights.get(pCanon); + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pCanon, ++size); + } + + return new PersistentSortedParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } + + private PersistentSortedUnionFind addTwoElementsAsSetAndCopy(T pE1, T pE2) { + + @Var PersistentSortedMap updatedNodesToParents; + updatedNodesToParents = mapOfNodesToParents.putAndCopy(pE1, pE1); + updatedNodesToParents = updatedNodesToParents.putAndCopy(pE2, pE1); + + PersistentSortedMap updatedRootsToWeights; + if (unionType == UnionType.UNION_BY_RANK) { + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pE1, 1); // rank + } else { + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pE1, 2); // size + } + + return new PersistentSortedParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } + + // pCanon1 will be new canonical element only if its set is actually bigger, otherwise pCanon2 new + // canon + private PersistentSortedUnionFind unionBySize(T pCanon1, T pCanon2) { + + int size1 = mapOfRootsToWeights.get(pCanon1); + int size2 = mapOfRootsToWeights.get(pCanon2); + + PersistentSortedMap updatedNodesToParents; + PersistentSortedMap updatedRootsToWeights; + + if (size1 > size2) { + updatedNodesToParents = mapOfNodesToParents.putAndCopy(pCanon2, pCanon1); + + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pCanon1, size1 + size2); + } else { + updatedNodesToParents = mapOfNodesToParents.putAndCopy(pCanon1, pCanon2); + + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pCanon2, size2 + size1); + } + + return new PersistentSortedParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } + + // pCanon1 will be new canonical element only if its rank is actually greater, otherwise pCanon2 + // new + // canon + private PersistentSortedUnionFind unionByRank(T pCanon1, T pCanon2) { + + int rank1 = mapOfRootsToWeights.get(pCanon1); + @Var int rank2 = mapOfRootsToWeights.get(pCanon2); + + PersistentSortedMap updatedNodesToParents; + PersistentSortedMap updatedRootsToWeights; + + if (rank1 > rank2) { + updatedNodesToParents = mapOfNodesToParents.putAndCopy(pCanon2, pCanon1); + + updatedRootsToWeights = mapOfRootsToWeights; + } else { + updatedNodesToParents = mapOfNodesToParents.putAndCopy(pCanon1, pCanon2); + + if (rank1 == rank2) { + updatedRootsToWeights = mapOfRootsToWeights.putAndCopy(pCanon2, ++rank2); + } else { + updatedRootsToWeights = mapOfRootsToWeights; + } + } + + return new PersistentSortedParentPointerTreeUnionFind<>( + updatedNodesToParents, updatedRootsToWeights, unionType); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/PersistentSortedUnionFind.java b/src/org/sosy_lab/common/collect/union_find/PersistentSortedUnionFind.java new file mode 100644 index 000000000..004075b3d --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/PersistentSortedUnionFind.java @@ -0,0 +1,48 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.DoNotCall; +import com.google.errorprone.annotations.Immutable; + +/** + * Interface for a persistent and sorted union-find. A persistent data structure is immutable, but + * provides cheap copy-and-write operations. Thus, all write operations ({@link #union(Comparable, + * Comparable)}) will not modify the current instance, but return a new instance instead. + * + *

All modifying operations inherited from {@link SortedUnionFind} are not supported and will + * always throw {@link UnsupportedOperationException}. + * + * @param The type of values. + */ +@Immutable(containerOf = "T") +public interface PersistentSortedUnionFind> + extends SortedUnionFind, PersistentUnionFind { + + /** + * Replacement for {@link #union(Comparable, Comparable)} that returns a fresh new instance. + * + * @param pE1 first element + * @param pE2 second element + * @return new instance that the desired changes have been applied to + */ + @Override + @CheckReturnValue + PersistentSortedUnionFind unionAndCopy(T pE1, T pE2); + + /** + * @throws UnsupportedOperationException Always. + * @deprecated Unsupported operation. + */ + @Deprecated + @Override + @DoNotCall + void union(T pE1, T pE2); +} diff --git a/src/org/sosy_lab/common/collect/union_find/PersistentUnionFind.java b/src/org/sosy_lab/common/collect/union_find/PersistentUnionFind.java new file mode 100644 index 000000000..4d3d03abb --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/PersistentUnionFind.java @@ -0,0 +1,46 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.DoNotCall; +import com.google.errorprone.annotations.Immutable; + +/** + * Interface for a persistent union-find. A persistent data structure is immutable, but provides + * cheap copy-and-write operations. Thus, all write operations ({@link #union(Object, Object)}) will + * not modify the current instance, but return a new instance instead. + * + *

All modifying operations inherited from {@link UnionFind} are not supported and will always + * throw {@link UnsupportedOperationException}. + * + * @param The type of values. + */ +@Immutable(containerOf = "T") +public interface PersistentUnionFind extends UnionFind { + + /** + * Replacement for {@link #union(Object, Object)} that returns a fresh new instance. + * + * @param pE1 first element + * @param pE2 second element + * @return new instance that the desired changes have been applied to + */ + @CheckReturnValue + PersistentUnionFind unionAndCopy(T pE1, T pE2); + + /** + * @throws UnsupportedOperationException Always. + * @deprecated Unsupported operation. + */ + @Deprecated + @Override + @DoNotCall + void union(T pE1, T pE2); +} diff --git a/src/org/sosy_lab/common/collect/union_find/RootNode.java b/src/org/sosy_lab/common/collect/union_find/RootNode.java new file mode 100644 index 000000000..0860d3944 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/RootNode.java @@ -0,0 +1,66 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +/** + * An implementation of {@link AbstractTreeNode} resulting in nodes that can be used as non-root + * nodes or root nodes. Their primary intended use is as root nodes. Rank describes the maximum + * height of the tree of this node and its child nodes (i.e. without path compression). Size + * describes the total number of elements in the tree represented by this root node. + * + * @param type of elements each node holds as value + */ +public final class RootNode extends AbstractTreeNode { + + private int rank; + private int size; + + /** + * Constructor for a root node. The parent variable points to itself, thus indicating this is a + * root node. If appended to another tree, parent can be reallocated to the new parent node, while + * the current node simply functions as a non-root node from then on. In the beginning, rank is 0 + * and size is 1. + * + * @param pValue element to be stored in the node + */ + public RootNode(T pValue) { + + super(pValue); + + this.rank = 0; + this.size = 1; + } + + public int getRank() { + return rank; + } + + public int getSize() { + return size; + } + + /** Increments rank by one. */ + public void incrementRankByOne() { + rank++; + } + + /** Increments size by one. */ + public void incrementSizeByOne() { + size++; + } + + /** + * Increments size by pN. + * + * @param pN number by which size is to be increased. + */ + public void incrementSizeBy(int pN) { + size += pN; + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/SortedParentPointerTreeUnionFind.java b/src/org/sosy_lab/common/collect/union_find/SortedParentPointerTreeUnionFind.java new file mode 100644 index 000000000..d2097dd93 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/SortedParentPointerTreeUnionFind.java @@ -0,0 +1,69 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import java.util.Collection; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * A sorted implementation of {@link UnionFind} using a {@link Map} of {@link AbstractTreeNode}s. In + * order to represent subsets by canonical elements, each one is mapped to its representative + * canonical element. This is always the first element added to the subset, unless it has changed + * due to union operations. Each subset is stored as a parent pointer tree comprised of {@link + * NonRootNode}s with exactly one {@link RootNode} as the root. The union can be performed either by + * size or by rank, determined by a constructor parameter. + * + * @param type of elements added to the Union-Find. Must be comparable. + */ +public class SortedParentPointerTreeUnionFind> + extends ParentPointerTreeUnionFind implements SortedUnionFind { + + /** + * Creates an empty instance. + * + * @param pUnionType type of union to be performed for all unions on this instance + */ + public SortedParentPointerTreeUnionFind(UnionType pUnionType) { + super(pUnionType); + } + + /** + * Provides a {@link Collection} containing all current subsets. It contains the subsets sorted by + * their canonical elements in ascending order. The contents of the subsets are also sorted in + * ascending order. + * + * @return sorted {@link Collection} containing all current subsets + */ + // subsets are in order of their canonical elements; elements in subsets are sorted as well + @Override + public Collection> getAllSubsets() { + + NavigableMap> allSubsets = new TreeMap<>(); + + for (AbstractTreeNode node : allNodes.values()) { + + T canon = find(node.getValue()); + + if (allSubsets.containsKey(canon)) { + allSubsets.get(canon).add(node.getValue()); + } else { + NavigableSet set = new TreeSet<>(); + set.add(canon); + set.add(node.getValue()); + allSubsets.put(canon, set); + } + } + + return allSubsets.values(); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/SortedTreeSetUnionFind.java b/src/org/sosy_lab/common/collect/union_find/SortedTreeSetUnionFind.java new file mode 100644 index 000000000..869422969 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/SortedTreeSetUnionFind.java @@ -0,0 +1,44 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import java.util.HashMap; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * An implementation of {@link SortedUnionFind} using a {@link HashMap} of {@link TreeSet}s. In + * order to represent subsets by canonical elements, each one is mapped to its representative + * canonical element. This is always the first element added to the subset, unless it has changed + * due to union operations. The union is implemented as union by size. + * + * @param type of elements added to the Union-Find. Must be {@link Comparable} to ensure correct + * ordering. + */ +public class SortedTreeSetUnionFind> + extends AbstractGenericUnionFind, NavigableMap>> + implements SortedUnionFind { + + /** Generates an empty {@link SortedTreeSetUnionFind}. */ + public SortedTreeSetUnionFind() {} + + @Override + protected Set getEmptySet() { + return new TreeSet<>(); + } + + @Override + protected Map> getEmptyMap() { + return new TreeMap<>(); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/SortedUnionFind.java b/src/org/sosy_lab/common/collect/union_find/SortedUnionFind.java new file mode 100644 index 000000000..66f2f9da0 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/SortedUnionFind.java @@ -0,0 +1,31 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import java.util.Collection; +import java.util.NavigableSet; +import java.util.Set; + +/** + * Interface for a sorted Union-Find or Disjoint-Set data structure. Uses a {@link Collection} of + * {@link Set}s. + * + * @param type of elements added to the Union-Find. Must be {@link Comparable} to ensure correct + * ordering. + */ +public interface SortedUnionFind> extends UnionFind { + + /** + * Provides a {@link Collection} containing all current subsets. + * + * @return {@link Collection} containing all current subsets + */ + @Override + Collection> getAllSubsets(); +} diff --git a/src/org/sosy_lab/common/collect/union_find/UnionFind.java b/src/org/sosy_lab/common/collect/union_find/UnionFind.java new file mode 100644 index 000000000..03cfb99a7 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/UnionFind.java @@ -0,0 +1,52 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find; + +import java.util.Collection; +import java.util.Set; + +/** + * Interface for a sorted Union-Find or Disjoint-Set data structure. Uses a {@link Collection} of + * {@link Set}s. + * + * @param type of elements added to the Union-Find. + */ +public interface UnionFind { + /** + * Returns the canonical element of the set containing the provided element. + * + * @param pE element for which set is to be found + * @return canonical element of the found set + */ + T find(T pE); + + /** + * Merges the sets represented by the two input values according to standard Union-Find behaviour. + * + * @param pE1 first element + * @param pE2 second element + */ + void union(T pE1, T pE2); + + /** + * Provides a {@link Collection} containing all current subsets. + * + * @return {@link Collection} containing all current subsets + */ + Collection> getAllSubsets(); + + /** + * Checks whether the provided element is contained in any current subset and returns true or + * false accordingly. + * + * @param pE element to be searched for + * @return true if contained, false if not + */ + boolean contains(T pE); +} diff --git a/src/org/sosy_lab/common/collect/union_find/benchmarking/Algs4DatasetBenchmark.java b/src/org/sosy_lab/common/collect/union_find/benchmarking/Algs4DatasetBenchmark.java new file mode 100644 index 000000000..b13b28ece --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/benchmarking/Algs4DatasetBenchmark.java @@ -0,0 +1,191 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.benchmarking; + +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Var; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.sosy_lab.common.collect.union_find.AbstractImmutableParentPointerTreeBuilder; +import org.sosy_lab.common.collect.union_find.AbstractImmutableUnionFind; +import org.sosy_lab.common.collect.union_find.ImmutableParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ImmutableSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; +import org.sosy_lab.common.collect.union_find.PersistentParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentUnionFind; +import org.sosy_lab.common.collect.union_find.SortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.SortedTreeSetUnionFind; +import org.sosy_lab.common.collect.union_find.UnionFind; + +public final class Algs4DatasetBenchmark { + + static final Pattern PATTERN = Pattern.compile("\\s+"); + + public static void main(String[] args) { + + @Var boolean naive = false; + @Var boolean immutable = false; + @Var boolean persistent = false; + @Var boolean sorted = false; + @Var boolean unionByRank = false; + @Var + @Nullable Path filePath = null; + List unionInput = new ArrayList<>(); + + if (args.length == 0) { + System.exit(1); + } + + for (String string : args) { + + switch (string) { + case "-naive" -> naive = true; + + case "-immutable" -> immutable = true; + + case "-persistent" -> persistent = true; + + case "-sorted" -> sorted = true; + + case "-rank" -> unionByRank = true; + + default -> { + filePath = Path.of(string); + } + } + } + + try { + Preconditions.checkNotNull(filePath); + + for (String line : Files.readAllLines(filePath)) { + + String trimmedLine = line.trim(); + + if (trimmedLine.isEmpty()) { + continue; + } + + List tokens = Splitter.on(PATTERN).splitToList(trimmedLine); + + unionInput.add(Integer.parseInt(tokens.get(0))); + unionInput.add(Integer.parseInt(tokens.get(1))); + } + } catch (IOException e) { + System.exit(1); + } + + Iterator iterator = unionInput.iterator(); + + if (naive) { + mutable(new SortedTreeSetUnionFind<>(), iterator); + } else if (!immutable && !persistent && !sorted) { + if (unionByRank) { + mutable(new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK), iterator); + } else { + mutable(new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE), iterator); + } + } else if (!immutable && !persistent && sorted) { + if (unionByRank) { + mutable(new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK), iterator); + } else { + mutable(new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE), iterator); + } + } else if (immutable && !sorted) { + if (unionByRank) { + immutable( + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_RANK), + iterator); + } else { + immutable( + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE), + iterator); + } + } else if (immutable && sorted) { + if (unionByRank) { + immutable( + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_RANK), + iterator); + } else { + immutable( + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE), + iterator); + } + } else if (persistent && !sorted) { + if (unionByRank) { + persistent(PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_RANK), iterator); + } else { + persistent(PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE), iterator); + } + } else if (persistent && sorted) { + if (unionByRank) { + persistent( + PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_RANK), iterator); + } else { + persistent( + PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE), iterator); + } + } else { + System.exit(1); + } + System.exit(0); + } + + private static void mutable(UnionFind pUnionFind, Iterator pIterator) { + + while (pIterator.hasNext()) { + + int a = pIterator.next(); + int b = pIterator.next(); + + pUnionFind.union(a, b); + } + } + + @CanIgnoreReturnValue + private static AbstractImmutableUnionFind immutable( + AbstractImmutableParentPointerTreeBuilder pBuilder, Iterator pIterator) { + + while (pIterator.hasNext()) { + + int a = pIterator.next(); + int b = pIterator.next(); + + pBuilder.union(a, b); + } + + return pBuilder.build(); + } + + private static void persistent( + PersistentUnionFind pUnionFind, Iterator pIterator) { + + @Var PersistentUnionFind unionFind = pUnionFind; + + while (pIterator.hasNext()) { + + int a = pIterator.next(); + int b = pIterator.next(); + + unionFind = unionFind.unionAndCopy(a, b); + } + } + + private Algs4DatasetBenchmark() {} +} diff --git a/src/org/sosy_lab/common/collect/union_find/benchmarking/FindNewToOldSingleSetBenchmark.java b/src/org/sosy_lab/common/collect/union_find/benchmarking/FindNewToOldSingleSetBenchmark.java new file mode 100644 index 000000000..40ca67586 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/benchmarking/FindNewToOldSingleSetBenchmark.java @@ -0,0 +1,164 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.benchmarking; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Var; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sosy_lab.common.collect.union_find.AbstractImmutableParentPointerTreeBuilder; +import org.sosy_lab.common.collect.union_find.ImmutableParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ImmutableSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; +import org.sosy_lab.common.collect.union_find.PersistentParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentUnionFind; +import org.sosy_lab.common.collect.union_find.SortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.SortedTreeSetUnionFind; +import org.sosy_lab.common.collect.union_find.UnionFind; + +public final class FindNewToOldSingleSetBenchmark { + + static final Pattern PATTERN = Pattern.compile("n_(\\d+)\\.txt$"); + + public static void main(String[] args) { + + @Var boolean naive = false; + @Var boolean immutable = false; + @Var boolean persistent = false; + @Var boolean sorted = false; + @Var boolean unionByRank = false; + @Var int n = 0; + + if (args.length == 0) { + System.exit(1); + } + + for (String string : args) { + + switch (string) { + case "-naive" -> naive = true; + + case "-immutable" -> immutable = true; + + case "-persistent" -> persistent = true; + + case "-sorted" -> sorted = true; + + case "-rank" -> unionByRank = true; + + default -> { + Matcher matcher = PATTERN.matcher(string); + + if (matcher.find()) { + n = Integer.parseInt(matcher.group(1)); + } else { + throw new IllegalArgumentException("Incompatible args"); + } + } + } + } + + if (naive) { + mutable(new SortedTreeSetUnionFind<>(), n); + } else if (!immutable && !persistent && !sorted) { + if (unionByRank) { + mutable(new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK), n); + } else { + mutable(new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE), n); + } + } else if (!immutable && !persistent && sorted) { + if (unionByRank) { + mutable(new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK), n); + } else { + mutable(new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE), n); + } + } else if (immutable && !sorted) { + if (unionByRank) { + immutable( + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_RANK), n); + } else { + immutable( + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE), n); + } + } else if (immutable && sorted) { + if (unionByRank) { + immutable( + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_RANK), + n); + } else { + immutable( + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE), + n); + } + } else if (persistent && !sorted) { + if (unionByRank) { + persistent(PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_RANK), n); + } else { + persistent(PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE), n); + } + } else if (persistent && sorted) { + if (unionByRank) { + persistent(PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_RANK), n); + } else { + persistent(PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE), n); + } + } else { + System.exit(1); + } + + System.exit(0); + } + + private static void mutable(UnionFind pUnionFind, int pN) { + + for (int i = 0; i < pN; i++) { + pUnionFind.union(0, i); + } + + performFinds(pUnionFind, pN); + } + + private static void immutable( + AbstractImmutableParentPointerTreeBuilder pBuilder, int pN) { + + for (int i = 0; i < pN; i++) { + pBuilder.union(0, i); + } + + performFinds(pBuilder.build(), pN); + } + + private static void persistent(PersistentUnionFind pUnionFind, int pN) { + + @Var PersistentUnionFind unionFind = pUnionFind; + + for (int i = 0; i < pN; i++) { + unionFind = unionFind.unionAndCopy(0, i); + } + + performFinds(unionFind, pN); + } + + @CanIgnoreReturnValue + private static int performFinds(UnionFind pUnionFind, int pN) { + + @Var int root = 0; + + for (int i = pN; i >= 0; --i) { + + root = pUnionFind.find(i); + } + + return root; + } + + private FindNewToOldSingleSetBenchmark() {} +} diff --git a/src/org/sosy_lab/common/collect/union_find/benchmarking/FindOldToNewSingleSetBenchmark.java b/src/org/sosy_lab/common/collect/union_find/benchmarking/FindOldToNewSingleSetBenchmark.java new file mode 100644 index 000000000..6a6ce3949 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/benchmarking/FindOldToNewSingleSetBenchmark.java @@ -0,0 +1,164 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.benchmarking; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Var; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sosy_lab.common.collect.union_find.AbstractImmutableParentPointerTreeBuilder; +import org.sosy_lab.common.collect.union_find.ImmutableParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ImmutableSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; +import org.sosy_lab.common.collect.union_find.PersistentParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentUnionFind; +import org.sosy_lab.common.collect.union_find.SortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.SortedTreeSetUnionFind; +import org.sosy_lab.common.collect.union_find.UnionFind; + +public final class FindOldToNewSingleSetBenchmark { + + static final Pattern PATTERN = Pattern.compile("n_(\\d+)\\.txt$"); + + public static void main(String[] args) { + + @Var boolean naive = false; + @Var boolean immutable = false; + @Var boolean persistent = false; + @Var boolean sorted = false; + @Var boolean unionByRank = false; + @Var int n = 0; + + if (args.length == 0) { + System.exit(1); + } + + for (String string : args) { + + switch (string) { + case "-naive" -> naive = true; + + case "-immutable" -> immutable = true; + + case "-persistent" -> persistent = true; + + case "-sorted" -> sorted = true; + + case "-rank" -> unionByRank = true; + + default -> { + Matcher matcher = PATTERN.matcher(string); + + if (matcher.find()) { + n = Integer.parseInt(matcher.group(1)); + } else { + throw new IllegalArgumentException("Incompatible args"); + } + } + } + } + + if (naive) { + mutable(new SortedTreeSetUnionFind<>(), n); + } else if (!immutable && !persistent && !sorted) { + if (unionByRank) { + mutable(new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK), n); + } else { + mutable(new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE), n); + } + } else if (!immutable && !persistent && sorted) { + if (unionByRank) { + mutable(new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK), n); + } else { + mutable(new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE), n); + } + } else if (immutable && !sorted) { + if (unionByRank) { + immutable( + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_RANK), n); + } else { + immutable( + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE), n); + } + } else if (immutable && sorted) { + if (unionByRank) { + immutable( + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_RANK), + n); + } else { + immutable( + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE), + n); + } + } else if (persistent && !sorted) { + if (unionByRank) { + persistent(PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_RANK), n); + } else { + persistent(PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE), n); + } + } else if (persistent && sorted) { + if (unionByRank) { + persistent(PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_RANK), n); + } else { + persistent(PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE), n); + } + } else { + System.exit(1); + } + + System.exit(0); + } + + private static void mutable(UnionFind pUnionFind, int pN) { + + for (int i = 0; i < pN; i++) { + pUnionFind.union(0, i); + } + + performFinds(pUnionFind, pN); + } + + private static void immutable( + AbstractImmutableParentPointerTreeBuilder pBuilder, int pN) { + + for (int i = 0; i < pN; i++) { + pBuilder.union(0, i); + } + + performFinds(pBuilder.build(), pN); + } + + private static void persistent(PersistentUnionFind pUnionFind, int pN) { + + @Var PersistentUnionFind unionFind = pUnionFind; + + for (int i = 0; i < pN; i++) { + unionFind = unionFind.unionAndCopy(0, i); + } + + performFinds(unionFind, pN); + } + + @CanIgnoreReturnValue + private static int performFinds(UnionFind pUnionFind, int pN) { + + @Var int root = 0; + + for (int i = 0; i < pN; i++) { + + root = pUnionFind.find(i); + } + + return root; + } + + private FindOldToNewSingleSetBenchmark() {} +} diff --git a/src/org/sosy_lab/common/collect/union_find/benchmarking/UnionSingleElementsIntoExistingSetBenchmark.java b/src/org/sosy_lab/common/collect/union_find/benchmarking/UnionSingleElementsIntoExistingSetBenchmark.java new file mode 100644 index 000000000..322a1ffc6 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/benchmarking/UnionSingleElementsIntoExistingSetBenchmark.java @@ -0,0 +1,148 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.benchmarking; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.Var; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sosy_lab.common.collect.union_find.AbstractImmutableParentPointerTreeBuilder; +import org.sosy_lab.common.collect.union_find.AbstractImmutableUnionFind; +import org.sosy_lab.common.collect.union_find.ImmutableParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ImmutableSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; +import org.sosy_lab.common.collect.union_find.PersistentParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentUnionFind; +import org.sosy_lab.common.collect.union_find.SortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.SortedTreeSetUnionFind; +import org.sosy_lab.common.collect.union_find.UnionFind; + +public final class UnionSingleElementsIntoExistingSetBenchmark { + + static final Pattern PATTERN = Pattern.compile("n_(\\d+)\\.txt$"); + + public static void main(String[] args) { + + @Var boolean naive = false; + @Var boolean immutable = false; + @Var boolean persistent = false; + @Var boolean sorted = false; + @Var boolean unionByRank = false; + @Var int n = 0; + + if (args.length == 0) { + System.exit(1); + } + + for (String string : args) { + + switch (string) { + case "-naive" -> naive = true; + + case "-immutable" -> immutable = true; + + case "-persistent" -> persistent = true; + + case "-sorted" -> sorted = true; + + case "-rank" -> unionByRank = true; + + default -> { + Matcher matcher = PATTERN.matcher(string); + + if (matcher.find()) { + n = Integer.parseInt(matcher.group(1)); + } else { + throw new IllegalArgumentException("Incompatible args"); + } + } + } + } + + if (naive) { + mutable(new SortedTreeSetUnionFind<>(), n); + } else if (!immutable && !persistent && !sorted) { + if (unionByRank) { + mutable(new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK), n); + } else { + mutable(new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE), n); + } + } else if (!immutable && !persistent && sorted) { + if (unionByRank) { + mutable(new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK), n); + } else { + mutable(new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE), n); + } + } else if (immutable && !sorted) { + if (unionByRank) { + immutable( + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_RANK), n); + } else { + immutable( + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE), n); + } + } else if (immutable && sorted) { + if (unionByRank) { + immutable( + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_RANK), + n); + } else { + immutable( + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE), + n); + } + } else if (persistent && !sorted) { + if (unionByRank) { + persistent(PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_RANK), n); + } else { + persistent(PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE), n); + } + } else if (persistent && sorted) { + if (unionByRank) { + persistent(PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_RANK), n); + } else { + persistent(PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE), n); + } + } else { + System.exit(1); + } + System.exit(0); + } + + private static void mutable(UnionFind pUnionFind, int pN) { + + for (int i = 0; i < pN; i++) { + pUnionFind.union(0, i); + } + } + + @CanIgnoreReturnValue + private static AbstractImmutableUnionFind immutable( + AbstractImmutableParentPointerTreeBuilder pBuilder, int pN) { + + for (int i = 0; i < pN; i++) { + pBuilder.union(0, i); + } + + return pBuilder.build(); + } + + private static void persistent(PersistentUnionFind pUnionFind, int pN) { + + @Var PersistentUnionFind unionFind = pUnionFind; + + for (int i = 0; i < pN; i++) { + unionFind = unionFind.unionAndCopy(0, i); + } + } + + private UnionSingleElementsIntoExistingSetBenchmark() {} +} diff --git a/src/org/sosy_lab/common/collect/union_find/benchmarking/package-info.java b/src/org/sosy_lab/common/collect/union_find/benchmarking/package-info.java new file mode 100644 index 000000000..acce67737 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/benchmarking/package-info.java @@ -0,0 +1,14 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +/** This package contains all benchmarking classes related to union-find. */ +@com.google.errorprone.annotations.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +@org.sosy_lab.common.annotations.ReturnValuesAreNonnullByDefault +@org.sosy_lab.common.annotations.FieldsAreNonnullByDefault +package org.sosy_lab.common.collect.union_find.benchmarking; diff --git a/src/org/sosy_lab/common/collect/union_find/package-info.java b/src/org/sosy_lab/common/collect/union_find/package-info.java new file mode 100644 index 000000000..0061c6a88 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/package-info.java @@ -0,0 +1,14 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +/** This package contains all interfaces and classes related to union-find. */ +@com.google.errorprone.annotations.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +@org.sosy_lab.common.annotations.ReturnValuesAreNonnullByDefault +@org.sosy_lab.common.annotations.FieldsAreNonnullByDefault +package org.sosy_lab.common.collect.union_find; diff --git a/src/org/sosy_lab/common/collect/union_find/tests/ImmutableParentPointerTreeUnionFindSortednessTest.java b/src/org/sosy_lab/common/collect/union_find/tests/ImmutableParentPointerTreeUnionFindSortednessTest.java new file mode 100644 index 000000000..3eefe2a2a --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/ImmutableParentPointerTreeUnionFindSortednessTest.java @@ -0,0 +1,193 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NavigableSet; +import org.junit.Test; +import org.sosy_lab.common.collect.union_find.ImmutableSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; + +public class ImmutableParentPointerTreeUnionFindSortednessTest { + + private static ImmutableSortedParentPointerTreeUnionFind buildImmutableSorted( + UnionType pUnionType, int[]... pUnions) { + + ImmutableSortedParentPointerTreeUnionFind.Builder builder = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(pUnionType); + + for (int[] pair : pUnions) { + builder.union(pair[0], pair[1]); + } + + return builder.build(); + } + + private static > NavigableSet onlySubsetOf( + ImmutableSortedParentPointerTreeUnionFind pSortedUnionFind) { + + Collection> subsets = pSortedUnionFind.getAllSubsets(); + + assertThat(subsets).hasSize(1); + + return subsets.iterator().next(); + } + + @Test + public void testGetAllSubsets_elementsAddedInAscendingOrder_remainSorted() { + + ImmutableSortedParentPointerTreeUnionFind sortedUnionFind = + buildImmutableSorted( + UnionType.UNION_BY_SIZE, new int[] {0, 1}, new int[] {0, 2}, new int[] {0, 3}); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3).inOrder(); + } + + @Test + public void testGetAllSubsets_elementsAddedInDescendingOrder_areSortedAscending() { + + ImmutableSortedParentPointerTreeUnionFind sortedUnionFind = + buildImmutableSorted( + UnionType.UNION_BY_SIZE, new int[] {3, 2}, new int[] {3, 1}, new int[] {3, 0}); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3).inOrder(); + } + + @Test + public void testGetAllSubsets_nonLinearInsertionOrder_areReturnedSorted() { + + ImmutableSortedParentPointerTreeUnionFind sortedUnionFind = + buildImmutableSorted( + UnionType.UNION_BY_SIZE, + new int[] {3, 2}, + new int[] {3, 4}, + new int[] {3, 0}, + new int[] {3, 5}, + new int[] {3, 1}); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3, 4, 5).inOrder(); + } + + @Test + public void testGetAllSubsets_multipleSubsets_eachSortedIndependently() { + + ImmutableSortedParentPointerTreeUnionFind sortedUnionFind = + buildImmutableSorted( + UnionType.UNION_BY_SIZE, + new int[] {0, 1}, + new int[] {0, 2}, + new int[] {10, 11}, + new int[] {10, 12}); + + for (NavigableSet subset : sortedUnionFind.getAllSubsets()) { + + assertThat(subset).isInOrder(); + } + } + + @Test + public void testGetAllSubsets_afterMergingTwoSubsets_resultIsSorted() { + + ImmutableSortedParentPointerTreeUnionFind sortedUnionFind = + buildImmutableSorted( + UnionType.UNION_BY_SIZE, + new int[] {0, 1}, + new int[] {0, 2}, + new int[] {3, 4}, + new int[] {3, 5}, + new int[] {0, 3}); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3, 4, 5).inOrder(); + } + + @Test + public void testGetAllSubsets_stringElements_areSortedAlphabetically() { + + ImmutableSortedParentPointerTreeUnionFind.Builder builder = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + String[] expected = {"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + + for (int i = 0; i <= 2; i++) { + builder.union("0", Integer.toString(i)); + } + for (int i = 3; i <= 5; i++) { + builder.union("3", Integer.toString(i)); + } + for (int i = 6; i <= 8; i++) { + builder.union("6", Integer.toString(i)); + } + + builder.union("9", "9"); + builder.union("0", "6"); + builder.union("6", "-1"); + builder.union("1", "4"); + builder.union("0", "9"); + + ImmutableSortedParentPointerTreeUnionFind stringSortedUnionFind = builder.build(); + + assertThat(onlySubsetOf(stringSortedUnionFind)).containsExactlyElementsIn(expected).inOrder(); + } + + @Test + public void testGetAllSubsets_isSortedRegardlessOfUnionType() { + + int[][] unions = {{3, 2}, {3, 4}, {3, 0}, {3, 5}, {3, 1}}; + + for (UnionType unionType : UnionType.values()) { + + ImmutableSortedParentPointerTreeUnionFind sortedUnionFind = + buildImmutableSorted(unionType, unions); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3, 4, 5).inOrder(); + } + } + + @Test + public void testGetAllSubsets_subsetsThemselvesAreOrderedByCanonicalElement() { + + ImmutableSortedParentPointerTreeUnionFind sortedUnionFind = + buildImmutableSorted( + UnionType.UNION_BY_SIZE, + new int[] {10, 11}, + new int[] {10, 12}, + new int[] {0, 1}, + new int[] {0, 2}); + + Collection> subsets = sortedUnionFind.getAllSubsets(); + assertThat(subsets).hasSize(2); + + Iterator> iterator = subsets.iterator(); + NavigableSet firstSubset = iterator.next(); + NavigableSet secondSubset = iterator.next(); + + assertThat(firstSubset.last()).isLessThan(secondSubset.first()); + } + + @Test + public void testGetAllSubsets_returnedSetSupportsNavigableSetOperations() { + + ImmutableSortedParentPointerTreeUnionFind sortedUnionFind = + buildImmutableSorted( + UnionType.UNION_BY_SIZE, + new int[] {0, 1}, + new int[] {0, 2}, + new int[] {0, 3}, + new int[] {0, 4}); + + NavigableSet subset = onlySubsetOf(sortedUnionFind); + + assertThat(subset.first()).isEqualTo(0); + assertThat(subset.last()).isEqualTo(4); + assertThat(subset.higher(1)).isEqualTo(2); + assertThat(subset.lower(3)).isEqualTo(2); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/ImmutableParentPointerTreeUnionFindTest.java b/src/org/sosy_lab/common/collect/union_find/tests/ImmutableParentPointerTreeUnionFindTest.java new file mode 100644 index 000000000..0f78d975f --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/ImmutableParentPointerTreeUnionFindTest.java @@ -0,0 +1,399 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import java.util.Collection; +import java.util.Set; +import org.junit.Test; +import org.sosy_lab.common.collect.union_find.AbstractImmutableParentPointerTreeBuilder; +import org.sosy_lab.common.collect.union_find.AbstractImmutableUnionFind; +import org.sosy_lab.common.collect.union_find.ImmutableParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ImmutableSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; +import org.sosy_lab.common.collect.union_find.UnionFind; + +public class ImmutableParentPointerTreeUnionFindTest { + + private final int[] simpleUnionArgs = new int[] {0, 1}; + + private static AbstractImmutableUnionFind buildImmutable( + UnionType pUnionType, int[]... pUnions) { + + AbstractImmutableParentPointerTreeBuilder builder = + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(pUnionType); + + for (int[] pair : pUnions) { + builder.union(pair[0], pair[1]); + } + + return builder.build(); + } + + private static ImmutableSortedParentPointerTreeUnionFind buildImmutableSorted( + UnionType pUnionType, int[]... pUnions) { + + ImmutableSortedParentPointerTreeUnionFind.Builder builder = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(pUnionType); + + for (int[] pair : pUnions) { + builder.union(pair[0], pair[1]); + } + + return builder.build(); + } + + private static ImmutableList> buildUnsortedAndSorted( + UnionType pUnionType, int[]... pUnions) { + + return ImmutableList.of( + buildImmutable(pUnionType, pUnions), buildImmutableSorted(pUnionType, pUnions)); + } + + @Test + public void testFind_afterSelfUnion_returnsItself() { + + int[] unionArgs = new int[] {0, 0}; + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, unionArgs)) { + + assertThat(unionFind.find(0)).isEqualTo(0); + } + } + + @Test + public void testFind_null_throwsNullPointerException() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThrows(NullPointerException.class, () -> unionFind.find(null)); + } + } + + @Test + public void testFind_elementNotContained_throwsIllegalArgumentException() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThrows(IllegalArgumentException.class, () -> unionFind.find(10)); + } + } + + @Test + public void testUnion_twoNewElements_producesSingleSubsetOfSizeTwo() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThat(unionFind.find(0)).isEqualTo(unionFind.find(1)); + assertThat(unionFind.getAllSubsets()).hasSize(1); + } + } + + @Test + public void testUnion_disjointPairs_produceDistinctSubsets() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, new int[] {0, 1}, new int[] {2, 3})) { + + assertThat(unionFind.find(0)).isNotEqualTo(unionFind.find(2)); + assertThat(unionFind.getAllSubsets()).hasSize(2); + } + } + + @Test + public void testUnion_severalElementsToSameSubset() { + + for (UnionFind unionFind : + buildUnsortedAndSorted( + UnionType.UNION_BY_SIZE, new int[] {0, 1}, new int[] {1, 2}, new int[] {2, 3})) { + + int canon = unionFind.find(0); + + assertThat(unionFind.find(1)).isEqualTo(canon); + assertThat(unionFind.find(2)).isEqualTo(canon); + assertThat(unionFind.find(3)).isEqualTo(canon); + assertThat(unionFind.getAllSubsets()).hasSize(1); + } + } + + @Test + public void testUnion_duplicateUnionCall_doesNotLeadToDuplicates() { + + for (UnionFind unionFind : + buildUnsortedAndSorted( + UnionType.UNION_BY_SIZE, new int[] {0, 1}, new int[] {0, 1}, new int[] {1, 0})) { + + assertThat(unionFind.find(0)).isEqualTo(unionFind.find(1)); + assertThat(unionFind.getAllSubsets()).hasSize(1); + assertThat(unionFind.getAllSubsets().iterator().next()).hasSize(2); + } + } + + @Test + public void testUnion_mergesTwoExistingMultiElementSubsets() { + + for (UnionFind unionFind : + buildUnsortedAndSorted( + UnionType.UNION_BY_SIZE, + new int[] {0, 1}, + new int[] {0, 2}, + new int[] {3, 4}, + new int[] {3, 5}, + new int[] {0, 3})) { + + assertThat(unionFind.getAllSubsets()).hasSize(1); + + int canon = unionFind.find(0); + + for (int i = 0; i <= 5; i++) { + assertThat(unionFind.find(i)).isEqualTo(canon); + } + } + } + + @Test + public void testUnion_constantCanonicalElementDuringNonLinearInsertion() { + + for (UnionFind unionFind : + buildUnsortedAndSorted( + UnionType.UNION_BY_SIZE, + new int[] {3, 3}, + new int[] {3, 2}, + new int[] {3, 5}, + new int[] {3, 1}, + new int[] {3, 8}, + new int[] {3, 6}, + new int[] {3, 9}, + new int[] {3, 7}, + new int[] {3, 4}, + new int[] {3, 0})) { + + assertThat(unionFind.getAllSubsets()).hasSize(1); + + int canon = unionFind.find(3); + + for (int i = 0; i <= 9; i++) { + assertThat(unionFind.find(i)).isEqualTo(canon); + } + } + } + + @Test + public void testUnion_bothUnionTypes_produceSameGrouping() { + + int[][] unions = {{0, 1}, {0, 2}, {3, 4}, {3, 5}, {0, 3}}; + + for (UnionType unionType : UnionType.values()) { + for (UnionFind unionFind : buildUnsortedAndSorted(unionType, unions)) { + + assertThat(unionFind.getAllSubsets()).hasSize(1); + + int canon = unionFind.find(0); + + for (int i = 0; i <= 5; i++) { + assertThat(unionFind.find(i)).isEqualTo(canon); + } + } + } + } + + @Test + public void testUnion_stringElements() { + + AbstractImmutableParentPointerTreeBuilder unsortedBuilder = + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + AbstractImmutableParentPointerTreeBuilder sortedBuilder = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + + AbstractImmutableUnionFind unsortedStringUnionFind = + unsortedBuilder.union("0", "1").union("0", "2").union("3", "4").build(); + AbstractImmutableUnionFind sortedStringUnionFind = + sortedBuilder.union("0", "1").union("0", "2").union("3", "4").build(); + + assertThat(unsortedStringUnionFind.find("0")).isEqualTo(unsortedStringUnionFind.find("2")); + assertThat(unsortedStringUnionFind.find("0")).isNotEqualTo(unsortedStringUnionFind.find("3")); + assertThat(unsortedStringUnionFind.getAllSubsets()).hasSize(2); + + assertThat(sortedStringUnionFind.find("0")).isEqualTo(sortedStringUnionFind.find("2")); + assertThat(sortedStringUnionFind.find("0")).isNotEqualTo(sortedStringUnionFind.find("3")); + assertThat(sortedStringUnionFind.getAllSubsets()).hasSize(2); + } + + @Test + public void testGetAllSubsets_reflectsCorrectMembershipAfterMultipleUnions() { + + int[][] unions = { + {0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {5, 5}, {5, 6}, {5, 7}, {5, 8}, {5, 9}, + }; + + for (UnionFind unionFind : buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, unions)) { + + Collection> subsets = unionFind.getAllSubsets(); + + assertThat(subsets).hasSize(2); + + for (Set subset : subsets) { + assertThat(subset).hasSize(5); + } + } + } + + @Test + public void testGetAllSubsets_emptyUnionFind_isEmpty() { + + for (UnionFind unionFind : buildUnsortedAndSorted(UnionType.UNION_BY_SIZE)) { + + assertThat(unionFind.getAllSubsets()).isEmpty(); + } + } + + @Test + public void testContains_elementInSubset_returnsTrue() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThat(unionFind.contains(0)).isTrue(); + assertThat(unionFind.contains(1)).isTrue(); + } + } + + @Test + public void testContains_elementNotContained_returnsFalse() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThat(unionFind.contains(10)).isFalse(); + } + } + + @Test + public void testContains_null_returnsFalse() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThat(unionFind.contains(null)).isFalse(); + } + } + + @Test + public void testBuilder_union_nullElement_throwsNullPointerException() { + + AbstractImmutableParentPointerTreeBuilder unsortedBuilder = + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + AbstractImmutableParentPointerTreeBuilder sortedBuilder = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + + assertThrows(NullPointerException.class, () -> unsortedBuilder.union(null, 1)); + assertThrows(NullPointerException.class, () -> sortedBuilder.union(null, 1)); + } + + @Test + public void testBuilder_union_returnsSameBuilderInstance() { + + AbstractImmutableParentPointerTreeBuilder unsortedBuilder = + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + AbstractImmutableParentPointerTreeBuilder sortedBuilder = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + + assertThat(unsortedBuilder.union(0, 1)).isSameInstanceAs(unsortedBuilder); + assertThat(sortedBuilder.union(0, 1)).isSameInstanceAs(sortedBuilder); + } + + @Test + public void testBuilder_getBuilder_returnsIndependentBuilders() { + + AbstractImmutableParentPointerTreeBuilder unsortedBuilder1 = + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + AbstractImmutableParentPointerTreeBuilder unsortedBuilder2 = + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + + AbstractImmutableParentPointerTreeBuilder sortedBuilder1 = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + AbstractImmutableParentPointerTreeBuilder sortedBuilder2 = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + + unsortedBuilder1.union(0, 1); + sortedBuilder1.union(0, 1); + + assertThat(unsortedBuilder1.build().contains(0)).isTrue(); + assertThat(unsortedBuilder2.build().contains(0)).isFalse(); + + assertThat(sortedBuilder1.build().contains(0)).isTrue(); + assertThat(sortedBuilder2.build().contains(0)).isFalse(); + } + + @Test + public void testBuilder_build_laterMutationsDoNotAffectPreviousResult() { + + AbstractImmutableParentPointerTreeBuilder unsortedBuilder = + ImmutableParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + AbstractImmutableParentPointerTreeBuilder sortedBuilder = + ImmutableSortedParentPointerTreeUnionFind.Builder.getBuilder(UnionType.UNION_BY_SIZE); + + unsortedBuilder.union(0, 1); + sortedBuilder.union(0, 1); + + AbstractImmutableUnionFind firstUnsortedResult = unsortedBuilder.build(); + AbstractImmutableUnionFind firstSortedResult = sortedBuilder.build(); + + unsortedBuilder.union(2, 3); + sortedBuilder.union(2, 3); + + AbstractImmutableUnionFind secondUnsortedResult = unsortedBuilder.build(); + AbstractImmutableUnionFind secondSortedResult = sortedBuilder.build(); + + assertThat(firstUnsortedResult.contains(2)).isFalse(); + assertThat(firstUnsortedResult.getAllSubsets()).hasSize(1); + + assertThat(firstSortedResult.contains(2)).isFalse(); + assertThat(firstSortedResult.getAllSubsets()).hasSize(1); + + assertThat(secondUnsortedResult.contains(2)).isFalse(); + assertThat(secondUnsortedResult.getAllSubsets()).hasSize(1); + + assertThat(secondSortedResult.contains(2)).isFalse(); + assertThat(secondSortedResult.getAllSubsets()).hasSize(1); + } + + @Test + public void testMutableUnion_isUnsupported() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + assertThrows(UnsupportedOperationException.class, () -> unionFind.union(0, 1)); + } + } + + @Test + public void testGetAllSubsets_mutatingReturnedCollection_doesNotAffectSubsequentCalls() { + + for (UnionFind unionFind : + buildUnsortedAndSorted(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + Collection> firstCall = unionFind.getAllSubsets(); + + firstCall.iterator().next().clear(); + + Collection> secondCall = unionFind.getAllSubsets(); + + assertThat(secondCall).hasSize(1); + assertThat(secondCall.iterator().next()).containsExactly(0, 1); + } + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindByRankTest.java b/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindByRankTest.java new file mode 100644 index 000000000..840f86211 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindByRankTest.java @@ -0,0 +1,153 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.Collection; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; + +public class ParentPointerTreeUnionFindByRankTest { + private ParentPointerTreeUnionFind integerUnionFind; + + @Before + public void setup() { + integerUnionFind = new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK); + } + + @Test + public void testFind_afterSelfUnion_returnsItself() { + + integerUnionFind.union(0, 0); + + assertThat(integerUnionFind.find(0)).isEqualTo(0); + } + + @Test + public void testUnion_twoNewElements_producesSingleSubsetOfSizeTwo() { + + integerUnionFind.union(0, 1); + + assertThat(integerUnionFind.find(0)).isEqualTo(integerUnionFind.find(1)); + assertThat(integerUnionFind.getAllSubsets()).hasSize(1); + } + + @Test + public void testUnion_disjointPairs_produceDistinctSubsets() { + + integerUnionFind.union(0, 1); + integerUnionFind.union(2, 3); + + assertThat(integerUnionFind.find(0)).isNotEqualTo(integerUnionFind.find(2)); + assertThat(integerUnionFind.getAllSubsets()).hasSize(2); + } + + @Test + public void testUnion_severalElementsToSameSubset() { + + integerUnionFind.union(0, 1); + integerUnionFind.union(1, 2); + integerUnionFind.union(2, 3); + + Integer canon = integerUnionFind.find(0); + assertThat(integerUnionFind.find(0)).isEqualTo(canon); + assertThat(integerUnionFind.find(1)).isEqualTo(canon); + assertThat(integerUnionFind.find(2)).isEqualTo(canon); + assertThat(integerUnionFind.find(3)).isEqualTo(canon); + assertThat(integerUnionFind.getAllSubsets()).hasSize(1); + } + + @Test + public void testUnion_duplicateUnionCall_doesNotLeadToDuplicates() { + + integerUnionFind.union(0, 1); + integerUnionFind.union(0, 1); + integerUnionFind.union(1, 0); + + assertThat(integerUnionFind.find(0)).isEqualTo(integerUnionFind.find(1)); + assertThat(integerUnionFind.getAllSubsets()).hasSize(1); + } + + @Test + public void testUnion_mergesTwoExistingMultiElementSubsets() { + + integerUnionFind.union(0, 1); + integerUnionFind.union(0, 2); + integerUnionFind.union(3, 4); + integerUnionFind.union(3, 5); + + assertThat(integerUnionFind.getAllSubsets()).hasSize(2); + + integerUnionFind.union(0, 3); + + Integer canon = integerUnionFind.find(0); + for (int i = 0; i <= 5; i++) { + assertThat(integerUnionFind.find(i)).isEqualTo(canon); + } + } + + @Test + public void testUnion_constantCanonicalElementDuringNonLinearInsertion() { + + integerUnionFind.union(3, 3); + integerUnionFind.union(3, 2); + integerUnionFind.union(3, 5); + integerUnionFind.union(3, 1); + integerUnionFind.union(3, 8); + integerUnionFind.union(3, 6); + integerUnionFind.union(3, 9); + integerUnionFind.union(3, 7); + integerUnionFind.union(3, 4); + integerUnionFind.union(3, 0); + + Integer canon = integerUnionFind.find(3); + for (int i = 0; i <= 9; i++) { + assertThat(integerUnionFind.find(i)).isEqualTo(canon); + } + } + + @Test + public void testGetAllSubsets_reflectsCorrectMembershipAfterMultipleUnions() { + + for (int i = 0; i <= 4; i++) { + integerUnionFind.union(0, i); + } + for (int i = 5; i <= 9; i++) { + integerUnionFind.union(5, i); + } + + Collection> subsets = integerUnionFind.getAllSubsets(); + + assertThat(subsets).hasSize(2); + for (Set subset : subsets) { + assertThat(subset).hasSize(5); + } + } + + @Test + public void testUnion_StringElements() { + + ParentPointerTreeUnionFind stringUnionFind = + new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_RANK); + + stringUnionFind.union(Integer.toString(0), Integer.toString(1)); + stringUnionFind.union(Integer.toString(0), Integer.toString(2)); + stringUnionFind.union(Integer.toString(3), Integer.toString(4)); + + assertThat(stringUnionFind.find(Integer.toString(0))) + .isEqualTo(stringUnionFind.find(Integer.toString(2))); + assertThat(stringUnionFind.find(Integer.toString(0))) + .isNotEqualTo(stringUnionFind.find(Integer.toString(3))); + assertThat(stringUnionFind.getAllSubsets()).hasSize(2); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindBySizeTest.java b/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindBySizeTest.java new file mode 100644 index 000000000..09eb3c0bc --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindBySizeTest.java @@ -0,0 +1,153 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.Collection; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; + +public class ParentPointerTreeUnionFindBySizeTest { + private ParentPointerTreeUnionFind integerUnionFind; + + @Before + public void setup() { + integerUnionFind = new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE); + } + + @Test + public void testFind_afterSelfUnion_returnsItself() { + + integerUnionFind.union(0, 0); + + assertThat(integerUnionFind.find(0)).isEqualTo(0); + } + + @Test + public void testUnion_twoNewElements_producesSingleSubsetOfSizeTwo() { + + integerUnionFind.union(0, 1); + + assertThat(integerUnionFind.find(0)).isEqualTo(integerUnionFind.find(1)); + assertThat(integerUnionFind.getAllSubsets()).hasSize(1); + } + + @Test + public void testUnion_disjointPairs_produceDistinctSubsets() { + + integerUnionFind.union(0, 1); + integerUnionFind.union(2, 3); + + assertThat(integerUnionFind.find(0)).isNotEqualTo(integerUnionFind.find(2)); + assertThat(integerUnionFind.getAllSubsets()).hasSize(2); + } + + @Test + public void testUnion_severalElementsToSameSubset() { + + integerUnionFind.union(0, 1); + integerUnionFind.union(1, 2); + integerUnionFind.union(2, 3); + + Integer canon = integerUnionFind.find(0); + assertThat(integerUnionFind.find(0)).isEqualTo(canon); + assertThat(integerUnionFind.find(1)).isEqualTo(canon); + assertThat(integerUnionFind.find(2)).isEqualTo(canon); + assertThat(integerUnionFind.find(3)).isEqualTo(canon); + assertThat(integerUnionFind.getAllSubsets()).hasSize(1); + } + + @Test + public void testUnion_duplicateUnionCall_doesNotLeadToDuplicates() { + + integerUnionFind.union(0, 1); + integerUnionFind.union(0, 1); + integerUnionFind.union(1, 0); + + assertThat(integerUnionFind.find(0)).isEqualTo(integerUnionFind.find(1)); + assertThat(integerUnionFind.getAllSubsets()).hasSize(1); + } + + @Test + public void testUnion_mergesTwoExistingMultiElementSubsets() { + + integerUnionFind.union(0, 1); + integerUnionFind.union(0, 2); + integerUnionFind.union(3, 4); + integerUnionFind.union(3, 5); + + assertThat(integerUnionFind.getAllSubsets()).hasSize(2); + + integerUnionFind.union(0, 3); + + Integer canon = integerUnionFind.find(0); + for (int i = 0; i <= 5; i++) { + assertThat(integerUnionFind.find(i)).isEqualTo(canon); + } + } + + @Test + public void testUnion_constantCanonicalElementDuringNonLinearInsertion() { + + integerUnionFind.union(3, 3); + integerUnionFind.union(3, 2); + integerUnionFind.union(3, 5); + integerUnionFind.union(3, 1); + integerUnionFind.union(3, 8); + integerUnionFind.union(3, 6); + integerUnionFind.union(3, 9); + integerUnionFind.union(3, 7); + integerUnionFind.union(3, 4); + integerUnionFind.union(3, 0); + + Integer canon = integerUnionFind.find(3); + for (int i = 0; i <= 9; i++) { + assertThat(integerUnionFind.find(i)).isEqualTo(canon); + } + } + + @Test + public void testGetAllSubsets_reflectsCorrectMembershipAfterMultipleUnions() { + + for (int i = 0; i <= 4; i++) { + integerUnionFind.union(0, i); + } + for (int i = 5; i <= 9; i++) { + integerUnionFind.union(5, i); + } + + Collection> subsets = integerUnionFind.getAllSubsets(); + + assertThat(subsets).hasSize(2); + for (Set subset : subsets) { + assertThat(subset).hasSize(5); + } + } + + @Test + public void testUnion_StringElements() { + + ParentPointerTreeUnionFind stringUnionFind = + new ParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE); + + stringUnionFind.union(Integer.toString(0), Integer.toString(1)); + stringUnionFind.union(Integer.toString(0), Integer.toString(2)); + stringUnionFind.union(Integer.toString(3), Integer.toString(4)); + + assertThat(stringUnionFind.find(Integer.toString(0))) + .isEqualTo(stringUnionFind.find(Integer.toString(2))); + assertThat(stringUnionFind.find(Integer.toString(0))) + .isNotEqualTo(stringUnionFind.find(Integer.toString(3))); + assertThat(stringUnionFind.getAllSubsets()).hasSize(2); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindSortednessTest.java b/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindSortednessTest.java new file mode 100644 index 000000000..7fd8cedec --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/ParentPointerTreeUnionFindSortednessTest.java @@ -0,0 +1,121 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.Collection; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; +import org.sosy_lab.common.collect.union_find.SortedParentPointerTreeUnionFind; + +public class ParentPointerTreeUnionFindSortednessTest { + + private SortedParentPointerTreeUnionFind sortedUnionFind; + + @Before + public void setup() { + sortedUnionFind = new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE); + } + + @Test + public void testGetAllSubsets_elementsAddedInAscendingOrder_remainSorted() { + + sortedUnionFind.union(0, 1); + sortedUnionFind.union(0, 2); + sortedUnionFind.union(0, 3); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3).inOrder(); + } + + @Test + public void testGetAllSubsets_elementsAddedInDescendingOrder_areSortedAscending() { + + sortedUnionFind.union(3, 2); + sortedUnionFind.union(3, 1); + sortedUnionFind.union(3, 0); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3).inOrder(); + } + + @Test + public void testGetAllSubsets_nonlinearInsertionOrder_areReturnedSorted() { + + sortedUnionFind.union(3, 2); + sortedUnionFind.union(3, 4); + sortedUnionFind.union(3, 0); + sortedUnionFind.union(3, 5); + sortedUnionFind.union(3, 1); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3, 4, 5).inOrder(); + } + + @Test + public void testGetAllSubsets_multipleSubsets_eachSortedIndependently() { + + sortedUnionFind.union(0, 1); + sortedUnionFind.union(0, 2); + sortedUnionFind.union(10, 11); + sortedUnionFind.union(10, 12); + + for (Collection subset : sortedUnionFind.getAllSubsets()) { + assertThat(subset).isInOrder(); + } + } + + @Test + public void testGetAllSubsets_afterMergingTwoSubsets_resultIsSorted() { + + sortedUnionFind.union(0, 1); + sortedUnionFind.union(0, 2); + sortedUnionFind.union(3, 4); + sortedUnionFind.union(3, 5); + + sortedUnionFind.union(0, 3); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3, 4, 5).inOrder(); + } + + @Test + public void testGetAllSubsets_stringElements_areSortedAlphabetically() { + + SortedParentPointerTreeUnionFind stringSortedUnionFind = + new SortedParentPointerTreeUnionFind<>(UnionType.UNION_BY_SIZE); + String[] expected = {"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + + for (int i = 0; i <= 2; i++) { + stringSortedUnionFind.union("0", Integer.toString(i)); + } + for (int i = 3; i <= 5; i++) { + stringSortedUnionFind.union("3", Integer.toString(i)); + } + for (int i = 6; i <= 8; i++) { + stringSortedUnionFind.union("6", Integer.toString(i)); + } + + stringSortedUnionFind.union("9", "9"); + stringSortedUnionFind.union("0", "6"); + stringSortedUnionFind.union("6", "-1"); + stringSortedUnionFind.union("1", "4"); + stringSortedUnionFind.union("0", "9"); + + assertThat(onlySubsetOf(stringSortedUnionFind)).containsExactlyElementsIn(expected).inOrder(); + } + + private static > Set onlySubsetOf( + SortedParentPointerTreeUnionFind sortedUnionFind) { + + Collection> subsets = sortedUnionFind.getAllSubsets(); + + assertThat(subsets).hasSize(1); + return subsets.iterator().next(); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/PersistentParentPointerTreeUnionFindSortednessTest.java b/src/org/sosy_lab/common/collect/union_find/tests/PersistentParentPointerTreeUnionFindSortednessTest.java new file mode 100644 index 000000000..18688b3ed --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/PersistentParentPointerTreeUnionFindSortednessTest.java @@ -0,0 +1,209 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.errorprone.annotations.Var; +import java.util.Collection; +import java.util.Iterator; +import java.util.NavigableSet; +import org.junit.Test; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; +import org.sosy_lab.common.collect.union_find.PersistentSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentSortedUnionFind; + +public class PersistentParentPointerTreeUnionFindSortednessTest { + + private static PersistentSortedUnionFind emptyPersistentSortedUnionFind( + UnionType pUnionType) { + return PersistentSortedParentPointerTreeUnionFind.of(pUnionType); + } + + private static PersistentSortedUnionFind applySortedUnions( + UnionType pUnionType, int[]... pUnions) { + + @Var PersistentSortedUnionFind unionFind = emptyPersistentSortedUnionFind(pUnionType); + + for (int[] pair : pUnions) { + unionFind = unionFind.unionAndCopy(pair[0], pair[1]); + } + + return unionFind; + } + + private static > NavigableSet onlySubsetOf( + PersistentSortedUnionFind pSortedUnionFind) { + + Collection> subsets = pSortedUnionFind.getAllSubsets(); + + assertThat(subsets).hasSize(1); + + return subsets.iterator().next(); + } + + @Test + public void testGetAllSubsets_elementsAddedInAscendingOrder_remainSorted() { + + PersistentSortedUnionFind sortedUnionFind = + applySortedUnions( + UnionType.UNION_BY_SIZE, new int[] {0, 1}, new int[] {0, 2}, new int[] {0, 3}); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3).inOrder(); + } + + @Test + public void testGetAllSubsets_elementsAddedInDescendingOrder_areSortedAscending() { + + PersistentSortedUnionFind sortedUnionFind = + applySortedUnions( + UnionType.UNION_BY_SIZE, new int[] {3, 2}, new int[] {3, 1}, new int[] {3, 0}); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3).inOrder(); + } + + @Test + public void testGetAllSubsets_nonLinearInsertionOrder_areReturnedSorted() { + + PersistentSortedUnionFind sortedUnionFind = + applySortedUnions( + UnionType.UNION_BY_SIZE, + new int[] {3, 2}, + new int[] {3, 4}, + new int[] {3, 0}, + new int[] {3, 5}, + new int[] {3, 1}); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3, 4, 5).inOrder(); + } + + @Test + public void testGetAllSubsets_multipleSubsets_eachSortedIndependently() { + + PersistentSortedUnionFind sortedUnionFind = + applySortedUnions( + UnionType.UNION_BY_SIZE, + new int[] {0, 1}, + new int[] {0, 2}, + new int[] {10, 11}, + new int[] {10, 12}); + + for (NavigableSet subset : sortedUnionFind.getAllSubsets()) { + + assertThat(subset).isInOrder(); + } + } + + @Test + public void testGetAllSubsets_afterMergingTwoSubsets_resultIsSorted() { + + PersistentSortedUnionFind sortedUnionFind = + applySortedUnions( + UnionType.UNION_BY_SIZE, + new int[] {0, 1}, + new int[] {0, 2}, + new int[] {3, 4}, + new int[] {3, 5}, + new int[] {0, 3}); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3, 4, 5).inOrder(); + } + + @Test + public void testGetAllSubsets_stringElements_areSortedAlphabetically() { + + @Var + PersistentSortedUnionFind stringSortedUnionFind = + PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE); + String[] expected = {"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + + for (int i = 0; i <= 2; i++) { + stringSortedUnionFind = stringSortedUnionFind.unionAndCopy("0", Integer.toString(i)); + } + for (int i = 3; i <= 5; i++) { + stringSortedUnionFind = stringSortedUnionFind.unionAndCopy("3", Integer.toString(i)); + } + for (int i = 6; i <= 8; i++) { + stringSortedUnionFind = stringSortedUnionFind.unionAndCopy("6", Integer.toString(i)); + } + + stringSortedUnionFind = stringSortedUnionFind.unionAndCopy("9", "9"); + stringSortedUnionFind = stringSortedUnionFind.unionAndCopy("0", "6"); + stringSortedUnionFind = stringSortedUnionFind.unionAndCopy("6", "-1"); + stringSortedUnionFind = stringSortedUnionFind.unionAndCopy("1", "4"); + stringSortedUnionFind = stringSortedUnionFind.unionAndCopy("0", "9"); + + assertThat(onlySubsetOf(stringSortedUnionFind)).containsExactlyElementsIn(expected).inOrder(); + } + + @Test + public void testGetAllSubsets_isSortedRegardlessOfUnionType() { + + int[][] unions = {{3, 2}, {3, 4}, {3, 0}, {3, 5}, {3, 1}}; + + for (UnionType unionType : UnionType.values()) { + + PersistentSortedUnionFind sortedUnionFind = applySortedUnions(unionType, unions); + + assertThat(onlySubsetOf(sortedUnionFind)).containsExactly(0, 1, 2, 3, 4, 5).inOrder(); + } + } + + @Test + public void testGetAllSubsets_subsetsThemselvesAreOrderedByCanonicalElement() { + + PersistentSortedUnionFind sortedUnionFind = + applySortedUnions( + UnionType.UNION_BY_SIZE, + new int[] {10, 11}, + new int[] {10, 12}, + new int[] {0, 1}, + new int[] {0, 2}); + + Collection> subsets = sortedUnionFind.getAllSubsets(); + assertThat(subsets).hasSize(2); + + Iterator> iterator = subsets.iterator(); + NavigableSet firstSubset = iterator.next(); + NavigableSet secondSubset = iterator.next(); + + assertThat(firstSubset.last()).isLessThan(secondSubset.first()); + } + + @Test + public void testGetAllSubsets_returnedSetSupportsNavigableSetOperations() { + + PersistentSortedUnionFind sortedUnionFind = + applySortedUnions( + UnionType.UNION_BY_SIZE, + new int[] {0, 1}, + new int[] {0, 2}, + new int[] {0, 3}, + new int[] {0, 4}); + + NavigableSet subset = onlySubsetOf(sortedUnionFind); + + assertThat(subset.first()).isEqualTo(0); + assertThat(subset.last()).isEqualTo(4); + assertThat(subset.higher(1)).isEqualTo(2); + assertThat(subset.lower(3)).isEqualTo(2); + } + + @Test + public void testGetAllSubsets_earlierVersionRemainsSortedAfterLaterUnions() { + + int[] union = new int[] {3, 1}; + + PersistentSortedUnionFind version0 = applySortedUnions(UnionType.UNION_BY_SIZE, union); + PersistentSortedUnionFind version1 = version0.unionAndCopy(3, 2); + + assertThat(onlySubsetOf(version0)).containsExactly(1, 3).inOrder(); + assertThat(onlySubsetOf(version1)).containsExactly(1, 2, 3).inOrder(); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/PersistentParentPointerTreeUnionFindTest.java b/src/org/sosy_lab/common/collect/union_find/tests/PersistentParentPointerTreeUnionFindTest.java new file mode 100644 index 000000000..7f1698122 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/PersistentParentPointerTreeUnionFindTest.java @@ -0,0 +1,425 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Var; +import java.util.Collection; +import java.util.Set; +import org.junit.Test; +import org.sosy_lab.common.collect.union_find.ParentPointerTreeUnionFind.UnionType; +import org.sosy_lab.common.collect.union_find.PersistentParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentSortedParentPointerTreeUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentSortedUnionFind; +import org.sosy_lab.common.collect.union_find.PersistentUnionFind; +import org.sosy_lab.common.collect.union_find.UnionFind; + +public class PersistentParentPointerTreeUnionFindTest { + + private final int[] simpleUnionArgs = new int[] {0, 1}; + + private static PersistentUnionFind emptyPersistentUnionFind(UnionType pUnionType) { + return PersistentParentPointerTreeUnionFind.of(pUnionType); + } + + private static PersistentSortedUnionFind emptyPersistentSortedUnionFind( + UnionType pUnionType) { + return PersistentSortedParentPointerTreeUnionFind.of(pUnionType); + } + + private static PersistentUnionFind applyUnions(UnionType pUnionType, int[]... pUnions) { + + @Var PersistentUnionFind unionFind = emptyPersistentUnionFind(pUnionType); + + for (int[] pair : pUnions) { + unionFind = unionFind.unionAndCopy(pair[0], pair[1]); + } + + return unionFind; + } + + private static PersistentSortedUnionFind applySortedUnions( + UnionType pUnionType, int[]... pUnions) { + + @Var PersistentSortedUnionFind unionFind = emptyPersistentSortedUnionFind(pUnionType); + + for (int[] pair : pUnions) { + unionFind = unionFind.unionAndCopy(pair[0], pair[1]); + } + + return unionFind; + } + + private static ImmutableList> bothVariants( + UnionType pUnionType, int[]... pUnions) { + + return ImmutableList.of( + applyUnions(pUnionType, pUnions), applySortedUnions(pUnionType, pUnions)); + } + + @Test + public void testFind_afterSelfUnion_returnsItself() { + + int[] unionArgs = new int[] {0, 0}; + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, unionArgs)) { + + assertThat(unionFind.find(0)).isEqualTo(0); + } + } + + @Test + public void testFind_null_throwsNullPointerException() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThrows(NullPointerException.class, () -> unionFind.find(null)); + } + } + + @Test + public void testFind_elementNotContained_throwsIllegalArgumentException() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThrows(IllegalArgumentException.class, () -> unionFind.find(10)); + } + } + + @Test + public void testUnion_twoNewElements_producesSingleSubsetOfSizeTwo() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThat(unionFind.find(0)).isEqualTo(unionFind.find(1)); + assertThat(unionFind.getAllSubsets()).hasSize(1); + } + } + + @Test + public void testUnion_disjointPairs_produceDistinctSubsets() { + + for (UnionFind unionFind : + bothVariants(UnionType.UNION_BY_SIZE, new int[] {0, 1}, new int[] {2, 3})) { + + assertThat(unionFind.find(0)).isNotEqualTo(unionFind.find(2)); + assertThat(unionFind.getAllSubsets()).hasSize(2); + } + } + + @Test + public void testUnion_severalElementsToSameSubset() { + + for (UnionFind unionFind : + bothVariants( + UnionType.UNION_BY_SIZE, new int[] {0, 1}, new int[] {1, 2}, new int[] {2, 3})) { + + int canon = unionFind.find(0); + + assertThat(unionFind.find(1)).isEqualTo(canon); + assertThat(unionFind.find(2)).isEqualTo(canon); + assertThat(unionFind.find(3)).isEqualTo(canon); + assertThat(unionFind.getAllSubsets()).hasSize(1); + } + } + + @Test + public void testUnion_duplicateUnionCall_doesNotLeadToDuplicates() { + + for (UnionFind unionFind : + bothVariants( + UnionType.UNION_BY_SIZE, new int[] {0, 1}, new int[] {0, 1}, new int[] {1, 0})) { + + assertThat(unionFind.find(0)).isEqualTo(unionFind.find(1)); + assertThat(unionFind.getAllSubsets()).hasSize(1); + assertThat(unionFind.getAllSubsets().iterator().next()).hasSize(2); + } + } + + @Test + public void testUnion_mergesTwoExistingMultiElementSubsets() { + + for (UnionFind unionFind : + bothVariants( + UnionType.UNION_BY_SIZE, + new int[] {0, 1}, + new int[] {0, 2}, + new int[] {3, 4}, + new int[] {3, 5}, + new int[] {0, 3})) { + + assertThat(unionFind.getAllSubsets()).hasSize(1); + + int canon = unionFind.find(0); + + for (int i = 0; i <= 5; i++) { + assertThat(unionFind.find(i)).isEqualTo(canon); + } + } + } + + @Test + public void testUnion_constantCanonicalElementDuringNonLinearInsertion() { + + for (UnionFind unionFind : + bothVariants( + UnionType.UNION_BY_SIZE, + new int[] {3, 3}, + new int[] {3, 2}, + new int[] {3, 5}, + new int[] {3, 1}, + new int[] {3, 8}, + new int[] {3, 6}, + new int[] {3, 9}, + new int[] {3, 7}, + new int[] {3, 4}, + new int[] {3, 0})) { + + assertThat(unionFind.getAllSubsets()).hasSize(1); + + int canon = unionFind.find(3); + + for (int i = 0; i <= 9; i++) { + assertThat(unionFind.find(i)).isEqualTo(canon); + } + } + } + + @Test + public void testUnion_bothUnionTypes_produceSameGrouping() { + + int[][] unions = {{0, 1}, {0, 2}, {3, 4}, {3, 5}, {0, 3}}; + + for (UnionType unionType : UnionType.values()) { + for (UnionFind unionFind : bothVariants(unionType, unions)) { + + assertThat(unionFind.getAllSubsets()).hasSize(1); + + int canon = unionFind.find(0); + + for (int i = 0; i <= 5; i++) { + assertThat(unionFind.find(i)).isEqualTo(canon); + } + } + } + } + + @Test + public void testUnion_stringElements() { + + @Var + PersistentUnionFind unsortedStringUnionFind = + PersistentParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE); + unsortedStringUnionFind = + unsortedStringUnionFind + .unionAndCopy("0", "1") + .unionAndCopy("0", "2") + .unionAndCopy("3", "4"); + @Var + PersistentSortedUnionFind sortedStringUnionFind = + PersistentSortedParentPointerTreeUnionFind.of(UnionType.UNION_BY_SIZE); + sortedStringUnionFind = + sortedStringUnionFind.unionAndCopy("0", "1").unionAndCopy("0", "2").unionAndCopy("3", "4"); + + assertThat(unsortedStringUnionFind.find("0")).isEqualTo(unsortedStringUnionFind.find("2")); + assertThat(unsortedStringUnionFind.find("0")).isNotEqualTo(unsortedStringUnionFind.find("3")); + assertThat(unsortedStringUnionFind.getAllSubsets()).hasSize(2); + + assertThat(sortedStringUnionFind.find("0")).isEqualTo(sortedStringUnionFind.find("2")); + assertThat(sortedStringUnionFind.find("0")).isNotEqualTo(sortedStringUnionFind.find("3")); + assertThat(sortedStringUnionFind.getAllSubsets()).hasSize(2); + } + + @Test + public void testGetAllSubsets_reflectsCorrectMembershipAfterMultipleUnions() { + + int[][] unions = { + {0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {5, 5}, {5, 6}, {5, 7}, {5, 8}, {5, 9}, + }; + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, unions)) { + + Collection> subsets = unionFind.getAllSubsets(); + + assertThat(subsets).hasSize(2); + + for (Set subset : subsets) { + assertThat(subset).hasSize(5); + } + } + } + + @Test + public void testGetAllSubsets_emptyUnionFind_isEmpty() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE)) { + + assertThat(unionFind.getAllSubsets()).isEmpty(); + } + } + + @Test + public void testContains_elementInSubset_returnsTrue() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThat(unionFind.contains(0)).isTrue(); + assertThat(unionFind.contains(1)).isTrue(); + } + } + + @Test + public void testContains_elementNotContained_returnsFalse() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThat(unionFind.contains(10)).isFalse(); + } + } + + @Test + public void testContains_null_throwsNullPointerException() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + assertThrows(NullPointerException.class, () -> unionFind.contains(null)); + } + } + + @Test + public void testUnion_nullElement_throwsNullPointerException() { + + PersistentUnionFind unsortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE); + PersistentUnionFind sortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE); + + assertThrows(NullPointerException.class, () -> unsortedUnionFind.unionAndCopy(null, 1)); + assertThrows(NullPointerException.class, () -> sortedUnionFind.unionAndCopy(null, 1)); + } + + @Test + public void testUnion_doesNotMutateOriginalInstance() { + + PersistentUnionFind originalUnsorted = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE).unionAndCopy(0, 1); + PersistentUnionFind updatedUnsorted = originalUnsorted.unionAndCopy(2, 3); + + assertThat(originalUnsorted.contains(2)).isFalse(); + assertThat(originalUnsorted.getAllSubsets()).hasSize(1); + assertThat(updatedUnsorted.contains(2)).isTrue(); + assertThat(updatedUnsorted.getAllSubsets()).hasSize(2); + + PersistentSortedUnionFind originalSorted = + emptyPersistentSortedUnionFind(UnionType.UNION_BY_SIZE).unionAndCopy(0, 1); + PersistentSortedUnionFind updatedSorted = originalSorted.unionAndCopy(2, 3); + + assertThat(originalSorted.contains(2)).isFalse(); + assertThat(originalSorted.getAllSubsets()).hasSize(1); + assertThat(updatedSorted.contains(2)).isTrue(); + assertThat(updatedSorted.getAllSubsets()).hasSize(2); + } + + @Test + public void testUnion_eachVersionKeepsItsOwnSnapshot() { + + PersistentUnionFind version0 = emptyPersistentUnionFind(UnionType.UNION_BY_SIZE); + PersistentUnionFind version1 = version0.unionAndCopy(0, 1); + PersistentUnionFind version2 = version1.unionAndCopy(0, 2); + PersistentUnionFind version3 = version2.unionAndCopy(3, 4); + + assertThat(version0.getAllSubsets()).isEmpty(); + + assertThat(version1.getAllSubsets()).hasSize(1); + assertThat(version1.getAllSubsets().iterator().next()).containsExactly(0, 1); + + assertThat(version2.getAllSubsets()).hasSize(1); + assertThat(version2.getAllSubsets().iterator().next()).containsExactly(0, 1, 2); + + assertThat(version3.getAllSubsets()).hasSize(2); + assertThat(version3.contains(3)).isTrue(); + assertThat(version2.contains(3)).isFalse(); + } + + @Test + public void testUnion_selfUnionOnExistingElement_returnsSameInstance() { + + PersistentUnionFind unsortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE).unionAndCopy(0, 1); + PersistentUnionFind sortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE).unionAndCopy(0, 1); + + assertThat(unsortedUnionFind.unionAndCopy(0, 0)).isSameInstanceAs(unsortedUnionFind); + assertThat(sortedUnionFind.unionAndCopy(0, 0)).isSameInstanceAs(sortedUnionFind); + } + + @Test + public void testUnion_bothElementsAlreadyInSameSubset_returnsSameInstance() { + + PersistentUnionFind unsortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE).unionAndCopy(0, 1).unionAndCopy(0, 2); + PersistentUnionFind sortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE).unionAndCopy(0, 1).unionAndCopy(0, 2); + + assertThat(unsortedUnionFind.unionAndCopy(1, 2)).isSameInstanceAs(unsortedUnionFind); + assertThat(sortedUnionFind.unionAndCopy(1, 2)).isSameInstanceAs(sortedUnionFind); + } + + @Test + public void testOf_returnsIndependentEmptyInstance() { + + @Var + PersistentUnionFind firstUnsortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE); + PersistentUnionFind secondUnsortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE); + + firstUnsortedUnionFind = firstUnsortedUnionFind.unionAndCopy(0, 1); + + assertThat(firstUnsortedUnionFind.contains(0)).isTrue(); + assertThat(secondUnsortedUnionFind.contains(0)).isFalse(); + + @Var + PersistentUnionFind firstSortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE); + PersistentUnionFind secondSortedUnionFind = + emptyPersistentUnionFind(UnionType.UNION_BY_SIZE); + + firstSortedUnionFind = firstSortedUnionFind.unionAndCopy(0, 1); + + assertThat(firstSortedUnionFind.contains(0)).isTrue(); + assertThat(secondSortedUnionFind.contains(0)).isFalse(); + } + + @Test + public void testMutableUnion_isUnsupported() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + assertThrows(UnsupportedOperationException.class, () -> unionFind.union(0, 1)); + } + } + + @Test + public void testGetAllSubsets_mutatingReturnedCollections_doesNotAffectSubsequentCalls() { + + for (UnionFind unionFind : bothVariants(UnionType.UNION_BY_SIZE, simpleUnionArgs)) { + + Collection> firstCall = unionFind.getAllSubsets(); + firstCall.iterator().next().clear(); + + Collection> secondCall = unionFind.getAllSubsets(); + + assertThat(secondCall).hasSize(1); + assertThat(secondCall.iterator().next()).containsExactly(0, 1); + } + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/SortedUnionFindTest.java b/src/org/sosy_lab/common/collect/union_find/tests/SortedUnionFindTest.java new file mode 100644 index 000000000..836c8cbcb --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/SortedUnionFindTest.java @@ -0,0 +1,144 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.Range; +import com.google.errorprone.annotations.Var; +import org.junit.BeforeClass; +import org.junit.Test; +import org.sosy_lab.common.collect.union_find.SortedTreeSetUnionFind; +import org.sosy_lab.common.collect.union_find.SortedUnionFind; + +public class SortedUnionFindTest { + + static final Range LOW_NUMS = Range.closed(0, 4); + static final Range HIGH_NUMS = Range.closed(5, 9); + + static SortedUnionFind unionFind = new SortedTreeSetUnionFind<>(); + + @BeforeClass + public static void setup() { + unionFind = new SortedTreeSetUnionFind<>(); + + for (int i = 0; i <= 4; i++) { + unionFind.union(0, i); + } + for (int i = 5; i <= 9; i++) { + unionFind.union(5, i); + } + } + + @Test + public void testFind_ElementNotContained() { + assertThat(LOW_NUMS.contains(unionFind.find(8))).isFalse(); + assertThat(HIGH_NUMS.contains(unionFind.find(2))).isFalse(); + } + + @Test + public void testFind_ElementContained() { + assertThat(LOW_NUMS.contains(unionFind.find(2))).isTrue(); + assertThat(HIGH_NUMS.contains(unionFind.find(8))).isTrue(); + } + + @Test + public void testUnion_CorrectCanonicalElementAndCorrectSubsetAfterUnionBySize() { + assertThat(unionFind.getAllSubsets().size() == 2).isTrue(); + + for (int i = 0; i <= 4; i++) { + assertThat(unionFind.find(i).equals(0)).isTrue(); + } + for (int i = 5; i <= 9; i++) { + assertThat(unionFind.find(i).equals(5)).isTrue(); + } + } + + @Test + public void testUnion_MergeExistingSubsets() { + unionFind.union(0, 5); + + assertThat(unionFind.getAllSubsets().size() == 1).isTrue(); + + @Var boolean canonUnknown = true; + @Var Integer canon = null; + + for (int i = 0; i <= 9; i++) { + if (canonUnknown) { + canon = unionFind.find(i); + canonUnknown = false; + } + assertThat(unionFind.find(i).equals(canon)).isTrue(); + } + } + + @Test + public void testUnion_ConstantCanonicalElementDuringNonlinearInsertion() { + SortedUnionFind newUnionFind = new SortedTreeSetUnionFind<>(); + + newUnionFind.union(3, 3); + newUnionFind.union(3, 2); + newUnionFind.union(3, 5); + newUnionFind.union(3, 1); + newUnionFind.union(3, 8); + newUnionFind.union(3, 6); + newUnionFind.union(3, 9); + newUnionFind.union(3, 7); + newUnionFind.union(3, 4); + + assertThat(newUnionFind.find(3)).isEqualTo(3); + assertThat(newUnionFind.find(2)).isEqualTo(3); + assertThat(newUnionFind.find(5)).isEqualTo(3); + assertThat(newUnionFind.find(1)).isEqualTo(3); + assertThat(newUnionFind.find(8)).isEqualTo(3); + assertThat(newUnionFind.find(6)).isEqualTo(3); + assertThat(newUnionFind.find(9)).isEqualTo(3); + assertThat(newUnionFind.find(7)).isEqualTo(3); + assertThat(newUnionFind.find(4)).isEqualTo(3); + } + + @Test + public void testUnion_Strings() { + SortedUnionFind unionFindString = new SortedTreeSetUnionFind<>(); + String[] expected = {"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + + for (int i = 0; i <= 2; i++) { + unionFindString.union(Integer.toString(0), Integer.toString(i)); + } + for (int i = 3; i <= 5; i++) { + unionFindString.union(Integer.toString(3), Integer.toString(i)); + } + for (int i = 6; i <= 8; i++) { + unionFindString.union(Integer.toString(6), Integer.toString(i)); + } + // case: both elements the same; to be added as new subset + unionFindString.union(Integer.toString(9), Integer.toString(9)); + assertThat(unionFindString.getAllSubsets()).hasSize(4); + + // case: both canonical elements + unionFindString.union(Integer.toString(0), Integer.toString(6)); + assertThat(unionFindString.getAllSubsets()).hasSize(3); + + // case: one contained but not canonical, one not contained + unionFindString.union(Integer.toString(7), Integer.toString(-1)); + assertThat(unionFindString.getAllSubsets()).hasSize(3); + + // case: both contained but neither canonical elements + unionFindString.union(Integer.toString(1), Integer.toString(4)); + assertThat(unionFindString.getAllSubsets()).hasSize(2); + + // case: both canonical elements + unionFindString.union(Integer.toString(0), Integer.toString(9)); + assertThat(unionFindString.getAllSubsets()).hasSize(1); + + assertThat(unionFindString.getAllSubsets().iterator().next()) + .containsExactlyElementsIn(expected) + .inOrder(); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/UnionFindSimpleBenchmarkTest.java b/src/org/sosy_lab/common/collect/union_find/tests/UnionFindSimpleBenchmarkTest.java new file mode 100644 index 000000000..3c4f3016a --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/UnionFindSimpleBenchmarkTest.java @@ -0,0 +1,225 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +package org.sosy_lab.common.collect.union_find.tests; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.Var; +import java.math.BigInteger; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.sosy_lab.common.collect.union_find.SortedTreeSetUnionFind; +import org.sosy_lab.common.collect.union_find.SortedUnionFind; + +@Ignore +@RunWith(Parameterized.class) +public class UnionFindSimpleBenchmarkTest { + + private static final int maximumLower = 8; + private static final int maximumUpper = 10; + + private final int lowerBound; + private final int upperBound; + + /** + * Builds parameters for lowerBound and upperBound (as 2-Tuples). lowerBounds are computed from 1 + * to maximumLower. And maximumUpper, for each lowerBound, from current lowerBound to + * maximumUpper. + */ + @Parameters(name = "{index}: lowerBound {0}, upperBound {1}") + public static ImmutableList getBounds() { + ImmutableList.Builder outer = ImmutableList.builder(); + for (int lower = 1; lower <= maximumLower; lower++) { + for (int upper = 2; upper <= maximumUpper; upper++) { + if (upper > lower) { + Integer[] inner = new Integer[2]; + inner[0] = lower; + inner[1] = upper; + outer.add(inner); + } + } + } + return outer.build(); + } + + public UnionFindSimpleBenchmarkTest(int lowerBound, int upperBound) { + this.lowerBound = lowerBound; + this.upperBound = upperBound; + } + + @Test + public void unionBigOQuadraticEvaluationTest() { + // BEGINNING of 1st loop + Set> allUnionFindsOfFirstLoop = new HashSet<>(); + + Duration timeBeforeFirstLoop = Duration.ofNanos(System.nanoTime()); + + List>> partitions1 = generatePartitions(lowerBound); + transformPartitionsToUnionFind(partitions1, allUnionFindsOfFirstLoop); + + Duration timeAfterFirstLoop = Duration.ofNanos(System.nanoTime()); + Duration timeOfFirstLoop = timeAfterFirstLoop.minus(timeBeforeFirstLoop); + // END of 1st loop + // System.out.println("Time for first loop: " + timeOfFirstLoop.getSeconds() + "s" + "\n"); + + // BEGINNING of 2nd loop + Set> allUnionFindsOfSecondLoop = new HashSet<>(); + + Duration timeBeforeSecondLoop = Duration.ofNanos(System.nanoTime()); + + List>> partitions2 = generatePartitions(upperBound); + transformPartitionsToUnionFind(partitions2, allUnionFindsOfSecondLoop); + + Duration timeAfterSecondLoop = Duration.ofNanos(System.nanoTime()); + Duration timeOfSecondLoop = timeAfterSecondLoop.minus(timeBeforeSecondLoop); + // END of 2nd loop + // System.out.println("Time for second loop: " + timeOfSecondLoop.getSeconds() + "s" + "\n"); + + // will throw for larger n's as they won't fit into long + long bellOfLowerBound = getBellNoOfN(lowerBound).longValueExact(); + long bellOfUpperBound = getBellNoOfN(upperBound).longValueExact(); + + assertThat(timeOfSecondLoop.dividedBy(bellOfUpperBound)) + .isLessThan( + timeOfFirstLoop.multipliedBy( + (bellOfUpperBound * bellOfUpperBound) / (bellOfLowerBound * bellOfLowerBound))); + } + + private static List>> generatePartitions(int pHighestNumber) { + List>> allPartitions = new ArrayList<>(); + Set> singletonSet = new HashSet<>(); + + for (int i = 1; i <= pHighestNumber; i++) { + Set singleNumberSet = new HashSet<>(); + singleNumberSet.add(i); + singletonSet.add(singleNumberSet); + } + allPartitions.add(singletonSet); + // System.out.println("New loop with upper bound " + pHighestNumber + ":"); + // System.out.println(singletonSet); + + // biggest subset size currently being created + for (int i = 2; i <= pHighestNumber; i++) { + // deep copy of allPartitions to iterate over + List>> partitionsSoFar = new ArrayList<>(); + for (Set> currentSet : allPartitions) { + partitionsSoFar.add(createDeepCopy(currentSet)); + } + + // iterate over all partitions that currently exist + for (Set> current : partitionsSoFar) { + // add no.s from 1 - pHighestNumber to subsets + for (int j = 1; j <= pHighestNumber; j++) { + // add j to each subset that doesn't contain it yet by merging with subset containing j + breaker: + for (Set currentSubset : current) { + if (!currentSubset.contains(j)) { + Set> copyOfCurrent = createDeepCopy(current); + + @Var Set toBeAddedTo = new HashSet<>(); + @Var Set toRemove = new HashSet<>(); + + for (Set subsetOfCopy : copyOfCurrent) { + if (subsetOfCopy.equals(currentSubset)) { + toBeAddedTo = subsetOfCopy; + } else if (subsetOfCopy.contains(j)) { + toRemove = subsetOfCopy; + } + } + + copyOfCurrent.remove(toRemove); + copyOfCurrent.remove(toBeAddedTo); + toBeAddedTo.addAll(toRemove); + copyOfCurrent.add(toBeAddedTo); + + for (Set> set : allPartitions) { + if (copyOfCurrent.equals(set)) { + continue breaker; + } + } + allPartitions.add(copyOfCurrent); + // System.out.println(copyOfCurrent); + } + } + } + } + } + + return allPartitions; + } + + private static Set> createDeepCopy(Set> pSets) { + Set> copy = new HashSet<>(); + + for (Set subset : pSets) { + Set copyOfSubset = new HashSet<>(subset); + copy.add(copyOfSubset); + } + + return copy; + } + + private static void transformPartitionsToUnionFind( + List>> pPartitions, Set> pSetOfUnionFinds) { + Preconditions.checkNotNull(pPartitions); + Preconditions.checkNotNull(pSetOfUnionFinds); + + for (Set> subsetOfSets : pPartitions) { + SortedUnionFind unionFind = new SortedTreeSetUnionFind<>(); + + for (Set subSubset : subsetOfSets) { + if (!subSubset.isEmpty()) { + Iterator iterator = subSubset.iterator(); + Integer value = iterator.next(); + unionFind.union(value, value); + if (subSubset.size() > 1) { + while (iterator.hasNext()) { + unionFind.union(value, iterator.next()); + } + } + } + } + + pSetOfUnionFinds.add(unionFind); + } + } + + // calculates the Bell Number of a given n>=0 using the Bell Triangle + // uses BigInteger because int/Integer would run out of space at comparatively small n's + private static BigInteger getBellNoOfN(int n) { + @Var List previousRow = new ArrayList<>(); + + // initialise for n=0 + previousRow.add(BigInteger.valueOf(1)); + + for (int i = 1; i < n; i++) { + List currentRow = new ArrayList<>(); + currentRow.add(previousRow.get(previousRow.size() - 1)); + + for (int j = 1; j <= i; j++) { + currentRow.add(previousRow.get(j - 1).add(currentRow.get(j - 1))); + } + + previousRow = currentRow; + } + + return previousRow.get(previousRow.size() - 1); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/tests/package-info.java b/src/org/sosy_lab/common/collect/union_find/tests/package-info.java new file mode 100644 index 000000000..12386aa48 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/tests/package-info.java @@ -0,0 +1,14 @@ +// This file is part of SoSy-Lab Common, +// a library of useful utilities: +// https://github.com/sosy-lab/java-common-lib +// +// SPDX-FileCopyrightText: 2026 Dirk Beyer +// +// SPDX-License-Identifier: Apache-2.0 + +/** This package contains all test classes related to union-find. */ +@com.google.errorprone.annotations.CheckReturnValue +@javax.annotation.ParametersAreNonnullByDefault +@org.sosy_lab.common.annotations.ReturnValuesAreNonnullByDefault +@org.sosy_lab.common.annotations.FieldsAreNonnullByDefault +package org.sosy_lab.common.collect.union_find.tests;