回溯算法總結

目錄

回溯算法簡介

回溯算法中的三個概念

回溯算法的步驟

具體流程

代碼實現框架

經典例子

選取數字集合

皇后問題

最大 k 乘積問題


回溯算法簡介

回溯法是一種類似枚舉的搜索嘗試過程,既然是枚舉,那麼就會遍歷解空間樹中的所有解(或者是“路徑”),搜索的過程按照DFS原則,而嘗試就意味着,在遍歷的過程中,有可能到達某一個結點後,發現不能夠滿足約束條件,在這次嘗試中,這條“路”是不優的,將走不通,即無法找到所求的解,那麼就會回退到上一步的狀態,重新作出選擇。如果即滿足約束條件,但是依然沒有獲得有效的解,那麼我們就需要在此基礎上做下一步選擇,即將當前結點當做一個新的根結點。所以經常會使用遞歸的方法。如果一步步下來的選擇結果正好滿足我們所求的問題,那麼就是一個有效的解。

回溯法思想:在包含問題所有解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度搜索解空間樹。當搜索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜索算法)。若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被檢索一遍才結束。而若使用回溯法求任一個解時,只要搜索到問題的一個解就可以結束。

若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被搜索遍才結束。

若使用回溯法求任一個解時,只要搜索到問題的一個解就可以結束。

回溯算法中的三個概念

(1)約束函數:約束函數是根據題意定出的。通過描述合法解的一般特徵用於去除不合法的解(即使這個解並不是完整的解),從而避免繼續搜索出這個不合法解的剩餘部分,起到了剪值函數的作用。因此,約束函數是對於解空間樹上的任何節點都有效、等價的。

(2)狀態空間樹:狀態空間樹是一個對所有解的圖形描述

(3)擴展節點:當前正求出它的子節點的節點,在DFS中,只允許有一個擴展節點。

        活節點:通過與約束函數對照,節點本身和其父節點均滿足約束函數要求的節點。

        死節點: 與活節點正好相反,死節點是在與約束函數對照中,不滿足約束函數的節點。那麼就不需要求該節點的子節點情況了。

回溯算法的步驟

在回溯法執行時,應當:保存當前步驟,如果是一個解就輸出;維護狀態,使搜索路徑(含子路徑)儘量不重複。必要時,應該對不可能爲解的部分進行剪枝(pruning)。

(1)定義一個解空間,它包含問題的解。明確問題的解空間樹內容,比如,在求組合問題時,最後有效解中元素是否可重複的,元素個數是否是固定的等。且保證問題的解空間中應至少包含問題的一個解。

(2)利用適於搜索的方法組織解空間。確定結點的擴展搜索規則,在回溯法中,就是深度優先遍歷規則。擴展節點就是當前正在求出它的子節點的節點,在DFS中,只允許有一個擴展節點。

(3)利用深度優先法搜索解空間。

(4)利用限界函數避免移動到不可能產生解的子空間。 構造約束條件。作爲剪枝函數,避免無效搜索。

 問題的解空間通常是在搜索問題的解的過程中動態產生的,這是回溯算法的一個重要特性。

具體流程

(1)設置初始化的方案(給變量賦初值,讀入已知數據等);

(2)選擇所到深度層(k)中其中的一個節點進行試探,如果k層的節點已經全部都試探完畢,則進入(7)

(3)如果試探不成功(不滿足約束條件),則轉入(2)

(4)如果試探成功,則進入下一個深度層進行試探

(5)如果正確解還沒找到,則進入(2)

(6)如果找到了正解,如果問題只求其中的一個解,那麼可以退出程序了,如果是求一組解,那麼將該種解存儲(一般用一個類中的變量);

(7)退回上一步的狀態(深度爲k-1時,變量的狀態),如果沒有退到頭,則進入(2)

(8)已退到頭則結束或打印無解

在使用回溯法解決實際問題時,往往會和遞歸算法結合,更容易解決問題,但是也可以使用非遞歸的方法實現,這就需要具體情況進行分析了。
 

代碼實現框架

bool finished = FALSE; /* 是否獲得全部解? */
backtrack(int a[], int k, data input)
{
    int c[MAXCANDIDATES]; /*這次搜索的候選 */
    int ncandidates; /* 候選數目 */
    int i; /* counter */
    if (is_a_solution(a,k,input))
    process_solution(a,k,input);
    else {
        k = k+1;
        construct_candidates(a,k,input,c,&ncandidates);
        for (i=0; i<ncandidates; i++) {
            a[k] = c[i];
            make_move(a,k,input);
            backtrack(a,k,input);
            unmake_move(a,k,input);
            if (finished) return; /* 如果符合終止條件就提前退出 */
        }
    }
}

對於其中的函數和變量,解釋如下:

a[]表示當前獲得的部分解;

k表示搜索深度;

input表示用於傳遞的更多的參數;

is_a_solution(a,k,input)判斷當前的部分解向量a[1...k]是否是一個符合條件的解

construct_candidates(a,k,input,c,ncandidates)根據目前狀態,構造這一步可能的選擇,存入c[]數組,其長度存ncandidates

