回溯法,采用试错的思想,分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确解答的时候,就会取消上一步或者上几步的运算,再通过其他的可能分步解答再次尝试寻找问题的答案。最经典的问题,就是八皇后问题。
1 n皇后问题
n-皇后 问题就是正确的在棋盘上面放置皇后的位置,从而使得任意两个皇后之间都无法攻击对方,攻击的方式是同行、同列或对角线。
给定 n, 要求返回n-皇后问题的所有解。
Each solution contains a distinct board configuration of the n-queens' placement, where 'Q'
and '.'
both indicate a queen and an empty space respectively.
举例,对于n为4的情况,存在如下这些解
[ [".Q..", // Solution 1 "...Q", "Q...", "..Q."], ["..Q.", // Solution 2 "Q...", "...Q", ".Q.."] ]
首先我们需要一个棋盘用来保存当前的状态,也就是那里已经放置了皇后,从而在进入新的一行的时候进行判断。棋盘状态可以用n*n的矩阵保存,但是也可以使用1*n的数组,数组的下标表示棋盘的行数,数组的值便是棋盘的列数,这样就充分的表示了哪行哪列可以放置皇后。算法的结构如下:
1 初始化棋盘数组
2 行数i: 1->n
列j: 1->n
对于每一个位置(i,j),检测是否可以放皇后,检测的原则是当前的列数不与之前放置的皇后的列数相等,同时也不能处于其对角线上,这里要注意的是,
如果在对角线上,就要满足如下性质:|i - row| == |j - col|
如果不能放皇后,就列数后移一位。
如果可以放皇后,就更新棋盘数组,同时行数下移一位。
如果第i行,没有找到放置皇后的位置。
如果i ==0, 说明已经回溯到第一行了,找到了所有的可能的解,退出。
如果i != 0, 那就把上一行的皇后位置向后移动一列,然后继续在这一行寻找合法的皇后位置。
如果行数到达了最后一行,说明一个解已经找到,回溯,寻找另外一个解。
这里首先给出一个非递归的解法:
vector<vector<string> > solveNQueens(int n) { int* a = new int[n]; //初始化 for(int k = 0 ; k < n ; k++) a[k] = -100; int j = 0,i = 0; vector<vector<string>> result; while(i < n) { while(j < n) { if(valid(i,j,a)) { a[i] = j; j = 0;//mark break; } else { j++; } } //如果第i行没有找到可以放置皇后的位置 if(a[i] == -100) { // 回溯到第一行,还是没找到可行的解,那就说明都遍历了 if(i == 0) break; // 否则的话回溯,并把上一行的皇后清空,再往后移动一位 else { --i; j = a[i] + 1; a[i] = -100; continue; } } //这个时候已经找到了全部需要的皇后 if(i == n -1) { pushIn(result,a,n); //但是由于还没完全返回全部的组合,所以还要继续找 j = a[i] + 1; a[i] = -100; continue; } i++; } return result; }
判断是否合法的函数:
bool valid(int row, int col, int *a) { for(int i = 0; i < row; i++)//a中之前的行 { if(a[i] == col || abs(row - i) == abs(col - a[i])) { return false; } } return true; }
把结果装入:
void pushIn(vector<vector<string>> &result, int *a,int n) { vector<string> tmp1; for(int i = 0; i < n; i++) { string tmp2; for(int j = 0; j < n ; j++) { if(a[i] == j) { tmp2+="Q"; } else tmp2+="."; } tmp1.push_back(tmp2); } result.push_back(tmp1); }
再给出一个递归的解法:
void queen(int row, int *a, int n,vector<vector<string>> &result) { if (n == row) //如果已经找到结果,则打印结果 pushIn(result,a,n); else { for (int k=0; k < n; k++) { //试探第row行每一个列 if (valid(row, k,a)) { a[row] = k; //放置皇后 queen(row + 1,a,n,result); //继续探测下一行 //返回上一级的时候要重新初始化, a[row] = -100; } } } }
vector<vector<string> > solveNQueens(int n) { // Start typing your C/C++ solution below // DO NOT write int main() function int* a = new int[n]; //初始化 for(int k = 0 ; k < n ; k++) a[k] = -100; vector<vector<string>> result; queen(0,a,n,result); return result; }
2 Unique Path独一无二的路
一个机器人处在一个m*n的方格区的左上角。机器人只能向下或者向右走。机器人的最终目的是右下角。求出一共有多少可能的独一无二的路径?
Note: m and n will be at most 100.
由于每次都只能向下或向右走,所以当前节点i,j 处的路径数应该等于其上部的节点i-1,j 以及其左部的节点i,j-1 的路径数之和。
path(i,j) = path(i-1,j) + path(i,j-1);
先给出一个比较直观的递归的解法:
int backtrack(int r, int c, int m, int n) { if (r == m && c == n) return 1; if (r > m || c > n) return 0; return backtrack(r+1, c, m, n) + backtrack(r, c+1, m, n);}
但是递归的话因为会有一些重复计算,所以效率不是太高。所以需要引入动态规划,保存每一个节点处的路径,从而避免重复计算,提升算法效率。
const int M_MAX = 100;const int N_MAX = 100; int backtrack(int r, int c, int m, int n, int mat[][N_MAX+2]) { if (r == m && c == n) return 1; if (r > m || c > n) return 0; //只要当没有计算的时候,才进行运算,否则的话就直接跳过。两个方向都计算了之后,再把结果给加起来。 if (mat[r+1][c] == -1) mat[r+1][c] = backtrack(r+1, c, m, n, mat); if (mat[r][c+1] == -1) mat[r][c+1] = backtrack(r, c+1, m, n, mat); return mat[r+1][c] + mat[r][c+1];} int bt(int m, int n) { int mat[M_MAX+2][N_MAX+2]; for (int i = 0; i < M_MAX+2; i++) { for (int j = 0; j < N_MAX+2; j++) { mat[i][j] = -1; } } return backtrack(1, 1, m, n, mat);}
3 Subsets 求出所有的子集
给定一系列的数的集合S, 返回所有可能的子集:
注意:
- 子集中的元素一定要是非递减的。
- 结果中不能包含重复的子集。
举例,
If S = [1,2,3]
, a solution is:
[ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
void subsetsAll(vector<vector<int>> & ans, vector<int> tmp,vector<int> &S, int n,int k,int index) { if(k == 0) { if(find(ans.begin(),ans.end(),tmp) == ans.end()) ans.push_back(tmp); } else if(k > 0 && index < n) { tmp.push_back(S.at(index)); subsetsAll(ans,tmp,S,n,k-1,index+1);//已经找到了第一个,在之后里面寻找k-1个。 tmp.pop_back(); subsetsAll(ans,tmp,S,n,k,index+1); //跳过第一个,在之后的里面寻找k个。 } }
主程序:只要把循环改成确定的k,就变成了(n,k)问题。
vector<vector<int> > subsets(vector<int> &S) { // Start typing your C/C++ solution below // DO NOT write int main() function sort(S.begin(),S.end()); int n = S.size(); vector<int> tmp; vector<vector<int>> ans; if(n==0) {ans.push_back(tmp); return ans;} for(int i=0;i<=n;i++) { subsetsAll(ans,tmp,S,n,i,0); } }