【Java】ランダムなミニナンプレをプログラムで作ってみる

【Java】ランダムなミニナンプレをプログラムで作ってみる

Javaで4×4のミニナンプレをランダムに生成するプログラムを書いてみます。

ミニナンプレとは

ナンプレはロジックパズルとして有名で馴染み深いものとして知られています。今回作るミニナンプレはナンプレを簡略化したもので、1~4の数字を使った4×4のパズルになります。下に、問題と解答の一例を示します。

$$ \large\begin{array}{|c|c|c|c|} \hline 1 & & & \\ \hline 3 & & 1 & 2 \\ \hline 4 & 3 & & 1 \\ \hline & & & 3 \\ \hline \end{array} \to \large\begin{array}{|c|c|c|c|} \hline 1 & 2 & 3 & 4 \\ \hline 3 & 4 & 1 & 2 \\ \hline 4 & 3 & 2 & 1 \\ \hline 2 & 1 & 4 & 3 \\ \hline \end{array} $$

縦と横の全ての列と中心で区切られた4マスのボックスに1~4の数字が全て入るように数字を当てはめます。

今回のプログラムを作成について、まず右側の完成した表から作っていき、そこから左側の問題となる表を求めたいと思います。

4×4のミニナンプレ表(完成表)を作成する

前回の記事で書いたプログラムを改変して使います。アルゴリズムとしては以下のような流れになります。

アルゴリズム
  1. ArrayListに1~4の数字を格納し、shuffleメソッドでシャッフルしてから4×4の配列に4回分のArrayListを格納して仮の表を作る
  2. 仮の表が条件に一致するまで、その都度シャッフルを繰り返す
  3. 条件に一致した仮表が出来たら完成表として出力する

アルゴリズムの詳細な部分は前回と被るところが多いので割愛します。

前回の魔方陣の時と違うところは、「全ての行・列・ボックスの合計が10である」「横の行については既にシャッフルによって完成しているので、縦の列とボックスのみ比較すれば良い」の2点です。縦の行とボックスはHashSetを使って数字を格納し、sizeメソッドを使ってサイズが4である(1から4全て使ってる)かチェックしました。

なお、ボックスの比較については、縦横が揃っていてボックスの4つの内どれか一つっでも条件を満たしていれば他の3つも自動的に条件を満たすため、左上のみ判定しています。また、前回と同様シャッフル回数測定のために変数countを設置しています。

実際にできたコードが下になります。

MakeMiniNumberPlace.java
 1import java.util.ArrayList;
 2import java.util.Collections;
 3import java.util.HashSet;
 4import java.util.List;
 5import java.util.Set;
 6
 7public class MakeMiniNumberPlace {
 8  /** 1から16までを格納するArrayList */
 9  List<Integer> list = new ArrayList<Integer>();
10
11  /** 魔方陣の数字を格納する配列 */
12  int[][] array = new int[4][4];
13
14  /** 重複チェック用のhashSet */
15  Set<Integer> hashSet = new HashSet<>();
16
17  /** シャッフル回数カウント用の変数 */
18  int count = 0;
19
20  /** 実行用mainメソッド */
21  public static void main(String[] args) {
22    MakeMiniNumberPlace mMiniNumberPlace = new MakeMiniNumberPlace();
23    mMiniNumberPlace.execute();
24  }
25
26  /** 魔方陣の作成 */
27  private void execute() {
28    generateList();
29
30    label: while (true) {
31      // 横の列を比較
32      for (int i = 0; i < 4; i++) {
33        for (int j = 0; j < 4; j++) {
34          array[i][j] = list.get(j);
35        }
36        Collections.shuffle(list);
37        count++;
38      }
39
40      // 縦の列を比較
41      hashSet.clear();
42      for (int i = 0; i < 4; i++) {
43        for (int j = 0; j < 4; j++) {
44          hashSet.add(array[j][i]);
45        }
46        if (hashSet.size() != 4) {
47          continue label;
48        }
49        hashSet.clear();
50      }
51
52      // 縦の列はOKなのでボックスの中身を比較
53      // 左上のボックスが条件を満たしていれば他のボックスも自動的にOK
54      hashSet.add(array[0][0]);
55      hashSet.add(array[0][1]);
56      hashSet.add(array[1][0]);
57      hashSet.add(array[1][1]);
58      if (hashSet.size() == 4) {
59        break;
60      }
61    }
62
63    showArray();
64  }
65
66  /** listに1~4を格納しシャッフル */
67  private void generateList() {
68    list.clear();
69    for (int i = 1; i <= 4; i++) {
70      list.add(i);
71    }
72    Collections.shuffle(list);
73  }
74
75  /** 配列を数表形式で出力する */
76  private void showArray() {
77    for (int[] a : array) {
78      for (int i : a) {
79        System.out.printf("%3d", i);
80      }
81      System.out.println();
82    }
83    System.out.println("シャッフル回数:" + count);
84  }
85}

