2015 Round 2

Pegman

While brute force would work on the small input, I believe the time spent writing the corresponding code outweighs the time it takes to find a shortcut. Furthermore, the shortcut is easy enough that we’re comfortable without checking our results against those of brute force.

Pegman can never walk off the edge if and only if every arrow points to another arrow.

Thus for each arrow pointing off the board, we try to rotate it so it points to another arrow. This is impossible if and only if no arrows lie in all four directions.

import Data.Array
import Jam

main = jam $ do
  [r, c] <- getints
  a <- listArray ((1, 1), (r, c)) . concat <$> getsn r
  let
    arrows = filter ((/= '.') . snd) $ assocs a
    bads   = filter (uncurry edgy) arrows
    edgy (i, j) '>' = dotty [(i, y) | y <- [j+1..c]]
    edgy (i, j) '<' = dotty [(i, y) | y <- [1..j-1]]
    edgy (i, j) '^' = dotty [(x, j) | x <- [1..i-1]]
    edgy (i, j) 'v' = dotty [(x, j) | x <- [i+1..r]]
    dotty  = all ((== '.') . (a!))
    lone z = all (edgy z) "<>^v"
  pure $ if any lone $ fst <$> bads then "IMPOSSIBLE" else show $ length bads

Kiddie Pool

For one water source we can fill the pool if and only if the source temperature equals the target temperature, in which case it will take v / r time. A similar computation applies if we have two or more water sources, all with the same temperature.

For two water sources with distinct temperatures c0, c1, we can solve equations to determine the volume of water we need from each source:

v0 +    v1    = v
v0*c0 + v1*c1 = v*x

If both volumes are nonnegative, then we can fill the pool, and the minimum time needed is the larger of v0/r0 and v1/r1.

The above are the only cases needed for the small input:

import Control.Monad
import Jam

main = jam $ do
  (_n:_vx) <- words <$> gets
  rcs <- replicateM (read _n) getdbls
  let
    [v, x] = read <$> _vx
    f [[r, c]] | c == x    = show $ v / r
               | otherwise = "IMPOSSIBLE"
    f [[r0, c0], [r1, c1]]
      | c0 == c1           = f [[r0 + r1, c0]]
      | v0 >= 0,   v1 >= 0 = show $ max (v0 / r0) (v1 / r1)
      | otherwise          = "IMPOSSIBLE"
      where
        v0 = v * (x - c1) / (c0 - c1)
        v1 = v * (x - c0) / (c1 - c0)
  pure $ f rcs

For the large input, perhaps it’s easiest to reframe the problem slightly. Instead of turning sources on and off at various times, we will first optionally reduce the rate of flow of any of the water sources, and then we will turn on all sources for the same amount of time.

For example, turning on a water source at the start then turning it off halfway through in the original problem is equivalent to leaving it on the whole time with a halved flow rate in our revised description.

Since all flows are active for the same amount of time, we seek the minimum total flow reduction.

Suppose there is no reduction, that is, we keep the original rates of flow. Then if rs are the rates of flow and cs the corresponding source temperatures, the temperature of the water after t minutes is:

a = sum (zipWith (*) rs cs) / sum rs

That is, the temperature is independent of t. If a equals the target temperature x then this is the best we can do, for we fill the pool at the maximum rate of flow.

Otherwise, we must reduce the rate of flow from at least one of the water sources.

Suppose a < x, that is, the water will be too cold if we simply turn on all water sources. We must reduce the rate of flow from at least one of the sources that is colder than x. But which ones?

Since c0 < c1 implies dr * c0 < dr * c1, we gain the most by reducing the flow of the coldest source. We first consider cutting off the coldest water source completely. If the result is still too cold, then if there are no water sources left, we declare the problem to be impossible; otherwise we remove the coldest source from consideration and repeat.

Otherwise, cutting off the coldest water source yields water of at least temperature x, in which case we can solve an equation to find a flow rate r for it that leads to the pool having the desired temperature.

A symmetric argument applies when a > x.

import Control.Monad
import Data.List
import Data.Ratio
import Jam

agg rcs = sum (zipWith (*) rs cs) / sum rs where
  rs = fst <$> rcs
  cs = snd <$> rcs

conv [r, c] = (approxRational r 0, approxRational c 0)

