日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

0-1 背包问题的 4 种解决方法算法策略

發布時間:2024/2/28 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 0-1 背包问题的 4 种解决方法算法策略 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

蠻力法

遞歸與分治策略

動態規劃

貪心算法

回溯法

分支限界法


前言

0-1 背包是一個經典的問題,而它能用不同的算法思想去解決。恰巧最近在看算法,學習算法就是學習解決問題的思路。現在將 0-1 背包問題與解決方法整理出來,這樣不僅能區分不同的算法思想,還能加深對 0-1 背包問題的理解。雖然有的算法思想并不能解決這一問題,但是為了對算法策略有一個較為整體的了解,所以在這里做一下簡單的介紹。

九種 0-1 背包問題詳解:https://tyler-zx.blog.csdn.net/article/details/107915127

?

蠻力法

蠻力法(brute force method,也稱為窮舉法或枚舉法)是一種簡單直接地解決問題的方法,常常直接基于問題的描述,所以,蠻力法也是最容易應用的方法。但是,用蠻力法設計的算法時間特性往往也是最低的,典型的指數時間算法一般都是通過蠻力搜索而得到的。

蠻力法所依賴的最基本技術是掃描技術,依次處理元素是蠻力法的關鍵,依次處理每個元素的方法是遍歷。雖然設計很高效的算法很少來自于蠻力法,基于以下原因,蠻力法也是一種重要的算法設計技術:

(1) 理論上,蠻力法可以解決可計算領域的各種問題。對于一些非常基本的問題,例如求一個序列的最大元素,計算 n 個數的和等,蠻力法是一種非常常用的算法設計技術。

(2) 蠻力法經常用來解決一些較小規模的問題。如果需要解決的問題規模不大,用蠻力法設計的算法其速度是可以接受的,此時,設計一個更高效算法的代價是不值得的。

(3) 對于一些重要的問題(例如排序、查找、字符串匹配),蠻力法可以產生一些合理的算法,他們具備一些實用價值,而且不受問題規模的限制。

(4) 蠻力法可以作為某類時間性能的底限,來衡量同樣問題的更高效算法。

?

蠻力法解0-1背包問題的基本思想:

對于有 n 種可選物品的 0/1 背包問題,其解空間由長度為 n 的 0-1 向量組成,可用子集數表示。在搜索解空間樹時,深度優先遍歷,搜索每一個結點,無論是否可能產生最優解,都遍歷至葉子結點,記錄每次得到的裝入總價值,然后記錄遍歷過的最大價值。

#include <iostream> #include<cstdio> using namespace std;#define N 100struct goods {int wight;//物品重量int value;//物品價值 };int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,背包容量 int X[N],cx[N];//最終存儲狀態,當前存儲狀態 struct goods goods[N];int Force(int i){if(i>n-1){if(bestValue < cv && cw <= C){for(int k=0;k<n;k++)X[k] = cx[k];//存儲最優路徑bestValue = cv;}return bestValue;}cw = cw + goods[i].wight;cv = cv + goods[i].value;cx[i] = 1;//裝入背包Force(i+1);cw = cw-goods[i].wight;cv = cv-goods[i].value;cx[i] = 0;//不裝入背包Force(i+1);return bestValue; }int main() {printf("物品種類n:");scanf("%d",&n);printf("背包容量C:");scanf("%d",&C);for(int i=0;i<n;i++){printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1);scanf("%d%d",&goods[i].wight,&goods[i].value);}int sum1 = Force(0);printf("蠻力法求解0/1背包問題:\nX=[");for(int i=0;i<n;i++){cout << X[i]<<" ";}printf("] 裝入總價值[%d]\n",sum1);return 0; }

force(0) 開始選擇第一個物品,不管什么情況都從選擇開始(開始遞歸,每個物品都有選或不選的可能,最后通過最大價值來判斷哪些物品選擇并記錄到數組中),然后進 force(1),進入 force(1),一直到 force(n+1),i>n,return 結果,跳出 force(n+1),在 force(n) 處從跳出的地方繼續向下走,就是進入減減減的環節了,然后繼續向下,還是一樣,加到 n+1 時就會跳出來當前的 force,調到前一個 force,繼續向下,循環進行。

蠻力法求解 0/1 背包問題的時間復雜度為:最好情況下為 O(2^n), 最壞情況下為 O(n*2^n)。

?

遞歸與分治策略

分治法的思想是,將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。如果原問題可以分割成 k 個子問題,1<k<=n,且這些子問題都可解,并可利用這些子問題的解求出原問題的解,那么這種分治法就是可行的。

