經典演算法之回溯法

無鞋童鞋發表於2017-08-12

1 綜述
  回溯法可以看成是蠻力法的升級版,它從解決問題每一步的所有可能選項裡系統的選擇出一個可行的解決方案。回溯法非常適合由多個步驟組成的問題,並且每個步驟都有多個選項。當我們在某一步選擇了其中一個選項時,就進入下一步,然後面臨新的選項。我們就這麼重複選擇,直至到達最終的狀態。
  用回溯法解決的問題的所有選項可以形象地用樹狀結構表示。在某一步有n個可能的選項,那麼該步驟可以看成是樹狀結構中的一個節點,每個選項看成樹中節點的連線線,經過這些連線線到達某個節點的n個子節點。樹的葉節點對應著終結狀態。如果在葉節點的狀態滿足題目的約束條件,那麼我們找到了一個可行的解決方案。
  如果在葉節點的狀態不滿足約束條件,那麼只好回溯到它的上一個節點再嘗試其他選項。如果上一個節點所有可能的選項都已經試過,並且不能達到滿足約束條件的終結狀態,則再次回溯到上一個節點。如果所有節點的所有選項都已經嘗試過仍然不能到達滿足約束條件的終結狀態,則該問題無解。
2 演算法用法
  回溯法按深度優先策略搜尋問題的解空間樹。首先從根節點出發搜尋解空間樹,當演算法搜尋至解空間樹的某一節點時,先利用剪枝函式判斷該節點是否可行(即能得到問題的解)。如果不可行,則跳過對該節點為根的子樹的搜尋,逐層向其祖先節點回溯;否則,進入該子樹,繼續按深度優先策略搜尋。
  回溯法的基本行為是搜尋,搜尋過程使用剪枝函式來為了避免無效的搜尋。剪枝函式包括兩類:①. 使用約束函式,剪去不滿足約束條件的路徑;②. 使用限界函式,剪去不能得到最優解的路徑。
  問題的關鍵在於如何定義問題的解空間,轉化成樹(即解空間樹)。解空間樹分為兩種:子集樹和排列樹。兩種在演算法結構和思路上大體相同。
  子集樹應用例如0-1揹包問題,決定多少物品能放進揹包收益最大,最終解是所有物品的一個子集,所以是典型的子集樹解空間;另一個種排列數例如走迷宮或者旅行售貨員,它需要記錄路徑上節點先後資訊。
3 演算法應用
  當問題是要求滿足某種性質(約束條件)的所有解或最優解時,往往使用回溯法。所以它有“通用解題法”之美譽。
  (1)裝載問題
  (2)0-1揹包問題
  (3)旅行售貨員問題
  (4)八皇后問題
  (5)迷宮問題
  (6)圖的m著色問題
4 面試題:矩陣中的路徑
 4.1 題目
  請設計一個函式,用來判斷在一個矩陣中是否存在一條包含某字串的所有字元的路徑。路徑可以從矩陣中的任意一格開始,每一步可以在矩陣中向左、右、上、下移動一格。如果一條路徑經過了矩陣的某個空格,那麼該路徑不能再次進入該格子。例如,下面的3×4的矩陣中包含一條字串“bfce”的路徑,但是並不包含一條字串“abfe”路徑,因為第一個字元b佔據了第一行第二列的格子,路徑不能再次進入這個格子。——劍指offer面試題:12
a  b  t  g
c  f  c  s
j  d  e  h
 4.2 分析
   這是一個經典的可以用回溯法解決的題。首先,我們從矩陣中任意一個字元出發。假設矩陣中某個格子的字元表示為char_one,並且這個格子將對應於路徑上的第i個字元。如果路徑上的第i個字元不是char_one,那麼其就不該出現在路徑上第i個位置,需要退回上個狀態i-1重新尋找第i個字元。如果恰好就是第i個字元,那麼我們就可以在相鄰的格子尋找路徑上第i+1個字元。矩陣除邊界上格子外相鄰都是4個格子。重複這個過程,直到路徑上的所有字元都能在矩陣中找到對應為重。整個過程又非常像棧資料結構的入棧與出棧操作。
 4.3 程式碼


#include <cstdio>
#include <string>
#include <stack>

using namespace std;

bool hasPathCore(const char* matrix, int rows, int cols, int row, int col, const char* str, int& pathLength, bool* visited);

bool hasPath(const char* matrix, int rows, int cols, const char* str)
{
    if(matrix == nullptr || rows < 1 || cols < 1 || str == nullptr)
        return false;

    bool *visited = new bool[rows * cols];
    memset(visited, 0, rows * cols);

    int pathLength = 0;
    for(int row = 0; row < rows; ++row)
    {
        for(int col = 0; col < cols; ++col)
        {
            if(hasPathCore(matrix, rows, cols, row, col, str,
                pathLength, visited))
            {
                return true;
            }
        }
    }

    delete[] visited;

    return false;
}

bool hasPathCore(const char* matrix, int rows, int cols, int row,
    int col, const char* str, int& pathLength, bool* visited)
{
    if(str[pathLength] == '\0')
        return true;

    bool hasPath = false;
    if(row >= 0 && row < rows && col >= 0 && col < cols
        && matrix[row * cols + col] == str[pathLength]
        && !visited[row * cols + col])
    {
        ++pathLength;
        visited[row * cols + col] = true;

        hasPath = hasPathCore(matrix, rows, cols, row, col - 1,
            str, pathLength, visited)
            || hasPathCore(matrix, rows, cols, row - 1, col,
                str, pathLength, visited)
            || hasPathCore(matrix, rows, cols, row, col + 1,
                str, pathLength, visited)
            || hasPathCore(matrix, rows, cols, row + 1, col,
                str, pathLength, visited);

        if(!hasPath)
        {
            --pathLength;
            visited[row * cols + col] = false;
        }
    }

    return hasPath;
}

  ——參考自劍指offer
 4.4 測試

int main()
{
    const char* matrix = "ABCEHJIGSFCSLOPQADEEMNOEADIDEJFMVCEIFGGS";
    const char* str = "SGGFIECVAASABCEEJIGOEM";

    Test("Test6", (const char*) matrix, 5, 8, str, false);
}

 參考自:
 演算法入門6:回溯法
 劍指offer

相關文章