1. 程式人生 > >回溯演算法 和 貪心演算法(全排列)

回溯演算法 和 貪心演算法(全排列)

一:簡介

(1)回溯法 又稱試探法

回溯法的基本做法是深度優先搜尋,是一種組織得井井有條的、能避免不必要重複搜尋的窮舉式搜尋演算法;基本思想是:從一條路往前走,能進則進,不能進則退回來,換一條路再試。
適用場景:當遇到某一類問題時,它的問題可以分解,但是又不能得出明確的動態規劃或是遞迴解法,此時可以考慮用回溯法解決此類問題。回溯法的優點在於其程式結構明確,可讀性強,易於理解,而且通過對問題的分析可以大大提高執行效率。但是,對於可以得出明顯的遞推公式迭代求解的問題,還是不要用回溯法,因為它花費的時間比較長(不到萬不得已,不要用回溯法,費時) —— 有時,感覺回溯法,非常類似於上一篇部落格DFS+剪枝


(2)貪心演算法 又稱貪婪演算法

在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的區域性最優解。
貪心演算法不是對所有問題都能得到整體最優解,關鍵是貪心策略的選擇,選擇的貪心策略必須具備無後效性,即某個狀態以前的過程不會影響以後的狀態,只與當前狀態有關。它與動態規劃相對的,動態規劃,是尋找子問題,子問題與原問題有極大的相似性,求解是全域性最優

(3)遞迴和回溯

遞迴是一種演算法結構,回溯是一種演算法思想;一個遞迴就是在函式中呼叫函式本身來解決問題;回溯就是通過不同的嘗試來生成問題的解,有點類似於窮舉,但是和窮舉不同的是回溯會“剪枝”

,意思就是對已經知道錯誤的結果沒必要再列舉接下來的答案了,比如一個有序數列1,2,3,4,5,我要找和為5的所有集合,從前往後搜尋我選了1,然後2,然後選3 的時候發現和已經大於預期,那麼4,5肯定也不行,這就是一種對搜尋過程的優化。

二:案例詳解

(1)回溯法詳解

首先要將問題轉化,得出狀態空間樹。這棵樹的每條完整路徑都代表了一種解的可能。通過深度優先搜尋這棵樹,列舉每種可能的解的情況;從而得出結果。但是,回溯法中通過構造約束函式,可以大大提升程式效率,因為在深度優先搜尋的過程中,不斷的將每個解(並不一定是完整的,事實上這也就是構造約束函式的意義所在)與約束函式進行對照從而刪除一些不可能的解,這樣就不必繼續把解的剩餘部分列出從而節省部分時間。
回溯法中三個概念:
 (一)約束函式:

約束函式是根據題意定出的。通過描述合法解的一般特徵用於去除不合法的解,從而避免繼續搜尋出這個不合法解的剩餘部分。因此,約束函式是對於任何狀態空間樹上的節點都有效、等價的。
 (二)狀態空間樹:剛剛已經提到,狀態空間樹是一個對所有解的圖形描述。樹上的每個子節點的解都只有一個部分與父節點不同。
 (三)擴充套件節點、活結點、死結點:所謂擴充套件節點,就是當前正在求出它的子節點的節點,在深度優先搜尋中,只允許有一個擴充套件節點。活結點就是通過與約束函式的對照,節點本身和其父節點均滿足約束函式要求的節點;死結點反之。由此很容易知道死結點是不必求出其子節點的(沒有意義)。
利用回溯法解題的具體步驟

(2)回溯法的實現步驟

通過讀題完成下面三個步驟:
1)描述解的形式,定義一個解空間,它包含問題的所有解。
2)構造狀態空間樹。
3)構造約束函式(用於殺死節點)。
然後就要通過深度優先搜尋思想完成回溯,完整過程如下:
1)設定初始化的方案(給變數賦初值,讀入已知資料等)。
2)變換方式去試探,若全部試完則轉(7)。
3)判斷此法是否成功(通過約束函式),不成功則轉(2)。
4)試探成功則前進一步再試探。
5)正確方案還未找到則轉(2)。
6)已找到一種方案則記錄並列印。
7)退回一步(回溯),若未退到頭則轉(2)。
8)已退到頭則結束或列印無解


(3)回溯法示例(連同圖格數)

#include<iostream>
#include<cstring>

using namespace std;
#define isBound(a,b) (a<0 || a>=b)
const int maxN = 21;
char map[maxN][maxN];
bool visit[maxN][maxN];
int w,h,ans;
void dfs(int row,int col)
{
    if(isBound(row,h)||isBound(col,w)||map[row][col]=='#'||visit[row][col])
        return;
    visit[row][col] = true;
    ans ++;
    dfs(row,col-1);
    dfs(row-1,col);
    dfs(row,col+1);
    dfs(row+1,col);
}
int main()
{
    int i,j,row,col;
    char flg = '@';
    while(cin>>w>>h&&(w||h))
    {
        ans = 0;
        memset(visit,false,sizeof(visit));
        for(i=0;i<h;++i)
        {
            for(j=0;j<w;++j)
            {
                cin >> map[i][j];
                if(flg == map[i][j])
                {
                    row = i;
                    col = j;
                }
            }
        }
        dfs(row,col);
        cout << ans << endl;
    }
    return 0;
}//POJ 1979 Red and Black(利用回溯法來掃圖
可以記憶搜尋了,設定一個visit陣列記錄是否可以到達。

如果一個位置visit為真,證明已經搜尋過這個點了,不再重複搜尋。通過這個點可以到達的位置必然已經標記為visit了(當然,也可以用map既做資料存貯,又做標識儲存,也可以的,只是原來的資料陣列改變了)。

(4)回溯法示例二(全排列)

# include <stdio.h>
void swap2 (char *x, char *y)
{
    char temp;
    temp = *x;
    *x = *y;
    *y = temp;
}

/* 列印字串的全排列*/
void permute(char *str, int i, int n)
{
   int j;
   if (i == n)
     printf("%s\n", str);
   else
   {
        for (j = i; j <= n; j++)
       {
          swap((str+i), (str+j));
          permute(str, i+1, n);
          swap((str+i), (str+j)); //回溯
       }
   }
}

/* 測試 */
int main()
{
   char str[] = "ABCD";
   int len = sizeof(str)/sizeof(char) - 2;
   permute(str, 0, len);
   getchar();
   return 0;
}

(4)貪心演算法

(1)貪心演算法的基本思路:
    1.建立數學模型來描述問題。
    2.把求解的問題分成若干個子問題。
    3.對每一子問題求解,得到子問題的區域性最優解。
    4.把子問題的解區域性最優解合成原來解問題的一個解。

(2)貪心演算法適用的問題

      貪心策略適用的前提是:區域性最優策略能導致產生全域性最優解。
    實際上,貪心演算法適用的情況很少。一般,對一個問題分析是否適用於貪心演算法,可以先選擇該問題下的幾個實際資料進行分析,就可做出判斷。

(3)貪心演算法的實現框架
    從問題的某一初始解出發;
    while (能朝給定總目標前進一步)
    { 
          利用可行的決策,求出可行解的一個解元素;
    }
    由所有解元素組合成問題的一個可行解;
  
(4)貪心策略的選擇
     因為用貪心演算法只能通過解區域性最優解的策略來達到全域性最優解,因此,一定要注意判斷問題是否適合採用貪心演算法策略,找到的解是否一定是問題的最優解。