私はPythonで独自のカウントダウンソルバーを作成しました。
これがコードです。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()
アルゴリズムは次のように機能します。
番号は、長さ6のタプルに降順で配置されます。次に、長さが1〜6のすべての一意のサブグループが作成され、最小のグループが最初に作成されます。
例:(75、50、5、9、1、1)-> {(75)、(50)、(9)、(5)、(1)、(75、50)、(75、9)、 (75、5)、...、(75、50、9、5、1、1)}。
次に、グループは階層ツリーに編成されます。すべてのグループは、空でないサブグループのすべての一意の順序付けられていないペアに分割されます。
例:(9、5、1、1)-> [(9、5、1)+(1)、(9、1、1)+(5)、(5、1、1)+(9)、 (9、5)+(1、1)、(9、1)+(5、1)]。
数値の各グループ内で、計算が実行され、結果が保存されます。長さ1のグループの場合、結果は単に数値そのものになります。より大きなグループの場合、計算はサブグループのすべてのペアで実行されます。各ペアで、最初のサブグループのすべての結果が+、-、x、および/を使用して2番目のサブグループのすべての結果と結合され、有効な結果が保存されます。
例:(75、5)はペア((75)、(5))で構成されます。(75)の結果は75です。(5)の結果は5です。(75、5)の結果は[75 + 5 = 80、75-5 = 70、75 * 5 = 375、75 / 5=15]です。
このようにして、最小のグループから最大のグループまで、すべての結果が生成されます。最後に、アルゴリズムはすべての結果を繰り返し処理し、ターゲット番号に最も近い結果を選択します。
m個の数のグループの場合、算術計算の最大数は次のとおりです。
comps[m] = 4*sum(binom(m, k)*comps[k]*comps[m-k]//(1 + (2*k)//m) for k in range(1, m//2+1))
長さが1から6のすべてのグループの場合、計算の最大総数は次のようになります。
total = sum(binom(n, m)*comps[m] for m in range(1, n+1))
これは1144386です。実際には、アルゴリズムが重複グループの結果を再利用し、些細な操作(0の加算、1の乗算など)を無視し、ゲームのルールで中間結果が正の整数(除算演算子の使用を制限します)。