回溯算法及其案例用途


回溯算法是一種遞歸模式,它是一種暴力求解方法(brute force method),用於求出所有可能的解,回溯算法通常會構建一個狀態空間樹(state space tree),

將可能的組和從根到葉節點進行展開,然后以深度優先的方式搜索遍歷狀態樹,遍歷過程中遇到不符合解的節點立馬返回進行新的遍歷,而不是繼續遍歷,

狀態空間樹的結構可用下圖進行描述:

 

 

 回溯算法不是用來求解最優解,而是用來求解可行解的方法,回溯算法的代碼結構:

Backtrack(x)
    if x is not a solution
        return false  // return directly
    if x is a new solution
        add to list of solutions
    backtrack(expand x)

根據以上結構,用具體的示例進行分析,例如:

1、排隊問題,假設有2個男孩和1個女孩,女孩不能站在男孩的中間,則可以用一個樹的結構進行描述:

 樹是構建好了,根據構建樹構建代碼:

arrange-boy-girl(p,index,result,mem)://index 用於記錄孩子的位置,result是最終的排列結果,mem用於記錄孩子是否已經排在隊中了

  if index == p.length:

    print(result);//輸出結果

  // expand p

  for i =0 to p.length:

    if(index == 2 && p[i]=='girl' || mem[i] ==1)://位置2不能是女孩,且該小孩沒有在隊列中,繼續進行循環

      continue;    

    result[index]=p[i];//將小孩排到隊中

    mem[i]=1;//記錄一下,下次這個小孩不能再排了,因為已經在隊伍中了

    index++;//排下一個位置

    arrange-boy-girl(p,index,result,mem);//遞歸調用,排下一個位置

    index--;//注意這里,index恢復原值,表示原來的index位置還可以安排下一個小孩,比如,位置0可以是boy1,也可以是boy2

    mem[i]=0;//這里也是,index恢復原值后,mem也要恢復原值

以上是一個全排列問題,但是它有一些限制,就是女孩不能排到男孩兒中間。

2、數的組合排列問題,給定一個數組 1 2 3,可以拼成多少種不同的兩位數,每個數都可以重復利用,先構建組合樹:

 

 

以上構建的是兩位數,根據同樣的方式還可以繼續構建,可以是3位數,4位數,根據構建樹編寫程序:

combine-num(arr,depth,result):

  //遞歸的結束條件

  if depth == 2:

    print(result);

  // expand array  

  for int i =0 to arr.length:    

    result.add(arr[i]);//添加元素

    depth++;//走到下一個位置

    combine-num(arr,depth,result);

    depth--;//回溯到上一個位置

    result.removeLastNum();//將當前位置元素刪除,相當於回溯到上一個位置以存放for循環中的第i+1個元素

本示例中,遞歸的結束條件是depth=2,如果需要是3位數,則depth=3,注意這里的“回溯”是在哪里體現的,就是“depth--”那個位置,表示返回

到上一個位置,將該位置放置第(i+1)個元素。

3、子集和的問題,給定一個集合{1,2,3,4,5,6,7,8,9,10}和一個值val=15,判斷該集合中是否存在元素和為val的子集,如果有則輸出該子集;

這個和上面的是一樣的,也是一個組合問題,但是為了避免重復解的出現,比如 7+8=15,8+7=15;可以讓每個元素只和它后面的元素進行組合;構造程序

如下:

def subset-sum(arr,fromIndex,sum,val,result):

  if sum==val:

    print(result);

  for i=fromIndex to arr.length:

    sum+=arr[i];//加上當前元素

    result.add(arr[i]);//將當前元素放到子集中

    subset-sum(arr,i+1,sum,val,result);// 注意這里,fromIndex變為了i+1,也就是當前元素只能和它后面的元素進行組合

    sum-=arr[i];//減去當前元素,回溯到上一個子集和的狀態

    result.removeLast();//刪除當前元素,回溯到上一個子集狀態

4、八皇后問題,在8*8的空格列表中,每個空格可以放一個皇后,皇后可以攻擊與它同行或同列的其他皇后,在該列表中擺放8個皇后,使其不能

互相攻擊,求一共有多少種擺放方法;首先構造樹結構,這里有一個4*4的列表結構:

 

 

從這棵樹的構造過程,可以大體得出代碼的構造過程,根節點到葉子節點的距離就是該葉子節點的深度,其實也就是行數rows;每行中空格的個數也就是

表格的列數cols,可以創建一個arr[rows][cols]的二維數組,然后構建代碼:

N-Queens(arr,rows,cols,row):

  if row == rows://當前樹的深度為表格的行數,也就是所有行都遍歷了,則打印出結果

    print(arr);

  else:

    for col=0 to cols://遍歷第depth行中的每個空格

      if isValid(arr,row,col,rows,cols):

        arr[row][col]=1;

        row+=1;//移動到下一行

        N-Queen(arr,rows,cols,row);//遍歷下一行

        row-=1;//回溯到上一行,對上一行的下一個元素繼續遞歸運算

        arr[row][col]=0;//將原來位置重新恢復原值0,注意這里代碼是有先后順序的,row-=1在前,恢復原值在后

      else:

        continue;//繼續遍歷該行的下一個元素

這里有一個isValid方法,用於判斷row行和col列中沒有皇后存在:

isValid(arr,row,col,rows,cols):

  for i=0 to rows and i!=row

    if arr[i][col]==1       return false; //存在皇后,該列非法

  for i=0 to cols and i!=col

    if arr[row][i]==1    return false; //存在皇后,該行非法     

  return true;//該空格所在的行和列都不存在皇后,是合法位置

總結:

回溯算法是和遞歸相關的,首先需要構建一個狀態空間樹,在代碼層面是通過遍歷和遞歸的形式構建一顆邏輯性的狀態空間樹,而非真的創建一個

樹型的數據結構,在遍歷和遞歸的過程中,這些執行路線構成了一個狀態空間樹,回溯算法的代碼接口可如下表示:

backtrace(arr,result,xx):

  if xx滿足條件:

    print(result)

  else

    for el in arr: //expand arr

      result.add(el);//做選擇

      update xx;//更新條件

      backtrace(arr,result,xx);

      cancel xx;//撤銷條件

      result.delete(el);//撤銷選擇  

回溯的難點就是構造樹的過程,這是一個邏輯形式上的構建,通過遍歷和遞歸加以實現,同時需要對執行過程對狀態更新和撤銷。 

 

 

 

 

   


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM