How to design an algorithm to calculate countdown style maths number puzzle

后端 未结 10 1100
清酒与你
清酒与你 2020-11-30 00:35

I have always wanted to do this but every time I start thinking about the problem it blows my mind because of its exponential nature.

The problem solver I want to be

相关标签:
10条回答
  • 2020-11-30 01:28

    Input is obviously a set of digits and operators: D={1,3,3,6,7,8,3} and Op={+,-,*,/}. The most straight forward algorithm would be a brute force solver, which enumerates all possible combinations of these sets. Where the elements of set Op can be used as often as wanted, but elements from set D are used exactly once. Pseudo code:

    D={1,3,3,6,7,8,3}
    Op={+,-,*,/}
    Solution=348
    for each permutation D_ of D:
       for each binary tree T with D_ as its leafs:
           for each sequence of operators Op_ from Op with length |D_|-1:
               label each inner tree node with operators from Op_
               result = compute T using infix traversal
               if result==Solution
                  return T
    return nil
    

    Other than that: read jedrus07's and HPM's answers.

    0 讨论(0)
  • 2020-11-30 01:30

    Given that Rachel Reiley does these in her head on camera (and Carole Vordeman did in shows gone-by), I'm thinking there's a non-brute-force solution lurking out there somewhere.

    0 讨论(0)
  • 2020-11-30 01:32

    Sure it's exponential but it's tiny so a good (enough) naive implementation would be a good start. I suggest you drop the usual infix notation with bracketing, and use postfix, it's easier to program. You can always prettify the outputs as a separate stage.

    Start by listing and evaluating all the (valid) sequences of numbers and operators. For example (in postfix):

    1 3 7 6 8 3 + + + + + -> 28
    1 3 7 6 8 3 + + + + - -> 26
    

    My Java is laughable, I don't come here to be laughed at so I'll leave coding this up to you.

    To all the smart people reading this: yes, I know that for even a small problem like this there are smarter approaches which are likely to be faster, I'm just pointing OP towards an initial working solution. Someone else can write the answer with the smarter solution(s).

    So, to answer your questions:

    • I begin with an algorithm that I think will lead me quickly to a working solution. In this case the obvious (to me) choice is exhaustive enumeration and testing of all possible calculations.
    • If the obvious algorithm looks unappealing for performance reasons I'll start thinking more deeply about it, recalling other algorithms that I know about which are likely to deliver better performance. I may start coding one of those first instead.
    • If I stick with the exhaustive algorithm and find that the run-time is, in practice, too long, then I might go back to the previous step and code again. But it has to be worth my while, there's a cost/benefit assessment to be made -- as long as my code can outperform Rachel Riley I'd be satisfied.
    • Important considerations include my time vs computer time, mine costs a helluva lot more.
    0 讨论(0)
  • 2020-11-30 01:36

    I've written my own countdown solver, in Python.

    Here's the code; it is also available on GitHub:

    #!/usr/bin/env python3
    
    import sys
    from itertools import combinations, product, zip_longest
    from functools import lru_cache
    
    assert sys.version_info >= (3, 6)
    
    
    class Solutions:
    
        def __init__(self, numbers):
            self.all_numbers = numbers
            self.size = len(numbers)
            self.all_groups = self.unique_groups()
    
        def unique_groups(self):
            all_groups = {}
            all_numbers, size = self.all_numbers, self.size
            for m in range(1, size+1):
                for numbers in combinations(all_numbers, m):
                    if numbers in all_groups:
                        continue
                    all_groups[numbers] = Group(numbers, all_groups)
            return all_groups
    
        def walk(self):
            for group in self.all_groups.values():
                yield from group.calculations
    
    
    class Group:
    
        def __init__(self, numbers, all_groups):
            self.numbers = numbers
            self.size = len(numbers)
            self.partitions = list(self.partition_into_unique_pairs(all_groups))
            self.calculations = list(self.perform_calculations())
    
        def __repr__(self):
            return str(self.numbers)
    
        def partition_into_unique_pairs(self, all_groups):
            # The pairs are unordered: a pair (a, b) is equivalent to (b, a).
            # Therefore, for pairs of equal length only half of all combinations
            # need to be generated to obtain all pairs; this is set by the limit.
            if self.size == 1:
                return
            numbers, size = self.numbers, self.size
            limits = (self.halfbinom(size, size//2), )
            unique_numbers = set()
            for m, limit in zip_longest(range((size+1)//2, size), limits):
                for numbers1, numbers2 in self.paired_combinations(numbers, m, limit):
                    if numbers1 in unique_numbers:
                        continue
                    unique_numbers.add(numbers1)
                    group1, group2 = all_groups[numbers1], all_groups[numbers2]
                    yield (group1, group2)
    
        def perform_calculations(self):
            if self.size == 1:
                yield Calculation.singleton(self.numbers[0])
                return
            for group1, group2 in self.partitions:
                for calc1, calc2 in product(group1.calculations, group2.calculations):
                    yield from Calculation.generate(calc1, calc2)
    
        @classmethod
        def paired_combinations(cls, numbers, m, limit):
            for cnt, numbers1 in enumerate(combinations(numbers, m), 1):
                numbers2 = tuple(cls.filtering(numbers, numbers1))
                yield (numbers1, numbers2)
                if cnt == limit:
                    return
    
        @staticmethod
        def filtering(iterable, elements):
            # filter elements out of an iterable, return the remaining elements
            elems = iter(elements)
            k = next(elems, None)
            for n in iterable:
                if n == k:
                    k = next(elems, None)
                else:
                    yield n
    
        @staticmethod
        @lru_cache()
        def halfbinom(n, k):
            if n % 2 == 1:
                return None
            prod = 1
            for m, l in zip(reversed(range(n+1-k, n+1)), range(1, k+1)):
                prod = (prod*m)//l
            return prod//2
    
    
    class Calculation:
    
        def __init__(self, expression, result, is_singleton=False):
            self.expr = expression
            self.result = result
            self.is_singleton = is_singleton
    
        def __repr__(self):
            return self.expr
    
        @classmethod
        def singleton(cls, n):
            return cls(f"{n}", n, is_singleton=True)
    
        @classmethod
        def generate(cls, calca, calcb):
            if calca.result < calcb.result:
                calca, calcb = calcb, calca
            for result, op in cls.operations(calca.result, calcb.result):
                expr1 = f"{calca.expr}" if calca.is_singleton else f"({calca.expr})"
                expr2 = f"{calcb.expr}" if calcb.is_singleton else f"({calcb.expr})"
                yield cls(f"{expr1} {op} {expr2}", result)
    
        @staticmethod
        def operations(x, y):
            yield (x + y, '+')
            if x > y:                     # exclude non-positive results
                yield (x - y, '-')
            if y > 1 and x > 1:           # exclude trivial results
                yield (x * y, 'x')
            if y > 1 and x % y == 0:      # exclude trivial and non-integer results
                yield (x // y, '/')
    
    
    def countdown_solver():
        # input: target and numbers. If you want to play with more or less than
        # 6 numbers, use the second version of 'unsorted_numbers'.
        try:
            target = int(sys.argv[1])
            unsorted_numbers = (int(sys.argv[n+2]) for n in range(6))  # for 6 numbers
    #        unsorted_numbers = (int(n) for n in sys.argv[2:])         # for any numbers
            numbers = tuple(sorted(unsorted_numbers, reverse=True))
        except (IndexError, ValueError):
            print("You must provide a target and numbers!")
            return
    
        solutions = Solutions(numbers)
        smallest_difference = target
        bestresults = []
        for calculation in solutions.walk():
            diff = abs(calculation.result - target)
            if diff <= smallest_difference:
                if diff < smallest_difference:
                    bestresults = [calculation]
                    smallest_difference = diff
                else:
                    bestresults.append(calculation)
        output(target, smallest_difference, bestresults)
    
    
    def output(target, diff, results):
        print(f"\nThe closest results differ from {target} by {diff}. They are:\n")
        for calculation in results:
            print(f"{calculation.result} = {calculation.expr}")
    
    
    if __name__ == "__main__":
    countdown_solver()
    

    The algorithm works as follows:

    1. The numbers are put into a tuple of length 6 in descending order. Then, all unique subgroups of lengths 1 to 6 are created, the smallest groups first.

      Example: (75, 50, 5, 9, 1, 1) -> {(75), (50), (9), (5), (1), (75, 50), (75, 9), (75, 5), ..., (75, 50, 9, 5, 1, 1)}.

    2. Next, the groups are organised into a hierarchical tree: every group is partitioned into all unique unordered pairs of its non-empty subgroups.

      Example: (9, 5, 1, 1) -> [(9, 5, 1) + (1), (9, 1, 1) + (5), (5, 1, 1) + (9), (9, 5) + (1, 1), (9, 1) + (5, 1)].

    3. Within each group of numbers, the calculations are performed and the results are stored. For groups of length 1, the result is simply the number itself. For larger groups, the calculations are carried out on every pair of subgroups: in each pair, all results of the first subgroup are combined with all results of the second subgroup using +, -, x and /, and the valid outcomes are stored.

      Example: (75, 5) consists of the pair ((75), (5)). The result of (75) is 75; the result of (5) is 5; the results of (75, 5) are [75+5=80, 75-5=70, 75*5=375, 75/5=15].

    4. In this manner, all results are generated, from the smallest groups to the largest. Finally, the algorithm iterates through all results and selects the ones that are the closest match to the target number.

    For a group of m numbers, the maximum number of arithmetic computations is

    comps[m] = 4*sum(binom(m, k)*comps[k]*comps[m-k]//(1 + (2*k)//m) for k in range(1, m//2+1))
    

    For all groups of length 1 to 6, the maximum total number of computations is then

    total = sum(binom(n, m)*comps[m] for m in range(1, n+1))
    

    which is 1144386. In practice, it will be much less, because the algorithm reuses the results of duplicate groups, ignores trivial operations (adding 0, multiplying by 1, etc), and because the rules of the game dictate that intermediate results must be positive integers (which limits the use of the division operator).

    0 讨论(0)
提交回复
热议问题