回溯法-01揹包問題之一:遞迴模式
一、回溯法
回溯法是一個既帶有系統性又帶有跳躍性的搜尋演算法。它在包含問題的所有解的解空間樹中按照深度優先的策略,從根節點出發搜尋解空間樹。演算法搜尋至解空間樹的任一節點時,總是先判斷該節點是否肯定不包含問題的解。如果肯定不包含,則跳過對以該節點為根的子樹的系統搜尋,逐層向其原先節點回溯。否則,進入該子樹,繼續按深度優先的策略進行搜尋。
運用回溯法解題通常包含以下三個步驟:
· 針對所給問題,定義問題的解空間;
· 確定易於搜尋的解空間結構;
· 以深度優先的方式搜尋解空間,並且在搜尋過程中用剪枝函式避免無效搜尋;
二、01揹包問題描述
01揹包問題,即向容量為M的揹包裝載物品,要麼放入要麼不放入。從n個物品中選取裝入揹包的物品,物品i的重量為Wi,價值為Pi。最佳裝載指裝入的物品價值最高,即∑PiXi(i=1..n)取最大值。約束條件為∑WiXi≤M且Xi∈[0,1](1≤i≤n)。
在這個表示式中,需求出Xi的值。Xi=1表示物品i裝入揹包,Xi=0表示物品i不裝入揹包。
· 即判斷可行解的約束條件是:∑WiXi≤M(i=0..n),Wi>0,Xi∈[0,1](1≤i≤n)
· 目標最大值:max∑PiXi(i=0..n-1),Pi>0,Xi=0或1(0≤i<n)
0/1揹包問題是一個自己選取問題,適合於用子集樹表示0/1揹包問題的解空間。在搜尋解空間樹時,只要左兒子節點是一個可行節點,搜尋就進入左子樹,在右子樹中有可能包含最優解才進入右子樹搜尋,否則將右子樹剪去。
三、關於剪枝函式
設當前剩餘物品價值總和為r,當前結點x價值為cp,當前x結點上界函式值為bp。L為當前已搜尋到的答案結點中受益的最大值,當cp+r=bp<L時可剪去以X為根的子樹。
計算右子樹中解上界方法是將剩餘物品按單位重量價值排序,一次放入物品直至裝不下為止,再裝入部分未裝入物品直至裝滿揹包,由此得到的價值是右子樹解上界。
四、遞迴實現
如下圖1所示為01揹包問題遞迴實現的示意圖,圖2是01揹包問題遞迴實現的流程圖,描述了程式碼實現方案。
圖1 01揹包問題遞迴描述圖 圖2 01揹包問題遞迴實現流程圖
圖1比較容易理解,是否已拿完物品也就是i<n(i是當前物品序號,n是物品總數目)是否成立,如果成立則遞迴結束並列印輸出路徑。如果i<n則判斷第i個物品能否放入揹包,如果不能放入,則考慮放置i+1個物品,如果能放入還存在當前第i個放入和不放入兩種情形後對第i+1個的影響。注意在“放入還是不放入”的部分可考慮加入剪枝函式。
五、遞迴的程式碼實現
程式碼1 Main函式測試程式碼:
public static void Main (string[] args)
{
Obj[] objs = new Obj[4];
objs[0] = new Obj() { Weight = 7, Price = 42 };
objs[1] = new Obj() { Weight = 3, Price = 12 };
objs[2] = new Obj() { Weight = 4, Price = 40 };
objs[3] = new Obj() { Weight = 5, Price = 25 };
Backtracking_Recursion1 r = new Backtracking_Recursion1();
r.W = 10;
r.objs = objs;
r.Backtracking(0);
Console.Read();
}
程式碼2Obj物品程式碼
public class Obj
{
/// <summary>
/// 物品重量
/// </summary>
public int Weight
{
get;
set;
}
/// <summary>
/// 物品價值
/// </summary>
public int Price
{
get;
set;
}
/// <summary>
/// 該物品是否放入包內
/// </summary>
internal bool Selected
{
get;
set;
}
}
程式碼3遞迴實現01揹包問題
class Backtracking_Recursion1
{
#region field
protected int m_currentWeight = 0;
protected int m_currentPrice = 0;
#endregion
#region property
/// <summary>
/// 揹包容量
/// </summary>
/// <value>預設20</value>
public int W
{
get;
set;
}
public int n
{
get
{
return objs == null ? 0 : objs.Length;
}
}
/// <summary>
/// 物品,包括重量/價值和數量
/// </summary>
/// <value>The objects.</value>
public Obj[] objs
{
get;
set;
}
#endregion
public void Backtracking(int i)
{
if (i >= n)
{
Printing();
return;
}
if (objs[i].Weight + m_currentWeight <= W)
{
m_currentWeight += objs[i].Weight;
m_currentPrice += objs[i].Price;
objs[i].Selected = true;
Backtracking(i + 1);
m_currentPrice -= objs[i].Price;
m_currentWeight -= objs[i].Weight;
}
objs[i].Selected = false;
Backtracking(i + 1);
}
/// <summary>
/// 輸出路徑
/// </summary>
protected void Printing()
{
Console.Write("<");
for (int i = 0; i < objs.Length; i++)
{
Console.Write(objs[i].Selected ? "1 " : "0 ");
}
Console.WriteLine(">, price: " + m_currentPrice.ToString()
+ "\t weight: " + m_currentWeight.ToString());
}
}
六執行結果
注:
1 程式碼3中Printing()函式呼叫後可判斷並記錄最優路徑;
2 下文將講述01揹包問題回溯法的順序執行方法,並通過模板模式整合兩種不同的實現方案。