實驗內容
本實驗要求基於算法設計與分析的一般過程(即待求解問題的描述、算法設計、算法描述、算法正確性證明、算法分析、算法實現與測試),通過回溯法的在實際問題求解實踐中,加深理解其基本原理和思想以及求解步驟。求解的問題為0-1背包。
作為挑戰:可以考慮回溯法在其他問題(如最大團問題、旅行商、圖的m着色問題)。
實驗目的
- 理解回溯法的核心思想以及求解過程(確定解的形式及解空間組織,分析出搜索過程中的剪枝函數即約束函數與限界函數)。
- 掌握對幾種解空間樹(子集樹、排列數、滿m叉樹)的回溯方法。
- 從算法分析與設計的角度,對0-1背包問題的基於回溯法求解有更進一步的理解。
實驗結果
步驟1:描述與分析
給定n種物品和一個背包。物品\(i\)的重量是\(w_i\) ,其價值為\(v_i\),背包的容量為W。一種物品要不全部裝入背包,要不不裝入背包,不允許部分物品裝入的情況。裝入背包的物品的總重量不能超過背包的容量,在這種情況下,問如何選擇轉入背包的物品,使得裝入背包的物品的總價值最大?需要采用回溯的方法進行問題的求解。
分析:
(1)問題的解空間:
將物品裝入背包,有且僅有兩個狀態。第\(i\)種物品對應\((x_1,x_2,...,x_n)\),其中\(x_i\)可以取0或1,分別代表不放入背包和放入背包。解空間有\(2^n\)種可能解,也就是\(n\)個元素組成的集合的所有子集的個數。采用一顆滿二叉樹來講解空間組織起來,解空間樹的深度為問題的規模\(n\)。
(2)約束條件:
(3)限界條件:
0-1背包問題的可行解不止一個,而目標是找到總價值最大的可行解。因此需要設置限界條件來加速找出最優解的速度。如果當前是第t個物體,那么1-t物體的狀態都已經被確定下來,剩下就是t+1~n物體的狀態,采用貪心算法計算當前剩余物品所能產生的最大價值是否大於最優解,如果小於最優解,那么被剪枝掉。
步驟2:策略以及數據結構
采用回溯法進行問題的求解,也就是具有約束函數/限界條件的深度優先搜索算法。
采用回溯法特有框架:
回溯算法()
如果到達邊界:
記錄當前的結果,進行處理
如果沒有到達邊界:
如果滿足限界條件:(左子樹)
進行處理
進行下一層的遞歸求解
將處理回退到處理之前
如果不滿足限界條件:(右子樹)
進行下一層遞歸處理
步驟3
描述算法。希望采用源代碼以外的形式,如偽代碼或流程圖等;
偽代碼:
遞歸式回溯算法:
BACKTRACK-REC(t) //t為擴展結點在樹中所處的層次
if t > n //已到葉子結點,輸出結果
OUTPUT(x)
//檢查擴展結點的每個分支。s(n,t)與e(n,t)分別為當前擴展結點處未搜索過
//的子樹的起始編號和終止編號
else
for i from s(n,t) to e(n,t)
x[t] = h(i) // h[i]: 在當前擴展結點處 x[t]的第i個可選值
if CONSTRAINT(t) && BOUND(t) //約束函數與限界函數
BACKTRACK-REC(t+1) //進入t+1層搜索
迭代式回溯算法:
BACKTRACK-ITE()
t = 1 //t為擴展結點在樹中所處的層次
while t > 0:
if s(n,t) <= e(n,t)
for i from s(n,t) to e(n,t)
do x[t] = h(i) //h[i]: 在當前擴展結點處x[t]的第i個可選值
if CONSTRINT(t) && BOUND(t) //滿足約束限界條件
if t > n //已到葉子結點,輸出結果
OUTPUT(x);
else
t ++ //前進到更深層搜索
else
t -- //回溯到上一層的活結點
步驟4
算法的正確性證明。需要這個環節,在理解的基礎上對算法的正確性給予證明
回溯算法適用條件:多米諾性質
假設解向量是n維的,則下面的k滿足:\(0<k<n ,P(x_1,x_2,x_3,…,x_{k+1})\)為解的部分向量可以推得\(P(x_1,x_2,x_3,…,x_k)\)也為解的部分向量
在0-1背包問題中,解空間為:\((x_1,x_2,...,x_n)\), 如果當前結果\(P_1 = (x_1,x_2,...,x_n)\)是最優解,那么\(P_2=(x_1,x_2,...,x_{n-1})\)的時候,也就是減少一個物品但不改變背包容量的時候,可以想到\(P_2\)依然是該問題的最優解。從子集樹角度來看,也就是最后一層結點全部去掉后的結果,那么當前結果也是最優的。
步驟5
算法復雜性分析,包括時間復雜性和空間復雜性;
算法的復雜性分析:
時間復雜度:$$ T(n)=O(2^n)+O(n2^n)+O(nlog(n)) = O(n2^n)$$
空間復雜度:$$O(nlog(n))$$
步驟6
算法實現與測試。附上代碼或以附件的形式提交,同時貼上算法運行結果截圖;
# -*- coding: utf-8 -*-
"""
Created on Mon Oct 22 08:49:13 2018
@author: pprp
"""
BV=0 # best value
CW=0 # current weight
CV=0 # current value
BX=None # best x result
def output(x):
for i in x:
print(" ",i,end="")
print()
class node(object):
def __init__(self,v,w):
self.v = v
self.w = w
self.per = float(v)/float(w)
def Bound(t):
print("bound:",t)
LC = c-CW # left C
B = BV # best value
#sort
nodes = []
for i in range(n):
nodes.append(node(v[i],w[i]))
nodes.sort(key=lambda x:x.per,reverse=True)
# 裝入背包
while t < n and w[t] <= LC:
LC -= w[t]
B += v[t]
t += 1
if t < n:
B += float(v[t])/float(w[t]) * LC
return B
def backtrack(t,n):
"""當前在第t層"""
print("current:",t)
global BV,CV,CW,x,BX
if t >= n:
if BV < CV:
BV=CV
BX=x[:]
else:
if CW+w[t] <= c: # 搜索左子樹,約束條件
x[t]=True
CW += w[t]
CV += v[t]
backtrack(t+1,n)
CW -= w[t]
CV -= v[t]
if Bound(t) > BV: # 搜索右子樹
x[t]=False
backtrack(t+1,n)
if __name__ == "__main__":
n=10
c=10
x=[False for i in range(n)]
w=[2,2,6,5,4,4,3,4,6,3]
v=[6,3,5,4,6,2,8,3,1,7]
backtrack(0,n)
print("Best Value :",BV)
print("Best Choice:",BX)
運行結果:
驗證:6+3+8+7=24
實驗總結
回溯法的思想:
能進則進,不進則換,不換則退.
回溯算法的框架:
以DFS的方式進行搜索,在搜索的過程中用剪枝條件(限界函數)避免無效搜索。約束函數,在擴展結點處剪去得不到可行解的子樹;限界函數:在擴展結點處剪去得不到最優解的子樹。
回溯算法求解問題的一般步驟:
1、 針對所給問題,定義問題的解空間,它至少包含問題的一個(最優)解。
2 、確定易於搜索的解空間結構,使得能用回溯法方便地搜索整個解空間 。
3 、以深度優先的方式搜索解空間,並且在搜索過程中用剪枝函數避免無效搜索。
常用剪枝函數:
用約束函數在擴展結點處剪去不滿足約束的子樹;
用限界函數剪去得不到最優解的子樹。
子集樹、滿m叉樹、排列樹區別:
子集樹:從n個元素的集合S中找到滿足某種性質的子集時,相應的解空間樹就成為了子集樹(典型問題:01背包問題)
滿m叉樹:所給問題中每一個元素均有m中選擇,要求確定其中的一種選擇,使得對這n個元素的選擇結果組成的向量滿足某種性質(經典問題:圖的m着色問題)
排列樹:從n個元素的排列樹中找出滿足某種性質的一個排列的時候,相應的解空間樹稱為排列樹(經典問題:TSP問題,n皇后問題)