直接或間接地調用自身的算法稱為遞歸算法。用函數自身給出定義的函數稱為遞歸函數。在計算機算法設計與分析中,遞歸技術是十分有用的。使用遞歸技術往往使函數的定義和算法的描述簡捷且易于理解。有些數據結構,如二叉樹等,由于其本身固有的遞歸特征,特別適合用遞歸的形式來描述。另外,還有一些問題,雖然其本身并沒有明顯的遞歸結構,但用遞歸技術來求解,可使設計出的算法簡捷易懂且易于分析。

例如:int factorial(int n) {if(n == 0)return 1;elsereturn n*factorial(n-1); }

每個遞歸函數都必須有非遞歸定義的初始值,否則,遞歸函數就無法計算。

?

分治法的基本思想是將一個規模為 n 的問題分解為 k 個規模較小的子問題,這些子問題互相獨立且與原問題相同。遞歸地解這些子問題,然后將各子問題的解合并得到原問題的解。它的一般的算法設計模式如下:

divide-and-conquer(P) {if(|P| <= n0) adhoc(P);divide P into smaller subinstances P1,P2,...,Pk;for(i = 1; i <= k; i++)yi = divide-and-conquer(Pi);return merge(y1,y2,...,yk); }

其中,|P| 表示問題 P 的規模,n0 為一閾值,表示當問題 P 的規模不超過 n0 時,問題已容易解出,不必再繼續分解。adhoc(P) 是該分治法中的基本子算法,用于直接解小規模的問題 P。當 P 的規模不超過 n0 時,直接用算法 adhoc(P) 求解。算法 merge(y1,y2,...,yk) 是該分治法中的合并子算法,用于將P的子問題 P1,P2,...,Pk 的解 y1,y2,...,yk 合并為 P 的解。

例子:快排,二分查找等。

?

動態規劃

動態規劃算法與分治算法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然后從這些子問題的解得到原問題的解。

與分治法不同的是,適合于用動態規劃法求解的問題,經分解得到的子問題往往不是相互獨立的。

該算法的有效性依賴于問題本身所具有的兩個重要性質:最優子結構性質子問題重疊性質。從一般意義上講,問題所具有的這兩個重要性質是該問題可用動態規劃算法求解的基本要素。這對于在設計求解具體問題的算法時,是否選擇動態規劃算法具有指導意義 。

?

最優子結構

設計動態規劃算法的第一步通常是要刻畫最優解的結構。當問題的最優解包含了其子問題的最優解時,稱該問題具有最優子結構性質。問題的最優子結構性質提供了該問題可用動態規劃算法求解的重要線索。利用問題的最優子結構性質,以自底向上的方式遞歸地從子問題的最優解逐步構造出整個問題的最優解。

?

重疊子問題

可用動態規劃算法求解的問題應具備的另一基本要素是子問題的重疊性質。在用遞歸算法自頂向下解此問題時,每次產生的子問題并不總是新問題,有些子問題被反復計算多次。動態規劃算法正是利用了這種子問題的重疊性質,對每一個子問題只解一次,而后將其解保存在一個表格中,當再次需要解此子問題時,只是簡單地用常數時間查看一下結果。通常,不同的子問題個數隨問題的大小呈多項式增長。因此,用動態規劃算法通常只需要多項式時間,從而獲得較高的解題效率。

?

備忘錄方法

備忘錄方法是動態規劃算法的變形。與動態規劃算法一樣,備忘錄方法用表格保存已解決的子問題的答案,在下次需要解此子問題時,只要簡單地查看該子問題的解答,而不必重新計算。與動態規劃算法不同的是,備忘錄方法的遞歸方式是自頂向下,而動態規劃算法則是自底向上遞歸的。因此備忘錄方法的控制結構與直接遞歸方法的控制結構相同,區別在于備忘錄方法為每個解過的子問題建立了備忘錄以備需要時查看,避免了相同子問題的重復求解。

動態規劃算法適用于解最優化問題。通常可按以下4個步驟設計: (1)找出最優解的性質,并刻畫其結構特征。 (2)遞歸地定義最優值。 (3)以自底向上的方式計算出最優值。 (4)根據計算最優值時得到的信息,構造最優解。

例子:0-1 背包問題、數組求斐波那契數列。

?

動態規劃解 0-1 背包問題的基本思想:

對于每個物品我們可以有兩個選擇,放入背包,或者不放入,有 n 個物品,故而我們需要做出 n 個選擇,于是我們設 F[i][v] 表示做出第 i 次選擇后,所選物品放入一個容量為 v 的背包獲得的最大價值。現在我們來找出遞推公式,對于第 i 件物品,有兩種選擇,放或者不放。

