回溯法
回溯法的算法框架
1. 綜述
- 從問題的 解空間樹 中,按照 深度優先 的策略,從根節點出發搜索解空間樹。
- 回溯法求所有解時,最終需要回溯到根,并且所有節點的字數都已被搜索遍才結束。求一個解時,遇到一個解便可以結束。
- 回溯法適用于組合數較大的問題。
2. 解空間
- 解空間應該至少包含問題的一個解
- 解空間應該很好地組織起來,通常組織成樹或者圖
3. 基本思想
- 活結點、擴展結點、死結點
- 約束函數剪去不滿足約束條件的子樹
- 限界函數剪去得不到最優解的子樹
- 基本步驟
- 針對所給的問題,定義問題的解空間;
- 確定易于搜索的解空間;
- 以深度優先方式搜索解空間,并在搜索的過程中用剪枝函數避免無效搜索。
4. 遞歸回溯
/*
t:遞歸深度
n:最大深度
f(n,t):當前擴展結點處未搜索過的子樹的起始編碼
g(n,t):當前擴展結點處未搜索過的子樹的終止編碼
Constraint(t):約束函數
Bound(t):限界函數
自頂向下,
對每個結點的分支進行遞歸調用 for(int i=f(n,t);i<=g(n,t);i++)
*/
void Backtrack(int t)
{
if(t > n) Output(x); //是否遞歸結束
else
{
for(int i=f(n,t);i<=g(n,t);i++) //保證所有子樹要不被遍歷,要么被剪枝
{
t=i;
if(Constraint(t)&&Bound(t)) Backtrack(i+1);
}
}
}
5. 迭代回溯
/*
自頂向下,
對每個結點的分支進行迭代 for(int i=f(n,t);i<=g(n,t);i++)
*/
void IterativeBacktrack(void)
{
int t=1;
while(t > 0)
{
if(f(n,t) <= g(n,t))
{
for(int i=f(n,t);i<=g(n,t);i++)
{
t=i;
if(Constraint(t)&&Bound(t))
{
if(Solution(t)) Output(x); //Solution(t)用于判斷問題是都得以解決
else t++;
}
else t--;
}
}
}
}
6. 子集樹
從結合S中尋找滿足某種性質的子集時,相應的解空間樹稱為子集樹,如0-1背包問題。
子集樹一般為完全二叉樹,也就是由“要、不要、要、不要等”形成。
void Backtrack(int t)
{
if(t > n) Output(x); //是否遞歸結束
else
{
for(int i=0;i<=1;i++) //保證所有子樹要不被遍歷,要么被剪枝
{
t=i;
if(Constraint(t)&&Bound(t)) Backtrack(i+1);
}
}
}
7. 排列樹
確定n個元素滿足某種性質的排列時,相應的解空間樹稱為排列樹。排列樹通常有n!個葉結點。例如旅行售貨員問題。
void Backtrack(int t)
{
if(t > n) Output(x);
else
{
for(int i=t;i<=n;i++)
{
Swap(x[t],x[i]);
if(Constraint(t)&&Bound(t)) Backtrack(i+1);
Swap(x[i],x[t]);
}
}
}
貨箱裝載
1. 問題描述
兩艘船,n個貨箱。第一艘載重量c1,第二艘載重量c2。wi是貨箱i的重量,∑wi<=c1+c2。確定一種方法把n個貨箱全部裝上船。
∑wi<=c1=c2,原問題等價于子集之和問題;c1=c2,原問題等價于分割問題。這兩個問題都是NP-復雜問題。
解決辦法 :盡可能將第一艘船轉載到它的轉載極限,在將剩余的裝載到第二艘。
為了將第一艘船盡可能裝滿,需要一個貨箱的子集,使得他們的總重量接近于c1。這個問題可以通過0/1背包問題來解決。
2. 遞歸回溯算法
屬于上述的子集樹解決辦法。
/*
貨箱重量weight[1:numberOfContainers]
rLoad(1):返回<=capacity的最大子集之和
*/
void rLoad(int currentLevel)
{
//從currentLevel處的節點開始搜索
if(currentLevel > numberOfContainers)
{
//到達一個葉節點處
if(weightOfCurrentLoading > maxWeightSoFar)
maxWeightSoFar = weightOfCurrentLoading;
return;
}
//還未到達葉節點,檢查子樹
if(weightOfCurrentLoading + weight[currentLevel] <= capacity)
{
//搜索左子樹,即x[currentLevel]=1
weightOfCurrentLoading += weight[currentLevel];
rLoad(currentLevel + 1);
weightOfCurrentLoading -= weight[currentLevel];
}
//搜索左子樹,即x[currentLevel]=0,既然為0那么可以無需檢查而得以繼續
rLoad(currentLevel + 1);
}
3. 尋找最優子集
增加代碼來尋找到當前的最優子集,為此使用一組數組bestLoadingSoFar,當且僅當bestLoadingSoFar[i]=1時,貨箱i屬于最優子集。
/*
報告最有裝載的預處理程序
*/
int maxLoading(int *theWeight, int theNumberOfContainers, int theCapacity, int *bestLoading)
{
/*
數組theWeight[1:theNumberOfContainers]是貨箱重量
theCapacity是船的載貨量
數組bestLoading[1:theNumberOfContainers]是解
返回最大載重量
*/
//初始化全局變量
numberOfContainers = theNumberOfContainers;
weight =theWeight;
capacity = theCapacity;
weightOfCurrentLoading = 0;
maxWeightSoFar = 0;
currentLoading = new int [numberOfContainers+1];
bestLoadingSoFar = bestLoading;
//remainingWeight的初始值是所有貨箱重量之和
for(int i=1;i<=numberOfContainers;i++)
{
remainingWeight += weight[i];
}
//計算最優裝載的重量
rLoad(1);
return maxWeightSoFar;
}
/*
報告最優裝載的回溯算法
*/
void rLoad(int currentLevel)
{
//從currentLevel處開始搜索
if(currentLevel > numberOfContainers)
{
//到達了一個葉節點,存儲一個更優解
for(int j=1; j <= numberOfContainers; j++)
bestLoadingSoFar[j] = currentLoading[j];
maxWeightSoFar = weightOfCurrentLoading;
return;
}
//沒有到達一個葉節點,檢查子樹
remainingWeight -= weight[currentLevel];
if(weightOfCurrentLoading + weight[currentLevel] <= capacity)
{
//搜索左子樹
currentLoading[currentLevel] = 1;
weightOfCurrentLoading += weight[currentLevel];
rLoad(currentLevel + 1);
weightOfCurrentLoading -= weight[currentLevel];
}
if(weightOfCurrentLoading + remainingWeight > maxWeightSoFar)
{
//搜索右子樹
rLoad(currentLevel + 1);
}
remainingWeight += weight[currentLevel];
}