前回はSEND+MORE=HONEYという覆面算を解くプログラムを作ってみました。
これを変更すれば任意の覆面算を解くことが可能ですが、変更箇所が結構多いので面倒です。
覆面算の式を書き変えるだけで済むようにしようというのが今回の目標です。
足し算だけではなく掛け算や割り算も使えるようにしたいです。
(A+B)*(C+D)=EFG
のような括弧を含む式も使えるようにしたいです。
きんが+しんねん=2017
のように数字を含む覆面算もありますので、これにも対応できるようにします。
まずは、式の構造を把握するところを考えます。
式の構造は演算の実行順序が決まれば決まります。
例えばA+B+C+Dという式はどう計算しても結果は同じですが、普通は左から順に計算します。
((A+B)+C)+D
という構造とみなせます。
右から計算する場合は、A+(B+(C+D))という構造になります。
3つ目、1つ目、2つ目の+の順に計算する場合、(A+B)+(C+D)という構造になります。
足し算の場合はどのような順番に計算しても結果は同じですが、引き算の場合は結果が異なります。
1-2-3-4=-8は ((1-2)-3)-4という計算をしています。
右から計算すると、1-(2-(3-4)))=-2 となります。
括弧のついてない引き算は必ず左から計算しないといけません。
演算子の実行順序を決定する方法を考えてみましょう。
使用する演算子は+,-,*,/の4つとします。
各演算子に優先度を設定します。
通常は乗除先行で、掛け算・割り算が優先されますので、
+,-の優先度は1、*,/の優先度は2とします。
(優先度が大きい方を優先します)
優先度が同じ演算子は左から順に実行します。
A-B*C+D/E
という式では、各演算子の優先度が、左から順に1,2,1,2となります。
優先度の最小値は1ですので、最後に実行される演算子は3番目の+と分かります。
()を含む式の場合、()内の計算が優先されます。
()が多くついているほど優先度が高いです。
式を左から順に見ていき、「(」が現れたら優先度を+10、「)」が現れたら優先度を-10とすれば
各演算子の順序付けができます。
((A+B)-C)*(D+E*(F-G)+H)
という式の場合、各演算子の優先度は21,11,2,11,12,21,11と計算できます。
3個目の*が優先度最小ですので最後に計算されます。
この式は、第一項目(A+B)-C、第二項目D+E*(F-G)+H、演算子*の
二項演算とみなすことができます。
(A+B)-Cは同様の計算をすることによって、
第一項目A+B、第二項目C、演算子-の二項演算と考えられます。
これを繰り返すと、各項目が単独の記号の二項演算に分解できます。
この分解には再帰呼び出しが使えます。
覆面算の場合はA+B=C+Dのような等式になっていますので、必ず=が含まれます。
式を変形して、A+B-(C+D)という形式にして=を除外することにします。
式の値が0になれば成立、0でなければ不成立と判定します。
=が複数ある場合は考えません。
括弧の対応等のチェックもしません。
正しく数式を書いた時に正しく計算されれば十分です。
正しく書かれていないときにどうなるのかは気にしないことにします。
-A+B+Cのような-が最初にある式の場合、上記の方法ではうまくいきません。
このような式でも扱えるようにするため、最初が-の場合はその前に0をつけてやります。
0-A+B+C
式が与えられたら、まずブランクを除去し、最初に-がついていたらその前に0を追加します。
=を-(で置き換え、最後に)を追加します。
右から1文字ずつ見ていって、
・(,)は優先度に加算する値を調整
・+,-,*,/は演算子
・0~9は数字
・その他の文字は(数字を置き換えた)記号とみなします
各演算子の優先度を計算し、優先度が最小のものを見つけます。
(優先度最小のものが複数ある場合は最も左のものを選びます)
その演算子の左側が第一項目、右側が第二項目です。
この二項演算を1番として記憶します。
第一項目、第二項目について同様の処理を行い、演算子を含まなくなるまで分解します。
式の構造の把握と同時に、使われている記号も取得します。
最上位の数字で0にはならない記号もここでチェックしておきましょう。
各記号の値を決めて、式が成立するかどうかチェックします。
記号の値を決めるところは前回作成したプログラムと同じ方法でいけます。
0が入らない記号については0を除外して、式が成立するかどうかチェックします。
成立チェックは式の値を計算する関数を作って、値が0だったら成立とみなします。
式の値を計算する関数は再帰呼び出しを使えば簡単にできます。
演算子を含む場合は、第一項目、第二項目それぞれについて関数を呼び出して値を計算し、
演算子に従って足し算、引き算、掛け算、割り算を行って値を求めます。
演算子を含まない場合は、その項目の値を計算します。
数字または記号だけでできていますので、記号のところにその記号の値をあてはめてやればよいです。
割り算を含まないときはこれで問題ないと思いますが、
割り算を含む場合は、丸め誤差によって誤判定される可能性があります。
例えば電卓で1÷3を計算すると、0.333・・・となり、
それを3倍すると、0.999・・・となり、1になりません。
このような誤差が生じないように、分数で計算することにします。
一つの数値を分子と分母の二つの数として記憶するのです。
というようなことを考えてプログラムを作ってみました(最後に貼り付けておきます)。
行き当たりばったりに作りましたので、統一感のないプログラムになっていますが、
動けばいいんです!
ERASE EQの後に
EQ(1)= "SEND+MORE=HONEY"
というように式を書けば任意の覆面算が解けます。
配列になっているのは連立覆面算も解けるようにしたためで、
EQ(1) = "1/(A+B-C)=12/BC"
EQ(2) = "A+B+C=10"
というように複数の式を書くと、すべての式を成立させる組み合わせを探します。
このプログラムでは最大10個まで指定できます。
使える演算子は+,-,*,/の4つ。()も使えます。
式の長さに制限はありませんが、数の桁数が大きいとオーバーフローエラーになります。
10桁以内の計算であれば大丈夫だと思います。
分数計算についてはTYPEを定義して関数の結果として渡せるようにしようとしたのですが、
なぜか実行するとオートメーションエラーとなって使えませんでした。
仕方ないのでPUBLIC変数を使って渡しています。
目標は達成しましたが、解くのにちょっと時間がかかるのが不満です。
記号が10種類の覆面算だと1分弱かかります。
途中で(応答不能)になるかと思いますが、待っていればきちんと終わります。
(応答不能)になるのが嫌な人はDoEventsを適当な場所に書けば出なくなります。
が、これをつけると速度ががくんと落ちますのでおすすめはしません。
時間がかかるといっても私が手作業で解くよりは圧倒的に早いですが、まだまだ満足できません。
人間の欲望は果てしないものなのですね。
再帰呼び出しをやめれば早くなるような気がしますが、面倒なのであまりやる気になれません。
実は、割り算がなければ早くすることは簡単なのですが、割り算はなくてもいいでしょうか?
1/A+1/B+1/C=1
という式は、
B*C+C*A+B*A=A*B*C
という式とほぼ同じですよね。
こういう形に変形して覆面算を解いて、分母が0になるものを除外すればいいですよね。
割り算を含む覆面算なんてめったに見ないですからね。
というわけで次回は足し算、引き算、掛け算で構成された覆面算を高速で解くことに挑戦です。
◇◇◇
Public PA(100) As Double
Public PB(100) As Double
Public VSU As Integer
Public VLIST As String
Public ESU As Integer
Public FSU As Integer
Public FORM(100) As String
Public ELM1(100) As Integer
Public ELM2(100) As Integer
Public OPR(100) As String
Public INTE(100) As Integer
Public DECI(100) As Integer
Public USE(10) As Integer
Public NONZERO(10) As Integer
Public OUTRAW As Integer
Public SU(9) As Integer
Public EFNO(10) As Integer
Sub SOLVE()
Dim EQ(10) As String
Erase EQ
EQ(1) = "1/(A+B-C)=12/BC"
EQ(2) = "1/(A-1)=1/(A-1)"
STARTTIME = Time
Dim FNO As Integer
FSU = 1
Erase FORM, ELM1, OPR, ELM2, USE, NONZERO
VSU = 0: VLIST = ""
ENO = 1: ESU = 0
Do While EQ(ENO) <> ""
SIKI = EQ(ENO)
SIKI = Replace(SIKI, " ", "")
If InStr(SIKI, "=") > 0 Then SIKI = Replace(SIKI, "=", "-(") + ")"
If Mid(SIKI, 1, 1) = "-" Then SIKI = "0" + SIKI
FSU = FSU + 1
FORM(FSU) = SIKI: EFNO(ENO) = FSU
SPLIT (FSU)
ENO = ENO + 1
Loop
If VSU <= 0 Or VSU > 10 Then
MsgBox ("入力エラー")
End If
FORM(0) = VLIST
ESU = ENO - 1
OUTRAW = 1
Cells(OUTRAW, 1) = VLIST
CHECK (0)
OUTRAW = OUTRAW + 1
ACOUNT = OUTRAW - 2
Cells(OUTRAW, 1) = "答えは" + Format(ACOUNT, "#0") + "個"
Cells(OUTRAW + 1, 1) = ""
SYORITIME = Time - STARTTIME
Debug.Print Minute(SYORITIME) & "分" & second(SYORITIME) & "秒"
End Sub
Function CHECK(ByVal VNO As Integer)
Dim FNO As Integer
Dim ENO As Integer
Dim ERRFLAG As Integer
If VNO = VSU Then
ENO = 1: ERRFLAG = 0
Do While ENO <= ESU And ERRFLAG = 0
FNO = EFNO(ENO)
EVAL (FNO)
If PA(FNO) <> 0 Or PB(FNO) = 0 Then ERRFLAG = 1
ENO = ENO + 1
Loop
If ERRFLAG = 0 Then
OUTRAW = OUTRAW + 1
EVAL (0)
Cells(OUTRAW, 1) = "'" + Format(PA(0), Mid("0000000000", 1, VSU))
End If
Else
For NM = 0 To 9
If USE(NM) = 0 Then
If NM <> 0 Or NONZERO(VNO) = 0 Then
USE(NM) = 1
SU(VNO) = NM
CHECK (VNO + 1)
USE(NM) = 0
End If
End If
Next NM
End If
End Function
Function EVAL(ByVal FNO As Integer)
Dim ERRFLAG As Integer
Dim I As Integer
Dim FUGO As Integer
Dim BUNSI As Long, BUNBO As Long, GC As Long
ERRFLG = 0
If OPR(FNO) = "" Then
SIKI = FORM(FNO)
ATAI = 0
For I = 1 To Len(SIKI)
CHARA = Mid(SIKI, I, 1)
Select Case CHARA
Case " "
Case "0" To "9"
ATAI = ATAI * 10 + Val(CHARA)
Case Else
CNO = InStr(VLIST, CHARA) - 1
ATAI = ATAI * 10 + SU(CNO)
End Select
Next I
PA(FNO) = ATAI: PB(FNO) = 1
If ERRFLG = 1 Then PB(FNO) = 0
Else
EVAL (ELM1(FNO))
B1 = PB(ELM1(FNO))
If B1 = 0 Then
PB(FNO) = 0
Else
EVAL (ELM2(FNO))
B2 = PB(ELM2(FNO))
If B2 = 0 Then
PB(FNO) = 0
Else
A1 = PA(ELM1(FNO)): A2 = PA(ELM2(FNO))
Select Case OPR(FNO)
Case "+"
BUNSI = A1 * B2 + B1 * A2: BUNBO = B1 * B2
Case "-"
BUNSI = A1 * B2 - B1 * A2: BUNBO = B1 * B2
Case "*"
BUNSI = A1 * A2: BUNBO = B1 * B2
Case "/"
BUNSI = A1 * B2: BUNBO = B1 * A2
End Select
FUGO = Sgn(BUNSI) * Sgn(BUNBO)
BUNSI = Abs(BUNSI): BUNBO = Abs(BUNBO)
If BUNSI = 0 Then BUNBO = 1
If FUGO <> 0 Then
GC = GCD(BUNSI, BUNBO)
BUNSI = BUNSI / GC: BUNBO = BUNBO / GC
End If
PA(FNO) = FUGO * BUNSI: PB(FNO) = BUNBO
End If
End If
End If
End Function
Function GCD(ByVal BUNSI As Long, ByVal BUNBO As Long)
Dim WW As Long
If BUNSI > BUNBO Then
WW = BUNSI: BUNSI = BUNBO: BUNBO = WW
End If
WW = BUNBO Mod BUNSI
If WW = 0 Then
GCD = BUNSI
Else
GCD = GCD(BUNSI, WW)
End If
End Function
Function SPLIT(ByVal FNO As Integer)
Dim SAVENO As Integer, SAVEPRI As Integer, SAVEPOS As Integer
Dim SCOUNT As Integer
Dim SIKI As String
Dim PAIR As Integer
Dim CHARA As String
Dim VNO As Integer
SAVENO = -1: SCOUNT = 0
SAVEPRI = 0: SAVEPOS = 0
SIKI = FORM(FNO)
PAIR = 0
For I = 1 To Len(SIKI)
PRI = 0
CHARA = Mid(SIKI, I, 1)
Select Case CHARA
Case " "
Case "+": PRI = 1
Case "-": PRI = 1
Case "*": PRI = 2
Case "/": PRI = 2
Case "(": PAIR = PAIR + 1
Case ")": PAIR = PAIR - 1
Case "0" To "9"
SCOUNT = SCOUNT + 1
Case Else
SCOUNT = SCOUNT + 1
VNO = InStr(VLIST, CHARA) - 1
If VNO < 0 Then
VSU = VSU + 1: VLIST = VLIST + CHARA
VNO = VSU
End If
If SCOUNT = 1 Then SAVENO = VNO
End Select
If PRI > 0 Then
PRI = PRI + PAIR * 10
If PRI <= SAVEPRI Or SAVEPRI = 0 Then
SAVEPRI = PRI: SAVEPOS = I
End If
End If
Next I
If SAVEPOS > 0 Then
OPR(FNO) = Mid(SIKI, SAVEPOS, 1)
FSU = FSU + 1
ELM1(FNO) = FSU
FORM(FSU) = Mid(SIKI, 1, SAVEPOS - 1)
FSU = FSU + 1
ELM2(FNO) = FSU
FORM(FSU) = Mid(SIKI, SAVEPOS + 1)
SPLIT (ELM1(FNO))
SPLIT (ELM2(FNO))
Else
OPR(FNO) = ""
FORM(FNO) = Replace(FORM(FNO), "(", "")
FORM(FNO) = Replace(FORM(FNO), ")", "")
If SAVENO >= 0 And SCOUNT > 1 Then NONZERO(SAVENO) = 1
End If
End Function
0 件のコメント:
コメントを投稿