もりさんのプログラミング手帳

教えることは、二度学ぶこと

スポンサーリンク

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

VBAでナンプレを解いてみよう【第4回目】です。

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

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


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

VBA初級者を想定した解説をしています。


解法2でも引き続き登場する変数をおさらいします。

変数名
データ型
定義
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の数字

【解法2】セル探し(エリア)

【例題】

A1セルを始点とする3×3エリア(赤枠)に着目します。

  • 空白セルは4個あり、数字2,5,7,8のいずれかが入る
  • A2セル・C2セル:同じ【行】に数字の5が存在する
  • B3セル:同じ【列】に数字の5が存在する

以上より、数字の5はA1セルにしか入らないことがわかります。

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


これがセル探し(エリア)の考え方です。

イメージづくり

解き方のイメージを描いていきます。

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

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

解法2は3×3エリアを1単位とした解き方です。

9×9エリアにおける現在地が黄色セルの時に、解法2を実行することとします。
f:id:excel-accounting:20180602202155p:plain:w400

事前チェック

解読対象のエリアがすでに完成している場合は解法2を実行する必要がありません。

そこで、3×3エリアの状態を最初にチェックして、空白セルがなければ終了とします。

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

チェック処理①

1~9の各数字が3×3エリアに存在するか検索します。

・数字の1は存在するので対象外。次の数字チェックへ。

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


・数字の2は存在しないので、次のチェック処理②へすすむ。

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

 :
 :
・数字の9は存在するので対象外。

1~9の数字をすべてチェックしてから次の処理に進むのではなく

数字が「あれば対象外」、「なければ次の処理へ」と繰り返していきます。

つまり、数字3のチェックに入る前に、数字2は次のチェック処理②に進みます。
(=数字2に関する処理がすべて終了してから、数字3のチェックに進む。)

チェック処理②

数字が空白セル(黄色セル①~④)に代入可能かチェックします。

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

ナンプレのルールどおり、同じまたはにその数字が存在する場合は代入不可です。

存在しない場合は、代入可能です。

ここで、代入可能という情報を保持させる3つの変数を用意します。

変数名
データ型
定義
SetCnt
Long型 代入可能なセルの個数をカウントする
y
Long型 代入可能なセルの番号を保持する
x
Long型 代入可能なセルの番号を保持する


【変数のイメージ】

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

【使い方】
f:id:excel-accounting:20180528234228p:plain:w400


代入可能な場合の処理は3つです。

  • カウント変数を+1
  • 現在地の番号を格納
  • 現在地の番号を格納

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


【ワンポイント解説】
パブリック変数のrとcがあるのにも関わらず、なぜ行番号と列番号の変数を新たに用意するのでしょうか?

解法2は3×3のエリアを1セルずつチェックしていくので、

「マクロ全体における現在地(r,c)」と「解法2における現在地」が異なるからです。

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

そこで、解法2専用の行番号・列番号の変数(y,x)を用意して、現在地の番号と番号を取得する必要があります。


この変数を使用しながら、数字の2をチェックする流れをみていきます。
f:id:excel-accounting:20180502233208p:plain:w300


変数SetCnt・y・xに格納される値の流れは下記のとおりです。
f:id:excel-accounting:20180517090950p:plain


SetCntは+1していき、y・xは値の代入(上書き)です。

以上で、数字2のチェックが終わりました。

解答の判定

数字2の代入先セルを決定できるか判定します。

「決定できるか・否か」の判定条件はこのようになります。

◆SetCnt > 1の場合 → 代入先不明
代入先の候補が2個(A1セルとB3セル)あるため、決定できません。

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

◆SetCnt = 1の場合 → 代入先決定

先に答えを言ってしまうと、数字の5と8は解答決定します。

・数字の5は、A1セルのみにしか入らない
f:id:excel-accounting:20180522235048p:plain:w400


・数字の8は、B3セルのみにしか入らない
f:id:excel-accounting:20180523093907p:plain:w400

代入先のセルが決定できたら、数字をセルに書き込みます。

解答が決まった場合の数字書き込み処理、覚えてますか?

前回作成した[共通部品]AnswerSetです。

f:id:excel-accounting:20180528235234p:plain:w450


数字の書き込みも無事に終わりました。

次の数字のチェックに進む前に、カウント変数を初期化します。(カウントを0に戻す)

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

(補足)
変数y(行番号)とx(列番号)は毎回上書きするため、リセットしません。

フローチャート作成

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

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

プログラム作成

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

呼び出す側の処理
If (r = 1 Or r = 4 Or r = 7) And (c = 1 Or c = 4 Or c = 7) Then Call Solution2

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

番号またはまたは
 かつ
番号またはまたは
の時に呼び出します。

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

解法2の各処理

フローチャートに沿ってプログラムを書いていきます。
これらはすべて解法2モジュールに記述します。

3×3エリアに空白セルがあるか

Dim BlockArea As Range
Set BlockArea = Range(Cells(r1, c1), Cells(r1 + 2, c1 + 2))
    
'3×3エリアが完成している場合は解法2不要のためExit
If WorksheetFunction.CountBlank(BlockArea) = 0 Then Exit Sub

対象の3×3エリアを変数BlockArea(Rangeオブジェクト)にセットします。

CountBlank関数でBlockAreaの空白セルをチェックし、空白セルが0の場合、その時点で解法2は終了とします。


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

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

これはもう大丈夫ですね!


3×3エリアにnumが存在するか

If isExistNum(r1, c1, r1 + 2, c1 + 2) = False Then
    '~処理~
End If

[共通部品]isExistNum(Functionプロシージャ)を呼び出します。

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

引数と返り値のおさらいです。

  1. 引数=[指定のセル範囲]を定める4データ
    • 始点セル」の番号
    • 始点セル」の番号
    • 終点セル」の番号
    • 終点セル」の番号
  2. 返り値
    • numが存在する場合→True
    • numが存在しない場合→False

返り値がFalseの場合、次の処理に進みます。


3×3エリアの各セルに対して繰り返す
セルが空白か?

Dim cell As Range
For Each cell In BlockArea '各セルに対して処理を繰り返す
        
    If cell.Value = "" Then
        '~処理~        
    End If
            
Next

ForEach~Next文でBlockAreaを1セルずつ順番にチェックして、

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

セルが空白の場合、次の処理に進みます。


【検索①】同じにnumが存在するか
【検索②】同じにnumが存在するか

Dim check1 As Boolean, check2 As Boolean

check1 = isExistNum(cell.Row, 1, cell.Row, 9) '①行検索
check2 = isExistNum(1, cell.Column, 9, cell.Column) '②列検索

★ポイント★
ここで渡す引数の行番号と列番号は9×9のパズルエリアのrとcではありません。

3×3エリア内の現在位置・変数cell(Rangeオブジェクト)の番号と番号です。

  • Rowプロパティ=行番号
  • Columnプロパティ=列番号


検索①②のどちらにも存在しない

If check1 = False And check2 = False Then
    '~処理~                    
End If

条件は「どちらにも存在しない」なので、返り値FalseAnd結合します。


変数へデータ格納(SetCnt・行列番号)

Dim SetCnt As Long
SetCnt = SetCnt + 1 '代入可能セルの個数
                        
Dim y As Long, x As Long
y = cell.Row '現在地の行番号
x = cell.Column '現在地の列番号

この処理内容です。

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

SetCnt=1か?
セルに解答(num)を代入

If SetCnt = 1 Then Call AnswerSet(y, x, num)

共通モジュールの[共通部品]AnswerSetを呼び出して、セルへの解答書き込みを行います。


SetCntの初期化

SetCnt = 0

これで1つの数字のチェックが終わりました。
カウント変数を初期化して、次の数字をチェックします。

解法2まとめ

今日のまとめです!

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

共通モジュール

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の呼び出し
        If (r = 1 Or r = 4 Or r = 7) And (c = 1 Or c = 4 Or c = 7) Then Call Solution2

        '解法3
            
    Next c
Next r

解法2モジュール

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

Sub Solution2()

    Dim BlockArea As Range
    Set BlockArea = Range(Cells(r1, c1), Cells(r1 + 2, c1 + 2))
    
    '3×3エリアが完成している場合は解法2不要のためExit
    If WorksheetFunction.CountBlank(BlockArea) = 0 Then Exit Sub

    For num = 1 To 9
    
        'BlockArea(3×3)にnumが存在しなければチェック開始
        If isExistNum(r1, c1, r1 + 2, c1 + 2) = False Then
            
            Dim cell As Range
            For Each cell In BlockArea '各セルに対して処理を繰り返す
            
                If cell.Value = "" Then
                
                    Dim check1 As Boolean, check2 As Boolean
                    check1 = isExistNum(cell.Row, 1, cell.Row, 9) '①行検索
                    check2 = isExistNum(1, cell.Column, 9, cell.Column) '②列検索
            
                    'numが【行】【列】どちらにも存在しなかったら
                    If check1 = False And check2 = False Then
            
                        Dim SetCnt As Long
                        SetCnt = SetCnt + 1 '代入可能セルの個数
                        
                        Dim y As Long, x As Long
                        y = cell.Row '現在地の行番号
                        x = cell.Column '現在地の列番号
                    
                    End If
            
                End If
            
            Next
            
            If SetCnt = 1 Then Call AnswerSet(y, x, num)
            
            SetCnt = 0 '次のnumに進むので、カウント変数を初期化
            
        End If
    
    Next num
    
End Sub

スポンサーリンク


次回予告

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

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

最後に

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

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

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

VBAでナンプレを解いてみよう⑤

スポンサーリンク