【leetcode】sudokuSolver数独解题

0.摘要

小时候在报纸上玩过数独,那时候觉得很难,前几天在leetcode上遇到了这个题,挺有意思于是记录下来

一般一道数独题,就像他给的例子这样,9*9的格子,满足 行,列 ,宫均取1-9的数,切互不相同。

   

那一般正常人的思路会去一点一点的推理,至少我小时候就是这么玩的,具体来说,好比 r7c9(第7行,第9列)的空格,我会找第7行有『6,2,8』,第9列有『3,1,6,5,9』,第9宫有『2,8,5,7,9』,这些的并集就是『1,2,3,5,6,7,8,9』,哦那么空格是4。就这么一点点继续往下推理。

1.余数法

他给的函数接口是这样

void solveSudoku(vector<vector<char>>& board){}

然后我就照着我小时候的思路写了一个版本

 1 void solveSudoku(vector<vector<char>>& board) {
 2     if(board.size()!=9||board[0].size()!=9)
 3         return;
 4     vector<vector<vector<bool>>> ex(9,vector<vector<bool>>(9,vector<bool>(9,false)));
 5     vector<vector<int>> count(9,vector<int>(9,0));
 6     queue<int> se;
 7     for(int i=0;i<9;i++)
 8     {
 9         for(int j=0;j<9;j++)
10             if(board[i][j]=='.')
11             {                    
12                 int block_i = i/3;
13                 int block_j = j/3;
14                 for(int c=0;c<9;c++)
15                 {
16                      if(board[i][c]!='.'&&!ex[i][j][board[i][c]-'1'])
17                     {
18                         ex[i][j][board[i][c]-'1'] = true;
19                         count[i][j]++;
20                     }
21                     if(board[c][j]!='.'&&!ex[i][j][board[c][j]-'1'])
22                     {
23                         ex[i][j][board[c][j]-'1'] = true;
24                         count[i][j]++;
25                     }
26                     int ii = block_i*3 + c/3;
27                     int jj = block_j*3 + c%3;
28                     if(board[ii][jj]!='.'&&!ex[i][j][board[ii][jj]-'1'])
29                     {
30                         ex[i][j][board[ii][jj]-'1'] = true;
31                         count[i][j]++;
32                     }
33                 }
34                 if(count[i][j]==8)
35                     se.push(i*9+j);
36             }
37     }
38     while(!se.empty())
39     {
40         int cur = se.front();
41         se.pop();
42         int i = cur/9;
43         int j = cur%9;
44         int block_i = i/3;
45         int block_j = j/3;
46         for(int c=0;c<9;c++)
47             if(!ex[i][j][c])
48             {
49                 board[i][j] = c + '1';
50                 break;
51             }
52         for(int c=0;c<9;c++)
53         {
54             if(board[i][c]=='.'&&!ex[i][c][board[i][j]-'1'])
55             {
56                 ex[i][c][board[i][j]-'1'] = true;
57                 count[i][c]++;
58                 if(count[i][c]==8)
59                     se.push(i*9+c);
60             }
61             if(board[c][j]=='.'&&!ex[c][j][board[i][j]-'1'])
62             {
63                 ex[c][j][board[i][j]-'1'] = true;
64                 count[c][j]++;
65                 if(count[c][j]==8)
66                     se.push(c*9+j);
67             }
68             int ii = block_i*3 + c/3;
69             int jj = block_j*3 + c%3;
70             if(board[ii][jj]=='.'&&!ex[ii][jj][board[i][j]-'1'])
71             {
72                 ex[ii][jj][board[i][j]-'1'] = true;
73                 count[ii][jj]++;
74                 if(count[ii][jj]==8)
75                     se.push(ii*9+jj);
76             }
77         }
78     }
79 }

这里ex是9*9*9的数组,对于非空格的位置ex没有意义,对于ricj的空格(例如r7c9的空格),ex[i][j][9]是一个bool[9]的数组,分别代表跟ricj相关的20个格子(行列宫一共20格)是否包含x {x=1...9};如果包含x,那么ex[i][j][x-1]就是true(例如ex[7][9][0,1,2,4,5,6,7,8]为true,ex[7][9][3]为false),同时为了方便建立一个count[9][9]记录true的个数,count[i][j]记录ex[i][j]中true的个数,一旦count[i][j]==8,那么这个格子就可以推理出来。

