summer_tree_home

Check iOでPython3をマスターするぜっ

Mathematically Lucky Tickets (Mine) - 115810で100を作る

どんな問題?

Mathematically Lucky Tickets
http://www.checkio.org/mission/mathematically-lucky-tickets/

6桁の数字がラッキーナンバーかどうかを判定せよ。6桁を分割して、いくつかの数字を作り、各数字を四則演算して結果が100になる組み合わせが無ければラッキーとなる。
括弧を追加して演算子の優先順位を指定できる。ただし、数字の順番は変更してはならない。

引数は、6桁の数字が文字列で与えられる。
戻り値は、ラッキーナンバーならTrue、でなければFalseを返す。

例題:

checkio('000000') == True
checkio('707409') == True
checkio('595347') == False  # (5 + ((9 * 5) + (3 + 47))) = 100
checkio('271353') == False  # (2 - (7 * (((1 / 3) - 5) * 3))) = 100


ちょっと前のGoogleのCMで見た、「1,1,5,8で10を作る」みたいな問題だなぁ。

Nexus 7 : 10 Puzzle - YouTube

どうやって解く?

まず、四則演算の前に、6桁の数字を分割する必要がある。例えば、123456 なら 1,2,3,4,5,6、123,45,6、12,34,56、、、などなど多くの分割パターンがある。

数字を分割したら、次は演算子(+-*/)の組み合わせを考える。12,34,56 と分割したなら、 12+34+56、12*34/56、12-34/56、12*34-56、、、などがあり、さらに演算子の優先順位も指定できるので、(12*34)-56 と 12*(34-56) とは別パターンとなる。なんだか、ものすごい組み合わせになりそうだ。


まず6桁の数字を分割する方法は、「1,2,3,4,5,6」の5つのコンマがあり、それぞれのコンマを消すか残すかだと考えればいい。全部で 25 の組み合わせがある。すべての分割パターンを求める関数がこちら。

from itertools import product

def get_all_groups(size):
    def get_range(split):
        split_index = [i + 1 for i in range(size - 1) if split[i]]
        return zip([0] + split_index, split_index + [size])  # list of (start,stop)

    return [get_range(split) for split in product([True, False], repeat=size - 1)]

sizeには数字の桁数(=6)を指定する。これで、(開始index,終了index)のリストが得られるので、

    numbers_list = [[int(data[i:j]) for i, j in g] for g in get_all_groups(6)]

とすれば、すべての分割パターンの数字リストが取得できる。


次に、数字を1つ選び、右隣の数字と四則演算を行って、2つの数字を1つにまとめる。DFSで結果が100になるまで続ける。
四則演算は、operatorモジュールのadd,sub,mul,truedivを使った。割り算の場合だけ0除算エラーにならないように事前に右辺が0かどうかをチェックしている。

どういう計算式で100になったかを知りたいので、数字の演算と同時に、計算式(expr)も作成するようにした。

from operator import add, sub, mul, truediv

def checkio(data):
    numbers_list = [[int(data[i:j]) for i, j in g] for g in get_all_groups(6)]
    stack = [([int(s) for s in numbers], [s for s in numbers]) for numbers in numbers_list]
    while stack:
        numbers, expr = stack.pop()
        if len(numbers) == 1:
            if numbers[0] == 100:
                print('{} -> {} = 100'.format(data, expr[0]))
                return False
        else:
            for i in range(len(numbers) - 1):
                for func, op in [(add, '+'), (sub, '-'), (mul, '*'), (truediv, '/')]:
                    if numbers[i + 1] == 0 and func == truediv:  # div by zero
                        continue
                    new_numbers, new_expr = numbers[:], expr[:]
                    new_numbers[i:i + 2] = [func(numbers[i], numbers[i + 1])]
                    new_expr[i:i + 2] = ['({}{}{})'.format(expr[i], op, expr[i + 1])]
                    stack.append((new_numbers, new_expr))
    return True

これで、あれこれ試すと、

012345 -> ((01+(23-4))*5) = 100
123456 -> (1+((2+(3+4))*(5+6))) = 100
314159 -> ((31*4)-(15+9)) = 100

のように表示される。面白いなぁ。

まとめ

実際の解答には計算式は必要ないので、不要な部分を削除したのがこちら。

from itertools import product
from operator import add, sub, mul, truediv

def get_all_groups(size):
    def get_range(split):
        split_index = [i + 1 for i in range(size - 1) if split[i]]
        return zip([0] + split_index, split_index + [size])  # list of (start,stop)

    return [get_range(split) for split in product([True, False], repeat=size - 1)]

def checkio(data):
    numbers_list = [[int(data[i:j]) for i, j in g] for g in get_all_groups(6)]
    while numbers_list:
        numbers = numbers_list.pop()
        if len(numbers) == 1:
            if numbers[0] == 100:
                return False
        else:
            for i in range(len(numbers) - 1):
                for func in [add, sub, mul, truediv]:
                    if numbers[i + 1] == 0 and func == truediv:  # div by zero
                        continue
                    new_numbers = numbers[:]
                    new_numbers[i:i + 2] = [func(numbers[i], numbers[i + 1])]
                    numbers_list.append(new_numbers)
    return True

http://www.checkio.org/mission/mathematically-lucky-tickets/publications/natsuki/python-3/first/


公開後に見直してみると、ちょっとわかりづらいので、一部修正。

def get_groups(data):
    for split in product([True, False], repeat=5):
        split_index = [i + 1 for i in range(5) if split[i]]
        ranges = zip([0] + split_index, split_index + [6])  # list of (start,stop)
        yield [int(data[i:j]) for i, j in ranges]

def checkio(data):
    numbers_list = list(get_groups(data))
    …

この方がわかりやすかったかな。