実行結果の一例が以下になります。

出力結果(一例)
1  2  3  1  4
2  4  1  3  2
3  1  2  4  3
4  3  4  2  1
5シャッフル回数:10688

完成表が出力されました。countは5000~10000になることが多かったですね。前回よりシャッフル回数も格段に少ないことが分かります。こちらの方が条件が緩いので、完成表を求めるのに試行回数が少なく済んでいます。

ミニナンプレの問題を作るプログラム

次は、今回得た表からどうやってミニナンプレを作るか検討していきます。完成表から10個穴を開けるやり方で実装してみます。

実際にできたコードが下になります。

PunchMiniNumberPlace.java
 1import java.util.ArrayList;
 2import java.util.Collections;
 3import java.util.List;
 4
 5public class PunchMiniNumberPlace {
 6
 7  /** 魔方陣の問題を格納する配列 */
 8  int[][] originalArray;
 9
10  /** 魔方陣の問題を格納する配列 */
11  int[][] probArray;
12
13  List<Integer> list = new ArrayList<Integer>();
14
15  /** 実行用mainメソッド */
16  public static void main(String[] args) {
17    PunchMiniNumberPlace pMiniNumberPlace = new PunchMiniNumberPlace();
18    pMiniNumberPlace.originalArray = new int[][] { { 1, 2, 3, 4 }, { 3, 4, 1, 2 }, { 4, 3, 2, 1 }, { 2, 1, 4, 3 } };
19    pMiniNumberPlace.probArray = new int[4][4];
20    pMiniNumberPlace.execute();
21    pMiniNumberPlace.showArray();
22  }
23
24  /** ミニナンプレの問題を作る */
25  private void execute() {
26    // 0に置き換える箇所を決める
27    for (int i = 0; i <= 15; i++) {
28      list.add(i);
29    }
30    Collections.shuffle(list);
31
32    for (int i = 0, x = 0; i < 4; i++) {
33      for (int j = 0; j < 4; j++) {
34        if (x == list.get(0) || x == list.get(1) || x == list.get(2) || x == list.get(3) || x == list.get(4)
35            || x == list.get(5)) {
36          probArray[i][j] = originalArray[i][j];
37        } else {
38          probArray[i][j] = 0;
39        }
40        x++;
41      }
42    }
43  }
44
45  /** 配列を数表形式で出力する */
46  private void showArray() {
47    for (int[] a : originalArray) {
48      for (int i : a) {
49        System.out.printf("%3d", i);
50      }
51      System.out.println();
52    }
53    System.out.println();
54    for (int[] a : probArray) {
55      for (int i : a) {
56        System.out.printf("%3d", i);
57      }
58      System.out.println();
59    }
60  }
61}

実行結果の一例が以下になります。

出力結果(一例)
1  1  2  3  4
2  3  4  1  2
3  4  3  2  1
4  2  1  4  3
5
6  0  0  0  4
7  3  0  0  0
8  4  3  0  0
9  0  0  4  3

完成表から10個のマスを0に変換した数表が出力されました。

ミニナンプレの問題を解くプログラム

次に、下の数表のように、ミニナンプレの空白の部分を0とした数列が与えられたとき、そこからミニナンプレを完成させるプログラムを完成させます。

11 0 0 0      1 2 3 4
23 0 1 2   →  3 4 1 2
34 3 0 1      4 3 2 1
40 0 0 0      2 1 4 3

実際にできたコードが下になります。

