// 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(); } } }