回溯算法 DFS深度優先搜索 (遞歸與非遞歸實現)


回溯法是一種選優搜索法(試探法),被稱爲通用的解題方法,這種方法適用於解一些組合數至關大的問題。經過剪枝(約束+限界)能夠大幅減小解決問題的計算量(搜索量)。html

基本思想

將n元問題P的狀態空間E表示成一棵高爲n的帶權有序樹T,把在E中求問題P的解轉化爲在T中搜索問題P的解。ios

深度優先搜索(Depth-First-Search,DFS)是一種用於遍歷或搜索樹或圖的算法。沿着樹的深度遍歷樹的節點,儘量深的搜索樹的分支。當節點v的所在邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的全部節點爲止。若是還存在未被發現的節點,則選擇其中一個做爲源節點並重復以上過程,整個進程反覆進行直到全部節點都被訪問爲止。 --from wiki算法

實現方法

一、按選優條件對T進行深度優先搜索,以達到目標。數組

二、從根結點出發深度優先搜索解空間樹函數

三、當探索到某一結點時,要先判斷該結點是否包含問題的解spa

  • 若是包含,就從該結點出發繼續按深度優先策略搜索
  • 不然逐層向其祖先結點回溯(退回一步從新選擇)
  • 知足回溯條件的某個狀態的點稱爲「回溯點」

四、算法結束條件.net

  • 求全部解:回溯到根,且根的全部子樹均已搜索完成
  • 求任一解:只要搜索到問題的一個解就能夠結束

遍歷過程

典型的解空間樹

第一類解空間樹:子集樹

當問題是:從n個元素的集合S中找出知足某種性質的子集時相應的解空間樹稱爲子集樹,例如n個物品的0/1揹包問題。指針

  • 這類子集樹一般有2^n個葉結點
  • 解空間樹的結點總數爲2^(n+1) - 1
  • 遍歷子集樹的算法需Ω(2^n)計算時間

第二類解空間樹:排列樹

當問題是:肯定n個元素知足某種性質的排列時相應的解空間樹稱爲排列樹,例如旅行商問題。code

DFS搜索在程序中能夠兩種方式來實現,分別是非遞歸方式遞歸方式。前者思路更加清晰,便於理解,後者代碼更加簡潔高效。htm

非遞歸實現

非遞歸實現須要藉助堆棧(先入後出,後入先出),在C++中使用stack容器便可。

問題

若給定一個序列,須要找到其中的一個子序列,判斷是否知足必定的條件。下面將程序實現DFS對子序列的搜索過程。

實現步驟:

一、首先將根節點放入堆棧中。

二、從堆棧中取出第一個節點,並檢驗它是否爲目標。

  • 若是找到目標,則結束搜尋並回傳結果。
  • 不然將它某一個還沒有檢驗過的直接子節點加入堆棧中。

三、重複步驟2。

四、若是不存在未檢測過的直接子節點。

  • 將上一級節點加入堆棧中。
  • 重複步驟2。

五、重複步驟4。

六、若堆棧爲空,表示整張圖都檢查過了——亦即圖中沒有欲搜尋的目標。結束搜尋並回傳「找不到目標」。

C++代碼

/*********************************************************************
*
* Ran Chen <wychencr@163.com>
*
* Back-track algorithm (by DFS)
*
*********************************************************************/


#include <iostream>
#include <vector>
#include <stack>

using namespace std;

class Node
{
public:
    int num;  // 節點中元素個數
    int sum;  // 節點中元素和
    int rank;  // 搜索樹的層級
    int flag;  // 0表示子節點都沒訪問過,1表示訪問過左節點,2表示訪問過左右節點
    vector <int> path;  // 節點元素

    Node();
    Node(const Node & nd);
};

// 默認構造函數
Node::Node()
{
    num = 0;
    sum = 0;
    rank = 0;
    flag = 0;
    // path is empty
}

// 複製構造函數
Node::Node(const Node & nd)
{
    num = nd.num;
    sum = nd.sum;
    rank = nd.rank;
    flag = nd.flag;
    path = nd.path;
}
// -----------------------------------------------------------------


