/* * Disjoint-set data structure - Library (TypeScript) * * Copyright (c) 2022 Project Nayuki. (MIT License) * https://www.nayuki.io/page/disjoint-set-data-structure * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * - The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * - The Software is provided "as is", without warranty of any kind, express or * implied, including but not limited to the warranties of merchantability, * fitness for a particular purpose and noninfringement. In no event shall the * authors or copyright holders be liable for any claim, damages or other * liability, whether in an action of contract, tort or otherwise, arising from, * out of or in connection with the Software or the use or other dealings in the * Software. */ /* * Represents a set of disjoint sets. Also known as the union-find data structure. * Main operations are querying if two elements are in the same set, and merging two sets together. * Useful for testing graph connectivity, and is used in Kruskal's algorithm. */ class DisjointSet { // Global properties private numSets: number = 0; // Per-node property arrays. This representation is more space-efficient than creating one node object per element. private parents: Array = []; // The index of the parent element. An element is a representative iff its parent is itself. private sizes : Array = []; // Positive number if the element is a representative, otherwise zero. // Constructs a new set containing the given number of singleton sets. // For example, new DisjointSet(3) --> {{0}, {1}, {2}}. public constructor(numElems: number) { if (numElems < 0) throw new RangeError("Number of elements must be non-negative"); for (let i = 0; i < numElems; i++) this.addSet(); } // Returns the number of elements among the set of disjoint sets. All the other methods // require the argument elemIndex to satisfy 0 <= elemIndex < getNumElements(). public getNumElements(): number { return this.parents.length; } // Returns the number of disjoint sets overall. 0 <= result <= getNumElements(). public getNumSets(): number { return this.numSets; } // Returns the representative element for the set containing the given element. This method is also // known as "find" in the literature. Also performs path compression, which alters the internal state to // improve the speed of future queries, but has no externally visible effect on the values returned. private getRepr(elemIndex: number): number { if (elemIndex < 0 || elemIndex >= this.parents.length) throw new RangeError("Element index out of bounds"); // Follow parent pointers until we reach a representative let parent: number = this.parents[elemIndex]; while (true) { const grandparent: number = this.parents[parent]; if (grandparent == parent) return parent; this.parents[elemIndex] = grandparent; // Partial path compression elemIndex = parent; parent = grandparent; } } // Returns the size of the set that the given element is a member of. 1 <= result <= getNumElements(). public getSizeOfSet(elemIndex: number): number { return this.sizes[this.getRepr(elemIndex)]; } // Tests whether the given two elements are members of the same set. Note that the arguments are orderless. public areInSameSet(elemIndex0: number, elemIndex1: number): boolean { return this.getRepr(elemIndex0) == this.getRepr(elemIndex1); } // Adds a new singleton set, incrementing getNumElements() and getNumSets(). // Returns the identity of the new element, which equals the old value of getNumElements(). public addSet(): number { const elemIndex: number = this.getNumElements(); this.parents.push(elemIndex); this.sizes.push(1); this.numSets++; return elemIndex; } // Merges together the sets that the given two elements belong to. This method is also known as "union" in the literature. // If the two elements belong to different sets, then the two sets are merged and the method returns true. // Otherwise they belong in the same set, nothing is changed and the method returns false. Note that the arguments are orderless. public mergeSets(elemIndex0: number, elemIndex1: number): boolean { // Get representatives let repr0: number = this.getRepr(elemIndex0); let repr1: number = this.getRepr(elemIndex1); if (repr0 == repr1) return false; // Compare sizes to choose parent node if (this.sizes[repr0] < this.sizes[repr1]) { let temp: number = repr0; repr0 = repr1; repr1 = temp; } // Now repr0's size >= repr1's size // Graft repr1's subtree onto node repr0 this.parents[repr1] = repr0; this.sizes[repr0] += this.sizes[repr1]; this.sizes[repr1] = 0; this.numSets--; return true; } // For unit tests. This detects many but not all invalid data structures, throwing an exception // if a structural invariant is known to be violated. This always returns silently on a valid object. public checkStructure(): void { let numRepr: number = 0; let sizeSum: number = 0; for (let i = 0; i < this.parents.length; i++) { const parent: number = this.parents[i]; const size : number = this.sizes [i]; const isRepr: boolean = parent == i; if (isRepr) numRepr++; let ok: boolean = true; ok = ok && 0 <= parent && parent < this.parents.length; ok = ok && (!isRepr && size == 0 || isRepr && 1 <= size && size <= this.parents.length); if (!ok) throw new Error("Assertion error"); sizeSum += size; } if (!(0 <= this.numSets && this.numSets == numRepr && this.numSets <= this.parents.length && this.parents.length == sizeSum)) throw new Error("Assertion error"); } }