main = jam $ do
  (_n:_vx) <- words <$> gets
  rcs <- sortOn snd . map conv <$> replicateM (read _n) getdbls
  let
    [v, x] = (`approxRational` 0) . read <$> _vx
    f rcs@(h:t)
      | a == x     = Just $ v / sum (fst <$> rcs)
      | [_] <- rcs = Nothing
      | a <  x, agg t < x = f t
      | a <  x            = Just $ g h (sum $ fst <$> t, agg t)
      | agg i > x         = f (init rcs)
      | otherwise         = Just $ g l (sum $ fst <$> i, agg i)
      where
        a = agg rcs
        i = init rcs
        l = last rcs
    g (_, c0) (r1, c1) = v / (r1 + r1*(x - c1) / (c0 - x))
  pure $ maybe "IMPOSSIBLE" (show . fromRational) $ f rcs

We use Data.Ratio to avoid floating-point woes. For example, the a == x check is susceptible to errors from divisions. There are at most 100 water sources, so working with rationals is tolerable. There are also other inefficiences which would only matter for inputs beyond the given limits.

Bilingual

For the small input, brute force implies checking up to 2^18 subsets which requries some care.

We remove the words common to the first two sentences from all sentences, as these must be words common to both languages. We’ll add their count back later, just before printing the final answer. After this, we only have to operate on sentences with at most ten words.

We replace each word with a unique integer identifier, a form of string interning.

We build the counts map to store the occurrences of each word across both English and French sentences. Thus we can find all bilingual words from a given set of English sentences by comparing the number of times they appear in English against their counts value.

We could use subsequences to enumerate all possible sets of English sentences, but we’re better off writing our own recursion to generate them, because we can incrementally update a map of word counts rather than creating them from scratch for each subset:

import Jam
import Data.List
import Data.Maybe
import qualified Data.Map as M

