基礎から学ぶVBAプログラミング教室

もりさんのお題を解きながら楽しく勉強しよう

スポンサーリンク

【連載】VBAでナンプレ(数独)を解いてみよう③<解法1>

VBAでナンプレを解いてみようシリーズ【第3回目】です。

f:id:excel-accounting:20180511135041p:plain:w400

連載記事の一覧はこちら→【VBAナンプレ連載】

このブログはノンプログラマーによるノンプログラマーのためのやさしい解説付きです。

f:id:excel-accounting:20180424002011p:plain:w150

【対象読者】
VBA初級者を想定した解説をしています。


これまでのおさらい

全体の枠組みとなる共通モジュールの[全体処理]と[共通部品]を作成しました。
f:id:excel-accounting:20180602001314p:plain:w400


変数もおさらいしておきましょう。

変数名
データ型
定義
PuzzleArea
Rangeオブジェクト パズル全体の9×9のエリア
BlockArea
Rangeオブジェクト PuzzleAreaの中の3×3エリア
r
Long型 9×9エリアの番号
c
Long型 9×9エリアの番号
r1
Long型 BlockArea始点セルの番号
c1
Long型 BlockArea始点セルの番号
num
Long型 1~9の数字

【解法1】数字探し

セルの重複禁止範囲を調べて、そのセルに入る「数字を探す」解き方です。

【例題】
A1セルに入る数字を探します。

「3×3のエリア」「行」「列」には5以外の数字がすべて存在しているため、A1セルには数字の5しか入りません。
f:id:excel-accounting:20180618151802p:plain:w350

イメージづくり

『解き方はわかるけど、これをどうやってプログラムにするの…?』

f:id:excel-accounting:20180424112139p:plain:w150

って思いますよね。


プログラムを書く前に大切なのは、日本語で処理を細かく書いてイメージをつくることです。

というわけで、処理の流れを整理していきますよ。

解法1を呼び出すタイミング

共通モジュールの[全体処理]から[解法1モジュール]が呼び出されます。

まずは、どのタイミングで[解法1]が呼び出されるか決めておきます。

解法1は「9×9エリア」の各セルに着目して、そのセルに入る数字を探す解き方です。

そこで、セルが空白の場合に解法1を実行することにします。

数字を検索する

1~9の数字が[指定のセル範囲]①②③に「ある」か「ない」か、順番に検索します。

ない=そのセルに入る解答候補

[指定のセル範囲]はナンプレルール上の重複禁止範囲です。

  • ①3×3エリア(BlockArea)
  • ②横方向(行)
  • ③縦方向(列)


すべての検索が終了した時点で「ない」数字が1~9のうち1つのみの場合、その数字を答えと判定します。
(解答候補が1つに特定できるため)

ここで、数字が「ある」か「ない」かの検索結果をどのように保持するか?が解法1の一番のポイントになってきます。

人間は、脳で記憶したり、紙にメモできます。
この「記憶」「メモ」という作業を、プログラムでも行う必要があります。

それが変数に保持するという考え方です。

1~9までのフラグを準備します。
f:id:excel-accounting:20180513235735p:plain:w300

フラグの情報は下記のとおりとします。

変数名
データ型
定義
CheckFlag(9)
Boolean型 numの検索結果(あり/なし)を保持する

CheckFlag(9)は開始値1の配列変数

【使い方】
f:id:excel-accounting:20180513235744p:plain:w300


例題を解くときのフラグの様子をみていきましょう。

【例題】
f:id:excel-accounting:20180518160933p:plain:w350


CheckFlag(1~9)の初期値はすべてFalse(倒れた状態)です。
f:id:excel-accounting:20180513235912p:plain:w300

1~9の数字を[指定のセル範囲]①②③で検索し、①②③のいずれかに存在したら、その数字のフラグを立てます。

f:id:excel-accounting:20180428172249p:plain:w200

