I don’t recall I actually had much time left for round 2’s problem D “Retiling”. More specifically: I did read all of the statements, but this one fell very accurately in my area of non-expertise; so it’s wasn’t prioritized too high.
We are given two bichromatic grid of squares, and want to find a minimum-cost transition from one to the other. The two available operations are:
- flip a tile (cost F)
- exchange two adjacent tiles without flipping them (cost S for “swap”)
By repeatedly exchanging adjacent tiles, we can generalize the operation to “exchange any two tiles for cost S times their Manhattan distance”.
Zooming in a bit, any given tile is either solved or in need of flipping. Whether that flipping is realized with an F or some combination of S operation is what we have to determine.
The S operation preserves the color distribution, so if it has to be changed, flips are unavoidable.
For any unsolved tile, we have to choose between flipping it directly, or finding a close-by compatible tile to exchange it with. What’s a compatible tile? One that also needs flipping, but in the other direction.
So we can partition all unsolved tiles by wanted flip direction, and seek a minimal-cost matching between the both. Both sets necessarily have the same cardinality, so we can complete the smaller one with a few “just flip” counterparts.
Actually, if flips are cheap and exchanges expensive, there’s no saying we want to match at all costs. So we either add “just flip” counterparts to both sets, or play on the cost function to decide on its own whether a long exchange or a double flip is preferable. It’s a constant-time decision per pair, it’s affordable.
The matching can be performed with our old friend the hungarian algorithm. Complexity is fuzzy, somewhere between O(N3) and O(N4) according to wikipedia.1 Input size happens to be at most 100, so it should be ok.
Let’s implement.
main :: IO ()
= do
main <- readLn :: IO Int
t 1..t] $ \i -> do
forM_ [<- map read . words <$> getLine
[r,_c,f,s] <- replicateM r getLine
src <- replicateM r getLine
dst putStrLn $ "Case #" ++ show i ++ ": " ++ show (solve f s src dst)
The main
wrapper is nothing too interesting, just read
the input, convert the integers and delegate the meat to the
solve
function.
solve :: Int -> Int -> [String] -> [String] -> Int
= cost
solve f s src dst where
The solve
workhorse is a bit of a wrapper itself. First
we’ll identify the tiling positions that need some change.
delta :: [[Bool]]
= (zipWith . zipWith) (/=) src dst delta
Next we’ll partition into those who start as a G and those who start as an M. The pipeline is a bit more convoluted than I’d like because it takes care of assigning coordinates on-the-fly
ms :: Set (Int,Int)
gs,= bimap (Set.fromList . map fst) (Set.fromList . map fst) $
(gs,ms) == 'G') . snd) $ concat $
partition ((zipWith3 (\i srcRow dstRow ->
-> ((i,j),se) <$ guard de)
mapMaybe (\(j,se,de) zip3 [0..] srcRow dstRow))
(0..] src delta [
Then construct a cost map of pairs from them and hand it to the
hungarian algorithm. To ensure every tile has a counterpart without too
many if
s no matter how they distribute initially, I’m
simply offering a “deactivated” version: the cost function will
recognize two “active” opposite tiles and pair them if it makes sense,
ignore two “inactive” opposite tiles, and consider an active-inactive
match as an unconditional flip.
= munkresI (Set.disjointUnion ms gs) (Set.disjointUnion gs ms) $
(cost,_) curry $ \ case (Right g,Right m) -> min (dist g m * s) (2*f)
Left _,Left _) -> 0
(-> f _
I’m skipping the hungarian algorithm itself, because in its current version it’s quite long, and will actually warrant a post of its own.
So there we have it. A solution to that remaining problem from round 2. It’s there with the others in the usual place on GitHub. See you soon for the round recap!
And my implementation uses maps and sets, so there’s probably a few O(lgN) thrown in for good measure.↩︎