2026/11/15

AI 時代だからこそ、プログラミングを楽しむ

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 件のコメント:

コメントを投稿