main = jam $ do
  [n] <- getints
  ss@(es:fs:_) <- map (nub . words) <$> getsn n
  let
    comm = es `intersect` fs
    m = M.fromList $ zip (nub (concat ss) \\ comm) [(0 :: Int)..]
    as@(e:_:t) = mapMaybe (`M.lookup` m) <$> ss
    counts = M.fromListWith (+) . zip (concat as) $ repeat 1
    best n []     = sum [fromEnum $ n M.! w /= counts M.! w | w <- M.keys n]
    best n (x:xs) = min (best n xs) $
      best (foldl' (\m k -> M.insertWith (+) k 1 m) n x) xs
  pure $ show $ (length comm +) $ best (M.fromList $ zip e $ repeat 1) t

We explicitly declare an Int to avoid the overhead incurred by Integer.

The above takes under a minute on my laptop to solve the small input. It turned out subsequences is fast enough, taking about two minutes, but if the worst case were exercised each time, it may have been a little too close for comfort.

The large input was challenging. I thought it could be nicely reduced to set cover or some kind of boolean satisfiability before I eventually realized graph theory was the best fit.

But I still got it wrong. I initially created a node to represent each sentence, and joined two nodes with an edge of weight 1 for every unique word they have in common. Alternatively, we can replace multiple edges with a single edge whose weight is the number of unique words in common. Then I thought the answer was the max-flow or min-cut between the first two nodes: the edges of a min-cut correspond to the words that are members in both languages.

This is false because different edges can represent the same word. We actually need a hypergraph, that is, we must generalize edges so they can comprise of an arbitrary number of vertices. Each sentence corresponds to a node, and each word corresponds to an edge that links the nodes corresponding to the sentences in which it appears. Now the answer is the maximum flow between the first two nodes.

A search online revealed surprisingly little on hypergraph max flow. I found a simple global mincut algorithm for hypergraphs due to Klimnek and Wagner. I was skeptical it worked, because I thought it would imply a deterministic global mincut algorithm for graphs without using max flow. But then I came across the Stoer-Wagner algorithm.

My textbooks from my undergraduate days predate these algorithms, and back then, min-cut was solved with max-flow, and global min-cut solved by trying min-cut for all pairs of vertices. In graduate school, I learned of Karger’s algorithm but failed to realize its significance at the time.

These algorithms are worth remembering, but it looks difficult to adapt a global min-cut algorithms to solve a particular s-t min-cut problem. Searching further, I found an abstract about an efficient hypergraph max flow algorithm by Pistorius and Minoux, but I got the impression this is relatively esoteric research, since I was unable to find a summary of the algorithm on popular websites. Code Jam would hardly require contestants to know of it.

The abstract mentions that the conventional method to solve max flow on hypergraphs is to transform to a graph first. This seems like the approach the Code Jam wanted, so I gave up hoping for some off-the-shelf hypergraph max flow library, and thought about transforming the hypergraph.

My first instinct was to create a node for each word as well as nodes for each sentence, and add an edge of weight 1 between a word and each sentence containing it. This fails because a max flow can involve more than one path going through a word node, while we want at most one path to go through a word node. (This transformation happens to work on the small input though!)

The trick is to use a directed graph, and represent a word with two nodes. For a given word, we call one of its nodes the entry node and the other the exit node. We add an edge from the entry node to the exit node, and for each sentence containing the word, we add an edge from the sentence to the entry node, and also an edge from the exit node node to the sentence node.

At last, the max flow from the first sentence to the second yields the answer.

The limits given in the problem description suggest this approach is feasible, provided we perform the same preprocessing as for the small input, namely, specially handling the words the first two sentences have in common.

import Data.Graph.Inductive.Graph
import Data.Graph.Inductive.PatriciaTree
import Data.Graph.Inductive.Query.MaxFlow
import Data.List
import Data.Maybe
import qualified Data.Map as M
import Jam

main = jam $ do
  [n] <- getints
  ss@(es:fs:_) <- map (nub . words) <$> getsn n
  let
    comm = es `intersect` fs
    m = M.fromList $ zip (nub (concat ss) \\ comm) [n..]
    e2es = [(j, j + M.size m, 1) | j <- M.elems m]
    edges = e2es ++ concat [[(i, j, 1), (j + M.size m, i, 1)] | i <- [0..n - 1],
      j <- mapMaybe (`M.lookup` m) (ss!!i)]
    vn = n + 2 * M.size m
    gr = mkGraph (zip [0..vn-1] $ repeat ()) edges :: Gr () Int
  pure $ show $ length comm + maxFlow gr 0 1

The Data.Graph.Inductive hails from the fgl package, which approaches graph theory from a functional programming perspective.

The above took over 4 minutes to complete the large input on my laptop.

Drum Decorator

A cell has at most 4 neighbours, so each number must lie in the range [1..4].

If a cell contains 4, then all its neighbours must also contain 4. This implies that every cell of the drum contains 4, which is impossible because the cells on the top and bottom only have 3 neighbours. Thus each number must lie in the range [1..3].

Suppose at least one cell contains 3. Then consider one of the topmost cells containing 3. Since no cell above this cell contains a 3, its left, right, and bottom neighbours must all contain 3s. Repeating this argument on the left and right neighbours shows that we must have two rows of cells all consisting of 3s.

Two rows of 3s are valid if and only if no 3s appear directly above or below them, so we have deduced all possible ways for 3s to appear on a drum: they must appear in two consecutive rows consisting of 3s, and be non-adjacent to other rows of 3s.

This leaves 1s and 2s. The 1s are easy: they show up as isolated dominoes, because a 1 must appear next to exactly one other 1. Since C is at least 3, we never have 1s all around the drum.

It remains to describe 2s. Suppose a drum contains at least one 2, and consider one of the topmost 2s. One possiblity is that the whole topmost row consists of 2s, in which case the row below contains no 2s. From above, the only number that may appear directly above or below a band of 2s, is 3.

Otherwise suppose the row contains a digit other than 2, which must a 1. By a case analysis whose details we skip here, we find the only possbilities are repetitions of one of the following three patterns:

222112
112222

122
122

2122
2121
2221

As a sanity check, we can brute force search for valid patterns consisting of 1s and 2s for a given drum size. The following is tolerable provided the total number of cells is 20 or so:

import Data.Array
import Jam

dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]

main = jamLn $ do
  [r, c] <- getints
  let
    valid a = all (\(ij, n) -> (n ==) $ length $ filter (n ==) $ nbrs a ij) $
      assocs a
    nbrs a ij = map (a!) $ filter (goodRow . fst) $ go ij <$> dirs
    go (x, y) (z, w) = (x + z, 1 + (y + w + c - 1) `mod` c)
    goodRow i = i >= 1 && i <= r

    pr a = unlines [concatMap show [a!(i, j) | j <- [1..c]] | i <- [1..r]]

  pure $ unlines $ map pr $ filter valid $
    listArray ((1, 1), (r, c)) <$> sequence (replicate (r * c) [1, 2])

