// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.DataStructure
{
using System;
using System.Collections.Generic;
using ProtoBuf;
using UnityEngine;
using WallstopStudios.UnityHelpers.Utils;
///
/// A disjoint-set (union-find) data structure with path compression and union by rank.
/// Essential for determining connectivity in graphs, procedural generation (maze/terrain),
/// and grouping/clustering algorithms. Near-constant time O(α(n)) operations where α is
/// the inverse Ackermann function. Works with integer indices for maximum performance.
///
///
///
///
[Serializable]
[ProtoContract]
public sealed class DisjointSet
{
[SerializeField]
[ProtoMember(1)]
private int[] _parent = Array.Empty();
[SerializeField]
[ProtoMember(2)]
private int[] _rank = Array.Empty();
[SerializeField]
[ProtoMember(3)]
private int _setCount;
///
/// Gets the number of elements in the disjoint set.
///
public int Count => _parent.Length;
///
/// Gets the number of distinct sets.
///
public int SetCount => _setCount;
private DisjointSet() { }
///
/// Constructs a disjoint set with n elements, each in its own set.
///
/// The number of elements.
public DisjointSet(int n)
{
if (n <= 0)
{
throw new ArgumentOutOfRangeException(nameof(n), "Count must be positive.");
}
_parent = new int[n];
_rank = new int[n];
_setCount = n;
for (int i = 0; i < n; i++)
{
_parent[i] = i;
_rank[i] = 0;
}
}
///
/// Attempts to find the representative (root) of the set containing element x.
/// Uses path compression for optimization.
///
/// The element to find.
/// The representative if found.
/// True if x is valid, false otherwise.
public bool TryFind(int x, out int representative)
{
if (x < 0 || x >= _parent.Length)
{
representative = -1;
return false;
}
// Path compression
if (_parent[x] != x)
{
_parent[x] = TryFindInternal(x);
}
representative = _parent[x];
return true;
}
private int TryFindInternal(int x)
{
if (_parent[x] != x)
{
_parent[x] = TryFindInternal(_parent[x]);
}
return _parent[x];
}
///
/// Attempts to union the sets containing elements x and y.
/// Uses union by rank for optimization.
///
/// First element.
/// Second element.
/// True if successfully unioned (sets were different), false if invalid indices or already in same set.
public bool TryUnion(int x, int y)
{
if (!TryFind(x, out int rootX) || !TryFind(y, out int rootY))
{
return false;
}
if (rootX == rootY)
{
return false; // Already in same set
}
// Union by rank
if (_rank[rootX] < _rank[rootY])
{
_parent[rootX] = rootY;
}
else if (_rank[rootX] > _rank[rootY])
{
_parent[rootY] = rootX;
}
else
{
_parent[rootY] = rootX;
_rank[rootX]++;
}
_setCount--;
return true;
}
///
/// Attempts to check if elements x and y are in the same set.
///
/// First element.
/// Second element.
/// True if connected, false otherwise.
/// True if both indices are valid, false otherwise.
public bool TryIsConnected(int x, int y, out bool connected)
{
if (!TryFind(x, out int rootX) || !TryFind(y, out int rootY))
{
connected = false;
return false;
}
connected = rootX == rootY;
return true;
}
///
/// Attempts to get the size of the set containing element x.
///
public bool TryGetSetSize(int x, out int size)
{
if (!TryFind(x, out int root))
{
size = 0;
return false;
}
size = 0;
for (int i = 0; i < _parent.Length; i++)
{
if (TryFind(i, out int currentRoot) && currentRoot == root)
{
size++;
}
}
return true;
}
///
/// Populates the provided list with all elements in the same set as element x.
/// Clears the list before adding. Returns the same list for chaining.
/// Returns null if x is invalid.
///
public List TryGetSet(int x, List results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
results.Clear();
if (!TryFind(x, out int root))
{
return results;
}
for (int i = 0; i < _parent.Length; i++)
{
if (TryFind(i, out int currentRoot) && currentRoot == root)
{
results.Add(i);
}
}
return results;
}
///
/// Populates the provided list with all distinct sets.
/// Clears the list before adding. Returns the same list for chaining.
/// Uses pooled temporary dictionary to avoid allocations.
///
public List> TryGetAllSets(List> results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
using PooledResource>> reuseStackResource = Buffers<
List
>.Stack.Get(out Stack> reuseStack);
foreach (List existing in results)
{
if (existing != null)
{
existing.Clear();
reuseStack.Push(existing);
}
}
results.Clear();
using PooledResource>> dictResource = DictionaryBuffer<
int,
List
>.Dictionary.Get(out Dictionary> setMap);
using PooledResource>>> scratchLeaseResource = Buffers<
PooledResource>
>.List.Get(out List>> scratchLeases);
for (int i = 0; i < _parent.Length; i++)
{
if (!TryFind(i, out int root))
{
continue;
}
if (!setMap.TryGetValue(root, out List scratch))
{
PooledResource> lease = Buffers.List.Get(out scratch);
scratchLeases.Add(lease);
setMap[root] = scratch;
}
scratch.Add(i);
}
foreach (List scratch in setMap.Values)
{
if (!reuseStack.TryPop(out List destination))
{
destination = new List(scratch.Count);
}
else
{
destination.Clear();
if (destination.Capacity < scratch.Count)
{
destination.Capacity = scratch.Count;
}
}
destination.AddRange(scratch);
results.Add(destination);
}
for (int i = 0; i < scratchLeases.Count; ++i)
{
scratchLeases[i].Dispose();
}
return results;
}
///
/// Resets the disjoint set so each element is in its own set.
///
public void Reset()
{
for (int i = 0; i < _parent.Length; i++)
{
_parent[i] = i;
_rank[i] = 0;
}
_setCount = _parent.Length;
}
}
///
/// A generic disjoint-set (union-find) data structure that maps elements of type T to indices.
/// Provides the same performance as with support for any element type.
/// Uses a dictionary to map elements to internal indices.
///
///
/// rooms = new DisjointSet(new[] { "Hall", "Kitchen", "Library" });
/// rooms.TryUnion("Hall", "Kitchen");
/// rooms.TryIsConnected("Hall", "Library", out bool linked);
/// ]]>
///
[Serializable]
public sealed class DisjointSet
{
private readonly DisjointSet _disjointSet;
private readonly Dictionary _elementToIndex;
private readonly List _indexToElement;
private readonly IEqualityComparer _comparer;
///
/// Gets the number of elements in the disjoint set.
///
public int Count => _disjointSet.Count;
///
/// Gets the number of distinct sets.
///
public int SetCount => _disjointSet.SetCount;
///
/// Constructs a disjoint set from a collection of elements.
///
public DisjointSet(IEnumerable elements, IEqualityComparer comparer = null)
{
if (elements == null)
{
throw new ArgumentNullException(nameof(elements));
}
_comparer = comparer ?? EqualityComparer.Default;
_elementToIndex = new Dictionary(_comparer);
_indexToElement = new List();
foreach (T element in elements)
{
if (_elementToIndex.TryAdd(element, _indexToElement.Count))
{
_indexToElement.Add(element);
}
}
if (_indexToElement.Count == 0)
{
throw new ArgumentException(
"Collection must contain at least one element.",
nameof(elements)
);
}
_disjointSet = new DisjointSet(_indexToElement.Count);
}
///
/// Attempts to find the representative element of the set containing x.
///
public bool TryFind(T x, out T representative)
{
if (!_elementToIndex.TryGetValue(x, out int index))
{
representative = default;
return false;
}
if (!_disjointSet.TryFind(index, out int rootIndex))
{
representative = default;
return false;
}
representative = _indexToElement[rootIndex];
return true;
}
///
/// Attempts to union the sets containing elements x and y.
///
/// True if the sets were unioned, false if invalid elements or already in same set.
public bool TryUnion(T x, T y)
{
if (!_elementToIndex.TryGetValue(x, out int indexX))
{
return false;
}
if (!_elementToIndex.TryGetValue(y, out int indexY))
{
return false;
}
return _disjointSet.TryUnion(indexX, indexY);
}
///
/// Attempts to check if elements x and y are in the same set.
///
public bool TryIsConnected(T x, T y, out bool connected)
{
if (!_elementToIndex.TryGetValue(x, out int indexX))
{
connected = false;
return false;
}
if (!_elementToIndex.TryGetValue(y, out int indexY))
{
connected = false;
return false;
}
return _disjointSet.TryIsConnected(indexX, indexY, out connected);
}
///
/// Attempts to get the size of the set containing element x.
///
public bool TryGetSetSize(T x, out int size)
{
if (!_elementToIndex.TryGetValue(x, out int index))
{
size = 0;
return false;
}
return _disjointSet.TryGetSetSize(index, out size);
}
///
/// Populates the provided list with all elements in the same set as x.
/// Clears the list before adding. Returns the same list for chaining.
/// Returns null if x is invalid.
///
public List TryGetSet(T x, List results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
results.Clear();
if (!_elementToIndex.TryGetValue(x, out int index))
{
return results;
}
using PooledResource> listResource = Buffers.List.Get(
out List indices
);
foreach (int i in _disjointSet.TryGetSet(index, indices))
{
results.Add(_indexToElement[i]);
}
return results;
}
///
/// Populates the provided list with all distinct sets.
/// Clears the list before adding. Returns the same list for chaining.
///
public List> TryGetAllSets(List> results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
using PooledResource>> stackResource = Buffers>.Stack.Get(
out Stack> stack
);
foreach (List input in results)
{
if (input != null)
{
input.Clear();
stack.Push(input);
}
}
results.Clear();
using PooledResource>> listResource = Buffers>.List.Get(
out List> indexSets
);
foreach (List indexSet in _disjointSet.TryGetAllSets(indexSets))
{
if (!stack.TryPop(out List elementSet))
{
elementSet = new List(indexSet.Count);
}
else
{
elementSet.Clear();
if (elementSet.Capacity < indexSet.Count)
{
elementSet.Capacity = indexSet.Count;
}
}
foreach (int i in indexSet)
{
elementSet.Add(_indexToElement[i]);
}
results.Add(elementSet);
}
return results;
}
///
/// Resets the disjoint set so each element is in its own set.
///
public void Reset()
{
_disjointSet.Reset();
}
}
}