那么刚开始先对整个数组扫描一遍,分别记录一遍ex和count,找到那些count==8的,放入一个队列。然后获得队列的对首ricj,把他的值填入(例如r7c9填”4“),同时找到与r7c9相关20个格子中是空格的位置,更新他们的ex和count(例如r1c9的ex[0][8][4-1]改为true),把count==8的push到对尾,如此往复,直到队列为空。

我当时的想法时队列空了应该就能解出来了吧。。。于是submit了,结果过了两个case,还有的case报错了。。。

 

咦?没解完。。解了的都对了。。我把剩下的手抄了一下,发现确实解不了,不是程序问题,原来我小时候一直解不出来是有原因的,不是我眼神不好,方法有问题。遂百度了一下,原来我的方法叫做”余数法“。余数法求解不了所有的数独问题,难的需要假设来推到出矛盾。但怎么假设好呢,也百度了一下。

2.递归+回溯

网上说的最多的方法,主要还是递归+回溯 暴力求解。

 1 int row[9][9] ;
 2 int col[9][9] ;
 3 int block[9][9] ;
 4 
 5 void solveSudoku(vector<vector<char>>& board) {
 6     if(board.size()!=9||board[0].size()!=9)
 7         return;
 8     memset(row, 0, sizeof(row));
 9     memset(col, 0, sizeof(col));
10     memset(block, 0, sizeof(block));
11     //memset(ex, 0, sizeof(ex));
12     //memset(count, 0, sizeof(count));
13     //推导
14     //forward(board);
15     //假设
16     for(int i=0;i<9;i++)
17         for(int j=0;j<9;j++)
18             if(board[i][j]!='.')
19             {
20                 row[i][board[i][j]-'1']=1;
21                 col[j][board[i][j]-'1']=1;
22                 block[(i/3)*3+j/3][board[i][j]-'1']=1;
23             }
24     assume(board,0,0);
25 }
26 
27 bool assume(vector<vector<char>>& board,int i,int j)
28 {
29     if(i==9)
30         return true;
31     if(board[i][j] != '.')
32         return assume(board,i+(j+1)/9,(j+1)%9);
33     else
34         for(int c=0;c<9;c++)
35         {
36             if(!row[i][c]&&!col[j][c]&&!block[i/3*3+j/3][c])
37             {
38                 board[i][j] = c+'1';
39                 row[i][c] = col[j][c] = block[i/3*3+j/3][c] = 1;
40                 if(assume(board,i+(j+1)/9,(j+1)%9))
41                     return true;
42                 board[i][j] = '.';
43                 row[i][c] = col[j][c] = block[i/3*3+j/3][c] = 0;
44             }
45         }
46         return false;
47 }

这种方法是从另外一个角度记录当前的数独数组的情况,维持3个bool类型的数组row[9][9],col[9][9],block[9][9],这里为了初始化memset方便设成了int型。row[i][j]的含义是第i行是否含有j(例如初始时r[1-1][5-1]为真,r[1-1][2-1]为假,第一行有5没有2),col,block同理。assume是递归函数,每次遇到空格就对他从i = 1开始假设,如果他所在的行,列,宫都没有i那就设他为i,继续递归往后填写,遇到矛盾(某个空格不能取1-9之间任何数)就返回。这样做就是所谓的暴力求解,这么做肯定是没问题了,可以求解出正确结果。提交,ac了,4ms,打败了83%的人。。。

 

3.预处理+递归+回溯

