1. 程式人生 > 實用技巧 >回溯演算法

回溯演算法

一、什麼是回溯演算法

回溯演算法實際上一個類似列舉的搜尋嘗試過程,主要是在搜尋嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。許多複雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。

回溯演算法實際上一個類似列舉的深度優先搜尋嘗試過程,主要是在搜尋嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回(也就是遞迴返回),嘗試別的路徑。

二、回溯演算法思想

回溯法一般都用在要給出多個可以實現最終條件的解的最終形式。回溯法要求對解要新增一些約束條件。總的來說,如果要解決一個回溯法的問題,通常要確定三個元素:

1、選擇。對於每個特定的解,肯定是由一步步構建而來的,而每一步怎麼構建,肯定都是有限個選擇,要怎麼選擇,這個要知道;同時,在程式設計時候要定下,優先或合法的每一步選擇的順序,一般是通過多個if或者for迴圈來排列。

2、條件。對於每個特定的解的某一步,他必然要符合某個解要求符合的條件,如果不符合條件,就要回溯,其實回溯也就是遞迴呼叫的返回。

3、結束。當到達一個特定結束條件時候,就認為這個一步步構建的解是符合要求的解了。把解存下來或者打印出來。對於這一步來說,有時候也可以另外寫一個issolution函式來進行判斷。注意,當到達第三步後,有時候還需要構建一個數據結構,把符合要求的解存起來,便於當得到所有解後,把解空間輸出來。這個資料結構必須是全域性的,作為引數之一傳遞給遞迴函式。

三、遞迴函式的引數的選擇,要遵循四個原則

1、必須要有一個臨時變數(可以就直接傳遞一個字面量或者常量進去)傳遞不完整的解,因為每一步選擇後,暫時還沒構成完整的解,這個時候這個選擇的不完整解,也要想辦法傳遞給遞迴函式。也就是,把每次遞迴的不同情況傳遞給遞迴呼叫的函式。

2、可以有一個全域性變數,用來儲存完整的每個解,一般是個集合容器(也不一定要有這樣一個變數,因為每次符合結束條件,不完整解就是完整解了,直接列印即可)。

3、最重要的一點,一定要在引數設計中,可以得到結束條件。一個選擇是可以傳遞一個量n,也許是陣列的長度,也許是數量,等等。

4、要保證遞迴函式返回後,狀態可以恢復到遞迴前,以此達到真正回溯。

四、例題

N皇后

難度:困難
n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。

上圖為 8 皇后問題的一種解法。
給定一個整數 n,返回所有不同的 n 皇后問題的解決方案。
每一種解法包含一個明確的 n 皇后問題的棋子放置方案,該方案中 ‘Q’ 和 ‘.’ 分別代表了皇后和空位。
示例:
輸入: 4
輸出:[
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],

