2023年12月31日日曜日

Powershellで計算ゲーム


PowerShellの学習のためテンパズルっぽいものを解いてみる。



スペース区切りで数字を入れ、最後の数字に計算結果となる数字を入れると計算式がなりたつようなものを作ってみる。

例えば入力が 1 2 3 4 5 6 とすると

((1+2)*((3+4)-5))=6

((1+2)*(3+(4-5)))=6

のような表示をするようなものを作ってみる。


そのなかで、PowerShellネイティブ型とdotnetのクラスでの相互のやり取りがどのぐらいスムーズにいくのかを体感してみる。


ロジックでは、

数字の並びと演算子の並びを別々に考えたほうが時間短縮になるかとおもうので、あえて逆ポーランド記法は使わない。

ただ計算途中の記憶にスタック構造を使うし、二項演算子の2つをスタックに積むので考え方は同じ。


まずは、何も工夫もせずに作った例。


#計算ゲームver1

function 数列の作成{
    param(
    $数字候補,
    $組み立て済数列
    )
    process{

        if ($数字候補.Count -gt 0){
            for($i=0;$i -lt $数字候補.Count;$i++){
                $取り出し数字=$数字候補[$i]
                $取り出し済の数字候補=$数字候補.Clone()
                $取り出し済の数字候補.RemoveAt($i)
                数列の作成 $取り出し済の数字候補 ($組み立て済数列+$取り出し数字)
            }
        } else {
           #完成数字列の登録
           $完成数字列.Add($組み立て済数列) | Out-Null
        }
    }
}

function 演算子列の作成 {
    param(
      $演算子列,
      $保留を使用した数,
      $ストックされた数,
      $残り必要な演算子数
    )
    process{
        if ($残り必要な演算子数 -gt 0 ){
           foreach ($演算子 in ("T","H","K","W", "S")){
             #前2つの数字に対する演算方法の取り決め
             #Sは演算の保留
             if ($演算子 -eq "S"){
               #演算の保留ができない場合(後に数字がない場合)は保留禁止
               # 残りの数字の数は、問題の数字の数-保留を行った数
               if ($問題の数字の数 -le $保留を使用した数){ continue}
               演算子列の作成 ($演算子列 + "S" ) ($保留を使用した数 + 1) ($ストックされた数 + 1) $残り必要な演算子数
             } else {
               #演算ができない場合は、演算禁止
               #ストックされた数が2以上でないと演算できない
               if($ストックされた数 -lt 2 ) { continue}
                 演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
             }
          }
        }
        else {
          $完成版演算子列.Add($演算子列) | Out-Null
        }
    }
}

#使う数字と答えを入れてもらう

$inputT=Read-Host
$input=[int[]]($inputT -split " ")
$inputnum,$ans=[System.Collections.ArrayList]::new($input[0..($input.Length-2)]),$input[-1]


$完成版演算子列 = [System.Collections.ArrayList]::new()
$完成数字列 = [System.Collections.ArrayList]::new()
$問題の数字の数=$inputnum.Count

#演算子列と数列を作成する。
演算子列の作成 "" 0 0 ($inputnum.Count-1) #2項演算子のため2つめの数字に対する演算はスキップ"S"で、演算子数は数字の個数-1
数列の作成 $inputnum @()
"====="

#答え合わせ
foreach ($今回の数字の並び in $完成数字列){
  foreach ($今回の演算子の並び in $完成版演算子列){
    $tmp数字列=$今回の数字の並び #Clone()
    $numIndex=0
    $ngflg=$false
    $数式stack=[System.Collections.Stack]::new()
    $stack=[System.Collections.Stack]::new()
    for ($index = 0;$index -lt $今回の演算子の並び.Length;$index ++){
      switch($今回の演算子の並び[$index]){
      "S" { 
        $stack.Push($tmp数字列[$numIndex])
        $数式stack.Push($今回の数字の並び[$numIndex].ToString())
        $numIndex++
      }
      default {
        $op2=$stack.Pop() ; $数式op2=$数式stack.Pop()
        $op1=$stack.Pop() ; $数式op1=$数式stack.Pop()
        switch($_){
        #数式の計算はすべて()付きで行う。
        "T"{ $stack.Push($op1+$op2); $数式stack.Push("("+$数式op1+"+"+$数式op2+")")}
        "H"{ $stack.Push($op1-$op2); $数式stack.Push("("+$数式op1+"-"+$数式op2+")")}
        "K"{ $stack.Push($op1*$op2); $数式stack.Push("("+$数式op1+"*"+$数式op2+")")}
        "W"{ 
            #0で割る事態が発生したら誤答とするが、とりあえずダミーの計算を入れる。

          if($op2 -eq 0){
            $ngflg=$true
            $stack.Push($op1)
          } else{
            $stack.Push($op1/$op2)

          }
          $数式stack.Push("("+$数式op1+"/"+$数式op2+")")
        }
      }
      }
    }
  }
  $op=$stack.Pop()
  $数式op=$数式stack.Pop()
  #小数点の計算には誤差が発生するため小数点第4桁まで一致で妥協する。
  if(!$ngflg -and ([Math]::Round($op,4) -eq[Math]::Round($ans,4)) ){
    "$数式op=$ans :数列→$tmpans $($今回の数字の並び -join ",")  演算子列→$今回の演算子の並び"
    if ((Invoke-Expression $数式op) -ne  $ans){
    "審議!"
    } 
    #"-"
    #$今回の数字の並び
    #$今回の演算子の並び
    #"--"
  }
  }
}

