We can also think of this in terms of two more fundamental operations:
The simplest possible approach would be to use an array of booleans; then marking a slot full is trivial, and finding the rightmost empty slot before index can be done with a linear scan leftwards from . But the challenge is this:
Can you think of a data structure to support both operations in time or better?
You can think of this in either functional or imperative terms. I know of two solutions, which I’ll share in a subsequent post, but I’m curious to see what people will come up with.
Note that in my scenario, slots never become empty again after becoming full. As an extra challenge, what if we relax this to allow setting slots arbitrarily?
]]>egcd
. In this post, I consider the problem of solving modular equivalences, building on code from the previous post.
A linear congruence is a modular equivalence of the form
.
Let’s write a function to solve such equivalences for . We want a pair of integers and such that is a solution to if and only if . This isn’t hard to write in the end, but takes a little bit of thought to do it properly.
First of all, if and are relatively prime (that is, ) then we know from the last post that has an inverse modulo ; multiplying both sides by yields the solution .
OK, but what if ? In this case there might not even be any solutions. For example, has no solutions: any even number will be equivalent to or modulo , so there is no value of such that double it will be equivalent to . On the other hand, is OK: this will be true for any odd value of , that is, . In fact, it is easy to see that any common divisor of and must also divide in order to have any solutions. In case the GCD of and does divide , we can simply divide through by the GCD (including dividing the modulus !) and then solve the resulting equivalence.
-- solveMod a b m solves ax = b (mod m), returning a pair (y,k) (with
-- 0 <= y < k) such that x is a solution iff x = y (mod k).
solveMod :: Integer -> Integer -> Integer -> Maybe (Integer, Integer)
solveMod a b m
| g == 1 = Just ((b * inverse m a) `mod` m, m)
| b `mod` g == 0 = solveMod (a `div` g) (b `div` g) (m `div` g)
| otherwise = Nothing
where
g = gcd a m
In its most basic form, the Chinese remainder theorem (CRT) says that if we have a system of two modular equations
then as long as and are relatively prime, there is a unique solution for modulo the product ; that is, the system of two equations is equivalent to a single equation of the form
We first compute the Bézout coefficients and such that using egcd
, and then compute the solution as . Indeed,
and hence ; similarly .
However, this is not quite general enough: we want to still be able to say something useful even if . I won’t go through the whole proof, but it turns out that there is a solution if and only if , and we can just divide everything through by , as we did for solving linear congruences. Here’s the code:
-- gcrt2 (a,n) (b,m) solves the pair of modular equations
--
-- x = a (mod n)
-- x = b (mod m)
--
-- It returns a pair (c, k) such that all solutions for x satisfy x =
-- c (mod k), that is, solutions are of the form x = kt + c for
-- integer t.
gcrt2 :: (Integer, Integer) -> (Integer, Integer) -> Maybe (Integer, Integer)
gcrt2 (a,n) (b,m)
| a `mod` g == b `mod` g = Just (((a*v*m + b*u*n) `div` g) `mod` k, k)
| otherwise = Nothing
where
(g,u,v) = egcd n m
k = (m*n) `div` g
From here we can bootstrap ourselves into solving systems of more than two equations, by iteratively combining two equations into one.
-- gcrt solves a system of modular equations. Each equation x = a
-- (mod n) is given as a pair (a,n). Returns a pair (z, k) such that
-- solutions for x satisfy x = z (mod k), that is, solutions are of
-- the form x = kt + z for integer t.
gcrt :: [(Integer, Integer)] -> Maybe (Integer, Integer)
gcrt [] = Nothing
gcrt [e] = Just e
gcrt (e1:e2:es) = gcrt2 e1 e2 >>= \e -> gcrt (e:es)
And here are a bunch of problems for you to practice!
]]>.
You might like to pause for a minute to think about how you would prove this! Of course, how you prove it depends on how you define , so you might like to think about that too.
The book expected them to do a proof by cases, with some sort of case split on the order of , , and . What they turned in was mostly pretty good, actually, but while grading it I became disgusted with the whole thing and thought there has to be a better way.
I was reminded of an example of Dijkstra’s that I remember reading. So I asked myself—what would Dijkstra do? The thing I remember reading may have, in fact, been this exact proof, but I couldn’t remember any details and I still can’t find it now, so I had to (re-)work out the details, guided only by some vague intuitions.
Dijkstra would certainly advocate proving associativity of using a calculational approach. Dijkstra would also advocate using a symmetric infix operator symbol for a commutative and associative operation, so let’s adopt the symbol for . ( would also be a reasonable choice, though I find it less mnemonic.)
How can we calculate with ? We have to come up with some way to characterize it that allows us to transform expressions involving into something else more fundamental. The most obvious definition would be “ if , and otherwise”. However, although this is a fantastic implementation of if you actually want to run it, it is not so great for reasoning about , precisely because it involves doing a case split on whether . This is the definition that leads to the ugly proof by cases.
How else could we define it? The usual more mathematically sophisticated way to define it would be as a greatest lower bound, that is, “ if and only if and and is the greatest such number, that is, for any other such that and , we have .” However, this is a bit roundabout and also not so conducive to calculation.
My first epiphany was that the best way to characterize is by its relationship to . After one or two abortive attempts, I hit upon the right idea:
That is, an arbitrary is less than or equal to the minimum of and precisely when it is less than or equal to both. In fact, this completely characterizes , and is equivalent to the second definition given above.^{1} (You should try convincing yourself of this!)
But how do we get anywhere from by itself? We need to somehow introduce a thing which is less than or equal to it, so we can apply our characterization. My second epiphany was that equality of real numbers can also be characterized by having the same “downsets”, i.e. two real numbers are equal if and only if the sets of real numbers less than or equal to them are the same. That is,
Now the proof almost writes itself. Let be arbitrary; we calculate as follows:
Of course this uses our characterization of via its relationship to , along with the fact that is associative. Since we have proven that if and only if for arbitrary , therefore .
As a brief aside, to be good at competitive programming it’s not enough to have a library of code at your disposal (though it certainly helps!). You must also deeply understand the code in your library and how it works—so that you know when it is applicable, what the potential pitfalls are, how to debug when things don’t work, and how to make modifications to the code to fit some new problem. I will try to explain all the code I exhibit here—why, and not just how, it works. But you’ll also ultimately be better off if you write your own code rather than using mine! Read my explanations for ideas, and then go see if you can replicate the functionality you need.
We start with a simple implementation of modular exponentiation, that is, computing , via repeated squaring. This comes up occasionally in both number theory problems (unsurprisingly) and combinatorics problems (because such problems often ask for a very large answer to be given modulo or some other large prime).
This works via the recurrence
and using the fact that taking the remainder commutes with multiplication.
modexp :: Integer -> Integer -> Integer -> Integer
modexp _ 0 _ = 1
modexp b e m
| even e = (r*r) `mod` m
| otherwise = (b*r*r) `mod` m
where
r = modexp b (e `div` 2) m
This could probably be slightly optimized, but it’s hardly worth it; since the number of multiplications performed is proportional to the logarithm of the exponent, it’s pretty much instantaneous for any inputs that would be used in practice.
However, there’s another technique, obvious in retrospect, that I have recently discovered. Many competitive programming problems ask you to compute the answer modulo some fixed number (usually a large prime). In this context, all arithmetic operations are going to be carried out modulo the same value. With Haskell’s great facilities for cheap abstraction it makes perfect sense to write something like this:
m :: Integer
m = 10^9 + 7 -- or whatever the modulus is supposed to be
-- Make a newtype for integers mod m
newtype M = M { unM :: Integer }
deriving (Eq, Ord)
instance Show M where show = show . unM
-- Do all arithmetic operations mod m
instance Num M where
fromInteger n = M (n `mod` m)
(M a) + (M b) = M ((a + b) `mod` m)
(M a) - (M b) = M ((a - b) `mod` m)
(M a) * (M b) = M ((a * b) `mod` m)
abs = undefined -- make the warnings stop
signum = undefined
The fun thing is that now the normal exponentiation operator (^)
does modular exponentiation for free! It is implemented using repeated squaring so it’s quite efficient. You can now write all your code using the M
type with normal arithmetic operations, and it will all be carried out mod m
automatically.
Here are a couple problems for you to try:
Beyond modular exponentiation, the workhorse of many number theory problems is the extended Euclidean Algorithm. It not only computes the GCD of and , but also computes and such that (which are guaranteed to exist by Bezout’s identity).
First, let’s recall how to compute the GCD via Euclid’s Algorithm:
gcd a 0 = abs a
gcd a b = gcd b (a `mod` b)
I won’t explain how this works here; you can go read about it at the link above, and it is well-covered elsewhere. But let’s think how we would find appropriate values and at the same time. Suppose the recursive call gcd b (a `mod` b)
, in addition to returning the greatest common divisor , were to also return values and such that . Then our goal is to find and such that , which we can compute as follows:
Hence and . Note the key step of writing : If we take the integer quotient of divided by and then multiply by again, we don’t necessarily get back exactly, but what we do get is the next smaller multiple of . Subtracting this from the original gives .
-- egcd a b = (g,x,y)
-- g is the gcd of a and b, and ax + by = g
egcd :: Integer -> Integer -> (Integer, Integer, Integer)
egcd a 0 = (abs a, signum a, 0)
egcd a b = (g, y, x - (a `div` b) * y)
where
(g,x,y) = egcd b (a `mod` b)
Finally, egcd
allows us to find modular inverses. The modular inverse of is a number such that , which will exist as long as : in that case, by Bezout’s identity, there exist and such that , and hence (since ). So is the desired modular inverse of .
-- inverse m a is the multiplicative inverse of a mod m.
inverse :: Integer -> Integer -> Integer
inverse m a = y `mod` m
where
(_,_,y) = egcd m a
Of course, this assumes that and are relatively prime; if not it will silently give a bogus answer. If you’re concerned about that you could check that the returned GCD is 1 and throw an error otherwise.
And here are a few problems for you to try!
In part 2 I’ll consider the task of solving modular equations.
]]>A few imports first:
import Control.Arrow
import Data.List (group, sort)
import Data.Map (Map)
import qualified Data.Map as M
We start with a basic definition of the list of primes, made with a simple recursive sieve, but with one very big optimization: when we find a prime , instead of simply filtering out all the multiples of in the rest of the list, we first take all the numbers less than and pass them through without testing; composite numbers less than would have already been filtered out by a smaller prime.
primes :: [Integer]
primes = 2 : sieve primes [3..]
where
sieve (p:ps) xs =
let (h,t) = span (< p*p) xs
in h ++ sieve ps (filter ((/=0).(`mod`p)) t)
I got this code from the Haskell wiki page on prime numbers. On my machine this allows us to find all the primes up to one million in about 4 seconds. Not blazing fast by any means, and of course this is not actually a true sieve—but it’s short, relatively easy to remember, and works just fine for many purposes. (There are some competitive programming problems requiring a true sieve, but I typically solve those in Java. Maybe someday I will figure out a concise way to solve them in Haskell.)
Now that we have our list of primes, we can write a function to find prime factorizations:
listFactors :: Integer -> [Integer]
listFactors = go primes
where
go _ 1 = []
go (p:ps) n
| p*p > n = [n]
| n `mod` p == 0 = p : go (p:ps) (n `div` p)
| otherwise = go ps n
This is relatively straightforward. Note how we stop when the next prime is greater than the square root of the number being tested, because if there were a prime factor we would have already found it by that point.
Finally we can use listFactors
to build a few other useful functions:
factor :: Integer -> Map Integer Int
factor = listFactors >>> group >>> map (head &&& length) >>> M.fromList
divisors :: Integer -> [Integer]
divisors = factor >>> M.assocs >>> map (\(p,k) -> take (k+1) (iterate (*p) 1))
>>> sequence >>> map product
totient :: Integer -> Integer
totient = factor >>> M.assocs >>> map (\(p,k) -> p^(k-1) * (p-1)) >>> product
factor
yields a Map
whose keys are unique primes and whose values are the corresponding powers; for example, factor 600 = M.fromList [(2,3), (3,1), (5,2)]
, corresponding to the factorization . It works by grouping together like prime factors (note that listFactors
guarantees to generate a sorted list of prime factors), counting each group, and building a Map
.
divisors n
generates a list of all divisors of n
. It works by generating all powers of each prime from up to , and combining them in all possible ways using sequence
. Note it does not guarantee to generate the divisors in order.
totient
implements the Euler totient function: totient n
says how many numbers from 1
to n
are relatively prime to n
. To understand how it works, see this series of four blog posts I wrote on my other blog: part 1, part 2, part 3, part 4.
Here are a few problems for you to try (ordered roughly from easier to more difficult):
In a subsequent post I’ll continue on the number theory theme and talk about modular arithmetic.
]]>I recently learned of an entirely different algorithm for achieving the same result. (In fact, I learned of it when I gave this problem on an exam and a student came up with an unexpected solution!) This solution does not use a divide-and-conquer approach at all, but hinges on a clever data structure.
Suppose we have a bag of values (i.e. a collection where duplicates are allowed) on which we can perform the following two operations:
We’ll call the second operation a rank query because it really amounts to finding the rank or index of a given value in the bag—how many values are greater than it (and thus how many are less than or equal to it)?
If we can do these two operations in logarithmic time (i.e. logarithmic in the number of values in the bag), then we can count inversions in time. Can you see how before reading on? You might also like to think about how we could actually implement a data structure that supports these operations.
So, let’s see how to use a bag with logarithmic insertion and rank queries to count inversions. Start with an empty bag. For each element in the sequence, see how many things in the bag are strictly greater than it, and add this count to a running total; then insert the element into the bag, and repeat with the next element. That is, for each element we compute the number of inversions of which it is the right end, by counting how many elements that came before it (and are hence in the bag already) are strictly greater than it. It’s easy to see that this will count every inversion exactly once. It’s also easy to see that it will take time: for each of the elements, we do two operations (one rank query and one insertion).
In fact, we can do a lot more with this data structure than just count inversions; it sometimes comes in handy for competitive programming problems. More in a future post, perhaps!
So how do we implement this magical data structure? First of all, we can use a balanced binary search tree to store the values in the bag; clearly this will allow us to insert in logarithmic time. However, a plain binary search tree wouldn’t allow us to quickly count the number of values strictly greater than a given query value. The trick is to augment the tree so that each node also caches the size of the subtree rooted at that node, being careful to maintain these counts while inserting and balancing.
Let’s see some code! In Haskell, probably the easiest type of balanced BST to implement is a red-black tree. (If I were implementing this in an imperative language I might use splay trees instead, but they are super annoying to implement in Haskell. (At least as far as I know. I will definitely take you out for a social beverage of your choice if you can show me an elegant Haskell implementation of splay trees! This is cool but somehow feels too complex.)) However, this isn’t going to be some fancy, type-indexed, correct-by-construction implementation of red-black trees, although that is certainly fun. I am actually going to implement left-leaning red-black trees, mostly following Sedgewick; see those slides for more explanation and proof. This is one of the simplest ways I know to implement red-black trees (though it’s not necessarily the most efficient).
First, a red-black tree is either empty, or a node with a color (which we imagine as the color of the incoming edge), a cached size, a value, and two subtrees.
> {-# LANGUAGE PatternSynonyms #-}
>
> data Color = R | B
> deriving Show
>
> otherColor :: Color -> Color
> otherColor R = B
> otherColor B = R
>
> data RBTree a
> = Empty
> | Node Color Int (RBTree a) a (RBTree a)
> deriving Show
To make some of the tree manipulation code easier to read, we make some convenient patterns for matching on the structure of a tree when we don’t care about the values or cached sizes: ANY
matches any tree and its subtrees, while RED
and BLACK
only match on nodes of the appropriate color. We also make a function to extract the cached size
of a subtree.
> pattern ANY l r <- Node _ _ l _ r
> pattern RED l r <- Node R _ l _ r
> pattern BLACK l r <- Node B _ l _ r
>
> size :: RBTree a -> Int
> size Empty = 0
> size (Node _ n _ _ _) = n
The next thing to implement is the workhorse of most balanced binary tree implementations: rotations. The fiddliest bit here is managing the cached sizes appropriately. When rotating, the size of the root node remains unchanged, but the new child node, as compared to the original, has lost one subtree and gained another. Note also that we will only ever rotate around red edges, so we pattern-match on the color as a sanity check, although this is not strictly necessary. The error
cases below should never happen.
> rotateL :: RBTree a -> RBTree a
> rotateL (Node c n t1 x (Node R m t2 y t3))
> = Node c n (Node R (m + size t1 - size t3) t1 x t2) y t3
> rotateL _ = error "rotateL on non-rotatable tree!"
>
> rotateR :: RBTree a -> RBTree a
> rotateR (Node c n (Node R m t1 x t2) y t3)
> = Node c n t1 x (Node R (m - size t1 + size t3) t2 y t3)
> rotateR _ = error "rotateR on non-rotatable tree!"
To recolor
a node, we just flip its color. We can then split
a tree with two red subtrees by recoloring all three nodes. (The “split” terminology comes from the isomorphism between red-black trees and 2-3-4 trees; red edges can be thought of as “gluing” nodes together into a larger node, and this recoloring operation corresponds to splitting a 4-node into three 2-nodes.)
> recolor :: RBTree a -> RBTree a
> recolor Empty = Empty
> recolor (Node c n l x r) = Node (otherColor c) n l x r
>
> split :: RBTree a -> RBTree a
> split (Node c n l@(RED _ _) x r@(RED _ _))
> = (Node (otherColor c) n (recolor l) x (recolor r))
> split _ = error "split on non-splittable tree!"
Finally, we implement a function to “fix up” the invariants by doing rotations as necessary: if we have two red subtrees we don’t touch them; if we have only one right red subtree we rotate it to the left (this is where the name “left-leaning” comes from), and if we have a left red child which itself has a left red child, we rotate right. (This function probably seems quite mysterious on its own; see Sedgewick for some nice pictures which explain it very well!)
> fixup :: RBTree a -> RBTree a
> fixup t@(ANY (RED _ _) (RED _ _)) = t
> fixup t@(ANY _ (RED _ _)) = rotateL t
> fixup t@(ANY (RED (RED _ _) _) _) = rotateR t
> fixup t = t
We can finally implement insertion. First, to insert into an empty tree, we create a red node with size 1.
> insert :: Ord a => a -> RBTree a -> RBTree a
> insert a Empty = Node R 1 Empty a Empty
If we encounter a node with two red children, we perform a split before continuing. This may violate the red-black invariants above us, but we will fix it up later on our way back up the tree.
> insert a t@(ANY (RED _ _) (RED _ _)) = insert a (split t)
Otherwise, we compare the element to be inserted with the root, insert on the left or right as appropriate, increment the cached size, and fixup
the result. Notice that we don’t stop recursing upon encountering a value that is equal to the value to be inserted, because our goal is to implement a bag rather than a set. Here I have chosen to put values equal to the root in the left subtree, but it really doesn’t matter.
> insert a (Node c n l x r)
> | a <= x = fixup (Node c (n+1) (insert a l) x r)
> | otherwise = fixup (Node c (n+1) l x (insert a r))
Now, thanks to the cached sizes, we can count the values greater than a query value.
> numGT :: Ord a => RBTree a -> a -> Int
The empty tree contains 0 values strictly greater than anything.
> numGT Empty _ = 0
For a non-empty tree, we distinguish two cases:
> numGT (Node _ n l x r) q
If the query value q
is less than the root, then we know that the root along with everything in the right subtree is strictly greater than q
, so we can just add 1 + size r
without recursing into the right subtree. We also recurse into the left subtree to count any values greater than q
it contains.
> | q < x = numGT l q + 1 + size r
Otherwise, if q
is greater than or equal to the root, any values strictly greater than q
must be in the right subtree, so we recurse to count them.
> | otherwise = numGT r q
By inspection we can see that numGT
calls itself at most once, moving one level down the tree with each recursive call, so it makes a logarithmic number of calls, with only a constant amount of work at each call—thanks to the fact that size
takes only constant time to look up a cached value.
Finally, we can put together the pieces to count inversions. The code is quite simple: recurse through the list with an accumulating red-black tree, doing a rank query on each value, and sum the results.
> inversions :: Ord a => [a] -> Int
> inversions = go Empty
> where
> go _ [] = 0
> go t (a:as) = numGT t a + go (insert a t) as
Let’s try it out!
λ> inversions [3,5,1,4,2]
6
λ> inversions [2,2,2,2,2,1]
5
λ> :set +s
λ> inversions [3000, 2999 .. 1]
4498500
(0.19 secs, 96,898,384 bytes)
It seems to work, and is reasonably fast!
Further augment each node with a counter representing the number of copies of the given value which are contained in the bag, and maintain the invariant that each distinct value occurs in only a single node.
Rewrite inversions
without a recursive helper function, using a scan, a zip, and a fold.
It should be possible to implement bags with rank queries using fingertrees instead of building our own custom balanced tree type (though it seems kind of overkill).
My intuition tells me that it is not possible to count inversions faster than . Prove it.
Euler’s proof is clever, incisive, not hard to understand, and a great introduction to the kind of abstract reasoning we can do about graphs. There’s little wonder that it is often used as one of the first nontrivial graph theory results students are introduced to, e.g. in a discrete mathematics course. (Indeed, I will be teaching discrete mathematics in the spring and certainly plan to talk about Eulerian paths!)
Euler’s 1735 solution was not constructive, and in fact he really only established one direction of the “if and only if”:
If a graph has an Eulerian path, then it has exactly zero or two vertices with odd degree.
This can be used to rule out the existence of Eulerian paths in graphs without the right vertex degrees, which was Euler’s specific motivation. However, one suspects that Euler knew it was an if and only if, and didn’t write about the other direction (if a graph has exactly zero or two vertices with odd degree, then it has an Eulerian path) because he thought it was trivial.^{1}
The first person to publish a full proof of both directions, including an actual algorithm for finding an Eulerian path, seems to be Carl Hierholzer, whose friend published a posthumous paper in Hierholzer’s name after his untimely death in 1871, a few weeks before his 31st birthday.^{2} (Notice that this was almost 150 years after Euler’s original paper!) If the vertex degrees cooperate, finding an Eulerian path is almost embarrassingly easy according to Hierholzer’s algorithm: starting at one of the odd-degree vertices (or anywhere you like if there are none), just start walking through the graph—any which way you please, it doesn’t matter!—visiting each edge at most once, until you get stuck. Then pick another part of the graph you haven’t visited, walk through it randomly, and splice that path into your original path. Repeat until you’ve explored the whole graph. And generalizing all of this to directed graphs isn’t much more complicated.
So, in summary, this is a well-studied problem, solved hundreds of years ago, that we present to students as a first example of a nontrivial yet still simple-to-understand graph proof and algorithm. So it should be pretty easy to code, right?
Recently I came across the eulerianpath problem on Open Kattis, and I realized that although I have understood this algorithm on a theoretical level for almost two decades (I almost certainly learned it as a young undergraduate), I have never actually implemented it! So I set out to solve it.
Right away the difficulty rating of 5.7 tells us that something strange is going on. “Easy” problems—the kind of problems you can give to an undergraduate at the point in their education when they might first be presented with the problem of finding Eulerian paths—typically have a difficulty rating below 3. As I dove into trying to implement it, I quickly realized two things. First of all, given an arbitrary graph, there’s a lot of somewhat finicky work that has to be done to check whether the graph even has an Eulerian path, before running the algorithm proper:
And if the graph is directed—as it is in the eulerianpath problem on Kattis—then the above steps get even more finicky. In step 1, we have to count the in- and outdegree of each vertex separately; in step 2, we have to check that the in- and outdegrees of all vertices are equal, except for possibly two vertices where one of them has exactly one more outgoing than incoming edge (which must be the start vertex), and vice versa for the other vertex; in step 4, we have to make sure to start the DFS from the chosen start vertex, because the graph need not be strongly connected, it’s enough for the entire graph to be reachable from the start vertex.
The second thing I realized is that Hierholzer’s algorithm proper—walk around until getting stuck, then repeatedly explore unexplored parts of the graph and splice them into the path being built—is still rather vague, and it’s nontrivial to figure out how to do it, and what data structures to use, so that everything runs in time linear in the number of edges. For example, we don’t want to iterate over the whole graph—or even just the whole path built so far—to find the next unexplored part of the graph every time we get stuck. We also need to be able to do the path splicing in constant time; so, for example, we can’t just store the path in a list or array, since then splicing in a new path segment would require copying the entire path after that point to make space. I finally found a clever solution that pushes the nodes being explored on a stack; when we get stuck, we start popping nodes, placing them into an array which will hold the final path (starting from the end), and keep popping until we find a node with an unexplored outgoing edge, then switch back into exploration mode, pushing things on the stack until we get stuck again, and so on. But this is also nontrivial to code correctly since there are many lurking off-by-one errors and so on. And I haven’t even talked about how we keep track of which edges have been explored and quickly find the next unexplored edge from a vertex.
I think it’s worth writing another blog post or two with more details of how the implementation works, both in an imperative language and in a pure functional language, and I may very well do just that. But in any case, what is it about this problem that results in such a large gap between the ease of understanding its solution theoretically, and the difficulty of actually implementing it?
Actually, the way I have stated the other direction of the if and only if is technically false!—can you spot the reason why?
Though apparently someone named Listing published the basic idea of the proof, with some details omitted, some decades earlier. I’ve gotten all this from Herbert Fleischner, Eulerian Graphs and Related Topics, Annals of Discrete Mathematics 45, Elsevier 1990. Fleischner reproduces Euler’s original paper as well as Hierholzer’s, together with English translations.
Scanner
combinator library for lightweight input parsing. It uses String
everywhere, and usually this is fine, but occasionally it’s not.
A good example is the Kattis problem Army Strength (Hard). There are a number of separate test cases; each test case consists of two lines of positive integers which record the strengths of monsters in two different armies. Supposedly the armies will have a sequence of battles, where the weakest monster dies each time, with some complex-sounding rules about how to break ties. It sounds way more complicated than it really is, though: a bit of thought reveals that to find out who wins we really just need to see which army’s maximum-strength monster is strongest.
So our strategy for each test case is to read in the two lists of integers, find the maximum of each list, and compare. Seems pretty straightforward, right? Something like this:
import Control.Arrow
import Data.List.Split
main = interact $
lines >>> drop 1 >>> chunksOf 4 >>>
map (drop 2 >>> map (words >>> map read) >>> solve) >>>
unlines
solve :: [[Int]] -> String
solve [gz, mgz] = case compare (maximum gz) (maximum mgz) of
LT -> "MechaGodzilla"
_ -> "Godzilla"
Note I didn’t actually use the Scanner
abstraction here, though I could have; it’s actually easier to just ignore the numbers telling us how many test cases there are and the length of each line, and just split up the input by lines and go from there.
This seems straightforward enough, but sadly, it results in a Time Limit Exceeded (TLE) error on the third of three test cases. Apparently this program takes longer than the allowed 1 second. What’s going on?
If we look carefully at the limits for the problem, we see that there could be up to 50 test cases, each test case could have two lists of length , and the numbers in the lists can be up to . If all those are maxed out (as they probably are in the third, secret test case), we are looking at an input file many megabytes in size. At this point the time to simply read the input is a big factor. Reading the input as a String
has a lot of overhead: each character gets its own cons cell; breaking the input into lines and words requires traversing over these cons cells one by one. We need a representation with less overhead.
Now, if this were a real application, we would reach for Text
, which is made for representing textual information and can correctly handle unicode encodings and all that good stuff. However, this isn’t a real application: competitive programming problems always limit the input and output strictly to ASCII, so characters are synonymous with bytes. Therefore we will commit a “double no-no”: not only are we going to use ByteString
to represent text, we’re going to use Data.ByteString.Lazy.Char8
which simply assumes that each 8 bits is one character. As explained in a previous post, however, I think this is one of those things that is usually a no-no but is completely justified in this context.
Let’s start by just replacing some of our string manipulation with corresponding ByteString
versions:
import Control.Arrow
import qualified Data.ByteString.Lazy.Char8 as C
import Data.List.Split
main = C.interact $
C.lines >>> drop 1 >>> chunksOf 4 >>>
map (drop 2 >>> map (C.words >>> map (C.unpack >>> read)) >>> solve) >>>
C.unlines
solve :: [[Int]] -> C.ByteString
solve [gz, mgz] = case compare (maximum gz) (maximum mgz) of
LT -> C.pack "MechaGodzilla"
_ -> C.pack "Godzilla"
This already helps a lot: this version is actually accepted, taking 0.66 seconds. (Note there’s no way to find out how long our first solution would take if allowed to run to completion: once it goes over the time limit Kattis just kills the process. So we really don’t know how much of an improvement this is, but hey, it’s accepted!)
But we can do even better: it turns out that read
also has a lot of overhead, and if we are specifically reading Int
values we can do something much better. The ByteString
module comes with a function
readInt :: C.ByteString -> Maybe (Int, C.ByteString)
Since, in this context, we know we will always get an integer with nothing left over, we can replace C.unpack >>> read
with C.readInt >>> fromJust >>> fst
. Let’s try it:
import Control.Arrow
import qualified Data.ByteString.Lazy.Char8 as C
import Data.List.Split
import Data.Maybe (fromJust)
main = C.interact $
C.lines >>> drop 1 >>> chunksOf 4 >>>
map (drop 2 >>> map (C.words >>> map readInt) >>> solve) >>>
C.unlines
where
readInt = C.readInt >>> fromJust >>> fst
solve :: [[Int]] -> C.ByteString
solve [gz, mgz] = case compare (maximum gz) (maximum mgz) of
LT -> C.pack "MechaGodzilla"
_ -> C.pack "Godzilla"
Now we’re talking — this version completes in a blazing 0.04 seconds!
We can take these principles and use them to make a variant of the Scanner
module from last time which uses (lazy, ASCII) ByteString
instead of String
, including the use of the readInt
functions to read Int
values quickly. You can find it here.
Data.Enumeration
module defines a type Enumeration a
, represented simply by a function Integer -> a
which picks out the value of type a
at a given index. This representation has a number of advantages, including the ability to quickly index into very large enumerations, and the convenience that comes from having Functor
, Applicative
, and Alternative
instances for Enumeration
.
I’ve just uploaded version 0.2 of the package, which adds a new Data.Enumeration.Invertible
module with a new type, IEnumeration a
, representing invertible enumerations. Whereas a normal enumeration is just a function from index to value, an invertible enumeration is a bijection between indices and values. In particular, alongside the Integer -> a
function for picking out the value at an index, an invertible enumeration also stores an inverse function a -> Integer
(called locate
) for finding the index of a given value.
On the one hand, this comes at a cost: because the type parameter a
now occurs both co- and contravariantly, IEnumeration
i s no longer an instance of Functor
, Applicative
, or Alternative
. There is a mapE
combinator provided for mapping IEnumeration a
to IEnumeration b
, but in order to work it needs both an a -> b
function and an inverse b -> a
.
On the other hand, we also gain something: of course the ability to look up the index of a value is nifty, and beyond that we also get a combinator
functionOf :: IEnumeration a -> IEnumeration b -> IEnumeration (a -> b)
which works as long as the IEnumeration a
is finite. This is not possible to implement with normal, non-invertible enumerations: we have to take an index and turn it into a function a -> b
, but that function has to take an a
as input and decide what to do with it. There’s nothing we can possibly do with a value of type a
unless we have a way to connect it back to the IEnumeration a
it came from.
Here’s a simple example of using the functionOf
combinator to enumerate all Bool -> Bool
functions, and then locating the index of not
:
>>> bbs = functionOf (boundedEnum @Bool) (boundedEnum @Bool)
>>> card bbs
Finite 4
>>> locate bbs not
2
>>> map (select bbs 2) [False, True]
[True,False]
And here’s an example of enumerating recursive trees, which is parallel to an example given in my previous post. Note, however, how we can no longer use combinators like <$>
, <*>
, and <|>
, but must explicitly use <+>
(disjoint sum of enumerations) and ><
(enumeration product) in combination with mapE
. In return, though, we can find the index of any given tree in addition to selecting trees by index.
data Tree = L | B Tree Tree
deriving Show
toTree :: Either () (Tree, Tree) -> Tree
toTree = either (const L) (uncurry B)
fromTree :: Tree -> Either () (Tree, Tree)
fromTree L = Left ()
fromTree (B l r) = Right (l,r)
trees :: IEnumeration Tree
trees = infinite $ mapE toTree fromTree (unit <+> (trees >< trees))
>>> locate trees (B (B L (B L L)) (B (B L (B L L)) (B L (B L L))))
123
>>> select trees 123
B (B L (B L L)) (B (B L (B L L)) (B L (B L L)))
Of course, the original Data.Enumeration
module remains available; there is clearly an inherent tradeoff to invertibility, and you are free to choose either style depending on your needs. Other than the tradeoffs outlined above and a couple other minor exceptions, the two modules export largely identical APIs.