Click here to Skip to main content
Click here to Skip to main content

Countdown Number Puzzle Solver

, 10 Mar 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
Describes an algorithm that solves the Countdown number puzzle written in c#

Introduction

I occasionally watch Countdown, a popular day time television game show on Channel 4 in the UK. One of the games is a number puzzle where a contestant picks 6 random numbers and then has to build an equation from them that equals a randomly generated target during a 30 second countdown.

I like playing along to this game but I find that after using up 4 or 5 numbers, I tend to forget which numbers I’ve already used. I could use a pen and paper to keep track but I don’t take it that seriously. This is however just the sort of puzzle that is an ideal candidate to be solved by a computer. It won't ever forget which numbers it's used and it can always find the really obvious solution that's staring you right in the face but just cannot see.

This article documents the algorithm that I came up with. As with most algorithms, the trick is doing it quickly.

Game Rules

The contestant picks numbers by choosing 6 cards from 24 placed face down in a grid pattern. The first row of the grid contains 4 large value cards having values 25, 50, 75 and 100. The remaining grid contains two sets of cards with values of 1 to 10. The contestant can choose none or up to 4 large cards from the first row and the remaining cards from the rest of the grid.

The random target number is generated in the range of 100 to 999 inclusive.

Only positive integer arithmetic is allowed using add, subtract, divide and multiply operators e.g. (3/4) and (3-4) are invalid but (75/5) is ok

Each card can only be used once but not all cards need to be used.

The UI

The UI for the Number Puzzle application is simply functional, only providing the basic features required to play the game and exercise the solver. It isn't up to commercial standards.

The Choose button picks cards and the target according to the games rules and the card options combo box. You can also type in whatever combination of cards and target you want.

The Timer button starts a 30 second timer. The exclamation system sound is played when your time is up.

The Solve button starts the PostFixSolvingEngine with the results being displayed in the list box in order of complexity. This has a context menu allowing the results to be copied to the clipboard.

How the Solving Engine Works

This uses a simple brute force approach where every possible equation is evaluated. The equations are constructed in postfix form. That removes the need for parentheses and this considerably simplifies the problem. The code knows how to construct the postfix equations by using a map which defines the sequence of pushing numbers onto the stack followed by executing an operator.

The solving engine Solve() method is shown below:

public void Solve()
{
    // k = 1
    SolveSingleDigit();
 
    for (int k = 2; k <= cards.Length; k++)
    {
        // get all the combinations for k digits
        Combinations<int> combinations = new Combinations<int>(cards, k);
        // get the associated map
        List<List<int>> map = postfixMap[k];
 
        foreach (List<int> combination in combinations)
        {
            // get all the permutations for the combination
            Permutations<int> permutations = new Permutations<int>(combination);
                        
            foreach (List<int> permutation in permutations)
            {
                foreach (List<int> mapRow in map)
                    SolveRecursive(-1, mapRow, 0, permutation, 0, 0);
            }
        }
    }
} 

As you can see, it loops choosing successive k combinations of the 6 cards and gets the postfix map entry for that number of digits. Then for each combination, it then calculates all the permutations for that combination. Then for each permutation, it tries to solve the puzzle by creating and solving all possible postfix equations using the map entries for that number of digits.

The solving engine SolveRecursive() method is the main work horse. The code is rather long winded due to functions being manually inlined, so I've shown the equivalent pseudo code:

SolveRecursive()
{
    // get the stack for this recursion depth
    stack = stacks[recursion_depth] ;
 
    stack.push ;  // 0 or more digits onto the stack as per map
 
    left_digit = stack.pop ;
    right_digit = stack.peek ;
 
    foreach (operator in {*, +, -, /})
    {
        result = evaluate (left_digit operator right_digit) ;
 
        if (there are more map entries)
        {
            // set up stack for next recursion
            next_stack = stacks[recursion_depth + 1]
            copy stack to next_stack 
            next_stack.poke = result ;
            
            SolveRecursive()
        }
        else
        {
            if (result == target)
            {
                record equation ;
                break ;
            }
 
            if (no targets found and result is close enough)
                record closest equation;
        }
    }
}

The method implements a sequence of pushing zero or more cards onto the stack before executing an operator. It then recurses executing another sequence. Each recursive call gets its own copy of the current stack so that when the recursion unwinds, the caller can simply continue with the next operator. The recursion ends when the map is completed.

Although it is a linear function conceptually it can be thought of as a tree. Each sequence splits into 4 branches, one for each operator, and for each recursion every branch further splits into 4 more branches. This repeats until the end of the map is reached when the final result is obtained for the 4 operators. After the first operator is executed, subsequent equations are calculated by executing only one operator rather than the complete equation. The same principal applies for each node in the tree as the recursion unwinds. Tree structures and recursion and are a thing of beauty.

Combinations and Permutations

