November 5, 2022
A recent 6 kyu kata on Codewars proposed by Paul Robertson defines an evil number as a nonnegative number with an even number of 1’s in its binary representation. The following table shows all nonnegative numbers up to 12 and indicate which ones are evil numbers:
\(n\) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Binary | 0 | 1 | 10 | 11 | 100 | 101 | 110 | 101 | 1000 | 1001 | 1010 | 1011 |
Evil? | Yes | No | No | Yes | No | No | Yes | Yes | No | Yes | Yes | No |
The kata asks for a function getEvil
so that
getEvil n
returns the n
th evil number. For
example, getEvil 1
should return 0
and
getEvil 5
should return 9
.
Before reading on, try to solve this kata yourself. In any case, this simple-looking kata is sufficiently rich for illustrating a bit of Haskell programming and mathematical thinking that are useful for learners.
We first develop a function isEvil
that tells us whether
or not a given nonnegative integer has an even number of 1’s in its
binary representation.
For our first attempt, we will simply do the obvious: Convert the
number to a string of binary digits, and then see if it has an even
number of ones. We will make use of the function
showIntAtBase
from the Numeric
module in
base
. This function has type
showIntAtBase :: Integral a => a -> (Int -> Char) -> a -> ShowS
The first argument is the target base. The second argument is a
function that converts a digit into a character. For this, we use
intToDigit
from the Data.Char
module since
every digit in bsae 2 is either the integer 0 or the integer 1. The
third argument is the number to be represented in the target base. The
output is a function of type ShowS
which, according to the
documentation,
prepends the output String
to an existing
String
.
For instance, to obtain the binary string for 1234
, we
can do the following in the ghci
REPL:
> import Numeric (showIntAtBase)
λ> import Data.Char (intToDigit)
λ> showIntAtBase 2 intToDigit 1234 ""
λ"10011010010"
Here is our first attempt at implementing isEvil
:
> isEvil n = even $ length $ filter (== '1') $ showIntAtBase 2 intToDigit n ""
λ> take 6 $ filter isEvil [0..]
λ0,3,5,6,9,10] [
So far so good. But the function looks incredibly inefficient; converting a integer to a string is rather expensive.
We can be more direct by just ‘peeling off’ one digit at a time by
obtain the quotient and remainder upon division by 2. For integer types
with standard width, some sort of bit shift operation will do the job.
But since we need to accommodate arbitrarily large integers, we use
quotRem
.
Here is our second attempt:
> :{
λ0 = True
isEvil' 1 = False
isEvil' = let (q,r) = n `quotRem` 2
isEvil' n in (r == 1) /= isEvil' q
> :}
λ> take 6 $ filter isEvil' [0..]
λ0,3,5,6,9,10] [
This works as expected, which is a good sign. Armed with this, we can simply solve the kata as follows, right?
> getEvil n = let es = filter isEvil' [0..] in es !! (n-1)
λ> map getEvil [1..6]
λ0,3,5,6,9,10] [
Being a 6 kyu kata, there must be a twist. In this case, the input
n
can be as large as \(10^{100}\) in the tests! The above
implementation of getEvil
will certainly not be able to
pass the tests within the given time limit.
The fact that the input can be so large gives us a hint: We cannot
afford to generate the whole list of evil numbers up to n
.
In other words, we need some sort of formula that returns the
n
th evil number as directly as possible.
Let us look at the table and see if there is a pattern.
Aha! If we consider the numbers 0 to 11 one pair at a time, we see that exactly one number in each pair is an evil number. Is there a simple mathematical explanation for this observation?
In the pair of numbers \(2m\) and
\(2m+1\), the binary representation of
the former ends in a 0 since it is a multiple of 2. Hence, the binary
representation of \(2m+1\) has one more
1 than the binary representation of \(2m\). Hence, if \(2m\) is an evil number, \(2m+1\) cannot be an evil number. And if
\(2m\) is not an evil number, \(2m+1\) must be an evil number. So we can
conclude that the n
th evil number must be either
2*(n-1)
or 2*n-1
. All we need to do is to test
if 2*(n-1)
is an evil number. If it is, we return it.
Otherwise, we return 2*n-1
. The following implementation of
getEvil
passes all the tests:
getEvil :: Integer -> Integer
=
getEvil n if isEvil' t then t else t+1
where
= 2 * (n-1) t