packages feed

g2-0.2.0.0: src/G2/Data/UnionFind.hs

-- | A union-find data structure.
-- Based on:
-- 
-- A Persistent Union-Find Data Structure
--
-- by Sylvain Conchon and Jean-Cristophe Filliatre

{-# OPTIONS_GHC -fno-warn-orphans -fno-full-laziness #-}

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE InstanceSigs #-}

module G2.Data.UnionFind ( UnionFind
                         , empty
                         , fromList
                         , toList
                         , toSet
                         , union
                         , unionOfUFs
                         , find) where

import Data.Data (Data (..), Typeable)
import Data.Hashable
import qualified Data.HashMap.Lazy as M
import qualified Data.HashSet as S
import Data.IORef
import Data.Semigroup (Semigroup (..))

import System.IO.Unsafe

import Text.Read
import qualified Text.Read.Lex as L
import GHC.Read

import Test.Tasty.QuickCheck

data UnionFind k = UF { rank :: M.HashMap k Int
                      , parent :: IORef (M.HashMap k k) }
                      deriving (Typeable, Data)

{-# NOINLINE empty #-}
-- | A `UnionFind` with nothing unioned. 
empty :: UnionFind k
empty = UF { rank = M.empty
           , parent = unsafePerformIO $ newIORef M.empty }

-- | Build a UnionFind uf, where if k1, k2 are in the same input list,
-- find k1 uf == find k2 uf
fromList :: (Eq k, Hashable k) => [[k]] -> UnionFind k
fromList = foldr unions empty

unions :: (Eq k, Hashable k) => [k] -> UnionFind k -> UnionFind k
unions ks uf = foldr (uncurry union) uf prod
    where prod = [(k1, k2) | k1 <- ks, k2 <- ks]

-- | Convert a `UnionFind` into a list of lists.
-- Elements in the same list in the returned list were unioned in the `UnionFind`. 
toList :: (Eq k, Hashable k) => UnionFind k -> [[k]]
toList = map S.toList . S.toList . toSet

-- | Convert a `UnionFind` into a `S.HashSet` of `S.HashSet`s.
-- Elements in the same `S.HashSet` in the returned `S.HashSet` were unioned in the `UnionFind`. 
toSet :: (Eq k, Hashable k) => UnionFind k -> S.HashSet (S.HashSet k)
toSet uf =
    let
        par = unsafePerformIO $ readIORef (parent uf)
        m = foldr (\k -> M.insertWith S.union (find k uf) $ S.singleton k) M.empty (M.keys par)
    in
    S.fromList . map (\(k, v) -> S.insert k v) $ M.toList m

{-# NOINLINE union #-}
-- | @`union` k1 k2 uf@ unions the keys @k1@ and @k2@ in @uf@.
union :: (Eq k, Hashable k) => k -> k -> UnionFind k -> UnionFind k
union x y h =
    let
        cx = find x h
        cy = find y h
    in
    if cx /= cy
        then
            let
                rx = M.lookupDefault 0 cx (rank h)
                ry = M.lookupDefault 0 cy (rank h)
            in
            if rx > ry
                then unsafePerformIO $ do
                    par_h <- readIORef (parent h)
                    par_h' <- newIORef (M.insert cy cx par_h)
                    return $ h { parent = par_h' } 
            else if rx < ry
                then unsafePerformIO $ do
                    par_h <- readIORef (parent h)
                    par_h' <- newIORef (M.insert cx cy par_h)
                    return $ h { parent = par_h' }
            else unsafePerformIO $ do
                par_h <- readIORef (parent h)
                par_h' <- newIORef (M.insert cy cx par_h)
                return $ h { rank = M.insert cx (rx + 1)  (rank h)
                           , parent = par_h' } 
        else h

-- | Take the union of two `UnionFind`s, by taking the union of any overlapping sets.
{-# NOINLINE unionOfUFs #-}
unionOfUFs :: (Eq k, Hashable k) => UnionFind k -> UnionFind k -> UnionFind k
unionOfUFs uf1 (UF { parent = par }) = unsafePerformIO $ do
    par' <- readIORef par
    return $ M.foldrWithKey union uf1 par'

{-# NOINLINE find #-}
-- | @`find` k uf@ returns the representative of @k@ in @uf@.
find :: (Eq k, Hashable k) => k -> UnionFind k -> k
find x h =
    unsafePerformIO (do
        h_par <- readIORef (parent h)
        let (cx, f) = findAux x h_par
        atomicWriteIORef (parent h) f
        return cx
    )

findAux :: (Eq k, Hashable k) => k -> M.HashMap k k -> (k, M.HashMap k k)
findAux i f =
    let fi = M.lookupDefault i i f in
    if fi == i
        then (i, f)
        else
            let
                (r, f') = findAux fi f
                f'' = M.insert i r f'
            in
            (r, f'')
            
instance (Eq k, Hashable k) => Eq (UnionFind k) where
    x == y = toSet x == toSet y 

instance (Eq k, Hashable k, Show k) => Show (UnionFind k) where
    {-# NOINLINE show #-}
    show uf = "fromList " ++ show (toList uf) 

instance (Eq k, Hashable k, Read k) => Read (UnionFind k) where
    readPrec = parens $
                    do expectP (L.Ident "fromList")
                       x <- step readListPrec
                       return (fromList x)
    readListPrec = readListPrecDefault 

instance (Eq k, Hashable k) => Hashable (UnionFind k) where
    hashWithSalt i = hashWithSalt i . toList

instance (Eq k, Hashable k) => Semigroup (UnionFind k) where
    (<>) = unionOfUFs

instance (Eq k, Hashable k) => Monoid (UnionFind k) where
    mempty = empty

instance (Arbitrary k, Eq k, Hashable k) => Arbitrary (UnionFind k) where
    arbitrary :: (Arbitrary k, Eq k, Hashable k) => Gen (UnionFind k)
    arbitrary = do       
        ks <- arbitrary

        return $ fromList ks

    shrink = map fromList . shrink . toList

-- Hack for compilation
instance Typeable a => Data (IORef a) where
  toConstr _   = error "toConstr"
  gunfold _ _  = error "gunfold"
  dataTypeOf _ = error "dataTypeOf"