5.算法設計與分析__回溯算法

回溯法是一種組織搜索的一般技術,有“通用的解題法”之稱,用它可以系統的搜索一個問題的所有解或任一解。
有許多問題,當需要找出它的解集或者要求回答什麼解是滿足某些約束條件的最佳解時,往往要使用回溯法。
可以系統地搜索一個問題的所有解或任意解,既有系統性又有跳躍性。
回溯法的基本做法是搜索,或是一種組織得井井有條的,能避免不必要搜索的窮舉式搜索法。
這種以深度優先的方式系統地搜索問題的解的方法稱爲回溯法。

1 回溯算法的理論基礎

1.1 問題的解空間

應用回溯法求解時,需要明確定義問題的解空間。問題的解空間應至少包含問題的一個(最優)解。
例如,對於有n種可選擇物品的0—1揹包問題,其解空間由長度爲n的0—1向量組成,該解空間包含了對變量的所有可能的0—1賦值
在這裏插入圖片描述

1.2 回溯法的基本思想

在生成解空間樹時,定義以下幾個相關概念:
活結點:如果已生成一個結點而它的所有兒子結點還沒有全部生成,則這個結點叫做活結點。
擴展結點:當前正在生成其兒子結點的活結點叫擴展結點(正擴展的結點)。
死結點:不再進一步擴展或者其兒子結點已全部生成的結點就是死結點。

在確定瞭解空間的組織結構後,回溯從開始結點(根結點)出發,以深度優先的方式搜索整個解空間。
這個開始結點成爲一個活結點,同時成爲當前的擴展結點。在當前的擴展結點,搜索向深度方向進入一個新的結點。這個新結點成爲一個新的活結點,併成爲當前的擴展結點。
若在當前擴展結點處不能再向深度方向移動,則當前的擴展結點成爲死結點,即該活結點成爲死結點。此時回溯到最近的一個活結點處,並使得這個活結點成爲當前的擴展結點。
回溯法以這樣的方式遞歸搜索整個解空間(樹),直至滿足中止條件。
在這裏插入圖片描述
在這裏插入圖片描述
設G=(V, E)是一個帶權圖,其每一條邊(u, v)∈E的費用(權)爲正數w(u, v)。目的是要找出G的一條經過每個頂點一次且僅經過一次的迴路,即漢密爾頓(Hamilton)迴路v1,v2 ,…,vn ,使迴路的總權值最小:
在這裏插入圖片描述

回溯法找最小費用周遊路線的主要過程
在這裏插入圖片描述
在回溯法搜索解空間樹時,通常採用兩種策略(剪枝函數)避免無效搜索以提高回溯法的搜索效率:
用約束函數在擴展結點處減去不滿足約束條件的子樹;
用限界函數減去不能得到最優解的子樹。
解0—1揹包問題的回溯法用剪枝函數剪去導致不可行解的子樹。
解旅行商問題的回溯算法中,如果從根結點到當前擴展結點的部分周遊路線的費用已超過當前找到的最好周遊路線費用,則以該結點爲根的子樹中不包括最優解,就可以剪枝。

1.3 子集樹與排列樹

有時問題是要從一個集合的所有子集中搜索一個集合,作爲問題的解。或者從一個集合的排列中搜索一個排列,作爲問題的解。
回溯算法可以很方便地遍歷一個集合的所有子集或者所有排列。
當問題是要計算n個元素的子集,以便達到某種優化目標時,可以把這個解空間組織成一棵子集樹。
例如,n個物品的0-1揹包問題相應的解空間樹就是一棵子集樹。
這類子集樹通常有2n個葉結點,結點總數爲2n +1-1。
遍歷子集樹的任何算法,其計算時間複雜度都是Ω(2n)。

僞代碼

//形參t爲樹的深度,根爲1
void backtrack (int t)
{
  if (t>n) update(x);
  else
    for (int i=0; i<=1; i++)  //每個結點只有兩個子樹
    {
      x[t]=i;         //即0/1
      if (constraint(t) && bound(t)) backtrack(t+1);
    }
}

約束函數constraint(t)和限界函數bound(t),稱爲剪枝函數。
函數update(x)是更新解向量x的。
約束函數constraint(t),一般可以從問題描述中找到。

當所給的問題是確定n個元素滿足某種性質的排列時,可以把這個解空間組織成一棵排列樹。
排列樹通常有n!個葉子結點。因此遍歷排列樹時,其計算時間複雜度是Ω(n!) 。
例如,旅行商問題就是一棵排列樹。
僞代碼