① 如果放入第 i 件物品,則 F[i][v] = F[ i-1 ][ v-w[i] ] + p[i] ,表示,前 i-1 次選擇后所選物品放入容量為 v-w[i] 的背包所獲得最大價值為 F[ i-1 ][ v-w[i] ] ,加上當前所選的第 i 個物品的價值 p[i] 即為 F[i][v] 。

② 如果不放入第 i 件物品,則有 F[i][v] = F[ i-1 ][ v ],表示當不選第i件物品時,F[i][v] 就轉化為前 i-1 次選擇后所選物品占容量為 v 時的最大價值 F[i-1][v]。則:F[i][v] = max{ F[ i-1 ][v], F[ i-1 ][ v-w[i] ]?+ p[i] }

#include <iostream> #include<cstdio> #define N 100 #define MAX(a,b) a < b ? b : a using namespace std;struct goods{int wight;//物品重量int value;//物品價值 };int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,背包容量 int X[N],cx[N];//最終存儲狀態,當前存儲狀態 struct goods goods[N];int KnapSack(int n,struct goods a[],int C,int x[]){int V[N][N+1];for(int i = 1; i <= n; i++)for(int j = 1; j <= C; j++)if(j < a[i-1].wight)V[i][j] = V[i-1][j];elseV[i][j] = MAX(V[i-1][j],V[i-1][j-a[i-1].wight] + a[i-1].value);for(int i = n,j = C; i > 0; i--){if(V[i][j] > V[i-1][j]){x[i-1] = 1;j = j - a[i-1].wight;}elsex[i-1] = 0;}return V[n][C]; }int main() {printf("物品種類n:");scanf("%d",&n);printf("背包容量C:");scanf("%d",&C);for(int i = 0; i < n; i++){printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1);scanf("%d%d",&goods[i].wight,&goods[i].value);}int sum = KnapSack(n,goods,C,X);printf("動態規劃法求解0/1背包問題:\nX=[");for(int i = 0; i < n; i++)cout<<X[i]<<" ";//輸出所求X[n]矩陣printf("] 裝入總價值[%d]\n", sum);return 0; }

動態規劃法求解 0/1 背包問題的時間復雜度為:O(min{nc,2^n})。

?

貪心算法

貪心算法通過一系列的選擇來得到問題的解。它所做的每一個選擇都是當前狀態下局部最好的選擇,即貪心選擇。對于一個具體的問題,能用貪心算法求解的問題一般具有兩個重要的性質:貪心選擇性質和最優子結構性質。

所謂的貪心選擇性質是指所求問題的整體最優解可以通過一系列局部最優的選擇,即貪心選擇來達到。這是貪心算法可行的第一個基本要素,也是貪心算法與動態規劃算法的主要區別。在動態規劃算法中,每步所做的選擇往往依賴于相關子問題的解。因而只有在解出相關子問題后,才能做出選擇。而在貪心算法中,僅在當前狀態下做出最好選擇,即局部最優選擇。然后去解做出這個選擇后產生的相應的子問題。貪心算法所做的貪心選擇可以依賴于以往所做過的選擇,但絕不依賴于將來所做的選擇,也不依賴于子問題的解。正是由于這種差別,動態規劃算法通常以自底向上的方式解各子問題,而貪心算法則通過以自頂向下的方式進行,以迭代的方式做出相繼的貪心選擇,每做一次貪心選擇就將所求問題簡化為規模更小的子問題。

對于一個具體的問題,要確定它是否具有貪心選擇性質,必須證明每一步所做的貪心選擇最終導致問題的整體最優解。首先考察問題的一個整體最優解,并證明可修改這個最優解,使其以貪心選擇開始。做了貪心選擇后,原問題簡化為規模更小的類似子問題。然后,用數學歸納法證明,通過每一步做貪心選擇,最終可得到問題的整體最優解。其中,證明貪心選擇后的問題簡化為規模更小的類似子問題的關鍵在于利用該問題的最優子結構性質。

?

最優子結構性質

當一個問題的最優解包含其子問題的最優解時,稱此問題具有最優子結構性質。問題的最優子結構性質是該問題可用動態規劃算法或貪心算法求解的關鍵特征。

貪心算法與動態規劃算法的差異,可以從?0-1 背包問題背包問題中看出。對于 0-1 背包問題,貪心算法之所以不能得到最優解是因為在這種情況下,它無法保證最終能將背包裝滿,部分閑置的背包空間使每千克背包空間的價值降低了。事實上,在考慮 0-1 背包問題時,應比較選擇該物品和不選擇該物品所導致的最終方案,然后再做出最好的選擇。由此可導出許多互相重疊的子問題。這正是該問題可用動態規劃算法求解的另一重要特征。

例如:哈夫曼編碼,最小生成樹,大幣找零。

?

回溯法