次に、計算式に無駄な括弧が多いので、不要な括弧を除く対応をしてみる。
足し算
(a+b)+(c+d)=a+b+c+d
(a-b)+(c-d)=a-b+c-d
(a*b)+(c*d)=a*b+c*d
(a/b)+(c/d)=a/b+c/d
足し算は、プラスの左側右側ともに括弧を外せる。
引き算
(a+b)-(c+d)=a+b-(c+d)≠a+b-c+d
(a-b)-(c-d)=a-b-(c-d)≠a-b-c-d
(a*b)-(c*d)=a*b-c*d
(a/b)-(c/d)=a/b-c/d
引き算は、マイナスの左側は括弧が外せるが、右側はプラスとマイナスがある場合は括弧が必要。
掛け算
(a+b)*(c+d)=(a+b)*(c+d)≠...
(a-b)*(c-d)=(a-b)*(c-d)≠...
(a*b)*(c*d)=a*b*c*d
(a/b)*(c/d)=a/b*c/d
掛け算は、プラスとマイナスの場合、左右とも括弧が必要で、掛けると割るは括弧が外せる。
割り算
(a+b)/(c+d)=(a+b)/(c+d)≠...
(a-b)/(c-d)=(a-b)/(c-d)≠...
(a*b)/(c*d)=a*b/(c*d)≠...
(a/b)/(c/d)=a/b/(c/d)≠...
割り算は、プラスとマイナスの場合、左右とも括弧が必要で、掛けると割るの場合は、左側は括弧が不要で右側に括弧が必要。

この判定としては、演算子の左側と右側のともに括弧の部分を除いた部分の判定となる。
括弧の部分は判定不要なので、括弧を取り除いた部分での判定となり、正規表現を使った置換で最外殻の部分を判定する。今回は.netフレームワークの正規表現なので、pythonやrubyなどの再帰を使えない。.netフレームワークの正規表現ではその代わりにキャプチャする変数をスタックとして利用できるので、それを利用する。

また、不要な括弧を除くと、前出力した計算式と今回の計算式が同じとなることがあるので、結果計算式が同じとなった場合は2個目以降の表示を抑止することにする。

#計算ゲームver2
#ver1からの改良点:不要な括弧をのぞき、結果として同じ数式となった場合の重複排除


function 数列の作成{
    param(
    $数字候補,
    $組み立て済数列
    )
    process{

        if ($数字候補.Count -gt 0){
            for($i=0;$i -lt $数字候補.Count;$i++){
                $取り出し数字=$数字候補[$i]
                $取り出し済の数字候補=$数字候補.Clone()
                $取り出し済の数字候補.RemoveAt($i)
                数列の作成 $取り出し済の数字候補 ($組み立て済数列+$取り出し数字)
            }
        } else {
           #完成数字列の登録
           $完成数字列.Add($組み立て済数列) | Out-Null
        }
    }
}

