【Python】4つの数字を四則演算で10にする遊びをコーディングしてみた


# 概要

車のナンバーを見ると、4つの数字を使って10を作りたくなることってありますよね。

簡単な数字だといいのですが、「これって10にできなくない？」というものもあり、本当に10にできないものと、実はうまく計算すれば10にできるものもあります。

今回はPythonを使って、10にできるかどうかを機械的に判断し、できる場合はその途中式を出力するプログラムを作成してみました。

# 目次
 - そもそもどういう遊びか？
 - 作成するにあたり
 - 実際のコーディング
 - 動作結果
 - さらに良いコードにするために
 - まとめ

# そもそもどういう遊びか？

4種類の数字があったとき、これらを四則演算+ - × ÷を使ってピッタリ10にすれば成功！という遊びです。
例えば1 3 6 7という数字があったとき、

1 + ( 6 ÷ 3 ) + 7 = 10
7 + ( 6 - 3 ) × 1 = 10

などとして10を作ることができます。
（数字の順番は変えてもOKです）

詳細は↓
> テンパズル
https://www.kotanin0.work/entry/2019/03/30/115800

# 作成するにあたり

4つの数字を+ - × ÷を使って計算される式は下記のように考えられる

　□　△　□　△　□　△　□

　□ …数字
　△…演算

となり、数字の並びパターンは
4×3×2×1=24通り

演算は「+, -, ×, ÷」の4種類が3ヶ所に入るので、
4×4×4=64通り

よって、数字＆演算の組合せは
24×64=1536通り

これに加えて（）で計算順序も考慮すると、

　　1.　　□　△　□　△　□　△　□
　　2.　　（　□　△　□　）　△　□　△　□
　　3.　　□　△　（　□　△　□　）　△　□　
　　4.　　□　△　□　△　（　□　△　□　）
　　5.　　（　□　△　□　△　□　）　△　□
　　6.　　□　△　（　□　△　□　△　□　）
　　7.　　（（　□　△　□　）　△　□　）　△　□
　　8.　　（　□　△　（　□　△　□　））　△　□　
　　9.　　□　△　（　□　△　（　□　△　□　））
　　10.　　□　△　（（　□　△　□　）　△　□　）

上記のように（）の場合分けを5通りで考えると

合計で
1536 × 10 = 15360 通り
となり、4桁の数字1種類にあたり 15360 通りものパターンがある。

式はいくつか重複してしまうものがあるが、今回はここら辺の効率を深く考えずに上記の流れでコーディングする。

# 実際のコーディング

今回はターミナルで処理が完結するようなコーディングで進めた。

<font color="Red">※2022/11/02追記
@shiracamusさん、@WolfMoonさんにコメントいただいた内容をコードに反映。
今後、@ttatsfさんのコメントを解読し、記事を修正予定。
</font>

4桁の数字を入力後、「計算して作成する値」を指定できるように修正した。
作成する値の取りうる範囲は、
　　最小値: 0　※今回はマイナスの値を作るケースを考えない。
　　最大値: 9 × 9 × 9 × 9 = 6561
として、0 ≦ x ≦ 6561　の間で値を指定できるようにした。
（指定がない場合は10を作る）

```Python
import random
import itertools


def ask_number():
    while True:
        print("4桁の数字を入力してください。(空欄の場合はランダムな数字を自動生成)")
        number = input()
        if number.isdigit() and len(number) == 4:
            return number
        if not number:
            number = ''.join(random.choices("0123456789", k=4))
            print(f"{number}で判定をします。")
            input()
            return number


def ask_targetNum():
    while True:
        print("続いて、作成する数値を入力してください。（0～6561）")
        targetNum = input()
        if targetNum.isdigit() and (0 <= int(targetNum) <= 6561):
            return targetNum
        if not targetNum:
            targetNum = 10
            print(f"{targetNum}で判定をします。")
            input()
            return targetNum


def calc(number, target):
    '''計算式を生成し、演算結果がtargetと等しい式を返す'''
    for a, b, c, d in sorted(set(itertools.permutations(number, 4))):
        for x, y, z in itertools.product("+-*/", repeat=3):
            expressions = [
                f"{a}{x}{b}{y}{c}{z}{d}",
                f"({a}{x}{b}){y}{c}{z}{d}",
                f"{a}{x}({b}{y}{c}){z}{d}",
                f"{a}{x}{b}{y}({c}{z}{d})",
                f"({a}{x}{b}{y}{c}){z}{d}",
                f"{a}{x}({b}{y}{c}{z}{d})",
                f"({a}{x}{b}){y}({c}{z}{d})",
                f"(({a}{x}{b}){y}{c}){z}{d}",
                f"({a}{x}({b}{y}{c})){z}{d}",
                f"{a}{x}(({b}{y}{c}){z}{d})",
                f"{a}{x}({b}{y}({c}{z}{d}))",
            ]
            for expression in expressions:
                try:
                    if eval(expression) == target:
                        yield expression
                        break  # 式の1つが合致したら他の式は省略する。
                except ZeroDivisionError:
                    pass  # 0で割るパターンはパス


def main():
    number = ask_number()
    targetNum = ask_targetNum()
    answers = list(calc(number, int(targetNum)))
    print("##### 判定結果 #####")
    if answers:
        for answer in answers:
            # 「* を ×」、「/ を ÷」に変換して表示
            print(answer.replace("*", "×").replace("/", "÷"))
    else:
        print(f"{number}は{targetNum}にできない数字の組合せでした")


if __name__ == "__main__":
    main()

```

