第4講 ヒントとなる数字が入っている数独を解く!
第6話 候補数字の個数が低い順にランキング
を実現するプロジェクト例
#include<iostream>
#include<conio.h> //while (!_kbhit())を使うために必要
using namespace std;
void f(int s); //sは次元番号=部屋番号
const int n = 9; //16次数独や25次数独も考えてnと一般化した。
int cn = 0; //順列を数える変数
int sudoku[n][n]; //解答用2次元配列(魔方陣の研究から始まったので本体をmとしてきた。
int mondai[n][n]; //sudoku[n][n]は解答用なので、問題用の2次元配列を用意した。
int sng = 1;//1ならば真
const int hnt = 30;
void syokika();
void hy();//結果をコンソール画面に表示する関数
void cellslstkaiseki();//全体構造解析 = 全セルリスト構造解析
void cellsrnk();//候補数字の個数が少ない順にランキング
int lst[9][9][9];//セルリスト構造解析によって可能な数字を収容する3次元配列
int mx[9][9];//セルの数字候補の個数
int y[81 - hnt], x[81 - hnt];
int krn[9][9];
int main() {
clock_t hj, ow;
hj = clock();
syokika();
f(0);
cout << "ヒント数 = " << hnt << endl;
cellslstkaiseki();
cellsrnk();
hy();
ow = clock();
if (sng == 1)cout << endl << "すべて正常でした。" <<
endl;
cout << "計算時間は平均で" << (double)(ow - hj) / CLOCKS_PER_SEC
<< "秒です。" << endl;
cout << "順列の場合の数は" << cn << "です。"
<< endl;
cout << "プロジェクト成功" << endl;
while (!_kbhit()); //待機させるための命令
return(0);
}
void syokika() {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
sudoku[i][j] = 0;
mondai[i][j] = 0;
}
}
}
void hy() {//結果をコンソール画面に表示する関数
cout << endl;
for (int i = 0; i < 32; i++)cout << " -"; //最初の横線
cout << endl;
for (int i = 0; i < n; i++) {
cout << "| ";//最初の縦線
for (int j = 0; j < n; j++) {
if (mondai[i][j] > 0) {
cout << " " << mondai[i][j] << "
"; //2次元配列を2次元に並べる
}
else {
if (krn[i][j] < 10)cout << " " << krn[i][j]
<< " "; //2次元配列を2次元に並べる
if (krn[i][j] >= 10)cout << " " << krn[i][j]
<< " "; //2次元配列を2次元に並べる
}
if (j % 3 == 2)cout << "| ";//2本目3本目4本目の縦線
}
if (i % 3 == 2) {
cout << endl;
for (int j = 0; j < 32; j++)cout << " -"; //2本目3本目4本目の横線
}
cout << endl;
}
}
void cellslstkaiseki() {//全体構造解析 = 全セルリスト構造解析
for (int a = 0; a < n; a++) {
for (int b = 0; b < n; b++) {
if (mondai[a][b] == 0) {//解析対象を空欄のみとする
for (int i = 0; i < n; i++)lst[a][b][i] = i + 1;//初期化 1から9までをリスト
for (int i = 0; i < n; i++) {//行の条件によって候補数字を外す
for (int j = 0; j < n; j++) {
if (j != b) {//自己以外のセルによってセルは条件づけられている
if (lst[a][b][i] == mondai[a][j]) {//過去に外されたものはlst[a][b][i]が0になっており、
//等式が成り立たないから問題はない
lst[a][b][i] = 0;//mondai[a][j]を候補数字から外す
}
}
}
}
for (int i = 0; i < n; i++) {//列の条件によって候補数字を外す
for (int j = 0; j < n; j++) {
if (j != a) {//自己以外のセルによってセルは条件づけられている
if (lst[a][b][i] == mondai[j][b]) {//過去に外されたものはlst[a][b][i]が0になっており、
//等式が成り立たないから問題はない
lst[a][b][i] = 0;//mondai[j][b]を候補数字から外す
}
}
}
}
//yとxはそれぞれブロックのトップ=一番上で一番左のセルの縦座標と横座標
int y = 3 * (a / 3);//aはmondai[a][b]のa
int x = 3 * (b / 3);//bはmondai[a][b]のb
//現在リスト構造解析を受けているセルは(a,b)でありブロック先頭の座標は(y,x)である
//ブロック先頭は(0,0),(0,3),(0,6),(3,0),(3,3),(3,6),(6,0),(6,3),(6,6)と動いている。
//それぞれのブロック先頭の座標をmonddai[a][b]の(a,b)から作り出さなければならない。
for (int i = 0; i < n; i++) {//ブロックの条件によって候補数字を外す
for (int j = 0; j < n; j++) {
if (y + (j / 3) != a || x + (j % 3) != b) {
//自己以外のセルによってセルは条件づけられているから
//自己自身は対象から外す
//自分の本質は、自分の中にはなくて外のセルとの関係によってきまる
if (lst[a][b][i] == mondai[y + (j / 3)][x + (j % 3)]) {
//過去に外されたものはlst[a][b][i]が0になっており、
//等式が成り立たないから問題はない
lst[a][b][i] = 0;//mondai[y + (j / 3)][x + (j % 3)]を候補数字から外す
}
}
}
}
mx[a][b] = 0;//候補数字いったん0に初期化しておく
for (int i = 0; i < n; i++) {
if (lst[a][b][i] > 0) {
lst[a][b][mx[a][b]] = lst[a][b][i];//0となっているところを詰める
mx[a][b]++;
}
}
cout << endl;
cout << "以下はセル(" << a << ", "
<< b << ")のリスト構造解析結果" << endl;
for (int i = 0; i < mx[a][b]; i++) {
cout << lst[a][b][i] << " ";
}
cout << endl;
cout << "候補数字の個数" << mx[a][b] << endl;
}
}
}
}
void cellsrnk() {//数字候補の個数が低い順にランキング
int onoff[n][n];
for (int i = 0; i < n; i++)for (int j = 0; j < n; j++)onoff[i][j]
= 0;
for (int g = 0; g < 81 - hnt; g++) {
int ik, jk;
int mn = 10;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (mondai[i][j] == 0) {//空欄のみをランキング対象にする
if (onoff[i][j] == 0) {//新部屋番号割り振られたとき1、まだ割り振られていないとき0
if (mx[i][j] < mn) {
mn = mx[i][j];
ik = i;
jk = j;
}
}
}
}
}
y[g] = ik;
x[g] = jk;
onoff[y[g]][x[g]] = 1;
krn[y[g]][x[g]] = g;
}
}
void f(int s) {
int y = s / 9; //縦座標
int x = s % 9; //横座標
unsigned u = (unsigned)time(NULL);
srand(2); //シード値を現在の時刻から取得 後に2はuに戻す
int ii = rand() % 9; //始まりをランダムにする
for (int i = 0; i < n; i++) {
sudoku[y][x] = (i + ii) % n + 1; //2次元配列に1から9までの整数を入力
if (x > 0) {
for (int j = 0; j < x; j++) {
if (sudoku[y][x] == sudoku[y][j])goto tobi; //行の重複を防ぐ
}
}
if (y > 0) {
for (int j = 0; j < y; j++) {
if (sudoku[j][x] == sudoku[y][x])goto tobi; //行の重複を防ぐ
}
}
if (y % 3 == 1) {
for (int j = 0; j < 3; j++) {
if ((x / 3) * 3 + j != x) {
if (sudoku[y][x] == sudoku[y - 1][(x / 3) * 3 + j])goto tobi;
//ブロックの重複を防ぐ
}
}
}
if (y % 3 == 2) {
for (int j = 0; j < 3; j++) {
if ((x / 3) * 3 + j != x) {
if (sudoku[y][x] == sudoku[y - 2][(x / 3) * 3 + j])goto tobi;
//ブロックの重複を防ぐ
if (sudoku[y][x] == sudoku[y - 1][(x / 3) * 3 + j])goto tobi;
//ブロックの重複を防ぐ
}
}
}
if (s + 1 < n * n)f(s + 1); //1つ奥の部屋に入室
if (cn == 1)return; //数独が1個できた時点で止める
if (sng == 0)return; //異常があった時点でプロジェクトを止める
if (s == n * n - 1) { //一番奥の部屋に到達
int hb; //部屋番号
int tbs[6] = { 11,13,17,19,23,29 }; //飛びの選択肢 素数であれば81と互いに素は保証されている
int st = rand() % 81; //始めの位置
int tb = tbs[rand() % 6]; //飛び
int h = 0; //可否の否
int* gohr = (int*)calloc(hnt, sizeof(int));
for (int t = 0; t < hnt; t++) {
gohr[t] = (st + t * tb) % 81;
}
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++) {
hb = n * j + k; //部屋番号の再初期化 = 空欄の数字候補の個数の小さい順に入れる
int h = 0; //可否の否
for (int t = 0; t < hnt; t++) {
if (gohr[t] == hb) {
h = 1;//可否の可
break;
}
}
if (h == 1) {
mondai[j][k] = sudoku[j][k]; //問題用の配列に代入
}
}
cout << endl;
}
free(gohr); //メモリ解放
hy();//結果をコンソール画面に表示する関数
int p[9];//重複検査のための配列
int kr;
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++)p[k] = 0;//初期化
kr = 1; //初期化
for (int k = 0; k < n; k++) {
p[sudoku[j][k] - 1] = 1;
}
for (int k = 0; k < n; k++) {
if (p[k] == 0) { //行の重複検査
cout << "×";
kr = 0;
sng = 0;
goto tobi1;
}
}
for (int k = 0; k < n; k++)p[k] = 0;//初期化
kr = 1; //初期化
for (int k = 0; k < n; k++) {
p[sudoku[k][j] - 1] = 1;
}
for (int k = 0; k < n; k++) {
if (p[k] == 0) { //列の重複検査
cout << "×";
kr = 0;
sng = 0;
goto tobi1;
}
}
for (int k = 0; k < n; k++)p[k] = 0;//初期化
kr = 1; //初期化
for (int k = 0; k < n; k++) {
p[sudoku[3 * (j / 3) + (k / 3)][3 * (j % 3) + (k % 3)] - 1] =
1;
}
for (int k = 0; k < n; k++) {
if (p[k] == 0) {//ブロックの重複検査
cout << "×";
kr = 0;
sng = 0;
goto tobi1;
}
}
}
tobi1:;
if (kr == 1)cout << "〇";
cn++;
if (cn == 1)return; //数独が1個できた時点でとめる
}
tobi:;
}
}
結果の表示がいろいろな場所にあってわかりにくくなっていましたので、表示の仕事を
void hy();//結果をコンソール画面に表示する関数
に任せて文字ずれの問題は解消しました。
第6話への課題は、部分構造解析です。
空欄に数字を入れる度に全体構造は姿を変えるわけですが、
オレンジ色のセルに数字を入れることによって影響を受けるセルは、
(すみません。図に間違いがありましたので2023年10月4日に直しました。)
の青の部分です。
残りの白い部分は全く影響が及ばないので、
その部分まで解析してしまったら、
時間の無駄です。
後には理詰めと言われる方法で解きますが、
今は試行錯誤法(= 仮定法 = 背理法)で解きますので、
オレンジの候補数字は{2,4}です。
2を入れる場合と4を入れる場合では、異なる部分構造解析となります。
尚、座標(0,1)から始める理由は
候補数字が{2, 4}の2個で最小値になっているからです。
ただし、候補数字の個数が2個のものは他にもあります。
同数の場合は左及び上を優先することになっていましたね。
皆さん、ごめんなさい。
部分構造解析を考えていて気が付きました。
ランキング0~50まで付けましたが、
必要なのは0だけです。
なぜなら、には
から、2または4が入りますが、いずれにしろ影響が及ぶのは
の青であったとしても、リストの構造が変わってしまい、
リストの構造から作ったランキングは0以外は変わってしまいます。
そこで
void cellsrnk()の名称をvoid nextcell()に変更して、
void cnextcell()の中身などを改良して
を実現してください。赤枠はvoid hy()を工夫しただけです。
今回の改良によって不要な配列ができますので、それを削ってください。
第5話へ 第7話へ
トップへ