回溯演算法(Backtracking)說明與例項
定義
回溯演算法(Backtracking)在很多場景中會使用,如N皇后,數迷,集合等,其是暴力求解的一種優化。參考https://en.wikipedia.org/wiki/Backtracking 中的說明,定義如下:
Backtracking is a general algorithm for finding all (or some) solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons each partial candidate c (“backtracks”) as soon as it determines that c cannot possibly be completed to a valid solution
從上文中可以得出, 核心的含義是 回溯演算法是通過一步一步(通常是用遞迴)構建可能”解”,並且回溯不可能”解”來求所有或者部分解決方案的通用演算法。其中“回溯”的具體意思就是將不可能解或者部分解的候選儘早的捨棄掉,“解”需要滿足一定的限制條件(constraint satisfaction)
通用演算法
ALGORITHM try(v1,...,vi) // 這裡的V1.....V2攜帶的引數說明 “可能解”
// 入口處驗證是否是全域性解,如果是,直接返回。
// 實際程式設計中也需要檢視是否是無效解,如果是,也是直接返回
IF (v1,...,vi) is a solution THEN RETURN (v1,... ,vi)
FOR each v DO // 對於每一個可能的解,進行檢視
// 下面的含義是形成一個可能解 進行遞迴
IF (v1,...,vi,v) is acceptable vector THEN
sol = try(v1,...,vi,v)
IF sol != () THEN RETURN sol
// 這個地方其實需要增加“回溯” 處理,實際程式設計中通常是函式引數的變化
END
END
RETURN ()
經典演算法
- N皇后問題
這個是一個比較經典的問題, 意思是將N個“皇后”放在N*N的棋盤上,每個皇后不能再同一列,同一行,和斜對角 (這個就是限制條件constraint satisfaction)。 下面是回溯演算法的需求所有的可能解。
演算法基本的步驟思想為:
1)從第一行開始
2 )如果所有的皇后已經放置完成, 生成解,並且返回true
3)嘗試當前行的所有列,如果當前行與列是合法的
3.1 修改棋盤讓其成為部分解,
3.2 然後遞迴檢視(主要是2, 3,4)該解是否合法
3.3 Backtrack 棋盤進行回溯
4) 如果上述所有的組合都為非法,返回false
// 尋找N皇后問題的可能解, 我們用 '.' 表示不放置皇后,用'Q'表示放置皇后
// 利用vector<string> 表示一個解決方案, vector<vector<string>> 表示
// 所有的解決方案。
// 在遞迴初,我們可以生成一個待解的棋盤
vector<vector<string>> solveNQueens(int n) {
string tmp (n, '.');
//生成一個N*N待解的棋盤,沒有任何皇后
vector<string> broad (n, tmp);
nQueue = n;
vector<vector<string>> ans;
solveNQueensHelper (ans, broad, 0, nQueue );
return ans;
}
// 回溯演算法的遞迴函式,
bool solveNQueensHelper (vector<vector<string>>& ans, vector<string> &broad, int row, int nQueue)
{
// 如果當前的行數大於或者等於皇后數,說明當前棋盤是一個解
// 直接返回
if(row >= nQueue){
ans.push_back(broad);
return true;
}
// 從當前行的列中選取一個可能解
for (int column = 0; column < nQueue; column++){
// 檢視一下,當前可能解是否有效,只有有效,才可能繼續遞迴
if(isOk (broad, row, column))
{
// 有效,修改可能解的棋盤
broad[row][column] = 'Q';
// 遞迴呼叫是否可能解
if (solveNQueensHelper (ans, broad, row + 1, nQueue)){
// return true;
}
// 回溯, 去生成其他解
broad[row][column] = '.';
}
}
return false;
}
// 檢視當前部分解是否有效
bool isOk (const vector<string> &broad, int row, int column){
// 檢視
for (int i = 0; i <= row; i++)
{
if(broad [i] [column] == 'Q'){
return false;
}
}
int tmpRow = row;
int tmpColumn = column;
while (tmpRow >= 0 && tmpColumn >= 0)
{
if(broad [tmpRow] [tmpColumn] == 'Q'){
return false;
}
tmpRow--,
tmpColumn--;
}
tmpRow = row;
tmpColumn = column;
while (tmpRow >= 0 && tmpColumn < nQueue)
{
if(broad [tmpRow] [tmpColumn] == 'Q'){
return false;
}
tmpRow--,
tmpColumn++;
}
return true;
}
};
其要求,1-9的數不能出現在同一列,同一行,或者同一個3*3的格子中。
演算法思想如下:
1 )找到一個空格子,如果找不到, 說明已經填滿,這是一個解,返回true
2 )從1 - 9 的數字中填上找到的空格子,如果合法
2.1 生成部分解,遞迴呼叫 1)2) 3)
2.2 如果遞迴呼叫返回true,說明合法
2.3 回溯部分解
3)上述都不成,直接返回false
c++的程式碼實現如下:
// board 是一個9*9的格子,如果空格子,用'.' 表示,否則為1-9的數字
void solveSudoku(vector<vector<char>>& board) {
// 遞迴呼叫該函式
helperSolveSudoku (board);
}
bool helperSolveSudoku(vector<vector<char>>& board) {
int row = 0;
int colum = 0;
// step1, 獲取一個空格, 如果沒找到,說明已經填滿。
if ( !GetNextUnsignedOne (board, row, colum) )
return true;
// Step2: 從1-9 分別填上棋盤
for (char num = '1'; num <= '9'; num++ ){
// 檢視當前的數字放入棋盤中是否合法
if (isNumOk (board, row, colum, num) ){
// step2.1 生成部分解,遞迴呼叫
board[row][colum] = num;
if (helperSolveSudoku (board) )
{
// 如果解合法,直接返回
return true;
}
// 回溯部分解
board[row][colum] = '.';
}
}
// step3 沒有解,直接返回false
return false;
}
// 幫助函式以獲取一個可能的空位置
bool GetNextUnsignedOne(const vector<vector<char>>& board, int &row, int& column){
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j++){
if (board [i] [j] == '.'){
row = i;
column = j;
return true;
}
}
}
return false;
}
// 幫助函式以驗證數字在當前是否合法
bool isNumOk (const vector<vector<char>>& board, int row, int column, char num){
// check the row is ok or not
for(int i = 0; i < 9; i++){
if(board [row] [i] == num){
return false;
}
}
// check the colum is ok or not
for(int i = 0; i < 9; i++){
if(board [i] [column] == num){
return false;
}
}
int startRow = row - row % 3;
int startcolumn = column - column %3;
// check the 3*3 inbox is ok for not.
for (int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++)
{
if(board [startRow + i] [startcolumn + j] == num){
return false;
}
}
}
return true;
}
- 集合問題
有一個數組,產生一個集合,該集合能包含所有的元素,如 【1,2,3】陣列可以滿足的集合就是【1,2,3】【1,3,2】【2,1,3】【2,3,1】【3,1,2】【3,2,1】.
這個也可以用回溯的方式實現
基本演算法思想為:
1) 檢視當前的集合,是否滿足所有元素的集合,如果滿足,返回
2) 從陣列挑選一個元素,檢視當前的元素是否屬於部分解
2.1 如果不屬於部分解,將元素加入到部分解
2.2 遞迴呼叫 1, 2, 3
2.3 回溯該元素
3 陣列元素完成,直接返回
具體實現的C++程式碼如下:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ans;
vector<int> tmpRes;
permuteHelper (ans, tmpRes, nums);
return ans;
}
// 幫助函式以實現資料集合功能
// ans 表示幾個陣列
// tmpRes 表示部分解
// nums 表示的當前陣列元素個數
void permuteHelper(vector<vector<int>>& ans, vector<int>&tmpRes, vector<int>& nums){
// step1 如果部分解的元素個數和數字的元素相同,直接插入
if(tmpRes.size () == nums.size ()){
ans.push_back (tmpRes);
return;
}
//step2 從集合中選定一個元素
for(int i = 0; i < nums.size(); i++)
{
if(std::find (tmpRes.begin(), tmpRes.end(), nums [i]) == tmpRes.end()){
//step 2.1 如果部分解不包含該元素,
// 其實也可以用一個set<int> 來判定是否含有該元素。
tmpRes.push_back (nums [i]);
//step 2.2 遞迴呼叫
permuteHelper (ans, tmpRes, nums);
// 回溯部分解
tmpRes.pop_back();
}
else{
continue;
}
}
// Step3,完成,直接返回
return;
}