void DFS(const vector <int> & deque)
{
    stack <Node *> stk;  // 存儲節點對應的指針
    stack <Node *> pre_stk;  // 存儲上一級節點(回溯隊列)
    Node * now = new Node;  // 指向當前節點
    Node * next = NULL;  // 指向下一個節點
    Node * previous = NULL;  // 指向上一個節點
    
    
    while (now)
    {
        if (now->rank < deque.size() && (now->flag == 0))
        {
            // 左葉子節點,選擇當前rank的數字
            next = new Node(*now);

            next->num++;
            next->sum += deque[next->rank];
            next->path.push_back(deque[next->rank]);
            next->rank++;
            next->flag = 0;
             
            stk.push(next);  // 將左節點加入堆棧中
            now->flag = 1;  // 改變標誌位

            // 將當前節點做爲上一級節點存儲並刪除
            previous = new Node(*now);
            pre_stk.push(previous);
            delete (now);

            // 取出堆棧中的待選節點做爲當前節點
            now = stk.top();
            stk.pop();


            // 顯示搜索路徑
            for (int i = 0; i < next->path.size(); ++i)
            {
                cout << " " << next->path[i] << " ";
            }
            cout << endl;

            continue;  // DFS每次僅選取一個子節點,再進入下一步循環
        }

        if (now->rank < deque.size() && (now->flag == 1))
        {
            // 右節點,不選擇當前rank的數字
            next = new Node(*now);

            next->rank++;
            next->flag = 0;

            stk.push(next);
            now->flag = 2;

            // 將當前節點做爲上一級節點存儲並刪除
            previous = new Node(*now);
            pre_stk.push(previous);
            delete (now);

            // 取出堆棧中的待選節點做爲當前節點
            now = stk.top();
            stk.pop();

            continue;

        }

        // 回溯結束
        if (pre_stk.empty())
        {
            break;
        }

        // 沒有子節點或者沒有未搜索過的子節點時,回退到上一級節點(回溯)
        if (now->rank >= deque.size() || now->flag == 2)
        {           
            delete (now);
            now = pre_stk.top();
            pre_stk.pop();

        }
        
    }
}

// -----------------------------------------------------------------

int main()
{
    stack <Node*> stk;
    vector <int> deque { 2,3,5,7 };
    DFS(deque);

    cin.get();
    return 0;
}

運行結果

程序說明

一、定義了一個Node節點類,表示當前狀態下已經搜索到的序列,path記錄了這個子序列的值,而且類中添加了num(子序列中元素數目)、sum(子序列元素和)等屬性,經過這些屬性能夠判斷是否找到滿意解或者用於剪枝。

二、對於原始序列中某個位置的數,其子序列中能夠包含這個數,也能夠不包含這個數,因此每次有兩種選擇,即每一個節點有兩個子節點。

三、flag屬性標識了當前節點的子節點遍歷狀況。若flag=0,表示子節點都沒訪問過,下一步優先訪問左節點,因此將左節點加入堆棧中;flag=1,表示訪問過左節點,下一步訪問右節點;flag=2,表示訪問過左右節點。

四、當沒有子節點(now->rank >= deque.size())或者左右節點都訪問過期(flag=2),回溯到上一級節點。

五、程序循環中,首先經過now當前節點,找到下一個子節點next,將其加入堆棧中,便於下一步循環。在now節點銷燬前,將其存到previous,並加入pre_stk堆棧中。這樣在下一輪循環中,previous相對於now就是上一級節點,若是now不能找到其子節點,就要返回上一級,這樣previous就能夠從新賦給now,達到返回上一級的目的。

六、整個程序的終止條件是pre_stk堆棧爲空時截止,說明全部節點都已經遍歷過,而且沒有再可回溯的節點了。實際運用中,能夠經過其餘屬性(搜索到可行解)來提早終止程序。

遞歸實現

參考自Coding_Or_Dead的博客

#include<cstdio>
#include <iostream>

int n, k; __int64 sum = 0;
int a[4] = { 2, 3, 5, 7 }, vis[4] = {0, 0, 0, 0};


void DFS(int i, int cnt, int sm)//i爲數組元素下標,sm爲cnt個數字的乘積
{
    if (cnt == k)  // 解中已包含k個數字
    { 
        sum = sum + sm; return; 
    }
    if (i >= n) 
        return;

    if (!vis[i])
    {   
        // 對第i個數字進行訪問
        vis[i] = 1;
        //a[i]被選,優先選擇第i個加入到解中,接下來搜索第i+1個數字
        DFS(i + 1, cnt + 1, sm*a[i]); 

        //a[i]不選,不選擇第i個,至關於右節點,接下來搜素第i+1個數字
        DFS(i + 1, cnt, sm);        
        vis[i] = 0;  // 回溯
    }
    return;
}
int main(void)
{
    n = 4, k = 2;

    DFS(0, 0, 1);
    printf("%I64d\n", sum);

    std::system("pause");
    return 0;
}

程序說明

一、程序目的:給定n個正整數,求出這n個正整數中全部任選k個相乘後的和,這裏的數組a[4]存儲原序列,vis[4]做爲訪問標誌,k取2,結果輸出爲101,對應的序列是{2, 3}{2, 5}{2, 7}{3, 5}{3, 7}{5, 7}

二、對於元素a[i],每次對應兩個選擇。若選擇將a[i]加入到解中,則解中元素個數+1,乘積結果*a[i],因此下一步更新爲DFS(i + 1, cnt + 1, sm*a[i])。若不選擇a[i],則解中的元素個數和乘積不變,下一步更新爲DFS(i + 1, cnt, sm)

三、回溯時要將標誌位重置。


References

相關文章
相關標籤/搜索