本實驗要求基於算法設計與分析的通常過程(即待求解問題的描述、算法設計、算法描述、算法正確性證實、算法分析、算法實現與測試),經過回溯法的在實際問題求解實踐中,加深理解其基本原理和思想以及求解步驟。求解的問題爲0-1揹包。python
做爲挑戰:能夠考慮回溯法在其餘問題(如最大團問題、旅行商、圖的m着色問題)。c++
給定n種物品和一個揹包。物品\(i\)的重量是\(w_i\) ,其價值爲\(v_i\),揹包的容量爲W。一種物品要不所有裝入揹包,要不不裝入揹包,不容許部分物品裝入的狀況。裝入揹包的物品的總重量不能超過揹包的容量,在這種狀況下,問如何選擇轉入揹包的物品,使得裝入揹包的物品的總價值最大?須要採用回溯的方法進行問題的求解。算法
分析:數據結構
(1)問題的解空間:app
將物品裝入揹包,有且僅有兩個狀態。第\(i\)種物品對應\((x_1,x_2,...,x_n)\),其中\(x_i\)能夠取0或1,分別表明不放入揹包和放入揹包。解空間有\(2^n\)種可能解,也就是\(n\)個元素組成的集合的全部子集的個數。採用一顆滿二叉樹來說解空間組織起來,解空間樹的深度爲問題的規模\(n\)。框架
(2)約束條件:
\[ \sum_{i=1}^nw_ix_i \le W \]
(3)限界條件:函數
0-1揹包問題的可行解不止一個,而目標是找到總價值最大的可行解。所以須要設置限界條件來加速找出最優解的速度。若是當前是第t個物體,那麼1-t物體的狀態都已經被肯定下來,剩下就是t+1~n物體的狀態,採用貪心算法計算當前剩餘物品所能產生的最大價值是否大於最優解,若是小於最優解,那麼被剪枝掉。測試
採用回溯法進行問題的求解,也就是具備約束函數/限界條件的深度優先搜索算法。spa
採用回溯法特有框架:
回溯算法() 若是到達邊界: 記錄當前的結果,進行處理 若是沒有到達邊界: 若是知足限界條件:(左子樹) 進行處理 進行下一層的遞歸求解 將處理回退處處理以前 若是不知足限界條件:(右子樹) 進行下一層遞歸處理
描述算法。但願採用源代碼之外的形式,如僞代碼或流程圖等;
僞代碼:
遞歸式回溯算法:
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 -- //回溯到上一層的活結點
算法的正確性證實。須要這個環節,在理解的基礎上對算法的正確性給予證實
回溯算法適用條件:多米諾性質
假設解向量是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\)依然是該問題的最優解。從子集樹角度來看,也就是最後一層結點所有去掉後的結果,那麼當前結果也是最優的。
算法複雜性分析,包括時間複雜性和空間複雜性;
算法的複雜性分析:
時間複雜度:\[ T(n)=O(2^n)+O(n2^n)+O(nlog(n)) = O(n2^n)\]
空間複雜度:\[O(nlog(n))\]
算法實現與測試。附上代碼或以附件的形式提交,同時貼上算法運行結果截圖;
# -*- 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的方式進行搜索,在搜索的過程當中用剪枝條件(限界函數)避免無效搜索。約束函數,在擴展結點處剪去得不到可行解的子樹;限界函數:在擴展結點處剪去得不到最優解的子樹。
回溯算法求解問題的通常步驟:
一、 針對所給問題,定義問題的解空間,它至少包含問題的一個(最優)解。
2 、肯定易於搜索的解空間結構,使得能用回溯法方便地搜索整個解空間 。
3 、以深度優先的方式搜索解空間,而且在搜索過程當中用剪枝函數避免無效搜索。
經常使用剪枝函數:
用約束函數在擴展結點處剪去不知足約束的子樹;
用限界函數剪去得不到最優解的子樹。
子集樹、滿m叉樹、排列樹區別:
子集樹:從n個元素的集合S中找到知足某種性質的子集時,相應的解空間樹就成爲了子集樹(典型問題:01揹包問題)
滿m叉樹:所給問題中每個元素均有m中選擇,要求肯定其中的一種選擇,使得對這n個元素的選擇結果組成的向量知足某種性質(經典問題:圖的m着色問題)
排列樹:從n個元素的排列樹中找出知足某種性質的一個排列的時候,相應的解空間樹稱爲排列樹(經典問題:TSP問題,n皇后問題)