function 演算子列の作成 {
    param(
      $演算子列,
      $保留を使用した数,
      $ストックされた数,
      $残り必要な演算子数
    )
    process{
        if ($残り必要な演算子数 -gt 0 ){
           foreach ($演算子 in ("T","H","K","W", "S")){
             #前2つの数字に対する演算方法の取り決め
             #Sは演算の保留
             if ($演算子 -eq "S"){
               #演算の保留ができない場合(後に数字がない場合)は保留禁止
               # 残りの数字の数は、問題の数字の数-保留を行った数
               if ($問題の数字の数 -le $保留を使用した数){ continue}
               演算子列の作成 ($演算子列 + "S" ) ($保留を使用した数 + 1) ($ストックされた数 + 1) $残り必要な演算子数
             } else {
               #演算ができない場合は、演算禁止
               #ストックされた数が2以上でないと演算できない
               if($ストックされた数 -lt 2 ) { continue}
                 演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
             }
          }
        }
        else {
          $完成版演算子列.Add($演算子列) | Out-Null
        }
    }
}

#使う数字と答えを入れてもらう

$inputT=Read-Host
$input=[int[]]($inputT -split " ")
$inputnum,$ans=[System.Collections.ArrayList]::new($input[0..($input.Length-2)]),$input[-1]


$完成版演算子列 = [System.Collections.ArrayList]::new()
$完成数字列 = [System.Collections.ArrayList]::new()
$検証済式の保存=[System.Collections.Generic.HashSet[string]]::new()
$問題の数字の数=$inputnum.Count

#演算子列と数列を作成する。
演算子列の作成 "" 0 0 ($inputnum.Count-1) #2項演算子のため2つめの数字に対する演算はスキップ"S"で、演算子数は数字の個数-1
数列の作成 $inputnum @()
"====="

#答え合わせ
foreach ($今回の数字の並び in $完成数字列){
  foreach ($今回の演算子の並び in $完成版演算子列){
    $tmp数字列=$今回の数字の並び #Clone()
    $numIndex=0
    $ngflg=$false
    $数式stack=[System.Collections.Stack]::new()
    $stack=[System.Collections.Stack]::new()
    for ($index = 0;$index -lt $今回の演算子の並び.Length;$index ++){
      switch($今回の演算子の並び[$index]){
      "S" { 
        $stack.Push($tmp数字列[$numIndex])
        $数式stack.Push($今回の数字の並び[$numIndex].ToString())
        $numIndex++
      }
      default {
        $op2=$stack.Pop() ; $数式op2=$数式stack.Pop()
        $op1=$stack.Pop() ; $数式op1=$数式stack.Pop()
        switch($_){
        "T"{ $stack.Push($op1+$op2); $数式stack.Push($数式op1+"+"+$数式op2)}
        "H"{ $stack.Push($op1-$op2);
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"-"+$数式op2)
           }
        "K"{ $stack.Push($op1*$op2); 
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op1="("+$数式op1+")"
             }
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"*"+$数式op2)
           }

        "W"{ 
             #0で割る事態が発生したら誤答とするが、とりあえずダミーの計算を入れる。
             if($op2 -eq 0){
               $ngflg=$true
               $stack.Push($op1)
             } else{
               $stack.Push($op1/$op2)
             }
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op1="("+$数式op1+")"
             }
             # op2のカッコの外側で+もしくは-もしくは/が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-|/|\*"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"/"+$数式op2)
           }
        }
      }
      }
    }

    $op=$stack.Pop()
    $数式op=$数式stack.Pop()
    #小数点の計算には誤差が発生するため小数点第4桁まで一致で妥協する。
    if(!$ngflg -and ([Math]::Round($op,4) -eq[Math]::Round($ans,4)) -and !($検証済式の保存.Contains($数式op))){
      $検証済式の保存.Add($数式op) | Out-Null
      "$数式op=$ans :数列→$tmpans $($今回の数字の並び -join ",")  演算子列→$今回の演算子の並び"
      if ((Invoke-Expression $数式op) -ne  $ans){
      "審議!"
      } 
    }  
  }
}


最後に割り算の計算誤差をごまかしていた部分の対応。
結局分数の形で割り算を扱えば、分母分子がそれぞれ整数でおさまるので、これで誤差の処理が不要となる。

#計算ゲームver3
#ver2からの改良点:割り算を分数として表現し、誤差補正処理を排除する
#分数表現は2項目の配列とし1個目を分子。2個目を分母とする。
#足し算引き算では通分演算する。約分はしない(lcd,gcdを使わない)

function 数列の作成{
    param(
    $数字候補,
    $組み立て済数列
    )
    process{

        if ($数字候補.Count -gt 0){
            for($i=0;$i -lt $数字候補.Count;$i++){
                $取り出し数字=$数字候補[$i]
                $取り出し済の数字候補=$数字候補.Clone()
                $取り出し済の数字候補.RemoveAt($i)
                数列の作成 $取り出し済の数字候補 ($組み立て済数列+$取り出し数字)
            }
        } else {
           #完成数字列の登録
           $完成数字列.Add($組み立て済数列) | Out-Null
        }
    }
}

