packages feed

assignment-0.0.1.0: Data/Algorithm/Assignment.hs

-- |
-- Module      :  Data.Algorithm.Assignment
-- Copyright   :  © 2024–present Mark Karpov
-- License     :  BSD 3 clause
--
-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- A solution to the assignment problem.
module Data.Algorithm.Assignment
  ( assign,
  )
where

import Control.Monad (forM_, void, when)
import Control.Monad.Fix (fix)
import Control.Monad.ST (ST, runST)
import Data.Array (Array)
import Data.Array.Base qualified as A
import Data.Array.ST (STUArray)
import Data.Array.ST qualified as ST
import Data.STRef (modifySTRef', newSTRef, readSTRef, writeSTRef)

type CostMatrix s = STUArray s (Int, Int) Int

type MarkMatrix s = STUArray s (Int, Int) Char

type CoverageVector s = STUArray s Int Bool

-- | \(\mathcal{O}(n^4)\). Assign elements from two collections to each
-- other so that the total cost is minimal. The cost of each combination is
-- given the by the first argument and it can be negative. If any of the
-- collections is empty the result is the empty list. The sizes of the
-- collections need not to match. Finally, there is no guarantees on the
-- order of elements in the returned list of pairs.
--
-- See: <https://en.wikipedia.org/wiki/Hungarian_algorithm#Matrix_interpretation>
assign ::
  -- | How to calculate the cost
  (a -> b -> Int) ->
  -- | The first collection
  [a] ->
  -- | The second collection
  [b] ->
  -- | The resulting optimal assignment (no guarantees about order)
  [(a, b)]
assign _ [] _ = []
assign _ _ [] = []
assign cost as bs = runST $ do
  let length_a = length as
      length_b = length bs
      aMinBound = 0
      aMaxBound = length_a - 1
      bMinBound = 0
      bMaxBound = length_b - 1
      abMaxBound = max aMaxBound bMaxBound
      asArray = A.listArray (aMinBound, aMaxBound) as
      bsArray = A.listArray (bMinBound, bMaxBound) bs
      matrixBounds = ((aMinBound, bMinBound), (abMaxBound, abMaxBound))
  c <- ST.newArray matrixBounds 0
  m <- ST.newArray matrixBounds noMark
  aCoverage <- ST.newArray (aMinBound, abMaxBound) False
  bCoverage <- ST.newArray (bMinBound, abMaxBound) False
  countFromTo aMinBound aMaxBound $ \i ->
    countFromTo bMinBound bMaxBound $ \j ->
      ST.writeArray c (i, j) (cost (asArray A.! i) (bsArray A.! j))
  if aMaxBound - aMinBound >= bMaxBound - bMinBound
    then normalizePerB c
    else normalizePerA c
  starZeros c m aCoverage bCoverage
  fix $ \recurse0 -> do
    done <- coverZeros m aCoverage bCoverage
    if done
      then recoverResults m asArray bsArray
      else fix $ \recurse1 -> do
        r <- primeUncoveredZero c m aCoverage bCoverage
        case r of
          Nothing -> do
            adjustCosts c aCoverage bCoverage
            recurse1
          Just z0 -> do
            adjustMarks m z0
            clearCoverage aCoverage bCoverage
            recurse0
{-# INLINEABLE assign #-}

normalizePerA :: CostMatrix s -> ST s ()
normalizePerA c = do
  ((aMinBound, bMinBound), (aMaxBound, bMaxBound)) <- ST.getBounds c
  countFromTo aMinBound aMaxBound $ \i -> do
    minValueRef <- newSTRef maxBound
    countFromTo bMinBound bMaxBound $ \j ->
      ST.readArray c (i, j) >>= modifySTRef' minValueRef . min
    minValue <- readSTRef minValueRef
    when (minValue /= 0) $ do
      countFromTo bMinBound bMaxBound $ \j -> do
        ST.modifyArray' c (i, j) (subtract minValue)
{-# INLINE normalizePerA #-}

normalizePerB :: CostMatrix s -> ST s ()
normalizePerB c = do
  ((aMinBound, bMinBound), (aMaxBound, bMaxBound)) <- ST.getBounds c
  countFromTo bMinBound bMaxBound $ \j -> do
    minValueRef <- newSTRef maxBound
    countFromTo aMinBound aMaxBound $ \i ->
      ST.readArray c (i, j) >>= modifySTRef' minValueRef . min
    minValue <- readSTRef minValueRef
    when (minValue /= 0) $ do
      countFromTo aMinBound aMaxBound $ \i -> do
        ST.modifyArray' c (i, j) (subtract minValue)
{-# INLINE normalizePerB #-}

starZeros ::
  CostMatrix s ->
  MarkMatrix s ->
  CoverageVector s ->
  CoverageVector s ->
  ST s ()
starZeros c m aCoverage bCoverage = do
  ((aMinBound, bMinBound), (aMaxBound, bMaxBound)) <- ST.getBounds c
  countFromTo aMinBound aMaxBound $ \i ->
    countFromTo bMinBound bMaxBound $ \j -> do
      x <- ST.readArray c (i, j)
      when (x == 0) $ do
        aCovered <- ST.readArray aCoverage i
        bCovered <- ST.readArray bCoverage j
        when (not aCovered && not bCovered) $ do
          ST.writeArray m (i, j) starMark
          ST.writeArray aCoverage i True
          ST.writeArray bCoverage j True
  clearCoverage aCoverage bCoverage
{-# INLINE starZeros #-}

coverZeros ::
  MarkMatrix s ->
  CoverageVector s ->
  CoverageVector s ->
  ST s Bool
coverZeros m _aCoverage bCoverage = do
  ((aMinBound, bMinBound), (aMaxBound, bMaxBound)) <- ST.getBounds m
  nRef <- newSTRef 0
  countFromTo aMinBound aMaxBound $ \i ->
    countFromTo bMinBound bMaxBound $ \j -> do
      x <- ST.readArray m (i, j)
      bCovered <- ST.readArray bCoverage j
      when (x == starMark && not bCovered) $ do
        ST.writeArray bCoverage j True
        modifySTRef' nRef (+ 1)
  n <- readSTRef nRef
  return (n > aMaxBound)
{-# INLINE coverZeros #-}

recoverResults ::
  MarkMatrix s ->
  Array Int a ->
  Array Int b ->
  ST s [(a, b)]
recoverResults m as bs = do
  ((aMinBound, bMinBound), (aMaxBound, bMaxBound)) <- ST.getBounds m
  resultRef <- newSTRef []
  countFromTo aMinBound aMaxBound $ \i ->
    countFromTo bMinBound bMaxBound $ \j -> do
      x <- ST.readArray m (i, j)
      when (x == starMark) $ do
        case (,) <$> (as A.!? i) <*> (bs A.!? j) of
          Nothing -> return ()
          Just (a, b) -> modifySTRef' resultRef ((a, b) :)
  readSTRef resultRef
{-# INLINE recoverResults #-}

primeUncoveredZero ::
  CostMatrix s ->
  MarkMatrix s ->
  CoverageVector s ->
  CoverageVector s ->
  ST s (Maybe (Int, Int))
primeUncoveredZero c m aCoverage bCoverage = do
  ((aMinBound, bMinBound), (aMaxBound, bMaxBound)) <- ST.getBounds m
  primedRef <- newSTRef Nothing
  void . countFromTo' aMinBound aMaxBound $ \i ->
    countFromTo' bMinBound bMaxBound $ \j -> do
      x <- ST.readArray c (i, j)
      if x == 0
        then do
          aCovered <- ST.readArray aCoverage i
          bCovered <- ST.readArray bCoverage j
          if not aCovered && not bCovered
            then False <$ writeSTRef primedRef (Just (i, j))
            else return True
        else return True
  r <- readSTRef primedRef
  case r of
    Nothing -> return Nothing
    Just (i, j) -> do
      ST.writeArray m (i, j) primeMark
      mj' <- findInA m starMark i
      case mj' of
        Nothing -> return (Just (i, j))
        Just j' -> do
          ST.writeArray aCoverage i True
          ST.writeArray bCoverage j' False
          primeUncoveredZero c m aCoverage bCoverage
{-# INLINEABLE primeUncoveredZero #-}

adjustMarks :: MarkMatrix s -> (Int, Int) -> ST s ()
adjustMarks m z0 = do
  ((aMinBound, bMinBound), (aMaxBound, bMaxBound)) <- ST.getBounds m
  let go (_, j) acc = do
        mi' <- findInB m starMark j
        case mi' of
          Nothing -> return acc
          Just i' -> do
            mj' <- findInA m primeMark i'
            case mj' of
              Nothing -> error "Data.Algorithm.Assignment.adjustMarks"
              Just j' -> go (i', j') ((i', j) : (i', j') : acc)
  path <- go z0 [z0]
  forM_ path $ \(i, j) -> do
    let adjust x =
          if x == starMark
            then noMark
            else starMark
    ST.modifyArray' m (i, j) adjust
  countFromTo aMinBound aMaxBound $ \i ->
    countFromTo bMinBound bMaxBound $ \j -> do
      let resetPrime x =
            if x == primeMark
              then noMark
              else x
      ST.modifyArray' m (i, j) resetPrime
{-# INLINE adjustMarks #-}

adjustCosts ::
  CostMatrix s ->
  CoverageVector s ->
  CoverageVector s ->
  ST s ()
adjustCosts c aCoverage bCoverage = do
  ((aMinBound, bMinBound), (aMaxBound, bMaxBound)) <- ST.getBounds c
  minUncoveredValueRef <- newSTRef maxBound
  countFromTo aMinBound aMaxBound $ \i ->
    countFromTo bMinBound bMaxBound $ \j -> do
      aCovered <- ST.readArray aCoverage i
      bCovered <- ST.readArray bCoverage j
      when (not aCovered && not bCovered) $ do
        ST.readArray c (i, j) >>= modifySTRef' minUncoveredValueRef . min
  minUncoveredValue <- readSTRef minUncoveredValueRef
  countFromTo aMinBound aMaxBound $ \i ->
    countFromTo bMinBound bMaxBound $ \j -> do
      aCovered <- ST.readArray aCoverage i
      bCovered <- ST.readArray bCoverage j
      if not aCovered && not bCovered
        then ST.modifyArray c (i, j) (subtract minUncoveredValue)
        else
          when (aCovered && bCovered) $
            ST.modifyArray c (i, j) (+ minUncoveredValue)
{-# INLINE adjustCosts #-}

clearCoverage ::
  CoverageVector s ->
  CoverageVector s ->
  ST s ()
clearCoverage aCoverage bCoverage = do
  let clearOne v = do
        (from, to) <- ST.getBounds v
        countFromTo from to $ \i ->
          ST.writeArray v i False
  clearOne aCoverage
  clearOne bCoverage
{-# INLINE clearCoverage #-}

findInA ::
  MarkMatrix s ->
  Char ->
  Int ->
  ST s (Maybe Int)
findInA m mark i = do
  ((_aMinBound, bMinBound), (_aMaxBound, bMaxBound)) <- ST.getBounds m
  starredRef <- newSTRef Nothing
  void . countFromTo' bMinBound bMaxBound $ \j -> do
    x <- ST.readArray m (i, j)
    if x == mark
      then False <$ writeSTRef starredRef (Just j)
      else return True
  readSTRef starredRef
{-# INLINE findInA #-}

findInB ::
  MarkMatrix s ->
  Char ->
  Int ->
  ST s (Maybe Int)
findInB m mark j = do
  ((aMinBound, _bMinBound), (aMaxBound, _bMaxBound)) <- ST.getBounds m
  starredRef <- newSTRef Nothing
  void . countFromTo' aMinBound aMaxBound $ \i -> do
    x <- ST.readArray m (i, j)
    if x == mark
      then False <$ writeSTRef starredRef (Just i)
      else return True
  readSTRef starredRef
{-# INLINE findInB #-}

countFromTo :: Int -> Int -> (Int -> ST s ()) -> ST s ()
countFromTo start end action = go start
  where
    go !n = when (n <= end) $ do
      action n
      go (n + 1)
{-# INLINE countFromTo #-}

countFromTo' :: Int -> Int -> (Int -> ST s Bool) -> ST s Bool
countFromTo' start end action = go start
  where
    go !n =
      if n <= end
        then do
          r <- action n
          if r then go (n + 1) else return False
        else return True
{-# INLINE countFromTo' #-}

noMark, starMark, primeMark :: Char
noMark = 'n'
starMark = 's'
primeMark = 'p'