結果、1,2,3,4,6,7,8,9が存在するので、それらのフラグが立ちます。

5のみが存在しないので倒れたままの状態です。

f:id:excel-accounting:20180514095059p:plain:w300

解答を判定する

つづいて、1~9のフラグの状態を順番にチェックして解答を判定します。

新たに2つの変数を準備します。

変数名
データ型
定義
cnt
Long型 CheckFlag(num)のFalseの数を数えるカウンター
answer
Long型 解答候補の数字を格納する

フラグが倒れたまま(False)であれば、2つの処理をします。

  • 処理①カウンターで数える(カウント変数を+1する)
  • 処理②そのフラグの数字を解答候補変数に入れる


f:id:excel-accounting:20180521150214p:plain:w300


1のフラグから順番にみていくと、

f:id:excel-accounting:20180514160330p:plain:w200

・・・(2~4チェック)・・・

f:id:excel-accounting:20180514160343p:plain:w200


Falseが見つかりました。

カウンター変数(cnt)を+1して、解答候補変数(answer)にフラグの数字を入れます。

f:id:excel-accounting:20180521150453p:plain:w300

・・・(6~9チェック)・・・

9までのフラグを確認し終えたら変数cntの値をチェックします。
解答候補が1つに絞れるか?という重要なチェックです。

◆cnt > 1 の場合 → 解答不明

f:id:excel-accounting:20180430163014p:plain:w200


◆cnt = 1 の場合 → 解答決定

f:id:excel-accounting:20180430163006p:plain:w200

変数answerに入っている数字が「答え」となります。
この数字をセルに書き込んで、終了です!


以上が、解法1全体の流れです。

フローチャート作成

解法1全体の流れをフローチャートにします。

全体を2つの単位に分けて考えます。(青枠で囲った範囲)

【ループ1】が「数字を検索する処理」で、

【ループ2】以降が「解答を判定する処理」です。

f:id:excel-accounting:20180528125054p:plain:w300


プログラム作成

ここからは具体的なプログラムを書いていきます。

呼び出す側の処理
If Cells(r, c).Value = "" Then Call Solution1

解法1のプロシージャ名はSolution1とします。

9×9エリアにおける現在地Cells(r, c)が空白の場合に解法1を呼び出します。

f:id:excel-accounting:20180602181951p:plain:w350

解法1の各処理

フローチャートに沿って解法1の各処理を作成します。

すべて解法1モジュールに記述します。

1~9のフラグを準備

Dim CheckFlag(9) As Boolean

通常、配列変数の開始値は0ですが、ナンプレの数字と合わせるため開始値を1に設定します。
(モジュール宣言セクションでOption Base 1を宣言)


【ループ1】num=1~9まで繰り返す

For num = 1 To 9
    '~処理~
Next num


検索①~③[指定のセル範囲]にnumが存在するか

Dim check1 As Boolean, check2 As Boolean, check3 As Boolean

check1 = isExistNum(r1, c1, r1 + 2, c1 + 2) '①BlockAreaの検索
check2 = isExistNum(r, 1, r, 9) '②横方向(行)の検索
check3 = isExistNum(1, c, 9, c) '③縦方向(列)の検索

[共通部品]isExistNumに引数を渡して呼び出します。
isExistNum(始点セルの行番号,始点セルの列番号,終点セルの行番号,終点セルの列番号)


(例1)3×3エリア(BlockArea)の検索
f:id:excel-accounting:20180528114812p:plain:w350

  • 始点セル(1, 4):[共通部品]SearchBlockAreaで求めたr1とc1
  • 終点セル(3, 6):始点セル+2で求まる