The Combinations and Permutations collection classes are implemented using the common pattern where the class's enumerator provides the next combination or permutation on demand. This is the only code that can realistically be reused so I've written them using generics.

Both classes use Donald Knuth algorithms to generate the data in lexicographic order. I think I can safely assume they are good solutions. The permutation algorithm is actually a port of the C++ standard template library function std::next_permutation().

The combination algorithm doesn't produce duplicates if the source collection contains unique values. In addition, I've modified the algorithm to filter out duplicate combinations produced when the source collection itself contain duplicates (when two cards have the same value). The source is sorted and the combinations are generated in lexicographic order. So to remove duplicates, it's simply a case of comparing the current combination with the previous. If it's greater, it's not a duplicate and that is the combination returned by the enumerator. If not, then the enumerator gets successive combinations until one is. I'm not an expert on combinatorics so I expect there may be more efficient ways of doing that.

Postfix Map

The PostfixMap class is used to direct the solving engine on how to construct all the possible postfix equations for each digit count. Postfix equations are built up of a repeating sequence of pushing digits on to a stack followed by executing operators. The diagram below shows the possible postfix equations for 6 cards:

The digits represent cards and the dots represent possible operator positions. All equations will start by pushing 2 digits followed by executing 0 or 1 operator, then another digit followed by 0 to 2 operators etc. There will always be one less operator than digits as you move from left to right and the final map entry will always be an operator.

Consider the case for 4 digits, there will be 5 map sub entries, one for each possible postfix equation:


Here the "o" represents actual operator positions within the postfix equation. The code that generates the map first counts all the variations of operators in the possible operator locations as shown in column two. It then converts these counts into a map entries where numbers greater than zero means push that number of digits onto the stack and zeros indicates that an operator should be executed.

So How Many Equations Are There?

It's the sum of the number of equations for each number of digits k. If there are no duplicate cards, then this is straight forward.

The 4 raised to the power of k-1 is the product of the operators. When k = 1, the equation is just a comparison and there is no map entry. So we have:

This gives a total of 33,665,406 equations. Note that k = 6 accounts for 92% of the total.

However, according to the rules of the game, there can be one or two pairs of duplicate cards. The problem then starts with calculating the number of combinations. Google found this elegantly explained solution[^]. Of course, some of the combinations produced would then also contain duplicates which in turn affect the number of permutations. I've left that problem for the mathematicians and just put some instrumentation into the code. The results are:

In practice, the number of equations fully evaluated will be considerably smaller due to the positive integer arithmetic rules of the game. If an equation fails one of the rules, it is discarded as soon as it fails.

In addition, I also discard any equation where an identity operation is performed, that is divide or multiple by one, since it will be a duplicate equation.

I also filter out duplicate equations by detecting if the operator is acting on card values directly rather than any intermediately derived value. If the operator is commutative, that is addition and multiplication; I discard the equation if the left digit is greater than the right.

The final filter is that multiplication is the first operator evaluated. If the equation is complete and the result is less than the target, then the other operators are skipped.

Ultimately, it will be dependent on the value of the cards chosen and the target as to how many equations are evaluated fully.

For typical sets of cards and targets:

The solver was run on my seven year old Sony Vaio laptop with a T5500 1.66MHz dual core processor.

Conclusion

Is there any point to this? Not really. It was just a fun academic exercise.

There are a few solvers out there already including commercial countdown training programs but I haven't found any that publishes the algorithm it uses.

The code was developed using Visual Studio 2010 Express and targets NET 3.5. Thanks to Microsoft for providing VS Express for free.

History

  • 1.1 - Move the timing code outside of the solving engine so that it's clear what is being measured. Complete the exception handling in the test code. The tests passed so unfortunately that area was neglected. Various small code refactoring. No bug fixes.

  • 1.0 - Initial release

References


License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

DavidHancock
Software Developer
United Kingdom United Kingdom
I started off working with digital hardware but after 10 years I moved over to the software side. I've written code for more then 15 years, working at several companies, writing desktop applications in Pascal, Java, C, C++ and my favourite C# from .Net1.0 onwards. A few years ago I decided it was time for a change and I'm now a self employed qualified electrician.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMember 1107268025-Oct-14 20:00 
QuestionIs it possible to compile it without VS? PinmemberMember 1107268016-Sep-14 22:52 
AnswerRe: Is it possible to compile it without VS? PinmemberMember 1107268025-Oct-14 19:56 
QuestionNeat PinmemberMike Ellison17-Mar-14 12:36 
AnswerRe: Neat PinprofessionalDavidHancock21-Mar-14 2:57 
GeneralFunctional version using Haskell PinmemberJon Clare10-Mar-14 8:46 
GeneralRe: Functional version using Haskell PinprofessionalDavidHancock10-Mar-14 9:13 
QuestionAnd the alternative version? PinprotectorDaveAuld10-Mar-14 6:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 10 Mar 2014
Article Copyright 2014 by DavidHancock
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid