36

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.


Nick Moore
  • 15,547
  • 6
  • 61
  • 83
Noldorin
  • 144,213
  • 56
  • 264
  • 302
  • Bah. I forgot to make this question community wiki. If any mod passing by could change that, that would be handy. Thanks. – Noldorin Jan 03 '11 at 17:52
  • 1
    how are divisions treated? Integer division or float? – BrokenGlass Jan 03 '11 at 17:53
  • @BrokenGlass: It's a mathematical game ultimately, so all operations have domain and range of the rationals (though commonly just the integers are needed). I've clarified this in the question; thanks. – Noldorin Jan 03 '11 at 17:54
  • 2
    We used to play this with a deck of cards. Deal 4 cards for the numbers (J=11, Q=12, K=13). Then deal 2 numbers for the target: target=10*t1+t2, so can be up to 13*11. You can almost always do it, and for the other cases I always wanted a program to verify no solution. – phkahler Jan 03 '11 at 18:11
  • This game is popular? (And this is coming from someone trained in mathematics.) – jason Jan 03 '11 at 18:13
  • Oh, very. It's a teatime game show - almost universal knowledge within Britain, but little popularity abroad I guess. I have no idea what point you were trying to make? – Noldorin Jan 03 '11 at 18:19
  • @Nolodrin: It seems boring; I'm surprised that it's popular. – jason Jan 03 '11 at 18:20
  • @Jason, @Noldorin: We have it in France (des chiffres et des lettres), and I think it stems from here. The letters part is boring, but the numbers part is "fun". – Alexandre C. Jan 03 '11 at 18:23
  • Must run in polynomial time with respect to what? Maybe a better requirement is to terminate within a reasonable amount of time (e.g. a few seconds) – Nabb Jan 03 '11 at 18:43
  • Similar to: http://stackoverflow.com/questions/2960242 – mob Jan 03 '11 at 20:49
  • @Broken on the game show, division was only ever exact, so the question never arose. @Noldorin Do you have to use all 6 numbers? In the original Countdown, this was not a requirement. – Philip Potter Jan 03 '11 at 21:33
  • @Jason: You're a harsh man. Millions of people also disagree it seems - go watch the show? – Noldorin Jan 03 '11 at 22:45
  • @Alexandre: Indeed, the original version is the French show as you pointed out - I didn't know that still existed however. And I agree, numbers parts is much more entertaining (?), hah. – Noldorin Jan 03 '11 at 22:46
  • @Nabb: With respect to number of digits. You're right, I should have specified that though. Also, time constraints are usually quite bad, considering they depend on so many other factors. – Noldorin Jan 03 '11 at 22:47
  • @Philip Potter: Yeah, you can use as many or as few numbers as you like. I think I mentioned that somewhere above (perhaps not very explicitly). – Noldorin Jan 03 '11 at 22:48
  • 1
    @Noldorin a big-O notation time constraint is meaningless if the input cannot grow arbitrarily large. – Philip Potter Jan 03 '11 at 22:51
  • Not at all. I'm just saying I want the algorithm to be "extensible". – Noldorin Jan 03 '11 at 22:53
  • @Nolodrin: I disagree with millions of people on many subjects. – jason Jan 04 '11 at 00:07
  • @Noldorin: It's nothing special; you do too. – jason Jan 04 '11 at 01:30
  • For some context, here's a YouTube clip of this game in action: http://www.youtube.com/watch?v=pfa3MHLLSWI – Nick Moore Jan 04 '11 at 14:43
  • @invariant: Thanks, that's a useful link! I'll put it in the question. :) – Noldorin Jan 04 '11 at 15:18
  • 2
    I think it can be proven that there isn't a `O(n+k)` solution. I think there isn't a polynomial time solution either. This _smells_ like an NP Hard problem. – deft_code Jan 04 '11 at 15:30
  • @Nas: I already explained that. `n` is number of digits, `k` is clearly any integer (representing polynomial time). – Noldorin Jan 04 '11 at 15:32
  • @deft_code: You may well be right, if someone can (half) convince me, I'll remove/change that restriction. – Noldorin Jan 04 '11 at 15:33
  • 1
    I don't see any way you could do this exactly in polynomial time. Imagine I gave you a fixed expression, say 1+2*3+4*5+6, and all you had to do was decide where the parentheses go to get closest to a target number, that's still (n-1)! choices (which order to do the operations in), which isn't polynomial. The original problem is quite a bit worse than that. – Keith Randall Jan 04 '11 at 18:50
  • Already, I'll remove the restriction and replace it with a simple time one. – Noldorin Jan 04 '11 at 18:58
  • 1
    It might be useful to have an example of a puzzle with no exact solution. Here's one: [3, 7, 6, 2, 1, 7], 824 – Nick Moore Jan 05 '11 at 09:21
  • @invariant: If you could provide a close solution and edit that into the question, I would be grateful. :) – Noldorin Jan 05 '11 at 20:34
  • It would be nice if divisions by non-factors (i.e. divisions that do not result in an integer) were forbidden, as in the show. – Ergwun Feb 11 '11 at 02:37

4 Answers4

11

Python

Number of characters: 548 482 425 421 416 413 408

from operator import *
n=len
def C(N,T):
 R=range(1<<n(N));M=[{}for i in R];p=1
 for i in range(n(N)):M[1<<i][1.*N[i]]="%d"%N[i]
 while p:
  p=0
  for i in R:
   for j in R:
    m=M[i|j];l=n(m)
    if not i&j:m.update((f(x,y),"("+s+o+t+")")for(y,t)in M[j].items()if y for(x,s)in M[i].items() for(o,f)in zip('+-*/',(add,sub,mul,div)))
    p|=l<n(m)
 return min((abs(x-T),e)for t in M for(x,e)in t.items())[1]

you can call it like this:

>>> print C([50, 100, 4, 2, 2, 4], 203)
((((4+2)*(2+100))/4)+50)

Takes about half a minute on the given examples on an oldish PC.

Here's the commented version:

def countdown(N,T):
  # M is a map: (bitmask of used input numbers -> (expression value -> expression text))                  
  M=[{} for i in range(1<<len(N))]

  # initialize M with single-number expressions                                                           
  for i in range(len(N)):
    M[1<<i][1.0*N[i]] = "%d" % N[i]

  # allowed operators                                                                                     
  ops = (("+",lambda x,y:x+y),("-",lambda x,y:x-y),("*",lambda x,y:x*y),("/",lambda x,y:x/y))

  # enumerate all expressions                                                                             
  n=0
  while 1:

    # test to see if we're done (last iteration didn't change anything)                                   
    c=0
    for x in M: c +=len(x)
    if c==n: break
    n=c

    # loop over all values we have so far, indexed by bitmask of used input numbers                       
    for i in range(len(M)):
      for j in range(len(M)):
        if i & j: continue    # skip if both expressions used the same input number                       
        for (x,s) in M[i].items():
          for (y,t) in M[j].items():
            if y: # avoid /0 (and +0,-0,*0 while we're at it)                                             
              for (o,f) in ops:
                M[i|j][f(x,y)]="(%s%s%s)"%(s,o,t)

  # pick best expression                                                                                  
  L=[]
  for t in M:
    for(x,e) in t.items():
      L+=[(abs(x-T),e)]
  L.sort();return L[0][1]

It works through exhaustive enumeration of all possibilities. It is a bit smart in that if there are two expressions with the same value that use the same input numbers, it discards one of them. It is also smart in how it considers new combinations, using the index into M to prune quickly all the potential combinations that share input numbers.

Sam Dolan
  • 31,966
  • 10
  • 88
  • 84
Keith Randall
  • 22,985
  • 2
  • 35
  • 54
  • @Keith: Great work! I pruned a bit the minified version, hope you don't mind :) (maybe I should adapt also the code in the expanded version that tests for completion to reflect the minified version) – Giuseppe Ottaviano Jan 05 '11 at 02:13
  • @Keith: Awesome stuff! I also shaved off a few chars. – Sam Dolan Jan 05 '11 at 02:29
  • 1
    Any reason you aren't hardcoding the lengths? o_o – Nabb Jan 05 '11 at 08:15
  • Nice solution. I was anticipating anything under 500 would be pretty good! :) – Noldorin Jan 05 '11 at 14:45
  • ..not sure why I'm not updating it myself, but a few comments: Alternate spaces and tabs in indentation (S, T, TS, TT, TTS, ...). Output in reverse polish (s+" "+t+o) possibly -- but if you leave it as infix you can just eval that and you won't have to import operators (or eval it with the already computed numbers if this takes too long). Initialize p somewhere else -- you're setting it to zero immediately after anyway. – Nabb Jan 09 '11 at 02:46
9

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
MtnViewMark
  • 5,120
  • 2
  • 20
  • 29
1

Ruby 1.9.2

Number of characters: 404

I give up for now, it works as long as there is an exact answer. If there isn't it takes way too long to enumerate all possibilities.

Fully Obfuscated

def b a,o,c,p,r
o+c==2*p ?r<<a :o<p ?b(a+['('],o+1,c,p,r):0;c<o ?b(a+[')'],o,c+1,p,r):0
end
w=a=%w{+ - * /}
4.times{w=w.product a}
b [],0,0,3,g=[]
*n,l=$<.read.split.map(&:to_f)
h={}
catch(0){w.product(g).each{|c,f|k=f.zip(c.flatten).each{|o|o.reverse! if o[0]=='('};n.permutation{|m|h[x=eval(d=m.zip(k)*'')]=d;throw 0 if x==l}}}
c=h[k=h.keys.min_by{|i|(i-l).abs}]
puts c.gsub(/(\d*)\.\d*/,'\1')+"=#{k}"

Decoded

Coming soon

Test script

#!/usr/bin/env ruby
[
  [[50,100,4,2,2,4],203],
  [[25,4,9,2,3,10],465],
  [[9,8,10,5,9,7],241],
  [[3,7,6,2,1,7],824]
].each do |b|
  start = Time.now
  puts "{[#{b[0]*', '}] #{b[1]}}  gives #{`echo "#{b[0]*' '} #{b[1]}" | ruby count-golf.rb`.strip} in #{Time.now-start}"
end

Output

→ ./test.rb
{[50, 100, 4, 2, 2, 4] 203}  gives 100+(4+(50-(2)/4)*2)=203.0 in 3.968534736
{[25, 4, 9, 2, 3, 10] 465}  gives 2+(3+(25+(9)*10)*4)=465.0 in 1.430715549
{[9, 8, 10, 5, 9, 7] 241}  gives 5+(9+(8)+10)*9-(7)=241.0 in 1.20045702
{[3, 7, 6, 2, 1, 7] 824}  gives 7*(6*(7*(3)-1)-2)=826.0 in 193.040054095

Details

The function used for generating the bracket pairs (b) is based off this one: Finding all combinations of well-formed brackets

Community
  • 1
  • 1
Nemo157
  • 3,559
  • 19
  • 27
0

Ruby 1.9.2 second attempt

Number of characters: 492 440(426)

Again there is a problem with the non-exact answer. This time this is easily fast enough but for some reason the closest it gets to 824 is 819 instead of 826.

I decided to put this in a new answer since it is using a very different method to my last attempt.

Removing the total of the output (as its not required by spec) is -14 characters.

Fully Obfuscated

def r d,c;d>4?[0]:(k=c.pop;a=[];r(d+1,c).each{|b|a<<[b,k,nil];a<<[nil,k,b]};a)end
def f t,n;[0,2].each{|a|Array===t[a] ?f(t[a],n): t[a]=n.pop}end
def d t;Float===t ?t:d(t[0]).send(t[1],d(t[2]))end
def o c;Float===c ?c.round: "(#{o c[0]}#{c[1]}#{o c[2]})"end
w=a=%w{+ - * /}
4.times{w=w.product a}
*n,l=$<.each(' ').map(&:to_f)
h={}
w.each{|y|r(0,y.flatten).each{|t|f t,n.dup;h[d t]=o t}}
puts h[k=h.keys.min_by{|i|(l-i).abs}]+"=#{k.round}"

Decoded

Coming soon

Test script

#!/usr/bin/env ruby
[
  [[50,100,4,2,2,4],203],
  [[25,4,9,2,3,10],465],
  [[9,8,10,5,9,7],241],
  [[3,7,6,2,1,7],824]
].each do |b|
  start = Time.now
  puts "{[#{b[0]*', '}] #{b[1]}}  gives #{`echo "#{b[0]*' '} #{b[1]}" | ruby count-golf.rb`.strip} in #{Time.now-start}"
end

Output

→ ./test.rb
{[50, 100, 4, 2, 2, 4] 203}  gives ((4-((2-(2*4))/100))*50)=203 in 1.089726252
{[25, 4, 9, 2, 3, 10] 465}  gives ((10*(((3+2)*9)+4))-25)=465 in 1.039455671
{[9, 8, 10, 5, 9, 7] 241}  gives (7+(((9/(5/10))+8)*9))=241 in 1.045774539
{[3, 7, 6, 2, 1, 7] 824}  gives ((((7-(1/2))*6)*7)*3)=819 in 1.012330419

Details

This constructs the set of ternary trees representing all possible combinations of 5 operators. It then goes through and inserts all permutations of the input numbers into the leaves of these trees. Finally it simply iterates through these possible equations storing them into a hash with the result as index. Then it's easy enough to pick the closest value to the required answer from the hash and display it.

Community
  • 1
  • 1
Nemo157
  • 3,559
  • 19
  • 27