We now have enough to count all possibilities with dynamic programming. Given an array of size (r, c), we can either try to fill the first two rows with 3s, and then fill the remaining rows starting with a non-3 pattern, or we can try to start with a non-3 pattern immediately.

A non-3 pattern is one of:

  • A single row of 2s.

  • If c is a multiple of 6, then two rows of the first pattern above.

  • If c is a multiple of 3, then two rows of the second pattern above.

  • If c is a multiple of 4, then three rows of the third pattern above.

If there are remaining rows, then the next two must be all 3s, and after that we need to start with a non-3 pattern. Thus we have outlined how to recursively count the number of ways of filling in an array starting with a non-3 pattern.

However, there is a complication. I overlooked it initially, and it took me some time to realize my mistake. The following 6x3 drum:

122
122
333
333
122
122

differs from the following 6x3 drum:

122
122
333
333
212
212

but my original solution failed to distinguish between them.

To fix this, we introduce a parameter d to the recursion that, in mathematical parlance, denotes the size of the orbit of the drum under rotation, In other words, the number of different arrays that arise from rotating the drum. Initially the drum is blank, so d starts at 1.

Each pattern has an orbit size. For rows of all 3s or all 2s, this is 1, while those that involve 1s have orbit sizes of 6, 3, and 4.

Then when adding a pattern with orbit size p to a partially filled drum with orbit size d, the new orbit size is lcm d p and there are gcd d p fundamentally different ways to add the pattern.

In Haskell:

import Jam

main = jam $ do
  [r, c] <- getints
  let
    f d 0 = 1
    f d r = g d (r - 1) + h 6 2 + h 3 2 + h 4 3 where
      h p q | c `mod` p > 0 || r < q = 0
            | otherwise              = gcd d p * g (lcm d p) (r - q)

    g _ 0 = 1  -- Done!
    g _ 1 = 0  -- Need 2+ rows for 3 pattern.
    g d r = f d (r - 2)

  pure $ show $ f 1 r + f 1 (r - 2)

This works for the small dataset. For the large dataset, we work modulo 10^9 + 7 and use Data.MemoTrie to memoize, thus reducing the complexity from exponential to polynomial.

A more traditional way is to write:

import Data.MemoTrie
import Jam

red = (`mod` (10^9 + 7))

main = jam $ do
  [r, c] <- getints
  let
    mf d 0 = 1
    mf d r = red $ g d (r - 1) + h 6 2 + h 3 2 + h 4 3 where
      h p q | c `mod` p > 0 || r < q = 0
            | otherwise              = red $ gcd d p * g (lcm d p) (r - q)

    g _ 0 = 1
    g _ 1 = 0
    g d r = f d (r - 2)

    f = memo2 mf

  pure $ show $ red $ f 1 r + f 1 (r - 2)

However, this approach is terrifying. We only get one attempt at the large dataset. If we forget calls to red, then our solution is wrong. Furthermore, there is no way to catch such a bug with the small input.

Perhaps a safer approach is to use Data.Modular, which uses a GHC extension:

{-# LANGUAGE DataKinds #-}
import Data.MemoTrie
import Data.Modular
import Jam

main = jam $ do
  [r, c] <- getints
  let
    mf :: Int -> Int -> Mod Integer 1000000007
    mf d 0 = 1
    mf d r = g d (r - 1) + h 6 2 + h 3 2 + h 4 3 where
      h p q | c `mod` p > 0 || r < q = 0
            | otherwise              = fromIntegral (gcd d p) *
              g (lcm d p) (r - q)

    g _ 0 = 1
    g _ 1 = 0
    g d r = f d (r - 2)

    f = memo2 mf
  pure $ show $ f 1 r + f 1 (r - 2)

There’s a different hair-raising aspect of this code. The modulus must be a constant. Instead of 10^9 + 7, we must write the number in full. Woe betide us if we miss a zero! Fortunately, the problem also shows this constant in full, and we can copy-and-paste it to be on the safe side.


Ben Lynn blynn@cs.stanford.edu 💡