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

后端 未结 10 1133
清酒与你
清酒与你 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: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).

提交回复
热议问题