union-find-ts
Version:
An immutable Union-Find structure.
127 lines (126 loc) • 4.79 kB
JavaScript
import { repeat, tail, prepend, groupBy, values, clone } from 'ramda';
const lenGtOne = (x) => x.length > 1;
/**
* Group the items in a UF structure according to the components built using `link()`.
* @param uf The UnionFind to group
* @returns A list of lists, each one of which is a component in [uf].
*/
export const toGroups = (uf) => values(groupBy(item => `${find(uf, uf.map(item))}`, tail(uf.items)));
/**
*
* @param uf A component map
* @returns The groups that are greater than size one.
*/
export const toConnectedGroups = (uf) => toGroups(uf).filter(lenGtOne);
/**
* Determines whether the item has been grouped yet (might be itself)
* @param uf A Component Map
* @param num Item number in the component
* @returns true if the roots object has a nonzero value for this item.
*/
export const hasGroup = (uf, num) => uf.roots[typeof num === 'number' ? num : uf.map(num)] !== 0;
/**
* Build an array of size [topNumber + 1] filled with numbers ascending 0 to topNumber.
* @param topNumber The last index of the array to create.
* @returns An array of size [topNumber + 1] filled with numbers ascending 0 to topNumber.
*/
const sequenceArray = (topNumber) => [...Array(topNumber + 1).keys()];
/**
* Build an array of length [topIndex + 1], full of zeroes.
* @param topIndex the last index of the array. Created array will have length == [topIndex + 1]
* @returns An array of length [topIndex + 1], full of zeroes.
*/
const zeroesArray = (topIndex) => repeat(0, topIndex + 1);
export function unionFind(itemsOrSize, map, linker) {
const items = (typeof itemsOrSize === 'number'
? sequenceArray(itemsOrSize)
: prepend(null, itemsOrSize));
const uf1 = {
items,
map,
ranks: zeroesArray(items.length - 1),
roots: zeroesArray(items.length - 1)
};
return linker
? flatten(typeof linker === 'function'
? tail(items).reduce((uf, item) => {
return linkItemAll(uf, item, linker(item));
}, uf1)
: linker.reduce((uf, [left, right]) => linkItem(uf, left, right), uf1))
: uf1;
}
/**
* Copy the given list, putting [val] at index [idx] in replacement of whatever was there.
* @param list
* @param idx
* @param val
* @returns
*/
const replaceAt = (list, idx, val) => [...list.slice(0, idx), val, ...list.slice(idx + 1)];
/**
* Gives the item a group number equal to itself, in the event that it
* is undefined (has not been linked.)
* @param roots roots object from a UnionFind
* @param item item ordinal
* @returns The value at roots[item] or else [item], if roots[item] is zero
*/
const rootOf = (roots, item) => (roots[item] === 0 ? item : roots[item]);
/**
* Find group of item in the given UnionFind.
* @param param0 UnionFind object
* @param item Item to find group.
* @returns Group number of [item]
*/
export const find = ({ roots }, item) => rootOf(roots, item) === item ? item : find({ roots }, rootOf(roots, item));
export const findItem = (uf, item) => find(uf, uf.map(item));
/**
* Remap the component names in the UnionFind, so that each item directly
* references its component.
* @param uf
* @returns an equivalent UnionFind with its paths shortened.
*/
const flatten = (uf, { roots } = uf) => ({ ...uf, roots: roots.map(it => find(uf, it)) });
/**
* When linkAll is called with an empty array, we simply define the
* one item in order to mark it later.
* @param uf
* @param item
* @returns
*/
function defineOne(uf, item) {
return {
...uf,
ranks: replaceAt(uf.ranks, item, item)
};
}
export function link(uf, left, right, leftComponent = find(uf, left), rightComponent = find(uf, right), { roots, ranks } = uf) {
if (leftComponent === rightComponent) {
return uf;
}
const newRoots = clone(roots);
const newRanks = clone(ranks);
// you want to set both components to the lower ranked of the two. If equal we will choose the right.
const rankComparison = ranks[leftComponent] - ranks[rightComponent];
const newParent = rankComparison < 0 ? leftComponent : rightComponent;
const setParent = (idx) => {
newRoots[idx] = newParent;
};
[left, right, leftComponent, rightComponent].forEach(setParent);
if (rankComparison === 0) {
newRanks[newParent]++;
}
return {
...uf,
roots: newRoots,
ranks: newRanks
};
}
export const linkItem = (uf, left, right) => link(uf, uf.map(left), uf.map(right));
export const linkItemAll = (uf, left, right) => {
return linkAll(uf, uf.map(left), right.map(uf.map));
};
export const linkAll = (uf, left, right) => {
return right.length === 0
? defineOne(uf, left)
: right.reduce((uf1, right1) => link(uf1, left, right1), uf);
};