packages feed

microecta-0.1.0.0: src/Data/Persistent/UnionFind.hs

{- | Lightweight union-find implementation suitable for nondeterministic search.

Mutable union-find, as in @Data.Equivalence.Monad@, should be faster overall,
but enumeration branches in the list monad need a structure that can be copied
and backtracked cheaply. This module stores parent pointers in an 'IntMap' and
returns updated structures from 'find' and 'union'.
-}
module Data.Persistent.UnionFind (
    UVarGen,
    initUVarGen,
    nextUVar,
    UVar,
    uvarToInt,
    intToUVar,
    UnionFind,
    empty,
    withInitialValues,
    union,
    find,
) where

import Control.Monad.State.Strict (State, execState, get, modify', put, runState)
import Data.Coerce (coerce)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap

----------------------------------------------------------

---------------------------
-------- UVarGen
---------------------------

-- | Fresh supply for enumeration variables.
newtype UVarGen = UVarGen Int
    deriving (Eq, Ord, Show)

-- | Initial variable supply.
initUVarGen :: UVarGen
initUVarGen = UVarGen 0

-- | Allocate one fresh variable and advance the supply.
nextUVar :: UVarGen -> (UVarGen, UVar)
nextUVar (UVarGen n) = (UVarGen (n + 1), UVar n)

---------------------------
-------- UVar
---------------------------

-- | Union-find variable identifier.
newtype UVar = UVar Int
    deriving (Eq, Ord, Show)

-- | Convert a variable to its dense integer id.
uvarToInt :: UVar -> Int
uvarToInt (UVar i) = i

-- | Reconstruct a variable from its dense integer id.
intToUVar :: Int -> UVar
intToUVar = UVar

---------------------------
-------- Union-find data structure
---------------------------

{- | Persistent union-find forest.

Roots store negative set sizes. Non-roots store their parent id.
-}
newtype UnionFind = UnionFind {getUnionFindMap :: IntMap Int}
    deriving (Eq, Ord, Show)

-- | Empty forest. Variables are inserted lazily by 'find'.
empty :: UnionFind
empty = UnionFind IntMap.empty

-- | Forest containing each supplied variable as a singleton set.
withInitialValues :: [UVar] -> UnionFind
withInitialValues uvs = UnionFind $ IntMap.fromList $ map (,-1) $ coerce uvs

---------------------------
-------- Union-find operations
---------------------------

-- | Merge the two variable classes, preferring the larger class as root.
union :: UVar -> UVar -> UnionFind -> UnionFind
union uv1 uv2 uf = flip execState uf $ do
    (uv1Rep, negativeUv1Size) <- findWithNegSize uv1
    (uv2Rep, negativeUv2Size) <- findWithNegSize uv2
    if uv1Rep == uv2Rep
        then
            return ()
        else
            if negativeUv1Size > negativeUv2Size
                then do
                    modify' (coerce (IntMap.insert @Int) uv1Rep uv2Rep)
                    modify' (coerce (IntMap.insert @Int) uv2Rep (negativeUv1Size + negativeUv2Size))
                else do
                    modify' (coerce (IntMap.insert @Int) uv2Rep uv1Rep)
                    modify' (coerce (IntMap.insert @Int) uv1Rep (negativeUv1Size + negativeUv2Size))

findWithNegSize :: UVar -> State UnionFind (UVar, Int)
findWithNegSize uv = do
    m <- get
    case coerce (IntMap.lookup @Int) uv m of
        Nothing -> put (coerce (IntMap.insert @Int) uv (-1 :: Int) m) >> return (uv, -1)
        Just x
            | x < 0 -> return (uv, x)
            | otherwise -> do
                (rep, size) <- findWithNegSize (UVar x)
                put (coerce (IntMap.insert @Int) uv rep m)
                return (rep, size)

-- | Find a variable's representative and return the path-compressed forest.
find :: UVar -> UnionFind -> (UVar, UnionFind)
find uv uf = coerce runState (fst <$> findWithNegSize uv) uf