回溯法有 "通用的解題法"?之稱。用它可以系統地搜索一個問題的所有解或任一解。回溯法是一個既帶有系統性又帶有跳躍性的搜索算法。它在問題的解空間樹中,按深度優先策略,從根節點出發搜索解空間樹。算法搜索至解空間樹的任一結點時,先判斷該結點是否包含問題的解。如果肯定不包含,則跳過對以該結點為根的子樹的搜索,逐層向其祖先結點回溯。否則,進入該子樹,繼續按深度優先策略搜索。回溯法求問題的所有解時,要回溯到根,且根結點的所有子樹都已被搜索才結束。回溯法求問題的一個解時,只要搜索到問題的一個解就可結束。這種以深度優先方式系統搜索問題解的算法稱為回溯法,它適用于解組合數較大的問題。

?

回溯法的算法框架

(1)問題的解空間

用回溯法解問題時,應明確定義問題的解空間。問題的解空間至少應包含問題的一個(最優)解。例如,對于有n中可選物品的0-1背包問題,其解空間由長度為n的0-1向量組成。該解空間包含對變量的所有可能的 0-1 賦值。當 n=3 時,其解空間是{ (0,0,0),(0,1,0),(0,0,1),(1,0,0),(0,1,1),(1,0,1),(1,1,0),(1,1,1) }。定義了問題的解空間后,還應將解空間很好地組織起來,使得能用回溯法方便地搜索整個解空間。通常將解空間組織成樹或圖的形式。

?

(2)回溯法的基本思想

確定了解空間的組織結構后,回溯法從根結點出發,以深度優先方式搜索整個解空間。這個開始結點稱為活結點,同時也稱為當前的擴展結點。在當前的擴展結點處,搜索向縱向移至一個新結點。這個新結點就成為新的活結點,并成為當前擴展結點。如果在當前的擴展結點處不能再縱向移動,則這個結點就是死結點。此時,應該往回移動(回溯)至最近的一個活結點處,并使這個活結點成為當前擴展結點。回溯法以這種工作方式遞歸地在解空間中搜索,直至找到所要求的解或解空間中已無活結點為止。

回溯法搜索解空間樹時,通常采用兩種策略避免無效搜索,提高回溯法的搜索效率。其一是用約束函數在擴展結點處剪去不滿足約束的子樹;其二是用限界函數剪去得不到最優解的子樹。這兩類函數統稱為剪枝函數。

用回溯法解題通常包含以下3個步驟: ①針對所給問題,定義問題的解空間; ②確定易于搜索的解空間結構; ③以深度優先方式搜索解空間,并在搜索過程中用剪枝函數避免無效搜索。

?

(3)遞歸回溯

回溯法對解空間作深度優先搜索,因此在一般情況下可用遞歸函數來實現回溯法。

void Backtrack(int t) {if( t > n) Output(x);elsefor(int i = f(n, t); i <= g(n,t); i++){x[t] = h(i);if(Constraint(t)&&Bound(t)) Backtrack(t+1);} }

其中,形參?t 表示遞歸深度,即當前擴展結點在解空間樹中的深度。n 用來控制遞歸深度,當 t>n 時,算法已搜索到葉結點。此時,有 Output(x) 記錄或輸出得到的可行解 x。算法 Backtrack 的 for 循環中 f(n, t) 和 g(n, t) 分別表示在當前擴展結點處未搜索過的子樹的起始編號和終止編號。h(i) 表示在當前擴展結點處 x[t] 的第 i 個可選值。Constraint(t) 和 Bound(t) 表示在當前擴展結點處的 x[1:t] 的取值滿足問題的約束條件,不滿足問題的約束條件,可剪去響應的子樹。Bound(t) 返回值為 true 時,在當前擴展結點處 x[1:t] 的取值未使目標函數越界,還需由 Backtrack(t+1) 對其相應的子樹進行進一步搜索。否則,當前擴展結點處 x[1:t] 的取值使目標函數越界,可剪去相應的子樹。執行了算法的 for 循環后,已搜索遍當前擴展結點的所有未搜索過的子樹。Backtrack(t) 執行完畢,返回 t-1 層繼續執行,對還沒有測試過的 x[t-1] 的值繼續搜索。當 t=1 時,若已測試完 x[1] 的所有可選值,外層調用就全部結束。顯然,這一搜索過程按深度優先方式進行。調用一次 Backtrack(1) 即可完成整個回溯搜索過程。

?

(4)迭代回溯

采用樹的非遞歸深度優先遍歷算法,也可將回溯法表示為一個非遞歸的迭代過程。

void InerativeBacktrack(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++){x[t] = h(i);if(Constraint(t) && Bound(t)){if(Solution(t)) ?Output(x);else t++;}}else t--;} }