function 演算子列の作成 {
    param(
      $演算子列,
      $保留を使用した数,
      $ストックされた数,
      $残り必要な演算子数
    )
    process{
        if ($残り必要な演算子数 -gt 0 ){
           foreach ($演算子 in ("T","H","K","W", "S")){
             #前2つの数字に対する演算方法の取り決め
             #Sは演算の保留
             if ($演算子 -eq "S"){
               #演算の保留ができない場合(後に数字がない場合)は保留禁止
               # 残りの数字の数は、問題の数字の数-保留を行った数
               if ($問題の数字の数 -le $保留を使用した数){ continue}
               演算子列の作成 ($演算子列 + "S" ) ($保留を使用した数 + 1) ($ストックされた数 + 1) $残り必要な演算子数
             } else {
               #演算ができない場合は、演算禁止
               #ストックされた数が2以上でないと演算できない
               if($ストックされた数 -lt 2 ) { continue}
                 演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
             }
          }
        }
        else {
          $完成版演算子列.Add($演算子列) | Out-Null
        }
    }
}

function 足す {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        ($被数分子*$原数分母+$原数分子*$被数分母) , ($被数分母*$原数分母) 
    }
}

function 引く {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        ($被数分子*$原数分母-$原数分子*$被数分母) , ($被数分母*$原数分母) 
    }
}
function 掛ける {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        ($被数分子*$原数分子) , ($被数分母*$原数分母) 
    }
}

function 割る {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        ($被数分子*$原数分母) , ($被数分母*$原数分子) 
    }
}


#使う数字と答えを入れてもらう

$inputT=Read-Host
$input=[int[]]($inputT -split " ")
$inputnum,$ans=[System.Collections.ArrayList]::new($input[0..($input.Length-2)]),$input[-1]


$完成版演算子列 = [System.Collections.ArrayList]::new()
$完成数字列 = [System.Collections.ArrayList]::new()
$検証済式の保存=[System.Collections.Generic.HashSet[string]]::new()
$問題の数字の数=$inputnum.Count

#演算子列と数列を作成する。
演算子列の作成 "" 0 0 ($inputnum.Count-1) #2項演算子のため2つめの数字に対する演算はスキップ"S"で、演算子数は数字の個数-1
数列の作成 $inputnum @()
"====="

#答え合わせ
foreach ($今回の数字の並び in $完成数字列){
  foreach ($今回の演算子の並び in $完成版演算子列){
    $tmp数字列=$今回の数字の並び #Clone()
    $numIndex=0
    $ngflg=$false
    $数式stack=[System.Collections.Stack]::new()
    $stack=[System.Collections.Stack]::new()
    for ($index = 0;$index -lt $今回の演算子の並び.Length;$index ++){
      switch($今回の演算子の並び[$index]){
      "S" { 
        $stack.Push(@($tmp数字列[$numIndex],1))
        $数式stack.Push($今回の数字の並び[$numIndex].ToString())
        $numIndex++
      }
      default {
        $op2=$stack.Pop() ; $数式op2=$数式stack.Pop()
        $op1=$stack.Pop() ; $数式op1=$数式stack.Pop()
        switch($_){
        "T"{ $stack.Push((足す $op1 $op2)); $数式stack.Push($数式op1+"+"+$数式op2)}
        "H"{ $stack.Push((引く $op1 $op2))
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"-"+$数式op2)
           }
        "K"{ $stack.Push((掛ける $op1 $op2)) 
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op1="("+$数式op1+")"
             }
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"*"+$数式op2)
           }

        "W"{ 
             #0で割る事態が発生したら誤答とするが、とりあえずダミーの計算を入れる。
             if($op2[0] -eq 0){
               $ngflg=$true
               $stack.Push($op1)
             } else{
               $stack.Push((割る $op1 $op2))
             }
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op1="("+$数式op1+")"
             }
             # op2のカッコの外側で+もしくは-もしくは/が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-|/|\*"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"/"+$数式op2)
           }
        }
      }
      }
    }

    $op=$stack.Pop()
    $数式op=$数式stack.Pop()
    if(!$ngflg -and $op[0] -eq ($ans * $op[1]) -and !($検証済式の保存.Contains($数式op))){
      $検証済式の保存.Add($数式op) | Out-Null
      "$数式op=$ans :数列→$tmpans $($今回の数字の並び -join ",")  演算子列→$今回の演算子の並び"
      if ((Invoke-Expression $数式op) -ne  $ans){
      "審議!"
      "$ans,op=$op, op0=$($op[0]),op1=$($op[1])"
      } 
    }  
  }
}