よって、渡す引数は(r1, c1, r1 + 2, c1 + 2

(例2)行の検索
f:id:excel-accounting:20180528114849p:plain:w400

  • 始点セル=(2, 1)
  • 終点セル=(2, 9)

行番号は変数 r
よって、渡す引数は(r, 1, r, 9

返り値を変数check1~3に格納します。

  • numが存在する場合、Trueが格納される
  • numが存在しない場合、Falseが格納される


検索①~③のいずれかでnumが見つかったか?

If check1 = True Or check2 = True Or check3 = True Then
    '~処理~
End If

変数check1~3の格納値をチェックします。
「いずれか」なので「Or」結合します。


CheckFlag(num) にTrueを代入

CheckFlag(num) = True


【ループ2】num=1~9まで繰り返す

(省略)

CheckFlag(num)=Falseの場合の処理

If CheckFlag(num) = False Then

    Dim cnt As Long 'Falseの数をカウント
    cnt = cnt + 1
    
    Dim answer As Long '解答候補
    answer = num

End If

カウント変数を+1して、解答候補変数にnumを代入します。

【ワンポイント解説】
(例)1~9のフラグでFalseが3つ見つかった場合(数字の3・5・7)

Falseが見つかるたびにcntは+1していくのに対し、answerにはその都度数字を代入するので3→5→7と値が上書きされます。

結果はcnt=3で「解答不明」のためanswerの格納値は不要となります。

変数answerはプロシージャレベル変数であり、プロシージャが終了すれば値がリセット(クリア)されるので問題ありません。


カウント変数cnt=1の場合、セルに解答代入

If cnt = 1 Then '解答決定ならセルへ書き込み
    Call AnswerSet(r, c, answer)
End If

1~9のフラグのうち、Falseが1つのみの場合「解答決定」です。

[共通部品]AnswerSetに3つの引数を渡して呼び出します。
AnswerSet(セルの行番号,セルの列番号,解答数字)

f:id:excel-accounting:20180521204350p:plain:w350

解法1まとめ

今日のまとめです!

呼び出す側の「共通モジュール」と、呼び出される側の「解法1モジュール」をまとめます。

共通モジュール

For r = 1 To 9
    For c = 1 To 9

        r1 = SearchBlockArea(r)
        c1 = SearchBlockArea(c)

        '解法1の呼び出し
        If Cells(r, c).Value = "" Then Call Solution1

        '~解法2~

        '~解法3~
            
    Next c
Next r

解法1モジュール

Option Base 1
' ------------------------
' * 解法1モジュール
' ------------------------

Sub Solution1()

    Dim CheckFlag(9) As Boolean

    For num = 1 To 9 '【ループ1】
        
        Dim check1 As Boolean, check2 As Boolean, check3 As Boolean
        
        check1 = isExistNum(r1, c1, r1 + 2, c1 + 2) '①BlockAreaの検索
        check2 = isExistNum(r, 1, r, 9) '②横方向(行)の検索
        check3 = isExistNum(1, c, 9, c) '③縦方向(列)の検索
        
        '①②③のいずれかにnumが見つかったらFlagをTrueにする
        If check1 = True Or check2 = True Or check3 = True Then
            CheckFlag(num) = True
        End If
    
    Next num

    For num = 1 To 9 '【ループ2】
    
        If CheckFlag(num) = False Then
        
            Dim cnt As Long 'Falseの数をカウント
            cnt = cnt + 1
            
            Dim answer As Long '解答候補
            answer = num
        
        End If
    
    Next num
    
    If cnt = 1 Then '解答決定ならセルへ書き込み
        Call AnswerSet(r, c, answer)
    End If
    
End Sub


スポンサーリンク

次回予告

次回は解法2の作成に入っていきます。

f:id:excel-accounting:20180618151642p:plain:w300

最後に

ここまでお読みいただきありがとうございました。

*引き続き連載を読んでくださるはてなブロガーの方はぜひ読者になってくださいね。

*ツイッターでもブログ更新のツイートをしているので、こちらもフォローしてくれると嬉しいです。

第4回目へつづきます
VBAでナンプレを解いてみよう④

スポンサーリンク