# 動作結果

いくつかのケースでの結果を添付。
<br>
数字を入力した結果は下記の通り
```
4桁の数字を入力してください。(空欄の場合はランダムな数字を自動生成)
3478
続いて、作成する数値を入力してください。（0～6561）
10
##### 判定結果 #####
(3-7÷4)×8
8×(3-7÷4)
```
<br>

4桁の数字と、作成する値を入力しない場合は下記の通り
```
4桁の数字を入力してください。(空欄の場合はランダムな数字を自動生成)

6131で判定をします。

続いて、作成する数値を入力してください。（0～6561）

10で判定をします。

##### 判定結果 #####
1+1×3+6
1×1+3+6
1÷1+3+6
1+1×6+3
1×1+6+3
1÷1+6+3
1+3+1×6
1+3×1+6
1+3÷1+6
(1-3)×(1-6)
1×3+1+6
1+3+6×1
1+3+6÷1
1×3+6+1
1+6+1×3
1+6×1+3
1+6÷1+3
~~~省略~~~
```

<br>

作成する値に  <b>37</b> を指定してみる。
```
4桁の数字を入力してください。(空欄の場合はランダムな数字を自動生成)
3478
続いて、作成する数値を入力してください。（0～6561）
37
##### 判定結果 #####
(3+8)×4-7
4×(3+8)-7
4×(8+3)-7
(8+3)×4-7
```

<br>
指定した数値にできない4桁の数字のケースはこの通り

```
4桁の数字を入力してください。(空欄の場合はランダムな数字を自動生成)
5684
続いて、作成する数値を入力してください。（0～6561）
1000
##### 判定結果 #####
5684は1000にできない数字の組合せでした
```
<br>

蛇足ですが、コーディングの過程で、1~9の中の重複がない4種類の数字は、全てのケースで四則演算して10にできることがわかった。

# さらに良いコードにするために

今回、コーディングをする中でいくつか課題を残した。

#### ・計算パターンの重複問題
このコードの決定的な欠点である。
例として、4桁の数字 1289 を 10 にするケースを実行した場合、
　( ( 1 × 2 ) × 9 ) - 8
　( ( 2 × 9 ) × 1 ) - 8
が出力され、処理としてはこの2つは同じことを実施している。

まったく同じ式であれば結果の重複を削ることができるが、数字の位置が変わっていて同じ計算をしているものに関しては、上手い処理が思いつかなかった。

**「（）がついた状態と外した状態で変わらず10になれば（）を外す」**
という処理をした後、
**「2種類の式で、四則演算の数が同じ（×が2個、-が1個）であれば、同じ式として扱う」**
という処理を加えることで重複を消せるかもしれないので、今後ブラッシュアップする機会があれば検討しようと思う。

#### ・計算順序の場合分けがイケてない
（）を使って複数通りの計算順序を考えたが、これをコードに直書きするのはイケてない気がする。
機械的に処理できる方法があれば採用したい。


# まとめ
課題はいくつか残ったが、自分の実現したいものをひとまず完成させることができた。

このコーディング中、最初に躓いたのが数字の並び順の表し方であったが、Googleで調べると「itertools 」というライブラリがあり、今回にばっちり適していた。

ライブラリの豊富さとコードの書きやすさがPythonの強みの一つであることを実感した。



<br>
<br>

以上！(^^)/