但是我想,递归的复杂度和剩余格子的总数有指数关系,直接递归有点浪费时间,何尝不先用余数法给”预处理“一下呢,减少递归次数。。于是,两种方法一起,哦了

  1 int row[9][9] ;
  2 int col[9][9] ;
  3 int block[9][9] ;
  4 void solveSudoku(vector<vector<char>>& board) {
  5     if(board.size()!=9||board[0].size()!=9)
  6         return;
  7     memset(row, 0, sizeof(row));
  8     memset(col, 0, sizeof(col));
  9     memset(block, 0, sizeof(block));
 10     //推导 derivation 对于九宫格中可能性唯一的数 直接求解 减少递归次数
 11     //余数法
 12     forward(board);
 13     for(int i=0;i<9;i++)
 14         for(int j=0;j<9;j++)
 15             if(board[i][j]!='.')
 16             {
 17                 row[i][board[i][j]-'1']=1;
 18                 col[j][board[i][j]-'1']=1;
 19                 block[(i/3)*3+j/3][board[i][j]-'1']=1;
 20             }
 21     //假设 assume 对于可能性不唯一的数 递归假设求解
 22     assume(board,0,0);
 23 }
 24 
 25 bool assume(vector<vector<char>>& board,int i,int j)
 26 {
 27     if(i==9)
 28         return true;
 29     if(board[i][j] != '.')
 30         return assume(board,i+(j+1)/9,(j+1)%9);
 31     else
 32         for(int c=0;c<9;c++)
 33         {
 34             if(!row[i][c]&&!col[j][c]&&!block[i/3*3+j/3][c])
 35             {
 36                 board[i][j] = c+'1';
 37                 row[i][c] = col[j][c] = block[i/3*3+j/3][c] = 1;
 38                 if(assume(board,i+(j+1)/9,(j+1)%9))
 39                     return true;
 40                 board[i][j] = '.';
 41                 row[i][c] = col[j][c] = block[i/3*3+j/3][c] = 0;
 42             }
 43         }
 44         return false;
 45 }
 46 void forward(vector<vector<char>>& board)
 47 {
 48     bool ex[9][9][9] ;
 49     int count[9][9] ;
 50     memset(ex, 0, sizeof(ex));
 51     memset(count, 0, sizeof(count));
 52     queue<int> se;
 53     //求解所有可能性唯一的
 54     //get all results with only one possible answer
 55     for(int i=0;i<9;i++)
 56         for(int j=0;j<9;j++)
 57             if(board[i][j]=='.')
 58             {                    
 59                 for(int c=0;c<9;c++)
 60                 {
 61                      if(board[i][c]!='.'&&!ex[i][j][board[i][c]-'1'])
 62                     {
 63                         ex[i][j][board[i][c]-'1'] = true;
 64                         count[i][j]++;
 65                     }
 66                     if(board[c][j]!='.'&&!ex[i][j][board[c][j]-'1'])
 67                     {
 68                         ex[i][j][board[c][j]-'1'] = true;
 69                         count[i][j]++;
 70                     }
 71                     int ii = (i/3)*3 + c/3;
 72                     int jj = (j/3)*3 + c%3;
 73                     if(board[ii][jj]!='.'&&!ex[i][j][board[ii][jj]-'1'])
 74                     {
 75                         ex[i][j][board[ii][jj]-'1'] = true;
 76                         count[i][j]++;
 77                     }
 78                 }
 79                 //答案唯一的 push到队列
 80                 if(count[i][j]==8)
 81                     se.push(i*9+j);
 82             }
 83     while(!se.empty())
 84     {
 85         int cur = se.front();
 86         se.pop();
 87         int i = cur/9;
 88         int j = cur%9;
 89         for(int c=0;c<9;c++)
 90             if(!ex[i][j][c])
 91             {
 92                 board[i][j] = c + '1';
 93                 break;
 94             }
 95         for(int c=0;c<9;c++)
 96         {
 97             if(board[i][c]=='.'&&!ex[i][c][board[i][j]-'1'])
 98             {
 99                 ex[i][c][board[i][j]-'1'] = true;
100                 count[i][c]++;
101                 if(count[i][c]==8)
102                     se.push(i*9+c);
103             }
104             if(board[c][j]=='.'&&!ex[c][j][board[i][j]-'1'])
105             {
106                 ex[c][j][board[i][j]-'1'] = true;
107                 count[c][j]++;
108                 if(count[c][j]==8)
109                     se.push(c*9+j);
110             }
111             int ii = (i/3)*3 + c/3;
112             int jj = (j/3)*3 + c%3;
113             if(board[ii][jj]=='.'&&!ex[ii][jj][board[i][j]-'1'])
114             {
115                 ex[ii][jj][board[i][j]-'1'] = true;
116                 count[ii][jj]++;
117                 if(count[ii][jj]==8)
118                     se.push(ii*9+jj);
119             }
120         }
121     }
122 }
0ms,ac。

总结就是,余数法推到减少假设次数 + 递归假设求解子问题 。因为问题规模固定是9*9,因此损失的空间复杂度也能接受。

 写了这个着实激发了我很大的兴趣,于是后面又写了生成题库的模块,图形界面的模块。。。

posted on 2015-09-05 20:46  冷豆东  阅读(468)  评论(0编辑  收藏  举报

导航