Search code examples
algorithmmathpermutationcode-golf

Code Golf: Countdown Number Game


Challenge

Here is the task, inspired by the well-known British TV game show Countdown. The challenge should be pretty clear even without any knowledge of the game, but feel free to ask for clarifications.

And if you fancy seeing a clip of this game in action, check out this YouTube clip. It features the wonderful late Richard Whitely in 1997.

You are given 6 numbers, chosen at random from the set {1, 2, 3, 4, 5, 6, 8, 9, 10, 25, 50, 75, 100}, and a random target number between 100 and 999. The aim is to use the six given numbers and the four common arithmetic operations (addition, subtraction, multiplication, division; all over the rational numbers) to generate the target - or as close as possible either side. Each number may only be used once at most, while each arithmetic operator may be used any number of times (including zero.) Note that it does not matter how many numbers are used.

Write a function that takes the target number and set of 6 numbers (can be represented as list/collection/array/sequence) and returns the solution in any standard numerical notation (e.g. infix, prefix, postfix). The function must always return the closest-possible result to the target, and must run in at most 1 minute on a standard PC. Note that in the case where more than one solution exists, any single solution is sufficient.

Examples:

  • {50, 100, 4, 2, 2, 4}, target 203
    e.g. 100 * 2 + 2 + (4 / 4) (exact)
    e.g. (100 + 50) * 4 * 2 / (4 + 2) (exact)

  • {25, 4, 9, 2, 3, 10}, target 465
    e.g. (25 + 10 - 4) * (9 * 2 - 3) (exact)

  • {9, 8, 10, 5, 9, 7}, target 241
    e.g. ((10 + 9) * 9 * 7) + 8) / 5 (exact)

  • {3, 7, 6, 2, 1, 7}, target 824
    e.g. ((7 * 3) - 1) * 6 - 2) * 7 (= 826; off by 2)

Rules

Other than mentioned in the problem statement, there are no further restrictions. You may write the function in any standard language (standard I/O is not necessary). The aim as always is to solve the task with the smallest number of characters of code.

Saying that, I may not simply accept the answer with the shortest code. I'll also be looking at elegance of the code and time complexity of the algorithm!

My Solution

I'm attempting an F# solution when I find the free time - will post it here when I have something!


Format

Please post all answers in the following format for the purpose of easy comparison:

Language

Number of characters: ???

Fully obfuscated function:

(code here)

Clear (ideally commented) function:

(code here)

Any notes on the algorithm/clever shortcuts it takes.



Solution

  • Haskell

    Number of characters: 361 350 338 322

    Fully obfuscated function:

    m=map
    f=toRational
    a%w=m(\(b,v)->(b,a:v))w
    p[]=[];p(a:w)=(a,w):a%p w
    q[]=[];q(a:w)=[((a,b),v)|(b,v)<-p w]++a%q w
    z(o,p)(a,w)(b,v)=[(a`o`b,'(':w++p:v++")")|b/=0]
    y=m z(zip[(-),(/),(+),(*)]"-/+*")++m flip(take 2 y)
    r w=do{((a,b),v)<-q w;o<-y;c<-o a b;c:r(c:v)}
    c t=snd.minimum.m(\a->(abs(fst a-f t),a)).r.m(\a->(f a,show a))
    

    Clear function:

    -- | add an element on to the front of the remainder list
    onRemainder :: a -> [(b,[a])] -> [(b,[a])]
    a`onRemainder`w = map (\(b,as)->(b,a:as)) w
    
    -- | all ways to pick one item from a list, returns item and remainder of list
    pick :: [a] -> [(a,[a])]
    pick [] = []
    pick (a:as) = (a,as) : a `onRemainder` (pick as)
    
    -- | all ways to pick two items from a list, returns items and remainder of list
    pick2 :: [a] -> [((a,a),[a])]
    pick2 [] = []
    pick2 (a:as) = [((a,b),cs) | (b,cs) <- pick as] ++ a `onRemainder` (pick2 as)    
    
    -- | a value, and how it was computed
    type Item = (Rational, String)
    
    -- | a specification of a binary operation
    type OpSpec = (Rational -> Rational -> Rational, String)
    
    -- | a binary operation on Items
    type Op = Item -> Item -> Maybe Item
    
    -- | turn an OpSpec into a operation
    -- applies the operator to the values, and builds up an expression string
    -- in this context there is no point to doing +0, -0, *0, or /0
    combine :: OpSpec -> Op
    combine (op,os) (ar,as) (br,bs)
        | br == 0   = Nothing
        | otherwise = Just (ar`op`br,"("++as++os++bs++")")
    
    -- | the operators we can use
    ops :: [Op]
    ops = map combine [ ((+),"+"), ((-), "-"), ((*), "*"), ((/), "/") ]
            ++ map (flip . combine) [((-), "-"), ((/), "/")]
    
    -- | recursive reduction of a list of items to a list of all possible values
    -- includes values that don't use all the items, includes multiple copies of
    -- some results          
    reduce :: [Item] -> [Item]
    reduce is = do
        ((a,b),js) <- pick2 is
        op <- ops
        c <- maybe [] (:[]) $ op a b
        c : reduce (c : js)
    
    -- | convert a list of real numbers to a list of items
    items :: (Real a, Show a) => [a] -> [Item]
    items = map (\a -> (toRational a, show a))
    
    -- | return the first reduction of a list of real numbers closest to some target
    countDown:: (Real a, Show a) => a -> [a] -> Item
    countDown t is = snd $ minimum $ map dist $ reduce $ items is
        where dist is = (abs . subtract t' . fst $ is, is)
              t' = toRational t
    

    Any notes on the algorithm/clever shortcuts it takes:

    • In the golf'd version, z returns in the list monad, rather than Maybe as ops does.
    • While the algorithm here is brute force, it operates in small, fixed, linear space due to Haskell's laziness. I coded the wonderful @keith-randall algorithm, but it ran in about the same time and took over 1.5G of memory in Haskell.
    • reduce generates some answers multiple times, in order to easily include solutions with fewer terms.
    • In the golf'd version, y is defined partially in terms of itself.
    • Results are computed with Rational values. Golf'd code would be 17 characters shorter, and faster if computed with Double.
    • Notice how the function onRemainder factors out the structural similarity between pick and pick2.

    Driver for golf'd version:

    main = do
        print $ c 203 [50, 100, 4, 2, 2, 4]
        print $ c 465 [25, 4, 9, 2, 3, 10]
        print $ c 241 [9, 8, 10, 5, 9, 7]
        print $ c 824 [3, 7, 6, 2, 1, 7]
    

    Run, with timing (still under one minute per result):

    [1076] : time ./Countdown
    (203 % 1,"(((((2*4)-2)/100)+4)*50)")
    (465 % 1,"(((((10-4)*25)+2)*3)+9)")
    (241 % 1,"(((((10*9)/5)+8)*9)+7)")
    (826 % 1,"(((((3*7)-1)*6)-2)*7)")
    
    real    2m24.213s
    user    2m22.063s
    sys     0m 0.913s