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..6e8a8ae32 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/AbstractGenericUnionFind.java @@ -0,0 +1,180 @@ +// 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 e 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 e) { + + Preconditions.checkNotNull(e); + + for (Entry mapping : mapOfSets.entrySet()) { + if (mapping.getValue().contains(e)) { + 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 e1 and e2. 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: e1, e2 canonical elements of sets to be merged. + * + * @param e1 first element + * @param e2 second element + */ + @SuppressWarnings("unchecked") + @Override + public void union(T e1, T e2) { + + Preconditions.checkNotNull(e1); + Preconditions.checkNotNull(e2); + + if (e1.equals(e2)) { + addElementAsNewSet(e1); + } else { + Set canonicalElements = mapOfSets.keySet(); + + if (canonicalElements.contains(e1)) { + if (canonicalElements.contains(e2)) { + mergeExistingSets(e1, e2); + } else { + addElementToExistingSet(e2, e1); + } + } else if (canonicalElements.contains(e2)) { + addElementToExistingSet(e1, e2); + } else { + + if (contains(e1)) { + if (contains(e2)) { + mergeExistingSets(find(e1), find(e2)); + } else { + addElementToExistingSet(e2, find(e1)); + } + } else { + addElementAsNewSet(e1); + addElementToExistingSet(e2, e1); + } + } + } + } + + @SuppressWarnings("unchecked") + private void addElementAsNewSet(T e) { + + if (!contains(e)) { + S newSet = (S) getEmptySet(); + newSet.add(e); + mapOfSets.put(e, newSet); + } + } + + private void addElementToExistingSet(T e, T canon) { + + if (!contains(e)) { + mapOfSets.get(canon).add(e); + } else { + mergeExistingSets(find(e), canon); + } + } + + // e1 will be new canonical element only if it's set is actually bigger, otherwise e2 new canon + private void mergeExistingSets(T e1, T e2) { + + S set1 = mapOfSets.get(e1); + S set2 = mapOfSets.get(e2); + + assert set1 != null; + assert set2 != null; + + int size1 = set1.size(); + int size2 = set2.size(); + + if (size1 > size2) { + set1.addAll(set2); + assert mapOfSets.remove(e2, set2); + } else { + set2.addAll(set1); + assert mapOfSets.remove(e1, 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 e element to be searched for + * @return true if contained, false if not + */ + @Override + public boolean contains(T e) { + + Preconditions.checkNotNull(e); + + for (S current : mapOfSets.values()) { + if (current.contains(e)) { + return true; + } + } + return false; + } + + protected abstract Set getEmptySet(); + + protected abstract Map> getEmptyMap(); +} 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..93f50bc82 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/AbstractImmutableSortedUnionFind.java @@ -0,0 +1,25 @@ +// 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; + +public abstract class AbstractImmutableSortedUnionFind> + implements SortedUnionFind { + /** + * @throws UnsupportedOperationException Always. + * @deprecated Unsupported operation. + */ + @Deprecated + @Override + @DoNotCall + public final void union(T e1, T e2) { + throw new UnsupportedOperationException(); + } +} 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..5291681c4 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/AbstractImmutableUnionFind.java @@ -0,0 +1,24 @@ +// 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; + +public abstract class AbstractImmutableUnionFind implements UnionFind { + /** + * @throws UnsupportedOperationException Always. + * @deprecated Unsupported operation. + */ + @Deprecated + @Override + @DoNotCall + public final void union(T e1, T e2) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/org/sosy_lab/common/collect/union_find/ParentPointerTree.java b/src/org/sosy_lab/common/collect/union_find/ParentPointerTree.java new file mode 100644 index 000000000..4a0ea0f51 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/ParentPointerTree.java @@ -0,0 +1,53 @@ +// 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.ArrayList; +import java.util.List; + +public class ParentPointerTree { + private final TreeNode root; + private final List> listOfNodes; + private int nextParentIndex; + private boolean timeToMoveOn; + private int size; + + public ParentPointerTree(T rootValue) { + this.root = TreeNode.getNewRootNode(rootValue); + listOfNodes = new ArrayList<>(); + listOfNodes.add(this.root); + nextParentIndex = 0; + timeToMoveOn = false; + size = 1; + } + + public TreeNode getRoot() { + return root; + } + + public int getSize() { + return size; + } + + public void addAsNewNode(T value) { + TreeNode node = TreeNode.getNewNode(listOfNodes.get(nextParentIndex), value); + listOfNodes.add(node); + updateNextParent(); + size++; + } + + // will currently create a binary tree as it increases counter every 2nd insert + private void updateNextParent() { + if (!timeToMoveOn) { + timeToMoveOn = true; + } else { + nextParentIndex++; + } + } +} 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..b70b987ca --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/ParentPointerTreeUnionFind.java @@ -0,0 +1,47 @@ +// 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ParentPointerTreeUnionFind implements UnionFind { + + private final Map> forest; + + public ParentPointerTreeUnionFind() { + forest = new HashMap<>(); + } + + @Override + public T find(T e) { + //TODO + return null; + } + + @Override + public void union(T e1, T e2) { + //TODO + } + + @Override + public Collection> getAllSubsets() { + //TODO + return List.of(); + } + + @Override + public boolean contains(T e) { + //TODO + return false; + } +} 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..1c9484ce1 --- /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; +import java.util.Map; +import java.util.NavigableSet; + +/** + * 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 { + + /** + * Replacement for {@link #union(Comparable, Comparable)} that returns a fresh new instance. + * + * @param e1 first element + * @param e2 second element + * @return new instance that the desired changes have been applied to + */ + @CheckReturnValue + Map> unionAndCopy(T e1, T e2); + + /** + * @throws UnsupportedOperationException Always. + * @deprecated Unsupported operation. + */ + @Deprecated + @Override + @DoNotCall + void union(T e1, T e2); +} 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..769c73c20 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/PersistentUnionFind.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; +import java.util.Map; +import java.util.NavigableSet; + +/** + * 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 e1 first element + * @param e2 second element + * @return new instance that the desired changes have been applied to + */ + @CheckReturnValue + Map> unionAndCopy(T e1, T e2); + + /** + * @throws UnsupportedOperationException Always. + * @deprecated Unsupported operation. + */ + @Deprecated + @Override + @DoNotCall + void union(T e1, T e2); +} 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..a8de46a58 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/SortedUnionFind.java @@ -0,0 +1,54 @@ +// 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> { + /** + * Returns the canonical element of the set containing the provided element. + * + * @param e element for which set is to be found + * @return canonical element of the found set + */ + T find(T e); + + /** + * Merges the sets represented by the two input values according to standard Union-Find behaviour. + * + * @param e1 first element + * @param e2 second element + */ + void union(T e1, T e2); + + /** + * 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 e element to be searched for + * @return true if contained, false if not + */ + boolean contains(T e); +} diff --git a/src/org/sosy_lab/common/collect/union_find/SortedUnionFindTest.java b/src/org/sosy_lab/common/collect/union_find/SortedUnionFindTest.java new file mode 100644 index 000000000..363bde722 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/SortedUnionFindTest.java @@ -0,0 +1,142 @@ +// 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 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; + +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/TreeNode.java b/src/org/sosy_lab/common/collect/union_find/TreeNode.java new file mode 100644 index 000000000..518abf04b --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/TreeNode.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 javax.annotation.Nullable; + +public class TreeNode { + + @Nullable TreeNode parent; + T value; + + private TreeNode(T value) { + this.parent = null; + this.value = value; + } + + private TreeNode(TreeNode parent, T value) { + this.parent = parent; + this.value = value; + } + + @Nullable + public TreeNode getParent() { + return parent; + } + + public T getValue() { + return value; + } + + public static TreeNode getNewRootNode(V value) { + return new TreeNode<>(value); + } + + public static TreeNode getNewNode(TreeNode parent, V value) { + return new TreeNode<>(parent, value); + } +} 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..1b87de772 --- /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 e element for which set is to be found + * @return canonical element of the found set + */ + T find(T e); + + /** + * Merges the sets represented by the two input values according to standard Union-Find behaviour. + * + * @param e1 first element + * @param e2 second element + */ + void union(T e1, T e2); + + /** + * 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 e element to be searched for + * @return true if contained, false if not + */ + boolean contains(T e); +} diff --git a/src/org/sosy_lab/common/collect/union_find/UnionFindSimpleBenchmarkTest.java b/src/org/sosy_lab/common/collect/union_find/UnionFindSimpleBenchmarkTest.java new file mode 100644 index 000000000..e41985f90 --- /dev/null +++ b/src/org/sosy_lab/common/collect/union_find/UnionFindSimpleBenchmarkTest.java @@ -0,0 +1,198 @@ +// 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 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; + +@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 List 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))); + } + + // TODO: add a method that computes only permutations with n elements. + + // TODO: this computes all permutations from 2 to pHighestNumber + 2 -> make it compute them only + // from 1 to pHighestNumber + private static List>> generatePartitions(int pHighestNumber) { + @Var List>> allPermutations = new ArrayList<>(); + + // initialise allPermutations + Set> init = new HashSet<>(); + Set initSubset = new HashSet<>(); + initSubset.add(0); + init.add(initSubset); + allPermutations.add(init); + + for (int i = 1; i <= pHighestNumber; i++) { + + List>> newSets = new ArrayList<>(); + + for (Set> existingSet : allPermutations) { + for (Set existingSubset : existingSet) { + Set> setWithNumber = new HashSet<>(existingSet); + setWithNumber.remove(existingSubset); + Set subsetWithNumber = new HashSet<>(existingSubset); + subsetWithNumber.add(i); + setWithNumber.add(subsetWithNumber); + newSets.add(setWithNumber); + } + + Set> currentExistingSet = new HashSet<>(existingSet); + Set subsetWithCurrentI = new HashSet<>(); + subsetWithCurrentI.add(i); + currentExistingSet.add(subsetWithCurrentI); + newSets.add(currentExistingSet); + } + + /* + // This prints all permutations that are added without duplicates + for (Set> newSet : newSets) { + if (!allPermutations.contains(newSet)) { + System.out.println(newSet); + } + } + */ + allPermutations = newSets; + } + + return allPermutations; + } + + 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/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;