["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]
解釋: 4 皇后問題存在兩個不同的解法。
提示:
皇后,是國際象棋中的棋子,意味著國王的妻子。皇后只做一件事,那就是“吃子”。當她遇見可以吃的棋子時,就迅速衝上去吃掉棋子。
當然,她橫、豎、斜都可走一到七步,可進可退。(引用自 百度百科 - 皇后 )
來源:新增連結描述
程式碼思路:
這道題是經典的回溯題,開始我們先要考慮一下怎麼生成棋盤並且將“皇后”放入其中,我們可以用二維陣列來建立起一個棋盤mark,之後將陣列中的每個元素設定成為0來代表是空位,之後用1來代表放置的皇后和皇后所能吃子的範圍(就是不能再放入新皇后的位置)之後我們用方向陣列的方法來將棋盤上不能放皇后的位置改成1,這樣棋盤的函式就寫好了。
接下來我們再建立一個和棋盤mark類似的陣列location來儲存皇后的位置,建立方式和棋盤一樣,只不過是用字串的形式,在沒有皇后的位置用‘.'來表示,用’Q‘來表示皇后的位置。
最後開始建立遞迴函式,因為每一行最多隻能有一個皇后,那麼我們就每次遞迴一行棋盤來考慮,每一行只要mark位置上是0就可以放皇后,之後每次遞迴到下一行,在遞迴之前用一個臨時棋盤來儲存現在的棋盤用來之後的回溯用,當我們遞迴到某行的時候發現沒有皇后的位置可以放置了,那麼我就要回溯了,返回到沒有放皇后的時候,之後換成另一個位置來放置皇后的位置,經過這樣的遞迴一直到n行都放置了皇后就可以結束遞迴了。

#include<vector>
#include<algorithm>
#include<cstring>
#include<string>
#include<iostream>
using namespace std;
class Solution {
public:
    vector<vector<string> >solveNQueens(int n)
    {
        vector<vector<string> >result;
        vector<vector<int> >mark;
        vector<string>location;
        for (int i = 0;i < n;++i)
        {
            mark.push_back((vector<int>()));
            for (int j = 0;j < n;++j)
            {
                mark[i].push_back(0);
            }
            location.push_back("");
            location[i].append(n, '.');
        }
        generate(0, n, location, result, mark);
        return result;
    }
private:
    void put_down_the_queen(int x, int y,//棋盤函式
        vector<vector<int> >& mark)
    {
        static const int dx[] = { -1,1,0,0,-1,-1,1,1 };
        static const int dy[] = { 0,0,-1,1,-1,1,-1,1 };
        mark[x][y] = 1;
        for (int i = 1;i < mark.size();++i)
        {
            for (int j = 0;j < 8;++j)
            {
                int new_x = x + i * dx[j];
                int new_y = y + i * dy[j];
                if (new_x >= 0 && new_x < mark.size()
                    && new_y >= 0 && new_y < mark.size())
                    mark[new_x][new_y] = 1;
            }
        }
    }
    void generate(int k, int n, vector<string>& location,//遞迴函式
        vector<vector<string> >& result, vector<vector<int> >& mark)
    {
        if (k == n)
        {
            result.push_back(location);
            return;
        }
        for (int i = 0;i < n;++i)
        {
            if (mark[k][i] == 0)
            {
                vector<vector<int> >tmp_mark = mark;
                location[k][i] = 'Q';
                put_down_the_queen(k, i, mark);
                generate(k + 1, n, location, result, mark);
                mark = tmp_mark;
                location[k][i] = '.';
            }
        }
    }
};

int main()
{
     //測試案例
    vector<vector<string> >result;
    Solution solve;
    result = solve.solveNQueens(4);
    for (int i = 0;i < result.size();++i)
    {
        cout << "i = " << i<<endl;
        for (int j = 0;j < result[i].size();j++)
        {
            cout << result[i][j].c_str()<<endl;
        }
        cout << endl;
    }
}

例二:

78. 子集

給定一組不含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例:
輸入:nums = [1,2,3]
輸出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
分析思路:

給定一個集合比如{1,2,3},求該集合的所有子集。
對於集合中的每一個元素,在某一子集中只有兩種狀態,要麼在子集中,要麼不在子集中。


因此對於一個含有n個元素的集合來說,對其中的某一個元素i,用xi來表示其在某一子集中的狀態,xi=1表示在子集中,xi=0表示不在子集中,因此,解可以表示為:
{x1,x2,x3,x4……xn};一共有2^n個向量。那麼可以寫程式碼如下.

#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
#include<string>
#include<queue>
using namespace std;
void function(int i, vector<int>& num, vector<int>& item, vector<vector<int> >& result)
{
    if (i == num.size()) return;
        item.push_back(num[i]);
        result.push_back(item);
        function(i+1, num, item, result);
        item.pop_back();
        function(i + 1, num, item, result);
}
int main()
{ 
    vector<int>num;
    num.push_back(1);
    num.push_back(2);
    num.push_back(3);
    vector<int>item;
    vector<vector<int> >result;
    function(0, num, item, result);
    for (int i = 0;i < result.size();++i)
    {
        for (int j = 0;j < result[i].size();++j)
        {
            cout << "[" << result[i][j] << "]";
        }
        cout << endl;
    }
}

另外一種解題方法(位運算):
對於集合{A,B,C}來說,可將每元素轉還成二進位制的100、010、001三個數,之後對於只有三個元素的集合來說,他的子集個數是2^3個,轉化成二進位制就是000、001、010、011、100、101、110、111八個數,之後對於每個元素是否出行只要用&運算出是否為真便可。

#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
#include<string>
#include<queue>
using namespace std;
vector<vector<int> > function(vector<int>& v)
{
    vector<vector<int> > result;
    int all = 1 << v.size();
    for (int i = 0;i < all;++i)
    {
        vector<int> item;
        for (int j = 0;j < v.size();++j)
        {
            if (i & (1 << j)) item.push_back(v[j]);
        }
        result.push_back(item); 
    }
    return result;
}
int main()
{ 
    vector<int>num;
    num.push_back(1);
    num.push_back(2);
    num.push_back(3);
    vector<int>item;
    vector<vector<int> >result=function(num);
    for (int i = 0;i < result.size();++i)
    {
        for (int j = 0;j < result[i].size();++j)
        {
            cout << "[" << result[i][j] << "]";
        }
        cout << endl;
    }
}