process_solution(a,k,input)對於符合條件的解進行處理,通常是輸出、計數等

make_move(a,k,input)unmake_move(a,k,input)前者將採取的選擇更新到原始數據結構上,後者把這一行爲撤銷。

 

經典例子

選取數字集合

做一個白話版的描述,給你兩個整數 n和k,從1-n中選擇k個數字的組合。比如n=4,k=2,那麼從1,2,3,4中選取兩個數字的組合

可以看到上面的每個變量都出現了,用一個類變量保存結果

public class Solution {
   List<List<Integer>> result=new ArrayList<List<Integer>>();
   public List<List<Integer>> combine(int n, int k) {
       List<Integer> list=new ArrayList<Integer>();
       backtracking(n,k,1,list);
       return result;
    }
   public void backtracking(int n,int k,int start,List<Integer>list){
       if(k<0) return ;
       else if(k==0){
           result.add(new ArrayList(list));
       }else{
           for(int i=start;i<=n;i++){
                list.add(i);
                backtracking(n,k-1,i+1,list);
                list.remove(list.size()-1);
            }
       }
    }
}

皇后問題

N皇后問題是指在N*N的棋盤上放置N個皇后,使這N個皇后無法吃掉對方(也就是說兩兩不在一行,不在一列,也不在對角線上)。經典的是8皇后問題,這裏我們爲了簡單,以4皇后爲例。

    首先利用回溯算法,先給第一個皇后安排位置,如下圖所示,安排在(1,1)然後給第二個皇后安排位置,可知(2,1),(2,2)都會產生衝突,因此可以安排在(2,3),然後安排第三個皇后,在第三行沒有合適的位置,因此回溯到第二個皇后,重新安排第二個皇后的位置,安排到(2,4),然後安排第三個皇后到(3,2),安排第四個皇后有衝突,因此要回溯到第三個皇后,可知第三個皇后也就僅此一個位置,無處可改,故繼續向上回溯到第二個皇后,也沒有位置可更改,因此回溯到第一個皇后,更改第一個皇后的位置,繼續上面的做法,直至找到所有皇后的位置,如下圖所示。

    這裏爲什麼我們用4皇后做例子呢?因爲3皇后是無解的。同時我們也可以看到回溯算法雖然也是Brute-Force,但是它可以避免去搜索很多的不可能的情況,因此算法是優於Brute-Force的。

這裏是每次確定第col行的棋子的位置

public class NQueensII {  
    int[] x;//當前解    
    int N;//皇后個數  
     int sum = 0;//當前已找到的可行方案數  
    public int totalNQueens(int n) {  
        N = n;  
        x = new int[N+1];  
        backTrace(1);  
        return sum;  
    }  
    /** 
     * col行這個點,x[col]列這個點,與已經存在的幾個皇后,是否符合要求,放到這個位置上, 
     * @param col 
     * @return 
     */  
    private boolean place(int col){  
        for(int i = 1; i < col; i++){  
            if(Math.abs(col - i)==Math.abs(x[col]-x[i])||x[col]==x[i]){  
                return false;  
            }  
        }  
        return true;  
    }  
    private void backTrace(int t) {  
        if(t>N){  
            sum++;  
        }else {  
            //第t行,遍歷所有的節點  
            for(int j = 1; j <= N; j++) {  
                 x[t] = j ;  
                 //如果第j個節點可以放下皇后  
                if(place(t)){  
                    //接着放下一個  
                    backTrace(t+1);  
                }  
            }  
        }  
          
    }  
    public static void main(String[] args) {  
        NQueensII n = new NQueensII();  
        System.out.println(n.totalNQueens(8));  
    }  
}

最大 k 乘積問題

設I是一個 n 位十進制整數。如果將 I 劃分爲 k 段,則可得到    k 個整數。這 k 個整數的乘積稱爲 I 的一個 k 乘積。試設計一個算法,對於給定的 I 和 k ,求出 I 的最大 k 乘積。

import java.util.Scanner;
 
 
public class 最大k乘積 {
    private static long ans;
    private static Scanner cin;
    static{
        cin = new Scanner(System.in);
    }
    static void work(int cur, int i, int k, long v){
        //System.out.println("i = " + i + " cur = " + cur + " k = " + k);
        if(i == k){
            ans = Math.max(ans, v);
            return;
        }
        if(cur == 0){
            return;
        }
        int MOD = 1;
        while(cur / MOD != 0){
            work(cur % MOD, i + 1, k, v * (cur / MOD));
            MOD *= 10;
        }
    }
    public static void main(String[] args) {
        int num, k;
        System.out.println("請輸入數字num和要分成的段數k: ");
        while(cin.hasNext()){
            num = cin.nextInt();
            k = cin.nextInt();
            ans = Long.MIN_VALUE;
            work(num, 0, k, 1L);
            if(ans == Long.MIN_VALUE){
                System.out.println("整數" + num + "不能被分成" + k + "段");
            }else{
                System.out.println(num + "的最大" + k + "乘積是: " + ans);
            }
            System.out.println("請輸入數字num和要分成的段數k: ");
        }
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章