AI 時代にプログラマは必要?
最近、AIの進化には本当に驚かされます。
ChatGPT 4o が登場したころ、LotusScript について質問しても、VBA のコードが混ざっているなど、そのままでは実行できないコードが返ってきたりすることがありました。実際に動かせるようになるまで、何度もやり取りを繰り返す必要がありました。
ところが最近では、かなり複雑な処理でも、ほぼ一発で実行できるコードが返ってくるようになりました。
こういう経験を繰り返すうちに、ふと考えてしまいます。
『これからの時代、プログラマの存在意義って何だろう?』
プログラムを書くこと自体が、特別な技術ではなくなっていくのかもしれません。
自動車の普及によって馬車が主要な移動手段ではなくなり、AT の普及によって MT 車が少数派になったように、プログラマという仕事も、いつか今とは違ったものになってしまうのでしょうか。
そんなことを考えると、少し寂しい気もします。そんなことを考えながら、今回はちょっとしたプログラムを作ってみました。
題材は『ナンプレ』です。
そもそも、ナンプレの問題を解くだけなら、今の AI に聞けば一瞬で答えが返ってきます。わざわざプログラムを作る必要などありません。
それでも、あえて LotusScript でナンプレを解くプログラムを作ってみることにしました。
ナンプレソルバーを作ってみた
過去記事『DXL 拡張ライブラリをリリースします!』で紹介した dxlSuite for LotusScript で作成したサンプルアプリに『ナンプレ』があります。
業務に行き詰まったり一息つきたいときに、気分転換で利用しています。ノーツでできているので、その文書を閉じて、別タブに移動すれば仕事をしているように見える優れものです。学生時代、MS-DOS のゲームにあった”ボスが来た機能”(MIFES でコーディングしている画面に瞬時に切り替える機能)のようですね(笑)。
先日、プレイしていたら解けないのではないかと思うステージに出くわしました。問題に入力ミスがあったのかもしれません。アプリストアに公開している手前、”やばい” と思い、ソルバーを制作して全ステージをチェックすることにしました。
動作イメージはこんな感じです。一瞬で解答を得ることができます。
ナンプレを解くための考え方
ナンプレには、3 つのルールがあります。
- 1 行には、1 ~ 9 の数字が一つずつ入る
- 1 列には、1 ~ 9 の数字が一つずつ入る
- 3 × 3 に分割された各ブロックにも、1 ~ 9 の数字が一つずつ入る
人間が解く場合、このルールに沿って、埋められるマスから順に数字を入れていく方法が一般的だと思います。今回はそのような論理的な解法を再現するのではなく、候補を仮置きしながら解答を探す、シンプルな手順で進めたいと思います。
具体的には、次のような手順となります。
このアルゴリズムはバックトラッキングと呼ばれる手法だそうです。実際のナンプレの問題を例に動作を図式化すると次のような感じになります。
このように、候補を仮置きして先に進み、行き詰まったらひとつ前のマスに戻って別の候補を試します。これを繰り返すことで、最終的に解答を見つけます。再帰呼び出しを使うと、この「進む」「戻る」という処理をシンプルに記述できます。
再帰呼び出しを使った実装
再帰呼び出しというのは、必要に応じて自分自身の関数を呼び出すことを言います。
単純な事例として n までの階乗を求める関数は次のようになります。関数内で自分自身を呼び出していることがわかります。
|
Function xFactorial(n As Integer) As Long If n <= 1 Then xFactorial = 1 Else xFactorial = n * xFactorial(n - 1) End If End Function |
例えば、n = 3 の場合、
- xFactorial(3) が xFactorial(2) を呼び出す
- xFactorial(2) が xFactorial(1) を呼び出す
- xFactorial(1) が1を返す
- xFactorial(2) が2を返す
- xFactorial(3) が6を返す
の順で実行されます。呼び出した順とは逆順に答えが返されます。スタックのように後入れ先出し(LIFO:Last In, First Out)で処理が実行されることになります。
重要なのは次の 2 点です。
- n <= 1 が終了条件
- 再帰呼び出しは n - 1 で実行
n は呼び出されるたびに減るので、無限に再帰呼び出しされることはありません。再帰呼び出しを作る際には、永久ループにならないよう注意する必要があります。
ナンプレソルバー
今回のナンプレソルバーはエージェントとして作成しました。Initialize は次の通りです。重要なのは次の 2 点です。
- ナンプレソルバーのメインルーチンは xNumberPlace_Solver 関数
- ナンプレの盤面は、9 x 9 の 2 次元配列 aby(行, 列) に格納
|
Sub Initialize Dim nuiw As New NotesUIWorkspace Dim nuid As NotesUIDocument Dim nd As NotesDocument Dim aby(8,8) As Byte 'ナンプレの盤 Dim v As Variant Dim iRow As Integer Dim iCol As Integer Dim lStep As Long '実行回数 '初期化 Set nuid = nuiw.CurrentDocument Set nd = nuid.Document '問題を取得して、9 x 9 の配列に展開(初期盤面の整合性チェックは省略) v = nd.CaseInput '問題取得 For iRow = 0 To 8 For iCol = 0 To 8 aby(iRow, iCol) = CByte(Mid(v(iRow), iCol+1, 1)) Next Next 'ナンプレソルバーの実行 if Not xNumberPlace_Solver(aby, lStep) Then nd.IsError = "1" '解けない場合 End If '結果出力 nd.Step = lStep nd.Result = xSetResult(aby) Call nuid.refresh() End Sub |
なお、今回は初期盤面の整合性チェックや、解が複数存在するかどうかの判定は省略しています。
メインルーチン
xNumberPlace_Solver 関数がナンプレソルバーのメインルーチンです。
引数の rlStep を使って関数の呼び出し回数をカウントするため、関数の先頭でカウンタを加算しています。これにより、ナンプレを解くために発生した関数呼び出し回数(最初の呼び出しを含む)を確認できます。
|
Private Function xNumberPlace_Solver(raby() As Byte, rlStep As Long) As Boolean Dim vCandidate As Variant '入れられる数字の候補(配列) Dim iRow As Integer Dim iCol As Integer Dim iCan As Integer Dim bFound As Boolean bFound = True 'すべて埋まっている時用 rlStep = rlStep + 1 '行、列の順に空きマスを検索 For iRow = 0 To 8 For iCol = 0 To 8 If raby(iRow, iCol) = 0 Then '空いているマスを発見 bFound = False '解が見つかったかどうか '入れられる数字を配列で取得 vCandidate = xGetCandidate(raby, iRow, iCol) If vCandidate(0) <> 0 Then '候補があったので順に調査 For iCan = 0 To UBound(vCandidate) '候補をいったんセット raby(iRow, iCol) = vCandidate(iCan) '候補をセットした状態で続きを調査 bFound = xNumberPlace_Solver(raby, rlStep) If bFound = True Then 'マスが埋まった Exit For End If Next End If If bFound = False Then '入る数字がなかったので空きマス(0)に戻す raby(iRow, iCol) = 0 GoTo ExitSolver End If End If Next Next ExitSolver: xNumberPlace_Solver = bFound End Function |
各マスを行、列の順でループさせ、空きマスを探索します。発見したら、そのマスに入る数字を配列で取得する xGetCandidate 関数をコールします(後述)。
候補の数字があれば、いったんセットして次のマスを埋める処理に入ります。この部分が再帰呼び出しで実現されているということですね(赤字)。
ループ内で bFound が False となるのは、空きマスに埋める数値が見つからなかった場合で、次のマスを検査するために仮にセットした候補を削除する処理となります。この部分がバックトラッキングで、ひとつ前の呼び出しに戻り、次の候補を試す役割を持ちます。
この呼び出し元との関係は LotusScript の実行環境が管理しており、プログラムで考慮する必要がありません。1 回のエージェント実行で、同じ関数がネストして実行されることになりますが、ローカル変数はそれぞれで管理され、値が混ざることはありません(連携したいものは引数と戻り値を利用)。
その他必要な関数
今回の主題の再帰呼び出しとは直接的に関係がないのですが、ナンプレソルバーの実行に必要な関数を紹介します。
◇ xGetCandidate
盤面を調査して、引数で指定したマスに入れられる数字を探し出し、配列で返す関数です。
| %REM 入れられる数字を配列で返す。 ない場合は要素 0 が 0. %END REM Private Function xGetCandidate(raby() As Byte, ByVal viRow As Integer, ByVal viCol As Integer) As Variant Dim abyCandidate() As Byte Dim i As Integer Dim iRow As Integer Dim iCol As Integer Dim iNum As Integer Dim bFound As Boolean ReDim abyCandidate(i) For iNum = 1 To 9 bFound = False '同じ列で同じ番号があるか? For iRow = 0 To 8 If raby(iRow, viCol) = iNum Then bFound = True Exit For End If Next '同じ行で同じ番号があるか? If bFound = False Then For iCol = 0 To 8 If raby(viRow, iCol) = iNum Then bFound = True Exit For End If Next End If '3 x 3 に同じ番号があるか? If bFound = False Then For iRow = Int(viRow / 3) * 3 To Int(viRow / 3) * 3 + 2 For iCol = Int(viCol / 3) * 3 To Int(viCol / 3) * 3 + 2 If raby(iRow, iCol) = iNum Then bFound = True Exit For End If Next If bFound = True Then Exit For Next End If '判定 If bFound = False Then '見つからなかったので戻り値に追加 ReDim Preserve abyCandidate(i) abyCandidate(i) = iNum i = i + 1 End If Next xGetCandidate = abyCandidate End Function |
◇ xSetResult
この関数は処理が終わったナンプレの配列をフィールドにセットするための文字列配列に変換する関数です。
|
Function xSetResult(vaby() As Byte) As Variant Dim iRow As Integer Dim iCol As Integer Dim asLine(8) As String For iRow = 0 To 8 For iCol = 0 To 8 asLine(iRow) = asLine(iRow) & CStr(vaby(iRow, iCol)) Next Next xSetResult = asLine End Function |
再帰呼び出しのメリットと注意点
今回は再帰呼び出しを紹介しました。最後に再帰呼び出しの特徴を整理します。
| メリット | 注意点 |
| プログラムをシンプルにできる | 呼び出し回数が増える |
| 階層的な処理を表現しやすい | スタックを消費する |
| 戻る処理を実行環境に任せてシンプルに記述できる | 終了条件がないと無限に再帰呼び出しされる |
| 処理の考え方が直感的になる | 深すぎる再帰はスタックオーバーフローの原因になる |
AI 時代だからこそ、プログラミングを楽しむ
AI がコードを書いてくれる時代になりました。これからは、プログラムを作ること自体の価値も変わっていくのかもしれません。
今回作成したナンプレソルバーも、AI に聞けば一瞬で答えが出ます。わざわざプログラムを作って解く必要はありません。効率だけを考えれば、まったく意味のないことかもしれません。
それでも、自分でアルゴリズムを考え、それをプログラムとして形にして、思い通りに動いたときの楽しさは変わらないと思います。今回も、ナンプレを解く仕組みを考えたり、再帰呼び出しを使ってシンプルに実装できたりしたことが、何より面白いところでした。
乗馬や MT 車の運転も、移動手段としての効率だけを考えれば、もっと便利な選択肢はいくらでもあります。それでも、そこには効率とは別の楽しさや価値があります。
プログラミングも、いつかそんな存在になっていくのかもしれません。
……などと、少し格好よくまとめてみましたが、そもそもナンプレをプログラムで解こうとすること自体、邪道なのかもしれませんね(笑)。


0 件のコメント:
コメントを投稿