解数独
解法一
纯暴力方法,未做任何优化
class Solution {
private:
bool is_valid_place(vector<vector<char>>& board, int x, int y, char ch) {
//行或者列放入字符ch是否有效
for (int i = 0; i < 9; i++) {
if (board[x][i] == ch || board[i][y] == ch) {
return false;
}
}
// 3x3 box放入字符ch是否有效
int xstart = x / 3 * 3;
int ystart = y / 3 * 3;
for (int i = xstart; i < xstart + 3; i++) {
for (int j = ystart; j < ystart + 3; j++) {
if (board[i][j] == ch) {
return false;
}
}
}
return true;
}
bool back_track(vector<vector<char>>& board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
// 找到空位置,开始从1到9,测试哪个数字有效
if (board[i][j] == '.') {
for (int k = 0; k <= 8; k++) {
if (is_valid_place(board, i, j, k + '1')) {
board[i][j] = k + '1';
if (back_track(board) == true) {
return true;
}
board[i][j] = '.';
}
}
//该位置填入任何数字都无效,返回上一层,改变上一层(也就是前一个数)或更上层的
//来使这一层填入有效数字
return false;
}
}
}
//程序能运行到这里,说明一个空位置都没有了,上面的程序又可以确定空位置放入的是正确的数字,且题目保证数独有唯一解,所以返回true,回到第31行
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
back_track(board);
}
};
解法二
解法一纯暴力搜索,只要是’.‘的位置,都会从’1’ - '9’挨个放进去判断,直到放入正确的为止。
解法二用三个数组
row数组记录每一行中,数字1-9的使用情况,被使用为false
,反之未被使用为true
。
col数组记录每一列。
box数组记录每一个3x3box。
bool row[9][9] = { false };
bool col[9][9] = { false };
bool box[3][3][9] = { false };
一个重要的数组就是tab,记录哪一个点可以使用,因此直接遍历tab就行。
vector<pair<int, int>>tab;
列号-1 1 2 3 4 5 6 7 8 9 row[4] F T F F T T T F T col[4] F F T T T F F F F box[3][3] T F F T T F T F T
class Solution {
private:
bool row[9][9] = { false };
bool col[9][9] = { false };
bool box[3][3][9] = { false };
vector<pair<int, int>>tab;
bool flag = false;
void back_track(vector<vector<char>>& board, int point) {
if (point >= tab.size()) {
flag = true;
return;//返回到上一层函数的调用,即第23行,这一层结束
}
int x = tab[point].first;
int y = tab[point].second;
for (char ch = 0; ch <= 8; ch++) {
if (row[x][ch] && col[y][ch] && box[x / 3][y / 3][ch]) {
board[x][y] = ch + '1';
//这一数字使已经在这一行出现,标记为false,不能再出现
row[x][ch] = false;
//这一数字使已经在这一列出现,标记为false,不能再出现
col[y][ch] = false;
//这一数字使已经在这一3x3的box出现,标记为false,不能再出现
box[x / 3][y / 3][ch] = false;
back_track(board, point + 1);
if (flag) {
//如果能进入这个if,那么会返回上一层back_track()函数的调用,即上一句
//此后会一直返回
return;
}
//运行到这,说明填入的数字不对,数组都要回到填入该数字之前的状态
board[x][y] = '.';
//让该行的其他列使用该数字
row[x][ch] = true;
//让该列的其他行使用该数字
col[y][ch] = true;
//让该3x3的box的其他位置使用该数字
box[x / 3][y / 3][ch] = true;
}
}
}
public:
void solveSudoku(vector<vector<char>>& board) {
memset(row, true, sizeof(row));
memset(col, true, sizeof(row));
memset(box, true, sizeof(row));
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') {
//这个点没有数字,保存它的横纵坐标
tab.emplace_back(i, j);
}
else {
//数字num已经被使用,它所在的行,列,box都要作个标记
int num = board[i][j] - '1';
row[i][num] = false;
col[j][num] = false;
box[i / 3][j / 3][num] = false;
}
}
}
//从第一个空位置开始遍历
back_track(board, 0);
}
};
解法三
位运算
方法二中,一个数据是否使用,用一个bool来记录,其实可以用一个大小为9个位的变量val
,足够记录1-9的使用情况
1 2 3 4 5 6 7 8 9 row[4] 1 0 1 1 0 0 0 1 0 col[4] 1 1 0 0 0 1 1 1 1 box[3][3] 0 0 1 0 0 1 0 1 0 or 1 1 1 1 0 1 1 1 1
一个数字字符,例如’3’,减去’1’后,为2,将1左移两位,二进制表示为pos = 0 0000 0100
这样用一个位就能表示被使用了。用这个方法处理三个数组,和它所在的行、列、box数组对应的数字进行逻辑异或。
pos & -pos
的含义是,取出最低位的位置来用,例如上面的3,如果4,5,6都没有被使用,那pos = 0 0011 1100
,进行pos & -pos
这个操作后等于num = 0 0000 0100
,这个数刚是2k=4,k = 2,字符’1’ + 2 = ‘3’。log2(num) = 2。
pos & pos - 1
的含义是,删掉最后一个最低位,还用上面的例子,如果4,5,6都没有被使用,那pos = 0 0011 1100
,进行pos & pos - 1
操作后,等于 0 0011 1000
。
这两个操作还可以看我的这篇文章,都有解释
class Solution {
private:
short row[9] = { 0 };
short col[9] = { 0 };
short box[9][9] = { 0 };
vector<pair<int, int>>point;
bool flag = false;
public:
void lgxor(int x, int y, int num) {
row[x] ^= num;
col[y] ^= num;
box[x / 3][y / 3] ^= num;
}
void back_track(vector<vector<char>>& board, int p) {
if (p >= point.size()) {
flag = true;//同方法二
return;
}
int x = point[p].first;
int y = point[p].second;
// 0x1ff = 1 1111 1111 九个位
int pos = 0x01ff & ~(row[x] | col[y] | box[x / 3][y / 3]);
while (0 != pos) {
int num = pos & (-pos);
pos &= pos - 1;
char ch = log2(num);
board[x][y] = '1' + ch;
//与num异或,该数字所在行和列和box不能再次出现
lgxor(x, y, num);
back_track(board, p + 1);
if (true == flag) {//同方法二
return;
}
//与num异或,该数字所在行和列和box可以再次出现
lgxor(x, y, num);
}
}
void solveSudoku(vector<vector<char>>& board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if ('.' == board[i][j]) {
point.emplace_back(i, j);
}
else {
int num = board[i][j] - '1';
lgxor(i, j, 1 << num);
}
}
}
back_track(board, 0);
}
};
解法三优化
解法二中,有一些点的数字可以直接确定下来,首先将这些点填好,会省下很多时间。
1 2 3 4 5 6 7 8 9 row[4] 1 0 1 1 0 0 0 1 0 col[4] 1 1 0 0 0 1 1 1 1 box[3][3] 0 0 1 0 0 1 0 1 0 or 1 1 1 1 0 1 1 1 1
这些点的性质就是,所在行、列、box的数字相或之后,有且仅有一个能放的数字,也就是说进行pos & pos - 1
操作,移除最后一个位置后,pos为0。
class Solution {
private:
short row[9] = { 0 };
short col[9] = { 0 };
short box[9][9] = { 0 };
vector<pair<int, int>>point;
bool flag = false;
public:
void lgxor(int x, int y, int num) {
row[x] ^= num;
col[y] ^= num;
box[x / 3][y / 3] ^= num;
}
void back_track(vector<vector<char>>& board, int p) {
if (p >= point.size()) {
flag = true;
return;
}
int x = point[p].first;
int y = point[p].second;
int pos = 0x01ff & ~(row[x] | col[y] | box[x / 3][y / 3]);
while (0 != pos) {
int num = pos & (-pos);
pos &= pos - 1;
char ch = log2(num);
board[x][y] = '1' + ch;
lgxor(x, y, num);
back_track(board, p + 1);
if (true == flag) {
return;
}
lgxor(x, y, num);
}
}
void solveSudoku(vector<vector<char>>& board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if ('.' != board[i][j]) {
int num = board[i][j] - '1';
lgxor(i, j, 1 << num);
}
}
}
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if ('.' == board[i][j]) {
int pos = 0x01ff & ~(row[i] | col[j] | box[i / 3][j / 3]);
if (0 == (pos & (pos - 1))) {
int num = log2(pos);
lgxor(i, j, 1 << num);
board[i][j] = '1' + num;
}
else {
point.emplace_back(i, j);
}
}
}
}
back_track(board, 0);
}
};
性能对比,用同一个解法解同一个数独500次,看它所耗费的时间,测试很粗糙,不具代表性。
单位,毫秒。