上述迭代回溯算法中,用?Solution(t) 判斷在當前擴展結點處是否已得到問題的可行解。它返回的值為 true 時,在當前擴展結點處 x[1:t] 是問題的可行解。此時,有 Output(x) 記錄或輸出得到的可行解。它返回的值為 false 時,在當前擴展結點處 x[1:t] 只是問題的部分解,還需向縱向繼續搜索。算法中 f(n, t) 和 g(n, t) 分別表示在當前擴展結點處未搜索過的子樹的起始編號和終止編號。h(i) 表示在當前擴展結點處 x[t] 的第 i 個可選值。Constraint(t) 和 Bound(t) 是當前擴展結點處的約束函數和限界函數。Constraint(t) 返回的值為 true 時,在當前擴展結點處 x[1:t] 的取值滿足問題的約束條件,否則不滿足問題的約束條件,可剪去相應的子樹。Bound(t) 返回的值為 true 時,在當前擴展結點處 x[1:t] 的取值未使目標函數越界,還需對其相應的子樹做進一步搜索。否則,當前擴展結點處 x[1:t] 的取值已使目標函數越界,可剪去相應的子樹。算法的 while 循環結束后,完成整個回溯搜索過程。

用回溯法解題的一個顯著特征是在搜索過程中動態產生問題的解空間。在任何時刻,算法只保存從根結點到當前擴展結點的路徑。如果解空間樹中從根結點到葉結點的最長路徑的長度為 h(n),則回溯法所需要的計算空間通常為 O(h(n)) 。而顯式地存儲整個解空間則需要 O(2^h(n)) 或 O(h(n)!) 內存空間。

?

(5)子集樹與排列樹

上面兩棵解空間樹是用回溯法解題時常遇到的兩類經典的解空間樹。當所給的問題是從 n 個元素的集合 S 中找出滿足某種性質的子集時,相應的解空間樹成為子集樹。例如,n 個物品的 0-1 背包問題所對應的解空間樹就是一棵子集樹。這類子集樹通常有 2^n 個葉結點,其結點總個數為 2^(n+1)-1 。遍歷子集樹的任何算法均需要 Ω(2^n) 的計算時間。當所給的問題是確定 n 個元素滿足某種性質的排列時,相應的解空間樹成為排列樹。排列樹通常有 n! 個葉結點。因此遍歷排列樹需要 Ω(n!) 的計算時間。旅行售貨員問題的解空間樹就是一棵排列樹。

用回溯法搜索子集樹的一般算法可描述如下:

void Backtrack(int t) {if( t > n) Output(x);elsefor(int i =0; i <= 1; i++){x[t] = i;if(Constraint(t)&&Bound(t)) Backtrack(t+1);} }

用回溯法搜索排列樹的算法框架可描述如下:

void Backtrack(int t) {if( t > n) Output(x);elsefor(int i =t; i <= n; i++){swap(x[t], x[i]);if(Constraint(t)&&Bound(t)) Backtrack(t+1);swap(x[t], x[i]);} }

例如:0-1 背包問題,n 后問題,旅行商問題等。

?

回溯法解 0-1 背包問題的基本思想:

為了避免生成那些不可能產生最佳解的問題狀態,要不斷地利用限界函數 (bounding function) 來處死那些實際上不可能產生所需解的活結點,以減少問題的計算量。這種具有限界函數的深度優先生成法稱為回溯法。對于有 n 種可選物品的 0/1 背包問題,其解空間由長度為 n 的 0-1 向量組成,可用子集數表示。在搜索解空間樹時,只要其左兒子結點是一個可行結點,搜索就進入左子樹。當右子樹中有可能包含最優解時就進入右子樹搜索。

