数独,在9x9的格子填入1到9的数字,识得每行、每列和每个3x3的格子里面,都包含完整不重复的1到9的九个数字。
用于解题的数独,会在随机空出一些格子,解题人需要根据数字相关性,推断空格处应该填写的数字。
一般而言,空的越多难度相对变大,不过空格设置不合理,就可能出现多个解都可行的情况。
针对怎么生成一个数独游戏,试验了几种思路:
1)从空白局面开始,每次随机选取一个空格位置,在该位置可行的数字中随机选取一个,填入该空格。
每次填入数字后,排除当前位置所在行、列、3x3格的其它格子,关于这个数字的可能性。
一般生成了二十几个数字后,看起来挺像一个数独求解局面了,不过这样生成的数独,基本上不可解,总会出现冲突。
2)从空白局面开始,先填充左上的3x3格,然后填充中上的3x3格,然后右上的3x3格,如此依次填入9个3x3格。
每次随机选择一个当前位置可能的数字进行填充,同时同时排除当前位置所在行、列、3x3格的其它格子,关于这个数字的可能性。
这样看起来可行的办法,在生成第二个到第三个3x3格时候,几乎总是会出现冲突,使得有的位置不能填入任何数字。
看起来从空白局面开始,简单的随机填入数字的做法,基本不可行。
那么就反过来,从完整局面开始,进行挖孔,这样就肯定存在解,只是不能确定是否是唯一解。
3)从空白局面开始,将某个数字依次填入第一行到第九行的随机可行位置,然后选择下一个数字,做同样的操作,直至所有数字都填完。
如果数字填到某一行没有可行的列位置,则回溯到上一行,将上一行的数字移入下一可行的列位置;
如果上一行的数字,没有下一可行的列位置,则继续向上回溯。
针对3)的思路,生成完整局面成功,下面就针对这个思路进行程序分析。
以下是示意图,其中标记为X的格子,表示前面的数字已经占据了该位置。
-1
+-------+-------+-------+
0 | | 1 | |
1 | 1 | | |
2 | | | 1 |
+-------+-------+-------+
3 | 1 | | |
4 | | 1 | |
5 | | | 1 |
+-------+-------+-------+
6 | | | 1 |
7 | | 1 | |
8 | 1 | | |
+-------+-------+-------+
+-------+-------+-------+
9 | | 2 X | |
10 | 2 X | | |
11 | | | 2 X |
+-------+-------+-------+
12 | X | | 2 |
13 | | X 2 | |
14 | 2 | | X |
+-------+-------+-------+
...
+-------+-------+-------+
78 | X X X | X X X | 9 X X |
79 | X X X | X X 9 | X X X |
80 | X 9 X | X X X | X X X |
+-------+-------+-------+
81
一般的空白局面是9x9的局面,为方便程序的行逻辑,这里把空白局面抽象为81行,9列。
这样,第0~8行填入1,第9~17行填入2,依次类推。行数和要填的数字有唯一对应关系。
注:对于第0~8行,可以类比为8皇后问题,在9行9列中,放入9个1,使得每行每列都有一个1。
从数据结构上面来说,分成两个层面来考虑:
1)对于每个数字行(第0~80行)而言,需要存储当前行数字可能放的列;
为了回溯的需要,需要存放当前从哪列开始放置,以及当前放置在哪个列。
2)对于数独局面而言,需要存储当前搜索到了哪个数字的哪一行;以及每一个数字行的信息。
根据BIT位定义每一列,是否填充了数字;方便对不同行进行位或操作,计算列是否已经填充。
// col mask
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
enum
{
COL_MASK_NONE = 0x000, // 0 0000 0000
COL_MASK_1 = 0x001, // 0 0000 0001
COL_MASK_2 = 0x002, // 0 0000 0010
COL_MASK_3 = 0x004, // 0 0000 0100
COL_MASK_4 = 0x008, // 0 0000 1000
COL_MASK_5 = 0x010, // 0 0001 0000
COL_MASK_6 = 0x020, // 0 0010 0000
COL_MASK_7 = 0x040, // 0 0100 0000
COL_MASK_8 = 0x080, // 0 1000 0000
COL_MASK_9 = 0x100, // 1 0000 0000
COL_MASK_123 = 0x007, // 0 0000 0111
COL_MASK_456 = 0x038, // 0 0011 1000
COL_MASK_789 = 0x1C0, // 1 1100 0000
COL_MASK_ALL = 0x1FF // 1 1111 1111
};
定义某一行的数据结构,base_col和curr_col记录当前行的起始搜索列和实际存储列;
注意:col_mask记录了根据前面所有行的数据,推算出当前行实际可行的位置,方便对当前行的搜索。
typedef struct
{
int base_col; // start col 0~8:valid, -1:invalid
int curr_col; // current col 0~8:valid, -1:invalid
unsigned int col_mask;
} col_index_t;
sodoku_t记录一个完整数独局面,long_row记录当前搜索到第几行;col_index[81]记录每一行实际搜索状态。
typedef struct{
int long_row; // long_row 0~80:valid, -1:init, 81:finish
col_index_t col_index[81]; // col_index[row]
} sodoku_t;
从算法而言,整体是一个回溯算法:
1)初始化数据结构。
2)设置long_row进入第一行,设置一个随机开始列base_col和cur_col。
3)如果没有到达最后一行,则继续搜索。
4)从当前搜索列cur_col开始,查找可放置的列位置,找到则放置在该位置(并更新cur_col),并转入下一行的搜索。
5)如果没有找到,则回溯到上一行。
6)全部数字放置完毕后,结束循环。
注:针对伪随机数的特征,我们可以设置随机数种子srand(0)、srand(1)、srand(2)等等,则可以得到伪随机序列数字所对应的局面。
int main(int argc, const char * argv[])
{
sodoku_t sodoku;
srand((unsigned int)time((time_t *)NULL));
init_sodoku(&sodoku);
goto_next_row(&sodoku);
while(sodoku_not_finish(&sodoku))
{
if(goto_next_col(&sodoku))
{
goto_next_row(&sodoku);
}
else
{
rollback_row(&sodoku);
}
}
print_sodoku(&sodoku);
return 0;
}
下面依次解释循环中的代码:
1)初始化局面,设置当前行为起始状态。
void init_sodoku(sodoku_t * sodoku)
{
do
{
if(sodoku == NULL)
break;
memset(sodoku, 0, sizeof(sodoku_t));
sodoku->long_row = -1; // init invalid long_row
} while(false);
}
2)判定有没有搜索完成
bool_t sodoku_not_finish(sodoku_t *sodoku)
{
bool_t not_finish = false;
do
{
if(sodoku == NULL)
break;
if(sodoku->long_row <= 80)
{
not_finish = true;
}
} while(false);
return not_finish;
}
3)进入下一行和回退到上一行
void goto_next_row(sodoku_t *sodoku)
{
do
{
if(sodoku == NULL)
break;
sodoku->long_row++;
if(sodoku->long_row > 80)
break;
/* init col index */
sodoku->col_index[sodoku->long_row].base_col = calc_rand_col(); // random col
sodoku->col_index[sodoku->long_row].curr_col = -1; // invalid col
/* init col mask */
calc_col_mask_row(sodoku, sodoku->long_row);
} while(false);
}
void rollback_row(sodoku_t *sodoku)
{
do
{
if(sodoku == NULL)
break;
sodoku->long_row--;
/* init col mask */
calc_col_mask_row(sodoku, sodoku->long_row);
} while(false);
}
4)辅助设置函数,对于col_mask是关于当前行可以放哪些列,根据规则,不能与与相同数字占据了相同行、列和3x3格。
int calc_rand_col(void)
{
int col;
col = rand() % 9; // col 0~8
return col;
}
int calc_next_col(int col)
{
return (col + 1) % 9; // col 012345678 => 123456780
}
void calc_col_mask_row(sodoku_t *sodoku, int long_row)
{
int row;
unsigned int mask = COL_MASK_NONE;
// clear cell that already fill by other digit(in the same row)
for(row = long_row % 9; row < long_row; row+=9)
{
mask |= sodoku->col_index[row].col_mask;
}
// clear cell that already fill by same digit(in the same col)
for(row = long_row - long_row % 9; row < long_row; row++)
{
mask |= sodoku->col_index[row].col_mask;
}
// clear cell that already fill by same digit(in the same 3x3box)
for(row = long_row - long_row % 3; row < long_row; row++)
{
if(sodoku->col_index[row].col_mask & COL_MASK_123)
mask |= COL_MASK_123;
else if(sodoku->col_index[row].col_mask & COL_MASK_456)
mask |= COL_MASK_456;
else // if(sodoku->col_index[row].col_mask & COL_MASK_789)
mask |= COL_MASK_789;
}
sodoku->col_index[long_row].col_mask = ~mask; // store cols that can place
}
5)选择下一列,也就是从当前列循环向右搜索,直到起始列结束,找到可行的列,则
bool_t goto_next_col(sodoku_t *sodoku)
{
bool_t success = false;
int base_col;
int curr_col;
int next_col;
bool_t found = false;
do
{
if(sodoku == NULL)
break;
base_col = sodoku->col_index[sodoku->long_row].base_col;
curr_col = sodoku->col_index[sodoku->long_row].curr_col;
found = false;
if(curr_col == -1)
{
// find first
next_col = base_col;
// base_col is valid
if(sodoku->col_index[sodoku->long_row].col_mask & (1 << base_col))
{
found = true;
next_col = base_col;
}
else
{
next_col = calc_next_col(base_col);
}
}
else
{
next_col = calc_next_col(curr_col);
}
if(!found)
{
// find next
while(next_col != base_col)
{
// next_col is valid
if(sodoku->col_index[sodoku->long_row].col_mask & (1 << next_col))
{
found = true;
break;
}
// find next
next_col = calc_next_col(next_col);
}
}
if(!found)
break;
// set next_col
sodoku->col_index[sodoku->long_row].curr_col = next_col;
sodoku->col_index[sodoku->long_row].col_mask = (1 << next_col); // store selected col
success = true;
} while(false);
return success;
}
6)最后是打印出来当前局面:
void print_sodoku(sodoku_t *sodoku)
{
int long_row;
int row;
int col;
int digit;
int cell[81] = { 0 };
do
{
if(sodoku == NULL)
break;
for(long_row = 0; long_row < sodoku->long_row; long_row++)
{
row = long_row % 9;
col = sodoku->col_index[long_row].curr_col;
digit = long_row / 9 + 1;
cell[row * 9 + col] = digit;
}
printf("Sodoku %d\n", sodoku->long_row);
for(row = 0; row < 9; row++)
{
if(row % 3 == 0)
printf("+-----+-----+-----+\n");
printf("|");
for(col = 0; col < 9; col++)
{
printf("%d", cell[row * 9 + col]);
if(col % 3 == 2)
printf("|");
else
printf(" ");
}
printf("\n");
}
printf("+-----+-----+-----+\n");
} while(false);
}
随机生成的一个局面:
Sodoku 81
+-----+-----+-----+
|2 1 6|5 9 7|3 4 8|
|9 3 4|8 1 2|5 6 7|
|5 8 7|3 4 6|1 2 9|
+-----+-----+-----+
|3 7 1|4 5 8|6 9 2|
|6 9 5|7 2 3|8 1 4|
|4 2 8|1 6 9|7 3 5|
+-----+-----+-----+
|7 4 2|6 8 1|9 5 3|
|8 6 9|2 3 5|4 7 1|
|1 5 3|9 7 4|2 8 6|
+-----+-----+-----+