leetcode的题目,求出数独游戏的一个解。
之前记得求解 全排列和八皇后问题也是用的回溯法。
分治法可以用递归是很显然的,它们俩是天生一对;回溯法也可以。因为 试探求解 子问题。
下面代码是我用递归回溯求解数独的一个解,6个测试用例,184ms,在C++分布段上处在中间,还行吧,以后有时间再研究。
关键是学到了一种方便地表示动态二维数组的新方法。一维的动态数组 就用new[]、vector、deque。二维的呢?
1、旧方法,比如:
int* baseAddr=new int[3*2]; // 3 行 2 列
typedef int int2 [2];
int2 * p=(int2 *)baseAddr;
for(int i=0;i<3;i++)
{
for(int j=0;j<2;j++)
{
p[i][j]=i*2+j;
}
}
delete [] baseAddr;
2、新方法,比如这道求解数独的题目:
#include <vector>
using namespace std;
void solveSudoku(vector<vector<char> > &board);
bool tillNow_boardIsValid_Try_thisNumber( vector<vector<char> > & board, int i, int j, char c);
inline bool checkValid( vector<vector<char> > & board, int i, int j, char c );
int main()
{
char* sd[]={"..9748...","7........",".2.1.9...","..7...24.",".64.1.59.",".98...3..","...8.3.2.","........6","...2759.."};
vector<vector<char>> bd;
for(int i=0;i<9;i++)
bd.push_back(vector<char>(sd[i],sd[i]+9));
solveSudoku(bd);
return 0;
}
void solveSudoku(vector<vector<char> > &board) {
for(int i=0;i<9;i++)
{
for(int j=0;j<9;j++)
{
if(board[i][j]!='.') {
continue;
}
for (char c='1';c<='9';c++)
{
if(tillNow_boardIsValid_Try_thisNumber(board,i,j,c)) {
return;
}
}
return;
}
}
}
bool tillNow_boardIsValid_Try_thisNumber( vector<vector<char> > & board, int i, int j, char c)
{
// the board is left unmodified if it returns false.
// the board has become the answer when it returns true.
if(checkValid(board,i,j,c)==false) return false;
board[i][j]=c; // try
int ni=i;
int nj=j+1;
if(nj==9) {
ni++;
nj=0;
}
if(ni==9) {
return true;
}
for(;ni<9;ni++)
{
for(;nj<9;nj++)
{
if(board[ni][nj]!='.') {
continue;
}
for (char cc='1';cc<='9';cc++)
{
if(tillNow_boardIsValid_Try_thisNumber(board,ni,nj,cc)) {
return true;
}
}
board[i][j]='.'; // The attempt ( board[i][j]=c ) has failed
return false;
}
nj=0;
}
return true;
}
inline bool checkValid( vector<vector<char> > & board, int i, int j, char c )
{
for(int k=0;k<9;k++)
{
if(board[i][k]=='.') {
continue;
}else if(board[i][k]==c) {
return false;
}
}
for(int k=0;k<9;k++)
{
if(board[k][j]=='.') {
continue;
}else if(board[k][j]==c) {
return false;
}
}
int ROW=i/3*3;
int COLUMN=j/3*3;
for (int row=0;row<3;row++)
{
for (int column=0;column<3;column++)
{
if(board[ROW+row][COLUMN+column]=='.') {
continue;
}else if(board[ROW+row][COLUMN+column]==c) {
return false;
}
}
}
return true;
}
第一个关键 写递归程序,就是写出递推关系式 的过程,比如求斐波那契数Fn:
F1=1,F2=1,F3=2,F4=3,F5=5,……Fn=?
不想去求Fn关于n的数学函数或者迭代过程:Fn= Φ(n) or Fn={ … repeat a procedure …}
而是得到 Fn=F(n-1)+F(n-2)这样一个自我指涉的式子,于是它就自动地运转起来,不需要你用循环人为地推动它。把人脑从繁琐的程序执行流程中解放了出来。
递归不像循环,先不考虑其实现细节,先定义好 子问题,利用子问题的结果,设计求解原问题的算法,即得到递推关系式(把子问题看做已经完成的方案)。有的问题明显蕴含着递归表达式,比如阶乘在数学上的定义,子问题、递推关系式都是显然的。然而有的问题不这样显而易见,我们得自己定义 子问题,比如汉诺塔。基础是分治法,把原问题分解为子问题(意味着问题的规模更小,n 更小),求解子问题,合并结果。回溯法的子问题就是试探求解。如何求解子问题呢?哈,“不用求”。因为递归就是要找 F( n ) 与 F (n-1) 的关系等式,不是求出F(n)=Φ(n)。Φ ()代表某一确定的,已知的过程,不像F()是未知的过程。
第二个关键 明确定义好 F(n) or F(n-1) 的功能接口,一丁点模糊的地方都不行。F(n-1) 执行前是什么样,执行中改变了什么(执行后变成什么样),必须完全确定下来。不过往往不是一步到位的,要看“利用子问题的结果设计求解原问题的算法”时的需求。定义F(n-1) 的功能接口与利用F(n-1),设计求解F(n)的算法两者是相关的(因为F(n) F(n-1) 要有相同的接口)。我一般按所问定义F(n-1) 的接口,利用它定义的F(n-1)所具有的功能,设计求解F(n)的算法,即去实现F(n)的接口。然后你就知道子问题以及子问题的接口定义的合不合适。