[問題]給定n種物品和1個揹包,揹包容許的最大重量爲Capacity。物品i的重量爲weight[i],價值爲value[i]。問應當選擇哪些物品裝入揹包,使揹包中的物品的總價值最大?數組
[解析]由於每種物品只有裝入揹包或不裝入揹包兩種選擇,因此該問題稱爲0-1揹包問題。函數
方法1.動態規劃法spa
按照從1到n的順序來依次決定每種物品是否裝入揹包中。設輪到決定物品i時揹包的剩餘容量爲C,此時利用從i開始的全部剩餘物品和揹包的剩餘容量能夠獲得的最大價值爲f(i,C),則指針
(1)若選擇將物品i裝入揹包,則所得的最大價值爲f(i+1,C-weight[i])+value[i]blog
(2)若選擇將物品i不裝入揹包,則所得的最大價值爲f(i+1,C)隊列
因此,f(i,C) = Max{f(i+1,C-weight[i])+value[i],f(i+1,C)}ci
由此可知,該問題具備最優子結構性質。it
方法2.回溯法io
先定義問題的解空間,該問題的解空間由長度爲n的0-1向量組成。該解空間包含對變量的全部0-1賦值。當n=3時,其解空間爲:class
{(0,0,0),(0,0,1),(0,1,0),(0,1,1),(1,0,0),(1,0,1),(1,1,0),(1,1,1)}
而後組織解空間,這裏可採用徹底二叉樹來表示其解空間,以下圖所示。
解空間樹的第i層到第i+1層的邊上的標號給出了0-1變量的值,從樹根到葉子的任一路徑表示解空間中的一個元素。例如,從根節點A到葉節點H的路徑相應於解空間中的元素(1,1,1)
最後從根節點A開始,對解空間樹進行DFS。所選用的剪枝函數爲判斷當前揹包的剩餘容量是否不小於0,屬於約束函數。搜索過程當中每到達一個葉節點,則記錄當前揹包的總價值,並與已找到的最大值進行比較,根據比較結果來更新最大值。
代碼以下:
// 標識編號爲i的物品(編號從0開始)是否裝入揹包,即當前路徑在解空間樹中層次爲i的節點處選擇的是其左連接(true)仍是右連接(false),其中根節點的層次爲0,數組初始化爲全false static bool selected[n]; // 存儲當前已找到的最優裝包方案,初始化爲全false static bool optimal[n]; // 當前已找到的最大價值,初始化爲0 static int maxTotalValue = 0; // 當前揹包的價值,初始化爲0 static int valueOfPackage = 0; // 當前揹包的剩餘容量,初始化爲揹包的容量 static int residualCapacity = Capacity; // 對解空間樹中層次爲i的當前節點進行DFS void BackTrack(int i) { // 檢查是否已到達葉節點 if(i == n) { // 若是已到達葉節點,則對當前路徑所表示的裝包方案進行處理 if(valueOfPackage > maxTotalValue) { // 存儲當前已找到的最優裝包方案 Copy(selected,optimal,n); // 更新最大價值 maxTotalValue = valueOfPackage; } } else // 若是還沒有到達葉節點,則沿着當前節點的左右兩條子連接分別進行DFS { #pragma region 沿着當前節點的左子連接進行DFS residualCapacity -= weight[i]; // 檢查當前節點的左子節點是否知足約束條件,即當前揹包的剩餘容量是否不小於0 if(residualCapacity >= 0) { // 若是知足,則推動到當前節點的左子結點 selected[i] = true; valueOfPackage += value[i]; // 對新的當前節點進行DFS BackTrack(i + 1); // 對新的當前節點進行DFS完畢後,回溯到其父節點,即原來的當前節點 selected[i] = false; valueOfPackage -= value[i]; residualCapacity += weight[i]; } else { // 若是不知足,則直接回溯到當前節點 residualCapacity += weight[i]; } #pragma endregion // 沿着當前節點的右子連接進行DFS BackTrack(i + 1); } }
也可添加一個限界函數進行剪枝,即計算以當前節點爲根的子樹中的全部解的上界(粗略計算爲到達當前節點時揹包已有的價值加上全部未裝入揹包的物品的總價值,更準確的計算方法見下一段),若是所得結果不大於當前已找到的最大價值,則沒必要再對當前節點進行DFS,而應直接回溯。
上面所述的計算以當前節點爲根的子樹中的全部解的上界的方法是粗略的,結果經常會大於準確值。咱們能夠有更準確的方法。初始時就將全部物品按照單位重量的價值遞減的順序排列。對於當前揹包的剩餘容量,咱們假設按照物品的順序依次裝入揹包,當物品i裝入後,再裝物品i+1時,發現揹包的剩餘容量不足,此時揹包的剩餘容量爲residualCapacity,則以當前節點爲根的子樹中的全部解的上界爲揹包中物品的總價值再加上residualCapacity * (value[i+1] / weight[i+1])
方法3.分支限界法
解空間與回溯法相同,所採用的限界函數爲在上一段中所計算的以當前節點爲根的子樹中的全部解的上界。若是當前節點的限界值不大於已找到的最大價值,則說明以當前節點爲根的子樹不可能包含比當前已找到的最優解更優的解,所以能夠剪去。若是到達一個葉節點,則說明其它活節點的限界值均不大於該葉節點所對應的解,即以其它活節點爲根的子樹不可能包含比該葉節點所對應的解更優的解,因此該葉節點所對應的解就是一個最優解。
若是須要得出最優解的構造,則應在搜索過程當中保存當前已構造出的部分解空間樹。這樣當搜索到達葉節點時,能夠在解空間樹中從該葉節點開始向根節點回溯,從而得出相應的最優解的構造。
代碼以下:
enum Child { Left, Right }; // 定義解空間樹中的節點 struct TreeNode { Link parent; // 指向父節點的指針 enum Child child; // 標識是父節點的左孩子仍是右孩子,即當前路徑在解空間樹中層次爲i的節點處選擇的是其左連接仍是右連接,即編號爲i的物品(編號從0開始)是否裝入揹包 }; // 定義活結點優先隊列的大頂堆實現中的節點 struct HeapNode { Link treeNode; // 指向活節點的指針 int UpBound; // 用限界函數對該活結點進行計算所得的上界,即以該節點爲根的子樹中的全部解的上界。用做優先隊列的優先級 int level; // 活結點在解空間樹中所處的層序號,其中根節點的層次爲0 int valueOfPackage; // 活結點處揹包的價值 int residualCapacity; // 活結點處揹包的剩餘容量 }; // 生成新的活結點,並添加到解空間樹和活結點優先隊列中 void AddLiveNode(Link parent,enum Child child,int upBound,int level,int valueOfPackage,int residualCapacity) { Link link = CreateTreeNode(parent,isLeft); HeapNode heapNode = CreateHeapNode(link,upBound,level,valueOfPackage,residualCapacity); PriorityQueueInsert(heapNode); } void Package() { // 存儲找到的最優裝包方案,optimal[i]表示編號爲i的物品(編號從0開始)是否裝入揹包 bool optimal[n]; // 當前已找到的最大價值,初始化爲0 int maxTotalValue = 0; // 解空間樹中的當前節點的限界值,初始化爲根節點的限界值 int upBound = GetUpBound(0,0); // 生成解空間樹的根節點 Link link = AddLiveNode(NULL,Right,upBound,0,0,Capacity); // 只要還沒有搜索到葉節點 while(link->level < n) { link = PriorityQueueDelelteMax() #pragma region 沿着當前節點的左右兩條子連接分別進行分支 #pragma region 沿着當前節點的左子連接進行分支 // 若是當前節點的左子節點知足約束條件,即當前揹包的剩餘容量不小於0,則對它進行分支 if(link->residualCapacity - weight[i] >= 0) { // 當前節點的左子節點的限界值與當前節點的限界值相等,而當前節點的限界值是全部活結點中最大的,因此其左子節點的限界值必不小於已找到的最大價值,因此應將其做爲活結點生成,並添加到解空間樹和活結點優先隊列中 AddLiveNode(link,Left,upBound.level + 1,link->valueOfPackage + value[i],link->residualCapacity - weight[i]); // 若是在新的活結點處揹包的價值大於當前已找到的最大價值 if(link->valueOfPackage + value[i] > maxTotalValue) { // 則更新當前已找到的最大價值 maxTotalValue = link->valueOfPackage + value[i]; } } #pragma endregion #pragma region 沿着當前節點的右子連接進行分支 // 計算當前節點的右子結點的限界值 upBound = GetUpBound(link->valueOfPackage,link->level + 1); // 若是當前節點的右子節點的限界值大於已找到的最大價值 if(upBound > maxTotalValue) { // 則將其做爲活結點生成,並添加到解空間樹和活結點優先隊列中 AddLiveNode(link,Right,upBound.level + 1,link->valueOfPackage,link->residualCapacity); } #pragma endregion } // 若是已到達葉節點,則輸出當前路徑所表示的最優解 for(int level = n - 1;level >= 0;level--) { if(link->child == Left) { optimal[level] = true; } else { optimal[level] = false; } link = link->parent; } }