//形參t爲樹的深度,根爲1
void backtrack (int t)
{
  if (t>n) update(x);
  else
    for (int i=t; i<=n; i++)
    {
      //爲了保證排列中每個元素不同,通過交換 來實現
      swap(x[t], x[i]);
      if (constraint(t) && bound(t)) backtrack(t+1);
      swap(x[t], x[i]);    //恢復狀態
    }
}

2 裝載問題

給定n個集裝箱要裝上一艘載重量爲c的輪船,其中集裝箱i的重量爲wi。集裝箱裝載問題要求確定在不超過輪船載重量的前提下,將儘可能多的集裝箱裝上輪船。
由於集裝箱問題是從n個集裝箱裏選擇一部分集裝箱,假設解向量爲X(x1, x2, …, xn),其中xi∈{0, 1}, xi =1表示集裝箱i裝上輪船, xi =0表示集裝箱i不裝上輪船。
輸入
每組測試數據:第1行有2個整數c和n。C是輪船的載重量(0<c<30000),n是集裝箱的個數(n≤20)。第2行有n個整數w1, w2, …, wn,分別表示n個集裝箱的重量。
輸出
對每個測試例,輸出兩行:第1行是裝載到輪船的最大載重量,第2行是集裝箱的編號。

該問題的形式化描述爲:
在這裏插入圖片描述

在5.4節,最優裝載問題中,是要求在裝載體積不受限制的情況下,而這裏是不超過輪船的載重量。
用回溯法解裝載問題時,其解空間是一棵子集樹,與0—1揹包問題的解空間樹相同。
在這裏插入圖片描述
可行性約束函數可剪去不滿足約束條件的子樹:
在這裏插入圖片描述

令cw(t)表示從根結點到第t層結點爲止裝入輪船的重量,即部分解(x1, x2 , …, xt)的重量:

在這裏插入圖片描述
當cw(t)>c時,表示該子樹中所有結點都不滿足約束條件,可將該子樹剪去。

算法6.3(1) 裝載問題回溯算法的數據結構

在這裏插入圖片描述
算法6.3(2) 裝載問題回溯算法的實現
在這裏插入圖片描述

算法6.3(3) 剩餘集裝箱的重量r初始化
在這裏插入圖片描述

3 0-1揹包問題

給定一個物品集合s={1,2,3,…,n},物品i的重量是wi,其價值是vi,揹包的容量爲W,即最大載重量不超過W。在限定的總重量W內,我們如何選擇物品,才能使得物品的總價值最大。
輸入
第一個數據是揹包的容量爲c(1≤c≤1500),第二個數據是物品的數量爲n(1≤n≤50)。接下來n行是物品i的重量是wi,其價值爲vi。所有的數據全部爲整數,且保證輸入數據中物品的總重量大於揹包的容量。
當c=0時,表示輸入數據結束。
輸出
對每組測試數據,輸出裝入揹包中物品的最大價值。
在這裏插入圖片描述
令cw(i)表示目前搜索到第i層已經裝入揹包的物品總重量,即部分解(x1, x2 , …, xi)的重量:

在這裏插入圖片描述
對於左子樹, xi =1 ,其約束函數爲:
在這裏插入圖片描述

若constraint(i)>W,則停止搜索左子樹,否則繼續搜索。
對於右子樹,爲了提高搜索效率,採用上界函數Bound(i)剪枝。
令cv(i)表示目前到第i層結點已經裝入揹包的物品價值:
在這裏插入圖片描述

令r(i)表示剩餘物品的總價值:

在這裏插入圖片描述
則限界函數Bound(i)爲:
在這裏插入圖片描述
假設當前最優值爲bestv,若Bound(i)<bestv,則停止搜索第i層結點及其子樹,否則繼續搜索。顯然r(i)越小, Bound(i)越小,剪掉的分支就越多。
爲了構造更小的r(i) ,將物品以單位重量價值比di=vi/wi遞減的順序進行排列:
d1≥d2≥… ≥dn
對於第i層,揹包的剩餘容量爲W-cw(i),採用貪心算法把剩餘的物品放進揹包,根據貪心算法理論,此時剩餘物品的價值已經是最優的,因爲對剩餘物品不存在比上述貪心裝載方案更優的方案。

算法6.4(1) 0-1揹包問題回溯算法的數據結構
在這裏插入圖片描述
對物品以單位重量價值比遞減排序的因子是:
bool cmp(Object a, Object b)
{
if(a.d>=b.d) return true;
else return false;
}
物品的單位重量價值比是在輸入數據時計算的:
for(int i=0; i<n; i++)
{
scanf("%d%d",&Q[i].w,&Q[i].v);
Q[i].d = 1.0*Q[i].v/Q[i].w;
}
使用C++標準模板庫的排序函數sort()排序:
sort(Q, Q+n, cmp);
算法6.4(2) 0-1揹包問題回溯算法的實現

