, Cloneable {
/**
* Comparator for elements. Our convention is that the representative of an equivalence class must always be a
* minimal element within the class in terms of this comparator.
*
* Use of the comparator is optional, if no element comparator is present, this class may choose any representative
* for each equivalence class.
*/
private final Comparator mElementComparator;
/**
* Maps an element to its equivalence class.
*
* Note that whenever updating values in this map other corresponding data-structures as {@link #mRepresentative}
* also need to be updated with the same new identity.
*/
private final Map> mEquivalenceClass = new HashMap<>();
/**
* Maps an equivalence class to its representative.
*
* The current implementation uses {@link ImmutableSet} to provide a fast key-based access.
*
* Therefore whenever changing keys in this set other corresponding data-structures like {@link #mEquivalenceClass}
* also need to be updated with the same new identity.
*/
private final Map, E> mRepresentative = new HashMap<>();
/**
* Constructor for new (empty) data structure.
*/
public UnionFind() {
mElementComparator = null;
}
/**
* Constructor for an empty union find data structure. Additionally takes a function that is used as a comparator
* between elements. (See the field's documentation for more details.)
*/
public UnionFind(final Comparator elementComparator) {
assert elementComparator != null : "use other constructor in this case!";
mElementComparator = elementComparator;
}
/**
* Copy constructor.
*
* @param unionFind
* the UnionFind instance to be copied
*/
protected UnionFind(final UnionFind unionFind) {
for (final Entry, E> entry : unionFind.mRepresentative.entrySet()) {
final E representative = entry.getValue();
final ImmutableSet equivalenceClass = entry.getKey();
for (final E equivalenceClassMember : equivalenceClass) {
final Set oldValue = this.mEquivalenceClass.put(equivalenceClassMember, equivalenceClass);
assert oldValue == null : "element was contained twice";
}
this.mRepresentative.put(equivalenceClass, representative);
}
mElementComparator = unionFind.mElementComparator;
assert representativesAreMinimal();
}
/**
* Add a whole equivalence class at once to this UnionFind.
*
* Assumes none of the given elements is part of an existing equivalence class in this UnionFind instance.
*
* @param newBlock
*/
public void addEquivalenceClass(final ImmutableSet newBlock) {
if (mElementComparator == null) {
addEquivalenceClass(newBlock, null);
} else {
addEquivalenceClass(newBlock, findMinimalElement(newBlock));
}
assert representativesAreMinimal();
}
/**
* A variant of {@link #addEquivalenceClass(Set)} where the caller specifies the representative that the newly added
* equivalence class must have.
*
* @param newBlock
* new equivalence class that is to be added to the equivalence relation
* @param newBlockRep
* the element that should be the representative of newBlock in this UnionFind, null for don't care
*/
public void addEquivalenceClass(final ImmutableSet newBlock, final E newBlockRep) {
assert DataStructureUtils.haveEmptyIntersection(newBlock, getAllElements());
assert !newBlock.isEmpty();
assert newBlockRep == null || newBlock.contains(newBlockRep);
assert mElementComparator == null || newBlockRep != null : "if we don't give a representative for the new block"
+ "here, this method might violate the invariant that the representative is always the minimal "
+ "element.";
assert mElementComparator == null || mElementComparator.compare(newBlockRep, findMinimalElement(newBlock)) <= 0;
for (final E elem : newBlock) {
mEquivalenceClass.put(elem, newBlock);
}
final E rep = newBlockRep == null ? newBlock.iterator().next() : newBlockRep;
assert mEquivalenceClass.get(rep) != null;
mRepresentative.put(newBlock, rep);
assert representativesAreMinimal();
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#clone()
*/
@Override
public UnionFind clone() {
return new UnionFind<>(this);
}
/**
* @param elem
* element
* @return the representative of the equivalence class of element elem. null if the given element is not in any
* equivalence class
*/
public E find(final E elem) {
final ImmutableSet set = mEquivalenceClass.get(elem);
return mRepresentative.get(set);
}
public Set find(final Set elemSet) {
final Set result = new HashSet<>();
for (final E elem : elemSet) {
final E rep = find(elem);
if (rep != null) {
result.add(rep);
}
}
return result;
}
/**
* @param elem
* element
* @return the representative of the equivalence class of element e. If there is no equivalence class for e this
* equivalence class is constructed.
*/
public E findAndConstructEquivalenceClassIfNeeded(final E elem) {
final E findResult = find(elem);
if (findResult == null) {
makeEquivalenceClass(elem);
return elem;
}
return findResult;
}
public Set getAllElements() {
return Collections.unmodifiableSet(mEquivalenceClass.keySet());
}
/**
* Returns a collection of all equivalence classes. A equivalence class is an unmodifiable set that contains
* elements. Each contained element is only in one equivalence class.
*
* @return A collection of all equivalence classes. If there are no, the collection is empty.
*/
public Collection> getAllEquivalenceClasses() {
final Collection> allEquivalenceClasses = new LinkedList<>();
for (final E representative : getAllRepresentatives()) {
allEquivalenceClasses.add(getEquivalenceClassMembers(representative));
}
return allEquivalenceClasses;
}
/**
* @return collection of all elements e such that e is representative of an equivalence class.
*/
public Collection getAllRepresentatives() {
return Collections.unmodifiableCollection(mRepresentative.values());
}
@Override
public ImmutableSet getContainingSet(final E elem) {
return getEquivalenceClassMembers(elem);
}
public Comparator getElementComparator() {
return mElementComparator;
}
/**
* @param elem
* element
* @return set of all elements that are in the same equivalence class than e. (Returned set also contains e).
*/
public ImmutableSet getEquivalenceClassMembers(final E elem) {
return mEquivalenceClass.get(elem);
}
@Override
public Iterator> iterator() {
return getAllEquivalenceClasses().iterator();
}
/**
* Construct a new equivalence class that is a singleton and contains only element e.
*
* @param elem
* element
*/
public void makeEquivalenceClass(final E elem) {
if (mEquivalenceClass.containsKey(elem)) {
throw new IllegalArgumentException("Already contained " + elem);
}
final ImmutableSet result = ImmutableSet.singleton(elem);
mEquivalenceClass.put(elem, result);
mRepresentative.put(result, elem);
}
/**
* Removes the given element from all the equivalence classes. If the element was a representative and was not the
* only element of its equivalence class, another representative for it is chosen for the new equivalence class.
*
* @param element
* element to be removed
*/
public void remove(final E element) {
remove(element, null);
}
/**
* Variant of remove(E) that allows the user to give a preference for which element should be the new representative
* of the equivalence class, if the removed element was the representative before.
*
* @param element
* the element to be removed
* @param newRepChoice
* must be in the same equivalence class as element; if the representative of element's equivalence class
* is changed by this method, newRepChoice will be the new representative. If newRepChoice is given as
* null, an arbitrary new representative will be chosen from the remaining members of element's
* equivalence class.
*/
public void remove(final E element, final E newRepChoice) {
assert newRepChoice == null || find(newRepChoice) == find(element);
final ImmutableSet eqc = mEquivalenceClass.get(element);
final HashSet newEqcElements = new HashSet<>(eqc);
newEqcElements.remove(element);
final ImmutableSet newEqc = ImmutableSet.of(newEqcElements);
/*
* if eqc is non-empty after the remove, make sure it gets another representative (keeps the old eqc, for now,
* will replace it with newEqc in next step)
*/
if (find(element).equals(element)) {
// element is the representative of its equivalence class
if (eqc.size() == 1) {
// element was alone in its equivalence class
mEquivalenceClass.remove(element);
mRepresentative.remove(eqc);
assert areMembersConsistent();
return;
}
// pick another element from the equivalence class, and make it the representative
E newRep;
if (mElementComparator == null) {
if (newRepChoice == null) {
newRep = newEqc.iterator().next();
} else {
assert newEqc.contains(newRepChoice);
newRep = newRepChoice;
}
} else {
final E minimum = findMinimalElement(newEqc);
if (newRepChoice == null) {
newRep = minimum;
} else {
if (mElementComparator.compare(newRepChoice, minimum) != 0) {
throw new IllegalArgumentException(
"newRepChoice must be compatible with the element " + "comparator!");
}
newRep = newRepChoice;
}
}
mRepresentative.put(eqc, newRep);
}
/*
* Replace the old eqc by newEqc
*/
for (final E eqcEl : newEqc) {
mEquivalenceClass.put(eqcEl, newEqc);
}
mEquivalenceClass.remove(element);
final E rep = mRepresentative.get(eqc);
mRepresentative.remove(eqc);
assert !rep.equals(element);
mRepresentative.put(newEqc, rep);
assert areMembersConsistent();
assert representativesAreMinimal();
}
/**
* Removes all given elements from all the equivalence classes. If an element was a representative and was not the
* only element of its equivalence class, another representative for it is chosen for the new equivalence class.
*
* @param elements
* The elements to remove
*/
public void removeAll(final Collection elements) {
elements.forEach(this::remove);
}
/**
* @return number of equivalence classes.
*/
@Override
public int size() {
return mRepresentative.size();
}
@Override
public String toString() {
return mRepresentative.toString();
}
public void transformElements(final Function elemTransformer) {
final HashMap, E> representativeCopy = new HashMap<>(mRepresentative);
for (final Entry, E> entry : representativeCopy.entrySet()) {
for (final E oldElem : entry.getKey()) {
mEquivalenceClass.remove(oldElem);
}
mRepresentative.remove(entry.getKey());
final E newRep = elemTransformer.apply(entry.getValue());
final ImmutableSet newEqClass =
entry.getKey().stream().map(elemTransformer).collect(ImmutableSet.collector());
for (final E newElem : newEqClass) {
mEquivalenceClass.put(newElem, newEqClass);
}
mRepresentative.put(newEqClass, newRep);
}
assert representativesAreMinimal();
}
/**
* Union operation for arbitrary number of arguments.
*
* @param elements
* elements
*/
public void union(final Collection elements) {
final Iterator it = elements.iterator();
if (!it.hasNext()) {
return;
}
final E firstElem = it.next();
while (it.hasNext()) {
union(firstElem, it.next());
}
}
/**
* Merge the equivalence classes of the elements e1 and e2. (e1 and e2 do not
* have to be the representatives of this equivalence classes).
*
* @param elem1 first element
* @param elem2 second element
* @return true iff two equivalence classes were merged
*/
public boolean union(final E elem1, final E elem2) {
final ImmutableSet set1 = mEquivalenceClass.get(elem1);
final ImmutableSet set2 = mEquivalenceClass.get(elem2);
if (set1 == set2) {
// already in same equivalence class
return false;
}
final boolean set1IsLarger = set1.size() > set2.size();
final ImmutableSet largerSet = set1IsLarger ? set1 : set2;
final ImmutableSet smallerSet = set1IsLarger ? set2 : set1;
final E newRep;
if (mElementComparator != null) {
final E rep1 = mRepresentative.get(set1);
final E rep2 = mRepresentative.get(set2);
newRep = mElementComparator.compare(rep1, rep2) > 0 ? rep2 : rep1;
} else {
newRep = mRepresentative.get(largerSet);
}
mRepresentative.remove(largerSet);
mRepresentative.remove(smallerSet);
final ImmutableSet union = ImmutableSet.of(DataStructureUtils.union(smallerSet, largerSet));
for (final E e : union) {
mEquivalenceClass.put(e, union);
}
mRepresentative.put(union, newRep);
assert representativesAreMinimal();
return true;
}
/**
* Computes a new UnionFind instance whose partitions are the intersections of the given UnionFind instance's
* equivalence classes. Only non-empty intersections are added to the new equivalence relation.
*
* If the input UnionFind instances are viewed as elements of an abstract domain, the result corresponds to their
* abstract join.
*
* Also returns a split info for each input UnionFind. A split info maps each representative in the original
* UnionFind to the (possibly many) corresponding representatives in the new UnionFind.
*
* @param
* element type
* @param uf1
* instance to be intersected with uf2
* @param uf2
* instance to be intersected with uf1
* @return UnionFind with intersected equivalence classes
*/
public static Triple, HashRelation, HashRelation>
intersectPartitionBlocks(final UnionFind uf1, final UnionFind uf2) {
assert uf1.mElementComparator == uf2.mElementComparator;
final UnionFind result = new UnionFind<>(uf1.mElementComparator);
final HashRelation uf1SplitInfo = new HashRelation<>();
final HashRelation uf2SplitInfo = new HashRelation<>();
for (final Set uf1Block : uf1.getAllEquivalenceClasses()) {
final E uf1Rep = uf1.find(uf1Block.iterator().next());
// map a representative of uf2 to the intersection of its block in uf2 with the current uf1Block
final HashRelation uf2RepToIntersection = new HashRelation<>();
for (final E uf1El : uf1Block) {
final E uf2Rep = uf2.find(uf1El);
uf2RepToIntersection.addPair(uf2Rep, uf1El);
}
for (final E uf2Rep : uf2RepToIntersection.getDomain()) {
final Set intersection = uf2RepToIntersection.getImage(uf2Rep);
// assert intersection.contains(uf2Rep) : "right?.."; // EDIT: not the case
result.addEquivalenceClass(ImmutableSet.of(intersection));
final E subBlockRep = result.find(intersection.iterator().next());
uf1SplitInfo.addPair(uf1Rep, subBlockRep);
uf2SplitInfo.addPair(uf2Rep, subBlockRep);
}
}
assert result.representativesAreMinimal();
assert sanityCheckIntersectPartitionBlocks(uf1, uf2, result, uf1SplitInfo, uf2SplitInfo);
return new Triple<>(result, uf1SplitInfo, uf2SplitInfo);
}
/**
* Computes a new UnionFind instance whose partitions are the unions of the given UnionFind instance's equivalence
* classes.
*
* @param
* element type
* @param uf1
* instance to be union'ed with uf2
* @param uf2
* instance to be union'ed with uf1
* @return UnionFind with union'ed equivalence classes
*/
public static UnionFind unionPartitionBlocks(final UnionFind uf1, final UnionFind uf2) {
if (uf1.mElementComparator != uf2.mElementComparator) {
throw new AssertionError("Cannot combine partitions with different comparators");
}
final UnionFind result = uf1.clone();
for (final Set eq : uf2.getAllEquivalenceClasses()) {
final Set newBlock = new HashSet<>();
final Set representatives = new HashSet<>();
for (final E elem : eq) {
final E rep = result.find(elem);
if (rep != null) {
// is already in result, we memorize the representative for the union operation
representatives.add(rep);
} else {
newBlock.add(elem);
}
}
if (!newBlock.isEmpty()) {
E repForNewBlock;
if (result.mElementComparator == null) {
// take some element as representative
repForNewBlock = newBlock.iterator().next();
} else {
repForNewBlock = result.findMinimalElement(newBlock);
}
result.addEquivalenceClass(ImmutableSet.of(newBlock), repForNewBlock);
representatives.add(repForNewBlock);
}
// make sure that all members of this uf2 class are in the same equivalence
// class of the result.
result.union(representatives);
}
assert result.areMembersConsistent();
assert result.representativesAreMinimal();
return result;
}
private boolean areMembersConsistent() {
for (final E e : mRepresentative.values()) {
if (!mEquivalenceClass.containsKey(e)) {
return false;
}
}
for (final Set eqc : mEquivalenceClass.values()) {
if (!mRepresentative.containsKey(eqc)) {
return false;
}
}
for (final Set eqc : mRepresentative.keySet()) {
if (!mEquivalenceClass.values().contains(eqc)) {
return false;
}
}
return true;
}
private E findMinimalElement(final Set newBlock) {
assert !newBlock.isEmpty();
final Iterator newBlockIt = newBlock.iterator();
E result = newBlockIt.next();
while (newBlockIt.hasNext()) {
final E current = newBlockIt.next();
result = mElementComparator.compare(current, result) < 0 ? current : result;
}
return result;
}
private boolean representativesAreMinimal() {
if (mElementComparator == null) {
return true;
}
// for (Entry> en : mEquivalenceClass.entrySet()) {
for (final Entry, E> en : mRepresentative.entrySet()) {
final E rep = en.getValue();
for (final E member : en.getKey()) {
assert mElementComparator.compare(rep, member) <= 0;
}
}
return true;
}
boolean sanityCheck() {
assert assertRepresentativeMapIsInjective();
return true;
}
private static boolean sanityCheckIntersectPartitionBlocks(final UnionFind uf1, final UnionFind uf2,
final UnionFind result, final HashRelation uf1SplitInfo, final HashRelation uf2SplitInfo) {
/*
* check the split infos: each pair's lhs must be a representative in the corresponding input UnionFind, and the
* rhs must be a representative in the output UnionFind
*/
for (final E l : uf1SplitInfo.getDomain()) {
if (uf1.find(l) != l) {
assert false;
return false;
}
for (final E r : uf1SplitInfo.getImage(l)) {
if (result.find(r) != r) {
assert false;
return false;
}
}
}
for (final E l : uf2SplitInfo.getDomain()) {
if (uf2.find(l) != l) {
assert false;
return false;
}
for (final E r : uf2SplitInfo.getImage(l)) {
if (result.find(r) != r) {
assert false;
return false;
}
}
}
return true;
}
/**
* Asserts a class invariant: mRepresentatives must be injective, i.e., a representative cannot represent two
* equivalence classes
*
* @return
*/
private boolean assertRepresentativeMapIsInjective() {
final Set alreadySeenRepresentatives = new HashSet<>();
for (final E rep : mRepresentative.values()) {
assert !alreadySeenRepresentatives.contains(rep);
alreadySeenRepresentatives.add(rep);
}
return true;
}
}