#include <iostream> #include <cstdio> #include <string.h> #include <algorithm>using namespace std;#define N 100struct goods{int wight;//物品重量int value;//物品價值 };int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,背包容量 int X[N],cx[N];//最終存儲狀態,當前存儲狀態 struct goods goods[N]; int Bound(int i);int BackTrack(int i) {if(i > n-1){if(bestValue < cv){for(int k = 0; k < n; k++)X[k] = cx[k];//存儲最優路徑bestValue = cv;}return bestValue;}if(cw + goods[i].wight <= C){ //進入左子樹cw += goods[i].wight;cv += goods[i].value;cx[i] = 1; //裝入背包BackTrack(i+1);cw -= goods[i].wight;cv -= goods[i].value;}int B = Bound(i+1);if(B > bestValue) //進入右子樹{cx[i] = 0;BackTrack(i+1);}return bestValue; }int Bound(int i) {int cleft = C - cw; //剩余容量int b = cv;//以物品單位重量價值遞減序列裝入物品while(i <= n && goods[i].wight <= cleft){cleft -= goods[i].wight;b += goods[i].value;i++;}//裝滿背包if(i <= n){b += goods[i].value * cleft/goods[i].wight;}return b; }bool m(struct goods a, struct goods b) {return (a.value/a.wight) > (b.value/b.wight); }int KnapSack(int n, struct goods a[], int C,int x[N]) {memset(x, 0, N);sort(a,a+n,m);//將各物品按單位重量價值降序排列BackTrack(0);return bestValue; } int main() {printf("物品種類n:");scanf("%d",&n);printf("背包容量C:");scanf("%d",&C);for(int i = 0; i < n; i++){printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1);scanf("%d%d",&goods[i].wight,&goods[i].value);}int sum = KnapSack(n,goods,C,X);printf("回溯法求解0/1背包問題:\nX=[");for(int i = 0; i < n; i++)cout << X[i] <<" ";//輸出所求X[n]矩陣printf("] 裝入總價值[%d]\n",sum);return 0; }

回溯法求解 0/1 背包問題的時間復雜度為:O(n*2^n)。

?

分支限界法

分支限界法類似于回溯法,也是在問題的解空間上搜索問題解的算法。一般情況下,分支限界法與回溯法的求解目標不同。回溯法的求解目標是找出解空間中滿足約束條件的所有解,而分支限界法的求解目標是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出使某一目標函數值達到極大或極小的解,即某種意義的最優解。

由于求解目標不同,導致分支限界法與回溯法對解空間的搜索方式也不相同。回溯法以深度優先的方式搜索解空間,而分支限界法則以廣度優先或最小消耗優先的方式搜索解空間。分支限界法的搜索策略是,在擴展結點處,先生成其所有的孩子結點(分支),然后再從當前的活結點表中選擇下一個擴展結點。為了有效地選擇下一擴展結點,加速搜索的進程,在每一個活結點處,計算一個函數值(限界),并根據函數值,從當前活結點表中選擇一個最有利的結點作為擴展結點,使搜索朝著解空間上有最優解的分支推進,以便盡快地找出一個最優解。這種方法稱為分支限界法。

?

分支限界法的基本思想

分支限界法常以廣度優先或以最小耗費(最大效益)優先的方式搜索問題的解空間樹。問題的解空間樹是表示問題解空間的一棵有序樹,常見的有子集樹和排列樹。在搜索問題的解空間樹時,分支限界法與回溯法的主要區別在于它們對當前擴展結點所采用的擴展方式不同。在分支限界法中,每一個活結點只有一次機會成為擴展結點。活結點一旦成為擴展結點,就一次性產生其所有孩子結點。在這些孩子結點中,導致不可行解或導致非最優解的孩子結點被舍棄,其余孩子結點被加入活結點表中。此后,從活結點表中取下一結點成為當前擴展結點,并重復上述結點擴展過程。這個過程一直持續到找到所需要的解或活結點表為空時為止。

從活結點表中選擇下一擴展結點的不同方式導致不同的分支限界法。最常見的有以下兩種方式。

(1)隊列(FIFO)分支限界法

隊列式分支限界法將活結點表組織成一個隊列,并按隊列的先進先出原則選取下一個結點為當前擴展結點。

(2)優先隊列式分支限界法

優先隊列式分支限界法將活結點表組織成一個優先隊列,并按優先隊列中規定的結點優先級選取優先級最高的下一個結點成為當前擴展結點。

?

優先隊列中規定的結點優先級常用一個與該結點相關的的數值 p 來表示。結點優先級的高低與p的大小相關。最大優先隊列規定 p 值較大的結點優先級較高。在算法實現時通常用最大堆來實現最大優先隊列,用最大堆的 Deletemax 運算抽取堆中下一個結點成為當前擴展結點,體現最大效益優先的原則。類似地,最小優先隊列規定 p 值越小的結點優先級越高。在算法實現時通常用最小堆來實現最小優先隊列,用最小堆的 Deletemin 運算來抽取堆中下一個結點成為當前擴展結點,體現最小費用優先的原則。

用優先隊列分支限界法解具體問題時,應該根據具體問題的特點確定選用最大優先隊列或最小優先隊列來表示解空間的活結點表。例如,考慮 n=3 時 0-1 背包問題的一個實例如下:w = [16,15,15],p = [45,25,25],c = 30。隊列式分支限界法用一個隊列來存儲活結點表,而優先隊列式分支限界法則將活結點表組成優先隊列并用最大堆來實現該優先隊列。該優先隊列的優先級定義為活結點所獲得的價值。

