式木、二項演算、括弧のネスト処理

Unity学習帳2冊目式木、二項演算、括弧のネスト処理

式木、二項演算、括弧のネスト処理 anchor.png

ネットをぶらぶら散策していると面白い問題を見つけた。25年前に活躍していた低価格パーソナルコンピュータMSXを扱った雑誌で、その標準機能だったBASICを使って問題を解くというクイズ記事。以下のリンクにそのページが公開されている
MSX_Magazine_1991-12_ASCII_JP
論理や算術演算を自動生成して目的に合わせ解く問題。非常に興味深いのでこの問題を解いてみる

資料:MSXマガジン_バックナンバーアーカイブ

Page Top

問題 anchor.png

0000~9999までの4桁の数の間に四則演算を入れて10が作れないものはいくつあるか?プログラムで求めよ

<ルール>

  1. +-×÷の記号のみ使用できる事とする。ルートやべき乗は使用不可
  2. 計算は一般の式と同じ順序で行う(×÷が優先、左から右に計算する)
  3. ()は使用できる
  4. 最初の数字の頭に-を付けて負の数にすることは出来る
  5. 数字の並べ替えは出来ない
  6. 二つ以上の連続する数字をまとめて2桁以上の数として扱うことは出来ない
Page Top

問題解決の履歴 anchor.png

この問題をエレガントに解く方法があるかもしれないが、筆者には全く思いつかない
なので一般的にブルートフォースと呼ばれる総当たり、力技で強引に問題を解決する手法を選択する
この章では問題の各ルールに対して、どう対処するかの基本方針を固める

Page Top

1.と2.について anchor.png

  • 算術演算処理(calculate)には二項演算の二分木、式木の仕組みを利用してプログラム処理する
  • 四則演算、括弧のネストの優先順位に従って式木を組む

資料:

Page Top

3.について anchor.png

  • 括弧を使ったネストのある数式を文字列として受け取り解析処理(Parse)する必要がある。処理のサンプルは上記の資料内にある。資料では括弧の処理に関し詳しい説明は省かれている。この辺りはコードを読む必要があった。問題に合わせ4桁の数字に対する括弧の作り方を適切に出力制御させる必要がある。文字列でなく式木の形で渡しても良いかもしれない。この辺りは組みながら考えることにする
  • 全ての論理式は同じ個数の右括弧と左括弧を持つ。片方の括弧が多いと式は成立しないので、それらはエラー処理する
Page Top

4.5.6.ついて anchor.png

  • 四則演算子+-×÷は算術演算子でもあり二項演算子である。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}\)として扱い処理することで解決する

Page Top

掘り下げて考える anchor.png

仕組みを掘り下げて考えてみる。まず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.の処理に関しては式木で対応する事で解決する。これに関しては資料で扱ったコードを改造して利用する

Everything is expanded.Everything is shortened.
  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
-
|
|
|
|
|
!
 
 
 
 
 
-
|
|
-
|
-
!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
|
|
-
|
!
-
|
|
|
|
|
-
|
!
|
-
!
-
-
!
|
|
-
|
|
|
!
|
-
|
!
-
!
|
|
-
!
|
|
-
!
!
|
-
|
!
-
-
!
|
|
|
-
!
|
-
|
|
|
|
|
|
|
!
|
|
|
|
-
!
|
-
!
|
|
|
!
|
-
!
-
|
|
|
|
|
|
|
|
-
!
-
|
-
|
|
|
|
|
|
|
|
!
|
-
|
|
|
!
-
|
|
!
!
|
|
!
|
|
-
|
|
|
|
|
|
!
|
|
-
|
|
|
|
|
|
|
|
|
|
|
|
|
!
|
|
-
|
|
|
|
|
|
!
|
-
|
!
-
-
!
-
-
!
|
|
-
!
-
|
|
|
|
|
!
!
-
|
!
!
!
!
//当コードの著作:総武ソフトウェア推進所 http://smdn.jp/
//ソース元 http://smdn.jp/programming/tips/polish/
//著作は元ソースの権利者に帰属
//
//ソース元のコードで一部動かない間違いがあったので修正しています
//理解しやすいように注釈の追加とunity用の改編を行っています
 
using UnityEngine;
using System.Collections;
using System;
 
public class Tree3 : MonoBehaviour
{
 
    void Start()
    {
 
        //両端を括弧で閉じるのはNG(最下層の括弧は必ず省略する必要がある)
        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;
        }
 
        //文字列の式をNode型に式木で分解する
        public void Parse()
        {
            //分解は一番優先順位の低い演算子の位置から始まる
            var posOperator = GetOperatorPos(Expression);
 
            if (posOperator < 0)
            {
                Left = null;
                Right = null;
                return;
            }
 
            //Nodeクラスのルートと左右の葉に適切にデーターを収納する
            //左右の葉では、それぞれ解析実行して再帰処理し文字列の式を分解していく
 
            // left-hand side
            Left = new Node(RemoveBracket(this.Expression.Substring(0, posOperator)));
            Left.Parse();
 
            // right-hand side
            Right = new Node(RemoveBracket(this.Expression.Substring(posOperator + 1)));
            Right.Parse();
 
            // operator
            this.Expression = this.Expression.Substring(posOperator, 1);
        }
 
