問題
0000~9999までの4桁の数の間に四則演算を入れて10が作れないものはいくつあるか?プログラムで求めよ
<ルール>
- +-×÷の記号のみ使用できる事とする。ルートやべき乗は使用不可
- 計算は一般の式と同じ順序で行う(×÷が優先、左から右に計算する)
- ()は使用できる
- 最初の数字の頭に-を付けて負の数にすることは出来る
- 数字の並べ替えは出来ない
- 二つ以上の連続する数字をまとめて2桁以上の数として扱うことは出来ない
問題解決の履歴
この問題をエレガントに解く方法があるかもしれないが、筆者には全く思いつかない
なので一般的にブルートフォースと呼ばれる総当たり、力技で強引に問題を解決する手法を選択する
この章では問題の各ルールに対して、どう対処するかの基本方針を固める
1.と2.について
- 算術演算処理(calculate)には二項演算の二分木、式木の仕組みを利用してプログラム処理する
- 四則演算、括弧のネストの優先順位に従って式木を組む
資料:
3.について
- 括弧を使ったネストのある数式を文字列として受け取り解析処理(Parse)する必要がある。処理のサンプルは上記の資料内にある。資料では括弧の処理に関し詳しい説明は省かれている。この辺りはコードを読む必要があった。問題に合わせ4桁の数字に対する括弧の作り方を適切に出力制御させる必要がある。文字列でなく式木の形で渡しても良いかもしれない。この辺りは組みながら考えることにする
- 全ての論理式は同じ個数の右括弧と左括弧を持つ。片方の括弧が多いと式は成立しないので、それらはエラー処理する
4.5.6.ついて
- 四則演算子+-×÷は算術演算子でもあり二項演算子である。A×BやA+Bのように二項の演算を扱う
- +-は単項演算子でもある。-xは単項で負符号を表す。x-yは二項演算。-x-yはこれら機能が複合していると考える
- +は省略できるが、-は省略できない
解決方法
\(a\cdot b\cdot c\cdot d\) の四則演算に対して \(-a\cdot b\cdot c\cdot d\) のケースの為に最初の数字を整数\(\mathbb{Z}\)。2番目以降を自然数\(\mathbb{N}\)として扱い処理することで解決する
掘り下げて考える
仕組みを掘り下げて考えてみる。まず4桁の四則演算を以下のように考えるとする
\(a①b②c③d\)
\(a=\left\{ k|-9\le k\le -1,1\le k\le 9 \right\} \\ b,c,d=\left\{ k|1\le k\le 9 \right\} \\ ①,②,③=\left\{ +,-,\times ,\div \right\} \)
つまり①②③は四則演算の二項結合子の集合。aは0を除いた-9~9までの数。b,c,dは1~9までの数となる
ルール1.2.の処理に関しては式木で対応する事で解決する。これに関しては資料で扱ったコードを改造して利用する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
| -
|
|
|
|
|
!
-
|
|
-
|
-
!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
|
|
-
|
!
-
|
|
|
|
|
-
|
!
|
-
!
-
-
!
|
|
-
|
|
|
!
|
-
|
!
-
!
|
|
-
!
|
|
-
!
!
|
-
|
!
-
-
!
|
|
|
-
!
|
-
|
|
|
|
|
|
|
!
|
|
|
|
-
!
|
-
!
|
|
|
!
|
-
!
-
|
|
|
|
|
|
|
|
-
!
-
|
-
|
|
|
|
|
|
|
|
!
|
-
|
|
|
!
-
|
|
!
!
|
|
!
|
|
-
|
|
|
|
|
|
!
|
|
-
|
|
|
|
|
|
|
|
|
|
|
|
|
!
|
|
-
|
|
|
|
|
|
!
|
-
|
!
-
-
!
-
-
!
|
|
-
!
-
|
|
|
|
|
!
!
-
|
!
!
!
!
|
using UnityEngine;
using System.Collections;
using System;
public class Tree3 : MonoBehaviour
{
void Start()
{
var root = new Node("5/(3+(2*4))-3");
print(string.Format("expression: {0}", root.Expression));
root.Parse();
print("reverse polish notation: ");
root.TraversePostorder();
print("");
print("infix notation: ");
root.TraverseInorder();
print("");
print("polish notation: ");
root.TraversePreorder();
print("");
print(string.Format("calculated result: {0}", root.Calculate()));
}
public class Node
{
public string Expression;
public Node Left = null;
public Node Right = null;
public Node(string expression)
{
this.Expression = expression;
}
public void Parse()
{
var posOperator = GetOperatorPos(Expression);
if (posOperator < 0)
{
Left = null;
Right = null;
return;
}
Left = new Node(RemoveBracket(this.Expression.Substring(0, posOperator)));
Left.Parse();
Right = new Node(RemoveBracket(this.Expression.Substring(posOperator + 1)));
Right.Parse();
this.Expression = this.Expression.Substring(posOperator, 1);
}
private static string RemoveBracket(string str)
{
if (!(str.StartsWith("(") && str.EndsWith(")")))
return str;
var nest = 1;
for (var i = 1; i < str.Length - 1; i++)
{
if (str[i] == '(')
nest++;
else if (str[i] == ')')
nest--;
if (nest == 0)
return str;
}
if (nest != 1)
throw new Exception(string.Format("unbalanced bracket: {0}", str));
str = str.Substring(1, str.Length - 2);
if (str.StartsWith("("))
return RemoveBracket(str);
else
return str;
}
private static int GetOperatorPos(string expression)
{
if (string.IsNullOrEmpty(expression))
return -1;
var pos = -1;
var nest = 0;
var priority = 0;
var lowestPriority = 4;
for (var i = 0; i < expression.Length; i++)
{
switch (expression[i])
{
case '=': priority = 1; break;
case '+': priority = 2; break;
case '-': priority = 2; break;
case '*': priority = 3; break;
case '/': priority = 3; break;
case '(': nest++; continue;
case ')': nest--; continue;
default: continue;
}
if (nest == 0 && priority <= lowestPriority)
{
lowestPriority = priority;
pos = i;
}
}
return pos;
}
public void TraversePostorder()
{
if (Left != null)
Left.TraversePostorder();
if (Right != null)
Right.TraversePostorder();
print(Expression);
}
public void TraverseInorder()
{
if (Left != null && Right != null)
print("(");
if (Left != null)
Left.TraverseInorder();
print(Expression);
if (Right != null)
Right.TraverseInorder();
if (Left != null && Right != null)
print(")");
}
public void TraversePreorder()
{
print(Expression);
if (Left != null)
Left.TraversePreorder();
if (Right != null)
Right.TraversePreorder();
}
public double Calculate()
{
if (Left != null && Right != null)
{
var leftOperand = Left.Calculate();
var rightOperand = Right.Calculate();
switch (this.Expression)
{
case "+": return leftOperand + rightOperand;
case "-": return leftOperand - rightOperand;
case "*": return leftOperand * rightOperand;
case "/": return leftOperand / rightOperand;
default: return 0.0d;
}
}
else {
return double.Parse(Expression); }
}
}
}
|
次にルール3.の括弧の処理を考える。例えば二項結合子と左右の二項を一ブロックに考えて \(a①b\) という式があった場合、これに括弧を付けると \((a①b)\) になる。\(a①b②c\) の場合に括弧を付ける場合、 \(\left( a①\left( b②c \right) \right) \) か \(\left( \left( a①b \right) ②c \right) \) となる。このことから、二項結合子を挟んだ2項に対して左右に括弧で囲めば式を生成できる。そこで①をtrue,falseを持つフラグだと考える
①がfalseの時は
\(a①b②c③d\)
①がtrueの時は
\((a①b)②c③d\)
このルールに従って表を作ると以下のようになる
①②③ | | |
000 | a①b②c③d | |
001 | a①b②(c③d) | |
010 | a①(b②c)③d | |
100 | (a①b)②c③d | |
101 | (a①b)②(c③d) | |
011 | a①(b②(c③d)) | a①((b②c)③d) |
110 | ((a①b)②c)③d | (a①(b②c))③d |
111 | (a①(b)②(c)③d) | |
括弧の処理順を考えると000⇔111で同値となっている。これはひとつにする。従ってabcdと①②③の中身を変化させず全部で9パターンの式を作れば括弧ありの式の状態を網羅したことになる。ここで011の括弧の生成をすると
例:"5/(3+(2*4))"
①=/
②=+
③=*
a=5
b=3
c=2
d=4
一番優先順位が低い結合子は ①=/ になる。従って式木は
この一番優先順位が低い結合子を選ぶアルゴリズムは資料のGetOperatorPos関数にある
括弧もここでうまく処理されているので注目。もう一例
例:"(5/(3+2))*4"
一番優先順位が低い結合子は ③=* になる。従って式木は
この要領で式木を生成すればいい。式木の末端程、計算優先順位は高くなるのでcalculate時は末端からトラバースして計算する
式木から数式のパターンを眺めてみる
ここで視点を変えて式木から数式のパターンを眺めてみる。よくある式の一つを眺めてみると以下のような構成となっている(見やすさを優先して横倒しにしています)
この①~③の並びを順列と考えて置換すると
①②③
①③②
②①③ ×
②③① ×
③①②
③②①
の \(3!=3\times 2\times 1=6\) パターンで全てを網羅する事ができる。そこで、この式木を実際に組んで性質をまとめてみる。まず、以下の約束事を定義する
\(①,②,③=\left\{ \times ,\div ,+,- \right\} \\ \overline { ① } ,\overline { ② } ,\overline { ③ } =\left\{ \times ,\div \right\} \\ \underline { ① } ,\underline { ② } ,\underline { ③ } =\left\{ +,- \right\} \)
二項結合演算子(この場合①~③で「×÷+-」の集合)に上線があるものは優先順位の高い演算子の集合「×÷」。下線のあるものは優先順位の低い演算子の「+-」の集合になっている。このルールに従い式木から数式を見ると以下のような分類が可能となる
式木123のパターン
\(a①\left( b②\left( c③d \right) \right) \quad \Leftrightarrow \quad 例:\quad 1\times \left( 2\times \left( 3\times 4 \right) \right) \quad \\ a\underline { ① } b\overline { ② } \left( c③d \right) \quad \Leftrightarrow \quad 例:\quad 1+2\times \left( 3\times 4 \right) \)
式木132のパターン
\(a\underline { ① } \left( b②c \right) \overline { ③ } d\quad \Leftrightarrow \quad 例:\quad 1+\left( 2\times 3 \right) \times 4\quad \\ a①\left( \left( b②c \right) ③d \right) \quad \Leftrightarrow \quad 例:\quad 1\times \left( \left( 2+3 \right) +4 \right) \quad \)
式木312のパターン
\(a\overline { ① } \left( b②c \right) \underline { ③ } d\quad \Leftrightarrow \quad 例:\quad 1\times \left( 2\times 3 \right) +4\quad \\ \left( a①\left( b②c \right) \right) ③d\quad \Leftrightarrow \quad 例:\quad \left( 1+\left( 2+3 \right) \right) \times 4\)
式木321のパターン
\(\left( a①b \right) \overline { ② } c\underline { ③ } d\quad \Leftrightarrow \quad 例:\quad \left( 1\times 2 \right) \times 3+4\quad \\ \left( \left( a①b \right) ②c \right) ③d\quad \Leftrightarrow \quad 例:\quad \left( \left( 1+2 \right) +3 \right) \times 4\)
式木亜種のパターン
\(\left( a①b \right) ②\left( c③d \right) \quad \Leftrightarrow \quad 例:\quad \left( 1+2 \right) \times \left( 3+4 \right) \\ a\overline { ① } b\underline { ② } \left( c③d \right) \quad \Leftrightarrow \quad 例:\quad 1\times 2+\left( 3\times 4 \right) \\ \left( a①b \right) \underline { ② } c\overline { ③ } d\quad \Leftrightarrow \quad 例:\quad \left( 1\times 2 \right) +3\times 4\)
式木から見て②から始まるパターンは使えない
式木から見て②から始まるパターンはありえない。そのことを以下で確認してみる
これは項同士が隣り合っていない、間に邪魔な項があり演算できない。従って式木が成立しない。231のパターンも同様である
括弧によるネストがない数式を式木として見てみる
次に括弧によるネストがない数式を式木として見てみると以下のように上記で紹介した各パターンに重複することがわかる
\(a\overline { ① } b\overline { ② } c\overline { ③ } d\quad \Leftrightarrow \quad a\times b\times c\times d\quad \Leftrightarrow \quad 123のパターン\\ a\underline { ① } b\overline { ② } c\overline { ③ } d\quad \Leftrightarrow \quad a+b\times c\times d\quad \Leftrightarrow \quad 132のパターン\\ a\underline { ① } b\overline { ② } c\underline { ③ } d\quad \Leftrightarrow \quad a+b\times c+d\quad \Leftrightarrow \quad 312のパターン\\ a\overline { ① } b\overline { ② } c\underline { ③ } d\quad \Leftrightarrow \quad a\times b\times c+d\quad \Leftrightarrow \quad 321のパターン\\ a\overline { ① } b\underline { ② } c\underline { ③ } d\quad \Leftrightarrow \quad a\times b+c+d\quad \Leftrightarrow \quad 321のパターン\\ a\underline { ① } b\underline { ② } c\underline { ③ } d\quad \Leftrightarrow \quad a+b+c+d\quad \Leftrightarrow \quad 321のパターン\\ a\underline { ① } b\underline { ② } c\overline { ③ } d\quad \Leftrightarrow \quad a+b+c\times d\quad \Leftrightarrow \quad 亜種のパターン\\ a\overline { ① } b\underline { ② } c\overline { ③ } d\quad \Leftrightarrow \quad a\times b+c\times d\quad \Leftrightarrow \quad 亜種のパターン\)
中間まとめ
以上の事から式木の5パターンで4項の括弧を含めた四則演算の優先順位が網羅できることが確認できる
この考えが正しいかどうか。まずブルートフォースの形式で力技で率直にアプローチしたコードと、5パターンにまでシンプル化した式木を利用したコードのふたつで検証を行い、どちらの答えも同一であれば、まず間違いはないだろうと考える。コーディングはまだ早い。もう少し考えを進めてみる
群(group)の理論で、式木の置換処理を考える
資料:群 (数学)
数学における「群」とは「似ているものをひっくるめる」理論といえる。似ているもの同士がどこが似ているかを人に説明するのは難しい。その対象が抽象的な物であればある程難しいものだと考えられる。