隊列式分支限界法解此問題時,算法從根結點 A 開始。初始時,活結點隊列為空,結點 A 是當前的擴展結點。結點 A 的 2 個孩子結點 B 和 C 均為可行結點,故將 2 個孩子結點按從左到右的順序加入活動結點隊列(類似于樹的層次遍歷),并且舍棄當前擴展結點 A。依照先進先出的原則,下一個擴展結點是活結點隊列的隊首結點 B。擴展結點 B 有孩子結點 D 和 E。由于 D 是不可行結點,故被舍棄(因為選了質量為 16 的這件物品,而 C=30,所以不能再選質量為 15 的物品了)。E 是可行結點,被加入活結點隊列。接下來,C 成為當前擴展結點,它的 2 個孩子結點 F 和 G 均為可行結點,因此被加入到活結點隊列中。擴展下一個結點 E 得到結點 J 和 K。J 是不可行結點,因此被舍棄。K 是可行的葉結點,表示所求問題的一個可行解,其價值為 45。

當活結點隊列的隊首結點 F 成為下一個擴展結點。它的 2 個孩子結點 L 和 M 均為葉結點。L 表示獲得價值為 50 的可行解;M 表示獲得價值為 25 的可行解。G 是最后一個擴展結點,其孩子結點 N 和 O 均為可行葉結點。最后,活結點隊列已空,算法終止。算法搜索得到的最優值是 50。從這個例子可以看出,隊列式分支限界法搜索解空間樹的方式與解空間樹的廣度優先遍歷算法極為相似。唯一的不同之處是隊列式分支限界法不搜索以不可行結點為根的子樹。

?

優先隊列式分支限界法從根結點 A 開始搜索解空間樹。用一個極大堆表示活結點表的優先隊列。初始時堆為空,擴展結點 A 得到它的 2 個孩子結點 B 和 C 。這兩個結點均為可行結點,因此被加入到堆中,結點 A 被舍棄。結點 B 獲得的當前價值為 45,而結點 C 的當前價值為 0。由于結點B的價值大于結點 C 的價值,所以結點 B 是堆中最大元素,從而成為下一個擴展結點。擴展結點 B 的孩子結點 D 和 E。D 不是可行結點,因而被舍棄。E 是可行結點被加入到堆中。E 的價值為45,成為當前堆中最大元素,從而成為下一個擴展結點。擴展結點 E 得到 2 個葉結點 J 和 K。J 是不可行結點,被舍棄。K 是可行葉結點,表示所求問題的一個可行解,其價值為 45。此時,堆中僅剩下一個活結點 C,它成為當前擴展結點。它的 2 個孩子結點 F 和 G 均為可行結點,因此插入到當前堆中。結點 F 的價值為 25,是堆中最大元素,成為下一個擴展結點。結點 F 的 2 個孩子結點 L 和 M 均為葉結點。葉結點 L 對應價值為 50 的可行解。葉結點 M 對應價值為 25 的可行解。葉結點 L 所對應的解成為當前最優解。最后,結點 G 成為擴展結點,其孩子結點 N 和 O 均為葉結點,它們的價值分別為 25 和 0。接下來,存儲活結點的堆已空,算法終止。算法搜索得到的最優值為 50。對應的最優解是從根結點 A 到葉結點 L 的路徑 (0,1,1)。

例如:0-1 背包問題,單源最短路徑問題,裝載問題等。

?

分支限界法解 0-1 背包問題的基本思想:

首先,要對輸入數據進行預處理,將各物品依其單位重量價值從大到小進行排列。在下面描述的優先隊列分支限界法中,節點的優先級由已裝袋的物品價值加上剩下的最大單位重量價值的物品裝滿剩余容量的價值和。算法首先檢查當前擴展結點的左兒子結點的可行性。如果該左兒子結點是可行結點,則將它加入到子集樹和活結點優先隊列中。當前擴展結點的右兒子結點一定是可行結點,僅當右兒子結點滿足上界約束時才將它加入子集樹和活結點優先隊列。當擴展到葉節點時為問題的最優值。