        //文字列の式から外側の括弧を消す処理
        //例:"((2*4)+3)"→"(2*4)+3"
        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;
                }
 
                //nestが0は括弧が閉じ切っている。ネストの底に位置している事を意味する
                //nestが0以上は括弧内なので記録しない。0以下はエラーを意味するのでこれも記録しない
                //左から順に右に向かって優先順位の低い結合子の位置を探す。同じ優先順位の結合子なら右にあるものほど低い
                //例:"5/(3+(2*4))-3"ならば一番優先順位が低い結合子は"-"となる。左項は"5/(3+(2*4))"、右項は"3"となる
                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();
        }
 
        //自分自身を必要回複数呼び出す再帰関数
        //ノードは結合子に、サブツリーの末端は左右の葉がnullになることを利用して計算処理している
        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\)

このルールに従って表を作ると以下のようになる

①②③
000a①b②c③d
001a①b②(c③d)
010a①(b②c)③d
100(a①b)②c③d
101(a①b)②(c③d)
011a①(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

一番優先順位が低い結合子は ①=/ になる。従って式木は
tree1.png

この一番優先順位が低い結合子を選ぶアルゴリズムは資料のGetOperatorPos関数にある
括弧もここでうまく処理されているので注目。もう一例

例:"(5/(3+2))*4"

一番優先順位が低い結合子は ③=* になる。従って式木は
tree2.png

この要領で式木を生成すればいい。式木の末端程、計算優先順位は高くなるのでcalculate時は末端からトラバースして計算する

Page Top

式木から数式のパターンを眺めてみる anchor.png

ここで視点を変えて式木から数式のパターンを眺めてみる。よくある式の一つを眺めてみると以下のような構成となっている(見やすさを優先して横倒しにしています)
tree3.png
この①~③の並びを順列と考えて置換すると

①②③
①③②
②①③ ×
②③① ×
③①②
③②①

の \(3!=3\times 2\times 1=6\) パターンで全てを網羅する事ができる。そこで、この式木を実際に組んで性質をまとめてみる。まず、以下の約束事を定義する

\(①,②,③=\left\{ \times ,\div ,+,- \right\} \\ \overline { ① } ,\overline { ② } ,\overline { ③ } =\left\{ \times ,\div \right\} \\ \underline { ① } ,\underline { ② } ,\underline { ③ } =\left\{ +,- \right\} \)

二項結合演算子(この場合①~③で「×÷+-」の集合)に上線があるものは優先順位の高い演算子の集合「×÷」。下線のあるものは優先順位の低い演算子の「+-」の集合になっている。このルールに従い式木から数式を見ると以下のような分類が可能となる

Page Top

式木123のパターン anchor.png

\(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) \)
tree3.png

Page Top

式木132のパターン anchor.png

\(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 \)
tree4.png

Page Top

式木312のパターン anchor.png

\(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\)
tree5.png

Page Top

式木321のパターン anchor.png

\(\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\)
tree6.png

Page Top

式木亜種のパターン anchor.png

\(\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\)
tree8.png

Page Top

式木から見て②から始まるパターンは使えない anchor.png

式木から見て②から始まるパターンはありえない。そのことを以下で確認してみる
tree7.png
これは項同士が隣り合っていない、間に邪魔な項があり演算できない。従って式木が成立しない。231のパターンも同様である

Page Top

括弧によるネストがない数式を式木として見てみる anchor.png

次に括弧によるネストがない数式を式木として見てみると以下のように上記で紹介した各パターンに重複することがわかる

\(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 亜種のパターン\)

Page Top

中間まとめ anchor.png

以上の事から式木の5パターンで4項の括弧を含めた四則演算の優先順位が網羅できることが確認できる

この考えが正しいかどうか。まずブルートフォースの形式で力技で率直にアプローチしたコードと、5パターンにまでシンプル化した式木を利用したコードのふたつで検証を行い、どちらの答えも同一であれば、まず間違いはないだろうと考える。コーディングはまだ早い。もう少し考えを進めてみる

Page Top

群(group)の理論で、式木の置換処理を考える anchor.png

資料:群 (数学)
数学における「群」とは「似ているものをひっくるめる」理論といえる。似ているもの同士がどこが似ているかを人に説明するのは難しい。その対象が抽象的な物であればある程難しいものだと考えられる。


添付ファイル:
filetree8.png 48件 [詳細] filetree7.png 49件 [詳細] filetree6.png 59件 [詳細] filetree5.png 50件 [詳細] filetree4.png 62件 [詳細] filetree3.png 45件 [詳細] filetree2.png 70件 [詳細] filetree1.png 52件 [詳細]

トップ   差分 バックアップ 複製 名前変更 リロード   ページ新規作成 全ページ一覧 単語検索 最新ページの一覧   ヘルプ   最新ページのRSS 1.0 最新ページのRSS 2.0 最新ページのRSS Atom
Last-modified: 2016-08-04 (木) 11:20:26 (JST) (2824d) by osinko