▶ 有关数独的两个问题。
▶ 36. 检测当前盘面是否有矛盾(同一行、同一列或 3 × 3 的小框内数字重复),而不关心该盘面是否有解。
● 初版代码,24 ms,没有将格子检测函数独立出去,行检测、列检测、框检测是否合并为一个循环对速度的影响不明显。最快的解法算法与之相同,蜜汁优化 static vector<vector<char>> board = []() {std::ios::sync_with_stdio(false); cin.tie(NULL); return vector<vector<char>>{}; }(); 后变为 7 ms 。
1 class Solution 2 { 3 public: 4 bool isValidSudoku(vector<vector<char>>& board) 5 { 6 int index, i, row, col; 7 char temp; 8 for (index = 0; index < 81; index++) 9 { 10 row = index / 9; 11 col = index % 9; 12 if ((temp = board[index / 9][index % 9]) < '1' || temp > '9') 13 continue; 14 temp = board[row][col]; 15 board[row][col] = '0'; 16 for (i = 0; i < 9; i++) 17 { 18 if (board[row][i] == temp || board[i][col] == temp || board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == temp) 19 return false; 20 } 21 board[row][col] = temp; 22 } 23 return true; 24 } 25 };
▶ 37. 数独求解。总是假设盘面具有唯一解(即求出第一个解即可)。
● 初版代码,13 ms,去掉了第36题中的盘面矛盾检测。
1 class Solution 2 { 3 public: 4 bool flag = false; 5 void cal(vector<vector<char>>& board, int index) 6 { 7 int i, j, row, col; 8 char temp; 9 bool permit; 10 for (i = index; i < 81 && (temp = board[i / 9][i % 9] >= '1') && temp <= '9'; i++); 11 if (i == 81) 12 { 13 flag = true; 14 return; 15 } 16 for (temp = '1'; temp <= '9' && !flag; temp++) 17 { 18 for (j = 0, row = i / 9, col = i % 9, permit = true; j < 9; j++) 19 { 20 if (board[row][j] == temp || board[j][col] == temp || board[row / 3 * 3 + j / 3][col / 3 * 3 + j % 3] == temp) 21 { 22 permit = false; 23 break; 24 } 25 } 26 if (permit) 27 { 28 board[i / 9][i % 9] = temp; 29 cal(board, i + 1); 30 } 31 } 32 if (temp > '9' && !flag) 33 board[i / 9][i % 9] = '+'; 34 return; 35 } 36 void solveSudoku(vector<vector<char>>& board) 37 { 38 cal(board, 0); 39 return; 40 } 41 };
● 改良代码,10 ms,干掉了变量 flag,在填表前先做第 36 题的矛盾检查。
1 class Solution 2 { 3 public: 4 bool cal(vector<vector<char>>& board, int index) 5 { 6 int i, j, row, col; 7 char temp; 8 bool conflict, finish; 9 for (i = index; i < 81 && (temp = board[i / 9][i % 9] >= '1') && temp <= '9'; i++); // 寻找下一个没有数字的格点 10 if (i == 81) 11 return true; 12 for (temp = '1', finish = false; temp <= '9' && !finish; temp++) // 尝试填写 '1' 到 '9' 13 { 14 for (j = 0, row = i / 9, col = i % 9, conflict = false; j < 9; j++) // 对欲填入的数字进行三种检查 15 { 16 if (board[row][j] == temp || board[j][col] == temp || board[row / 3 * 3 + j / 3][col / 3 * 3 + j % 3] == temp) 17 { 18 conflict = true; 19 break; 20 } 21 } 22 if (!conflict) // 通过了三种检查,确定填入数字 23 { 24 board[i / 9][i % 9] = temp; 25 finish = cal(board, i + 1); // 在填入该数字的基础上尝试填写下一个 26 } 27 } 28 if (temp > '9' && !finish) // 有错,回溯到前面格点重新填写 29 { 30 board[i / 9][i % 9] = '0'; 31 return false; 32 } 33 return true; 34 } 35 bool isValidSudoku(vector<vector<char>>& board) 36 { 37 int index, i, row, col; 38 char temp; 39 for (index = 0; index < 81; index++) 40 { 41 row = index / 9; 42 col = index % 9; 43 if ((temp = board[index / 9][index % 9]) < '1' || temp > '9') 44 continue; 45 temp = board[row][col]; 46 board[row][col] = '0'; 47 for (i = 0; i < 9; i++) 48 { 49 if (board[row][i] == temp || board[i][col] == temp || board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == temp) 50 return false; 51 } 52 board[row][col] = temp; 53 } 54 return true; 55 } 56 void solveSudoku(vector<vector<char>>& board) 57 { 58 isValidSudoku(board); 59 cal(board, 0); 60 return; 61 } 62 };
● 大佬代码, 0 ms,主要是使用一个 array<array<bitset<10>, 9>, 9> 结构的变量来保存所有格点可能填写的数字,使用一个 array<array<char, 9>, 9> 结构的变量来保存当前盘面,使用一个 vector<pair<int, int>> 结构的变量来保存所有空个点的位置,通过调整深度优先的便利顺序,减少了时间开销。
1 using _2D_bit10 = array<array<bitset<10>, 9>, 9>; 2 const int ORIGIN_STATE = 1022; // states 的初始值,后面有解释 3 4 class Solution 5 { 6 public: // 函数调用关系: solveSudoku{ set, dfs }, dfs{ set, dfs }, set{ constraint }, constraint{ }; 7 int defined_cnt = 0; // 已填写的格点数目 8 bool constraint(_2D_bit10 & states, array<array<char, 9>, 9> & bd, const int r, const int c, const int v) 9 { // 检查 bd[r][c] 的值是否不等于 v,即当 bd[r][c] == v 时返回 false,认为有矛盾 10 bitset<10> & st = states[r][c]; 11 if (bd[r][c] != 0) // 该位置上已经有数字 12 { 13 if (bd[r][c] == v) // 与已经有的数字重复,矛盾 14 return false; 15 else // 与已经有的数字不重复,通过 16 return true; 17 } 18 st[v] = 0; // 该位置上没有数字,说明是在填充其他格子的时候进行的检查,那么 bd[r][c] 就不再可能为 v 了 19 if (st.count() == 0) // bd[r][c] 一个能填的都不剩了,矛盾 20 return false; 21 if (st.count() > 1) // bd[r][c] 还剩填充其他数字的可能性,通过 22 return true; 23 for (int i = 1; i <= 9; ++i) // 当且仅当 st 中只有一个 1 位时进入, 24 { 25 if (st[i] == 1) 26 return set(states, bd, r, c, i);// 检查最后剩余的这一种可能是否有矛盾 27 } 28 } 29 bool set(_2D_bit10 & states, array<array<char, 9>, 9> & bd, const int r, const int c, const int v) 30 { // 在 bd[r][c] 尝试填入 v,检查是否有矛盾 31 bitset<10> & possib = states[r][c]; 32 int k, rr, cc; 33 possib = 0; 34 bd[r][c] = v; 35 defined_cnt++; 36 const int blk_r = (r / 3) * 3, blk_c = (c / 3) * 3; // bd[r][c] 所在的块段号 37 for (k = 0; k < 9; ++k) 38 { 39 if (c != k && !constraint(states, bd, r, k, v)) // 同行逐列检查 40 return false; 41 if (r != k && !constraint(states, bd, k, c, v)) // 同列逐行检查 42 return false; 43 rr = blk_r + k / 3, cc = blk_c + k % 3; // 同块逐格检查 44 if ((rr != r || cc != c) && !constraint(states, bd, rr, cc, v)) 45 return false; 46 } 47 return true; 48 } 49 bool dfs(const int i, vector<pair<int, int>> & unset_pts, array<array<char, 9>, 9> & bd, _2D_bit10 & states) 50 { 51 if (i == unset_pts.size() || 9 * 9 == defined_cnt) // i 为遍历深度,当达到最深层或者已填充的格子数等于 81 时结束遍历(此时所有层遍历均返回) 52 return true; 53 const int r = unset_pts[i].first, c = unset_pts[i].second, defined_cnt_copy = defined_cnt;// 取出行列号和备份数据 54 auto snap_shot_bd = bd; 55 auto snap_shot_st = states; 56 bitset<10> & st = states[r][c]; 57 if (bd[r][c] != 0) // ?当前位置已经有数字了,尝试 58 return dfs(i + 1, unset_pts, bd, states); 59 for (int v = 1; v <= 9; ++v)// 尝试向bd[r][c] 中填入 v,候选的 v 经由 states[r][c] 即这里的 st 筛选 60 { 61 if (st[v]) 62 { 63 if (set(states, bd, r, c, v) && dfs(i + 1, unset_pts, bd, states))// 尝试填写 v 成功 64 return true; 65 bd = snap_shot_bd; // 还原 bd,states,defined_cnt,清洗掉更深入的遍历导致的写入 66 states = snap_shot_st; 67 defined_cnt = defined_cnt_copy; 68 } 69 } 70 return false; 71 } 72 void solveSudoku(vector<vector<char>>& board) 73 { 74 _2D_bit10 states; // 每个格点可能填充数字表 75 array<array<char, 9>, 9> bd; // 当前盘面 76 vector<pair<int, int>> unset_pts; // 所有空着的格子的行列号 77 for (int r = 0; r < 9; ++r) // 初始化 states,bd 78 { 79 for (int c = 0; c < 9; ++c) 80 { 81 states[r][c] = ORIGIN_STATE; // states 每个元素赋值为 1111111110 各位分别对应 9 ~ 0 82 bd[r][c] = (board[r][c] == '.' ? 0 : board[r][c] - '0'); // board(.123456789) → bd(0123456789) 83 } 84 } 85 for (int r = 0; r < 9; ++r) // 检查原盘面,若存在矛盾则抛出异常(assert(false);) 86 { 87 for (int c = 0; c < 9; ++c) 88 { 89 if (bd[r][c] != 0) 90 assert(set(states, bd, r, c, bd[r][c])); 91 } 92 } 93 if (defined_cnt == 9 * 9) // 已填充的格子数等于 81,数独已经完成 94 return; 95 for (int r = 0; r < 9; ++r) // 初始化 unset_pts 96 { 97 for (int c = 0; c < 9; ++c) 98 { 99 if (bd[r][c] == 0) 100 unset_pts.emplace_back(r, c); 101 } 102 } 103 104 auto cmp_pt = [states](const pair<int, int> &l, const pair<int, int> & r)// 用于排序的比较函数,按照每个格子可能填充数字的个数升序排列 105 { 106 return states[l.first][l.second].count() < states[r.first][r.second].count(); 107 }; 108 std::sort(unset_pts.begin(), unset_pts.end(), cmp_pt);// 对所有空着的格子进行排序,情况数较少的靠前 109 assert(dfs(0, unset_pts, bd, states)); // 尝试对空着的格子进行深度优先遍历,遍历失败(出现矛盾)则抛出异常 110 for (int r = 0; r < 9; ++r) // 取出 bd 的数据填写 board 111 { 112 for (int c = 0; c < 9; ++c) 113 board[r][c] = bd[r][c] + '0'; 114 } 115 return; 116 } 117 };