//形參i是回溯的深度,從0開始
void backtrack(int i)
{
  //到達葉子結點時,更新最優值
  if (i+1>n) {bestv = cv; return;}
  //進入左子樹搜索
  if (cw+Q[i].w<=c)
  {
    cw += Q[i].w;
    cv += Q[i].v;
    backtrack(i+1);
    cw -= Q[i].w;
    cv -= Q[i].v;
  }
  //進入右子樹搜索
  if (Bound(i+1)>bestv) backtrack(i+1);
}

算法6.4(3) 限界函數Bound()的實現

//形參i是回溯的深度
int Bound(int i)
{
  int cleft = c-cw;   //揹包剩餘的容量
  int b = cv;      //上界
  //儘量裝滿揹包
  while (i<n && Q[i].w<=cleft)
  {
    cleft -= Q[i].w;
    b += Q[i].v;
    i++;
  }
  //剩餘的部分空間也裝滿
  if (i<n) b += 1.0*cleft*Q[i].v/Q[i].w;
  return b;
}

4 圖的m着色問題

給定無向連通圖G=(V, E)和m種不同的顏色,用這些顏色爲圖G的各頂點着色,每個頂點着一種顏色。是否有一種着色法使G中相鄰的兩個頂點有不同的顏色?
這個問題是圖的m可着色判定問題。若一個圖最少需要m種顏色才能使圖中每條邊連接的兩個頂點着不同顏色,則稱這個數m爲該圖的色數。求一個圖的色數m的問題稱爲圖的m可着色優化問題。
編程計算:給定圖G=(V, E)和m種不同的顏色,找出所有不同的着色法和着色總數。
輸入
第一行是頂點的個數n(2≤n≤10),顏色數m(1≤m≤n)。
接下來是頂點之間的相互關係:a b
表示a和b相鄰。當a,b同時爲0時表示輸入結束。
輸出
輸出所有的着色方案,表示某個頂點塗某種顏色號,每個數字的後面有一個空格。最後一行是着色方案總數。
在這裏插入圖片描述
對m種顏色編號爲1,2,…,m,由於每個頂點可從m種顏色中選擇一種顏色着色,如果無向連通圖G=(V, E)的頂點數爲n,則解空間的大小爲mn種,解空間是非常龐大的,它是一棵m叉樹。
當n=3,m=3時的解空間樹:
在這裏插入圖片描述
圖的m着色問題的約束函數是相鄰的兩個頂點需要着不同的顏色,但是沒有限界函數。
假設無向連通圖G=(V, E)的鄰接矩陣爲a,如果頂點i和j之間有邊,則a[i][j]=1,否則a[i][j]=0。
設問題的解向量爲X (x1, x2 , …, xm) ,其中xi∈{1, 2, …, m},表示頂點i所着的顏色是x[i],即解空間的每個結點都有m個兒子。
在這裏插入圖片描述
算法6.5(1) 圖的m着色問題回溯算法的數據結構
在這裏插入圖片描述

算法6.5(2) 圖的m着色問題回溯算法的實現

//形參t是回溯的深度,從1開始
void BackTrack(int t )
{
  int i;
  //到達葉子結點,獲得一個着色方案
  if( t > n )
  {
    sum ++ ;
    for(i=1; i<=n ;i++)
      printf("%d ",x[i]);
    printf("\n");
  }
  else 
    //搜索當前擴展結點的m個孩子
    for(i=1; i<=m; i++ )
    {
      x[t] = i;
      if( Same(t) ) BackTrack(t+1);
      x[t] = 0;
    }
}

在這裏插入圖片描述
算法6.5(3) 檢查相鄰結點的着色是否一樣的約束函數

//形參t是回溯的深度
bool Same(int t)
{
  int i;
  for(i=1; i<=n; i++ )
    if( (a[t][i] == 1) && (x[i] == x[t]))
      return false;
  return true;
}

算法BackTrack(int t)中,對每個內部結點,其子結點的一種着色是否可行,需要判斷子結點的着色與相鄰的n個頂點的着色是否相同,因此共需要耗時O(mn),而整個解空間樹的內部結點數是:
在這裏插入圖片描述

所以算法BackTrack(int t)的時間複雜度是:
在這裏插入圖片描述

5 n皇后問題

6 旅行商問題

7 流水作業調度問題

8 子集和問題

9 ZOJ1145-Dreisam Equations

10 ZOJ1157-A Plug for UNIX

11 ZOJ1166-Anagram Checker

12 ZOJ1213-Lumber Cutting

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