CalcMiniNumberPlace.java
  1import java.util.HashSet;
  2import java.util.Set;
  3
  4/** ミニナンプレを解くクラス */
  5public class CalcMiniNumberPlace {
  6
  7  /** 魔方陣の問題を格納する配列 */
  8  int[][] probArray;
  9
 10  /** 魔方陣の答えを格納する配列 */
 11  int[][] ansArray = new int[4][4];
 12
 13  /** 重複チェック用のhashSet */
 14  Set<Integer> hashSet = new HashSet<>();
 15
 16  /** 実行用mainメソッド */
 17  public static void main(String[] args) {
 18    CalcMiniNumberPlace cMiniNumberPlace = new CalcMiniNumberPlace();
 19    cMiniNumberPlace.probArray = new int[][] { { 1, 0, 0, 0 }, { 3, 0, 1, 2 }, { 4, 3, 0, 1 }, { 0, 0, 0, 0 } };
 20    cMiniNumberPlace.execute();
 21    cMiniNumberPlace.showArray();
 22  }
 23
 24  /** ミニナンプレを解く */
 25  private void execute() {
 26    for (int i = 0; i < 4; i++) {
 27      for (int j = 0; j < 4; j++) {
 28        if (probArray[i][j] != 0) {
 29          ansArray[i][j] = probArray[i][j];
 30          continue;
 31        }
 32      }
 33    }
 34    int x = 1;
 35    while (x != 0) {
 36      x = 0;
 37      for (int i = 0; i < 4; i++) {
 38        for (int j = 0; j < 4; j++) {
 39          // 縦
 40          if (ansArray[i][j] == 0) {
 41            ansArray[i][j] = getLastOne(ansArray[i][0], ansArray[i][1], ansArray[i][2],
 42                ansArray[i][3]);
 43            x++;
 44          }
 45          // 横
 46          if (ansArray[i][j] == 0) {
 47            ansArray[i][j] = getLastOne(ansArray[0][j], ansArray[1][j], ansArray[2][j],
 48                ansArray[3][j]);
 49            x++;
 50          }
 51          // 斜め
 52          if (i == j && ansArray[i][j] == 0) {
 53            ansArray[i][j] = getLastOne(ansArray[0][0], ansArray[1][1], ansArray[2][2],
 54                ansArray[3][3]);
 55            x++;
 56          }
 57          if (i + j == 3 && ansArray[i][j] == 0) {
 58            ansArray[i][j] = getLastOne(ansArray[0][3], ansArray[1][2], ansArray[2][1],
 59                ansArray[3][0]);
 60            x++;
 61          }
 62          // ボックス
 63          if (i < 2 && j < 2 && ansArray[i][j] == 0) {
 64            ansArray[i][j] = getLastOne(ansArray[0][0], ansArray[0][1], ansArray[1][0],
 65                ansArray[1][1]);
 66            x++;
 67          }
 68          if (i < 2 && j > 1 && ansArray[i][j] == 0) {
 69            ansArray[i][j] = getLastOne(ansArray[0][2], ansArray[0][3], ansArray[1][2],
 70                ansArray[1][3]);
 71            x++;
 72          }
 73          if (i > 1 && j < 2 && ansArray[i][j] == 0) {
 74            ansArray[i][j] = getLastOne(ansArray[2][0], ansArray[2][1], ansArray[3][0],
 75                ansArray[3][1]);
 76            x++;
 77          }
 78          if (i > 1 && j > 1 && ansArray[i][j] == 0) {
 79            ansArray[i][j] = getLastOne(ansArray[2][2], ansArray[2][3], ansArray[3][2],
 80                ansArray[3][3]);
 81            x++;
 82          }
 83        }
 84      }
 85    }
 86  }
 87
 88  /** 3個の数字から残りの1つを求めるメソッド */
 89  private int getLastOne(int num1, int num2, int num3, int num4) {
 90    hashSet.clear();
 91    hashSet.add(num1);
 92    hashSet.add(num2);
 93    hashSet.add(num3);
 94    hashSet.add(num4);
 95    if (hashSet.size() == 4) {
 96      return 10 - num1 - num2 - num3 - num4;
 97    }
 98    return 0;
 99  }
100
101  /** 配列を数表形式で出力する */
102  private void showArray() {
103    for (int[] a : ansArray) {
104      for (int i : a) {
105        System.out.printf("%3d", i);
106      }
107      System.out.println();
108    }
109  }
110
111}

実行結果の一例が以下になります。

出力結果(一例)
1  1  2  3  4
2  3  4  1  2
3  4  3  2  1
4  2  1  4  3

完成された数表が出力されました。


今回はJavaでミニナンプレを作成してみました。以上で記事を終わりにします。