ま、ここまでかなぁ。
最近これを見て作ってみたけど
2 3 6 6 を使って19を作るものって本当に1パターンしかないみたい。
(順番は変えて4つは出ているけど、すべて同じような計算でした。)
2 3 6 6 19
=====
(2/6+6)*3=19 :数列→ 2,6,6,3  演算子列→SSWSTSK
3*(2/6+6)=19 :数列→ 3,2,6,6  演算子列→SSSWSTK
3*(6+2/6)=19 :数列→ 3,6,2,6  演算子列→SSSSWTK
(6+2/6)*3=19 :数列→ 6,2,6,3  演算子列→SSSWTSK

-replace の後に検索文字列しか書いておらず置換文字列を書き忘れてた。検索文字列を空文字で置き換える(検索文字列を削除する)ことをしたかったのだけど、わかりずらかったかも。
もう1つ補足
演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
の部分の($ストックされた数 - 1 )というのは、2つの数字をスタックから取り出して演算した後、演算結果をスタックに戻すので、-2+1で-1です。


上でやめようと思ったけど、、
-replaceをちゃんと書いたものとやっぱり既約分数化しておいたものを書いておきます。
ただ、作成しているプログラムがどんどんだらしなくなってきているのでこれで最後かな?

数字列をパイプラインに流して、数字列が出来上がった直後からチェックが走るようにしようとしてみたけど、書き出しも速くなっていない。数字列を作る処理よりも(最初でまとめて行う)演算子の組み合わせを作る処理のほうがはるかに時間がかかるからといえばそれまでなんだけど、演算子と数字のならべかたを別々にしちゃったからなぁ。逆ポーランド記法のように数字と演算子の並びを一緒にやったら計算開始が早くなったのかもしれません。
#計算ゲームver4
#ver2からの改良点:割り算を分数として表現し、誤差補正処理を排除する
#分数表現は2項目の配列とし1個目を分子。2個目を分母とする。
#足し算引き算では通分演算する。計算結果を既約分数にする
#数字列をパイプラインでつなげて後続処理に流す。

function 数列の作成{
    param(
    $数字候補,
    $組み立て済数列
    )
    process{

        if ($数字候補.Count -gt 0){
            for($i=0;$i -lt $数字候補.Count;$i++){
                $取り出し数字=$数字候補[$i]
                $取り出し済の数字候補=$数字候補.Clone()
                $取り出し済の数字候補.RemoveAt($i)
                数列の作成 $取り出し済の数字候補 ($組み立て済数列+$取り出し数字)
            }
        } else {
           #完成数字列の出力,配列にしてしまうと各要素ごとにパイプライン処理が走りそうなので配列をオブジェクトでくるむ
           @{完成数字列=$組み立て済数列}
        }
    }
}

function 演算子列の作成 {
    param(
      $演算子列,
      $保留を使用した数,
      $ストックされた数,
      $残り必要な演算子数
    )
    process{
        if ($残り必要な演算子数 -gt 0 ){
           foreach ($演算子 in ("T","H","K","W", "S")){
             #前2つの数字に対する演算方法の取り決め
             #Sは演算の保留
             if ($演算子 -eq "S"){
               #演算の保留ができない場合(後に数字がない場合)は保留禁止
               # 残りの数字の数は、問題の数字の数-保留を行った数
               if ($問題の数字の数 -le $保留を使用した数){ continue}
               演算子列の作成 ($演算子列 + "S" ) ($保留を使用した数 + 1) ($ストックされた数 + 1) $残り必要な演算子数
             } else {
               #演算ができない場合は、演算禁止
               #ストックされた数が2以上でないと演算できない
               if($ストックされた数 -lt 2 ) { continue}
                 演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
             }
          }
        }
        else {
          $完成版演算子列.Add($演算子列) | Out-Null
        }
    }
}
function 最大公約数{
  $a,$b=[Math]::abs($args[0][0]),[Math]::abs($args[0][1])
  $a,$b = if ($a -gt $b ){$a,$b} else{$b,$a}
  while ($b -ne 0){
    $a,$b=$b,($a % $b)
  }
  $a
}
function 既約等{
  $a,$b=($args[0][0]),($args[0][1])
  if ($b -lt 0) { $a,$b=(-1*$a),(-1*$b)}
  $r=最大公約数 $a,$b
  ($a / $r ),($b / $r)
}