#include <iostream> #include <algorithm> #include <cstdio> using namespace std;#define N 100 //最多可能物體數 struct goods //物品結構體 {int sign; //物品序號int w; //物品重量int p; //物品價值 }a[N];bool m(goods a,goods b) {return (a.p/a.w) > (b.p/b.w); }int max(int a,int b) {return a<b?b:a; }int n,C,bestP=0,cp=0,cw=0;int X[N],cx[N];struct KNAPNODE //狀態結構體 {bool s1[N]; //當前放入物體int k; //搜索深度int b; //價值上界int w; //物體重量int p; //物體價值 };struct HEAP //堆元素結構體 {KNAPNODE *p; //結點數據int b; //所指結點的上界 };//交換兩個堆元素 void swap(HEAP &a, HEAP &b) {HEAP temp = a;a = b;b = temp; }//堆中元素上移 void mov_up(HEAP H[], int i) {bool done = false;if(i != 1){while(!done && i != 1){if(H[i].b > H[i/2].b){swap(H[i], H[i/2]);}else{done = true;}i = i/2;}} }//堆中元素下移 void mov_down(HEAP H[], int n, int i) {bool done = false;if((2*i)<=n){while(!done && ((i = 2*i) <= n)){if(i+1 <= n && H[i+1].b > H[i].b){i++;}if(H[i/2].b < H[i].b){swap(H[i/2], H[i]);}else{done = true;}}} }//往堆中插入結點 void insert(HEAP H[], HEAP x, int &n) {n++;H[n] = x;mov_up(H,n); }//刪除堆中結點 void del(HEAP H[], int &n, int i) {HEAP x, y;x = H[i]; y = H[n];n --;if(i <= n){H[i] = y;if(y.b >= x.b){mov_up(H,i);}else{mov_down(H, n, i);}} }//獲得堆頂元素并刪除 HEAP del_top(HEAP H[], int&n) {HEAP x = H[1];del(H, n, 1);return x; }//計算分支節點的上界 void bound( KNAPNODE* node,int M, goods a[], int n) {int i = node->k;float w = node->w;float p = node->p;if(node->w > M){ //物體重量超過背包載重量node->b = 0; //上界置為0}else{while((w+a[i].w <= M)&&(i < n)){ w += a[i].w; //計算背包已裝入載重p += a[i++].p; //計算背包已裝入價值}if(i<n){node->b = p + (M - w)*a[i].p/a[i].w;}else{node -> b = p;}} }//用分支限界法實現0/1背包問題 int KnapSack(int n,goods a[],int C, int X[]) {int i, k = 0; //堆中元素個數的計數器初始化為0int v;KNAPNODE *xnode, *ynode, *znode;HEAP x, y, z, *heap;heap = new HEAP[n*n]; //分配堆的存儲空間for(i = 0; i < n; i++){a[i].sign=i; //記錄物體的初始編號}sort(a,a+n,m); //對物體按照價值重量比排序xnode = new KNAPNODE; //建立父親結點for(i = 0; i < n; i++){ //初始化結點xnode->s1[i] = false;}xnode->k = xnode->w = xnode->p = 0;while(xnode->k < n) {ynode = new KNAPNODE; //建立結點y*ynode = *xnode; //結點x的數據復制到結點yynode->s1[ynode->k] = true; //裝入第k個物體ynode->w += a[ynode->k].w; //背包中物體重量累計ynode->p += a[ynode->k].p; //背包中物體價值累計ynode->k ++; //搜索深度++bound(ynode, C, a, n); //計算結點y的上界y.b = ynode->b;y.p = ynode;insert(heap, y, k); //結點y按上界的值插入堆中znode = new KNAPNODE; //建立結點z*znode = *xnode; //結點x的數據復制到結點zznode->k ++; //搜索深度++bound(znode, C, a, n); //計算節點z的上界z.b = znode->b;z.p = znode;insert(heap, z, k); //結點z按上界的值插入堆中delete xnode;x = del_top(heap, k); //獲得堆頂元素作為新的父親結點xnode = x.p;}v = xnode->p;for(i = 0; i < n; i++){ //取裝入背包中物體在排序前的序號if(xnode->s1[i]){X[a[i].sign] =1 ;}else{X[a[i].sign] = 0;}}delete xnode;delete heap;return v; //返回背包中物體的價值 }/*測試以上算法的主函數*/ int main() {goods b[N];printf("物品種數n: ");scanf("%d",&n); //輸入物品種數printf("背包容量C: ");scanf("%d",&C); //輸入背包容量for (int i=0;i<n;i++) //輸入物品i的重量w及其價值v{printf("物品%d的重量w[%d]及其價值v[%d]: ",i+1,i+1,i+1);scanf("%d%d",&a[i].w,&a[i].p);b[i]=a[i];}int sum=KnapSack(n,a,C,X);//調用分支限界法求0/1背包問題printf("分支限界法求解0/1背包問題:\nX=[ ");for(int i=0;i<n;i++)cout<<X[i]<<" ";//輸出所求X[n]矩陣printf("] 裝入總價值[%d]\n",sum);return 0; }

分支限界法求解 0/1 背包問題的時間復雜度為:O(2^n)。

參考:

https://www.cnblogs.com/xym4869/p/8513801.html

總結

以上是生活随笔為你收集整理的0-1 背包问题的 4 种解决方法算法策略的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。