function 足す {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        既約等 ($被数分子*$原数分母+$原数分子*$被数分母) , ($被数分母*$原数分母) 
    }
}

function 引く {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        既約等 ($被数分子*$原数分母-$原数分子*$被数分母) , ($被数分母*$原数分母) 
    }
}
function 掛ける {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        既約等 ($被数分子*$原数分子) , ($被数分母*$原数分母) 
    }
}

function 割る {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        既約等 ($被数分子*$原数分母) , ($被数分母*$原数分子)
    }
}


#使う数字と答えを入れてもらう

$inputT=Read-Host
$input=[int[]]($inputT -split " ")
$inputnum,$ans=[System.Collections.ArrayList]::new($input[0..($input.Length-2)]),$input[-1]


$完成版演算子列 = [System.Collections.ArrayList]::new()
$検証済式の保存=[System.Collections.Generic.HashSet[string]]::new()
$問題の数字の数=$inputnum.Count

#演算子列と数列を作成する。
演算子列の作成 "" 0 0 ($inputnum.Count-1) #2項演算子のため2つめの数字に対する演算はスキップ"S"で、演算子数は数字の個数-1
数列の作成 $inputnum @()|%{
#答え合わせ
   $今回の数字の並び=$_.完成数字列
  foreach ($今回の演算子の並び in $完成版演算子列){
    $tmp数字列=$今回の数字の並び #Clone()
    $numIndex=0
    $ngflg=$false
    $数式stack=[System.Collections.Stack]::new()
    $stack=[System.Collections.Stack]::new()
    for ($index = 0;$index -lt $今回の演算子の並び.Length;$index ++){
      switch($今回の演算子の並び[$index]){
      "S" { 
        $stack.Push(@($tmp数字列[$numIndex],1))
        $数式stack.Push($今回の数字の並び[$numIndex].ToString())
        $numIndex++
      }
      default {
        $op2=$stack.Pop() ; $数式op2=$数式stack.Pop()
        $op1=$stack.Pop() ; $数式op1=$数式stack.Pop()
        switch($_){
        #数式の計算はすべて()付きで行う。
        "T"{ $stack.Push((足す $op1 $op2)); $数式stack.Push("$数式op1+$数式op2")}
        "H"{ $stack.Push((引く $op1 $op2))
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)","" ) -match "\+|-"){
               $数式op2="($数式op2)"
             }
             $数式stack.Push("$数式op1-$数式op2")
           }
        "K"{ $stack.Push(@(掛ける $op1 $op2)) 
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)","" ) -match "\+|-"){
               $数式op1="($数式op1)"
             }
             # op2のカッコの外側で+もしくは-が使われていた場合、をかっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)","" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push("$数式op1*$数式op2")
           }

        "W"{ 
             #0で割る事態が発生したら誤答とするが、とりあえずダミーの計算を入れる。
             if($op2[0] -eq 0){
               $ngflg=$true
               $stack.Push($op1)
             } else{
               $stack.Push(@(割る $op1 $op2))
             }
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)","" ) -match "\+|-"){
               $数式op1="($数式op1)"
             }
             # op2のカッコの外側で+もしくは-もしくは/が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)" ) -match "\+|-|/|\*"){
               $数式op2="($数式op2)"
             }
             $数式stack.Push("$数式op1/$数式op2")
           }
        }
      }
      }
    }

    $op=$stack.Pop()
    $数式op=$数式stack.Pop()
    if(!$ngflg -and $op[0] -eq ($ans * $op[1]) -and !($検証済式の保存.Contains($数式op))){
      $検証済式の保存.Add($数式op) | Out-Null
      "$数式op=$ans :数列→$($今回の数字の並び -join ",")  演算子列→$今回の演算子の並び"
      if ((Invoke-Expression $数式op) -ne  $ans){
      "審議!"
      "$ans,op=$op, op0=$($op[0]),op1=$($op[1])"
      } 
    }  
  }
}
今後続けるとしたら逆ポーランド記法に書き換えるとパイプライン処理の出力開始が早くなるか、と同じ数字が複数回使われた時の数字列の組み合わせをちゃんと絞ることをするのかなぁ

0 件のコメント: