前面咱們介紹了一下回溯法的使用。node
如今咱們來給你們介紹一下它的好朋友——分支界限法。算法
若是說回溯法是使用深度優先遍歷算法,那麼分支界限法就是使用廣度優先遍歷算法。數組
深度優先遍歷能夠只使用一個屬性來存放當前狀態,可是廣度優先遍歷就不能夠了,因此廣度優先遍歷的節點必須用來存儲當前狀態,一個節點表明一個當前狀態,而一條邊就表明了一次操做,A狀態通過一條邊(操做)變爲B狀態。數據結構
我在寫這篇文章的時候搜遍了網上各類各樣的分支界限法來解決01揹包問題,看各個代碼都要一兩百行,都是優化以後的最優化版分支界限法,這樣是不利於新手進行理解的,因此我在此寫了一個最初級的分支界限法解決01揹包問題,能夠看到要不了50行就能解決01揹包問題。ide
對於分支界限法,網上有不少種解釋,這裏我依照本身的(死宅)觀點作了如下兩種通俗易懂的解釋:函數
正經版解釋:所謂「分支」就是採用廣度優先的策略,依次搜索E-結點的全部分支,也就是全部相鄰結點,拋棄不知足約束條件的結點,其他結點加入活結點表。而後從表中選擇一個結點做爲下一個E-結點,繼續搜索。優化
動漫版解釋:看過火影忍者的都知道,主角擁有影分身的能力,若是主角使用影分身從一個點出發,前往不一樣的分支,主角的運動速度相同的狀況下,同一時刻時分支的深度也應該相同,有的分身走到死路,有的分身達到界限沒法進行下去,當分身沒法進行下去時,那麼就解除該分身,直接放棄掉這個分身,固然,確定也會有分身成功到達目的地找到最優解,這與咱們今天要講的分支界限法極其類似。this
PS:雛田黨大獲全勝!spa
分支界限算法:是相似於廣度優先的搜索過程,也就是地毯式搜索,主要是在搜索過程當中尋找問題的解,當發現已不知足求解條件時,就捨棄該分身,無論了。
它是一種選優搜索法,按選優條件向前廣度優先搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就放棄該分身,不進行下一步退回,這種走不通就放棄分身的技術稱爲分支界限法。3d
所謂「分支」就是採用廣度優先的策略,依次搜索E-結點的全部分支,也就是全部相鄰結點,拋棄不知足約束條件的結點,其他結點加入活結點表。而後從表中選擇一個結點做爲下一個E-結點,繼續搜索。
選擇下一個E-結點的方式不一樣,則會有幾種不一樣的分支搜索方式。不用感到恐慌,其實這幾種不一樣的搜索方式很好實現,只須要換一下不一樣的數據結構容器便可。
FIFO搜索(使用隊列實現):按照先進先出原則選取下一個節點爲擴展節點。 活結點表是先進先出隊列。
LIFO搜索(使用棧實現):活結點表是堆棧。
優先隊列式搜索(使用優先隊列實現):按照優先隊列中規定的優先級選取優先級最高的節點成爲當前擴展節點。 活結點表是優先權隊列,LC分支限界法將選取具備最高優先級的活結點出隊列,成爲新的E-結點。
Java中的優先隊列PriorityQueue對元素採用的是堆排序,頭是按指定排序方式的最小元素。堆排序只能保證根是最大(最小),整個堆並非有序的。
優先隊列PriorityQueue是Queue接口的實現,能夠對其中元素進行排序,能夠放基本的包裝類型或自定義的類,對於基本類型的包裝類,優先隊列中元素的默認排列順序是升序,可是對於自定義類來講,須要自定義比較類
上圖爲01揹包問題的解空間樹,若是當前點不符合要求就放棄,直接剪枝。
在許多能使用回溯法的問題時,均可以使用分支界限法,算是給讀者一個新的思路去解決問題。
在包含問題的全部解的解空間樹中,按照廣度優先搜索的策略,從根結點出發廣度地毯式探索解空間樹。對於不一樣的分支搜索方式要使用不一樣的數據結構來實現。
當探索到某一結點時,要先判斷該結點是否包含問題的解:
結束條件:
分支界限法通常使用在問題能夠樹形化表示時的場景。
這樣說明的話可能有點抽象,那麼咱們來換個方法說明。
當你發現,你的問題須要用到多重循環,具體幾重循環你又沒辦法肯定,那麼就能夠使用咱們的分支界限算法來將循環一層一層的進行遍歷。
就像這樣:
void LevelOrder(BiTree T) { InitQueue(Q); //初始化輔助隊列 BiTNode *p; EnQueue(Q, T); //將根結點入隊 while(!IsEmpty(Q)) { //隊列不空循環 DeQueue(Q, p); //隊頭元素出隊,出隊指針纔是用來遍歷的遍歷指針 visit(p); //訪問當前p所指向結點 if(p->lchild != NULL) { //左子樹不空,則左子樹入隊列 EnQueue(Q, p->lchild); } if(p->rchild != NULL) { //右子樹不空,則右子樹入隊列 EnQueue(Q, p->rchild); } } }
這樣層次遍歷的話,不管多少重循環咱們均可以知足。
因爲上述網上的步驟太抽象了,因此在這裏我本身總結了分子界限三步走:
編寫檢測函數:檢測函數用來檢測此路徑是否知足題目條件,是否能經過。
這步不作硬性要求。。不必定須要
創建狀態結點:分支界限法中須要廣度優先遍歷整個分支樹,因此其結點都須要記錄下當前的狀態,不然到須要進行遍歷時咱們不能得知此結點的狀態,沒法進行操做。
與此相對的就是回溯法,回溯法因爲是一條路走到底,因此並不須要使用結點記錄下當前的狀態。
類比:作做業
回溯法:先作完數學做業再作英語做業,咱們的思路是完整的,是一步一步順着來的,不會被遺忘。
分支界限法:作一會數學做業,再作一會英語做業,這樣咱們爲了保證以前作的思路不會遺忘,咱們要使用結點記錄下當前的狀態。
明確全部分支(選擇):這個構思路徑最好用樹形圖表示。
例如:走迷宮有上下左右四個方向,也就是說咱們站在一個點處有四種選擇,咱們能夠畫成無限向下延伸的四叉樹。
直到向下延伸到葉子節點,那裏即是出口;
從根節點到葉子節點沿途所通過的節點就是咱們知足題目條件的選擇。
尋找界限條件:每個分支都須要進行判斷,判斷是否到達了界限,若是到達界限那麼咱們就無需再進行下去了,直接剪枝放棄該分支。
好比說,01揹包中的界限條件就是,在將物品放置進揹包前,要進行判斷放入揹包是否會形成超重,若是不超重,那就能夠放入揹包。
第一步,寫出檢測函數,來檢測這個路徑是否知足條件,是否能經過。
這個函數依據題目要求來編寫,固然,若是要求不止一個,可能須要編寫多個檢測函數。
分支界限法中須要廣度優先遍歷整個分支樹,因此其結點都須要記錄下當前的狀態,不然到須要進行遍歷時咱們不能得知此結點的狀態,沒法進行操做。
與此相對的就是回溯法,回溯法因爲是一條路走到底,因此並不須要使用結點記錄下當前的狀態。
類比:作做業
回溯法:先作完數學做業再作英語做業,咱們的思路是完整的,是一步一步順着來的,不會被遺忘。
分支界限法:作一會數學做業,再作一會英語做業,這樣咱們爲了保證以前作的思路不會遺忘,咱們要使用結點記錄下當前的狀態。
在01揹包問題中,咱們須要記錄的狀態是此時揹包內物品的重量與價值。因此咱們的狀態結點爲:
/** * 結點類,一個結點對象對應着一個當前的揹包狀態 */ class Node { public int weight; // 結點所相應的重量 public int value; // 結點所對應的價值 public Node() { } public Node(int weight, int value) { this.weight = weight; this.value = value; } }
這個構思路徑最好用樹形圖表示。
例如:走迷宮有上下左右四個方向,也就是說咱們站在一個點處有四種選擇,咱們能夠畫成無限向下延伸的四叉樹。
直到向下延伸到葉子節點,那裏即是出口;
從根節點到葉子節點沿途所通過的節點就是咱們知足題目條件的選擇。
第三步,要知道這個結點有幾個選擇,即 幾叉樹。
在01揹包問題中,每一個物品都有2個選擇,0不放入揹包,1放入揹包,兩條路,二叉樹。
每個分支都須要進行判斷,判斷是否到達了界限,若是到達界限那麼咱們就無需再進行下去了,直接剪枝放棄該分支。
好比說,01揹包中的界限條件就是,在將物品放置進揹包前,要進行判斷放入揹包是否會形成超重,若是不超重,那就能夠放入揹包。
前面咱們肯定了一個結點有兩條分支,一個是不裝入揹包,一個是裝入揹包。咱們如今須要爲每一個分支尋找它們的界限條件。
不裝入揹包固然沒有什麼界限條件,而裝入揹包則須要判斷,若是放入揹包是否會形成超重,若是不超重,那就能夠放入揹包。
代碼以下:
// 不放此p號物品的狀態 queue.add(new Node(nowBagNode.weight, nowBagNode.value)); // 放置此p號物品的狀態 if (nowBagNode.weight + weights[p] < maxWeight) { nowBagNode.weight += weights[p]; nowBagNode.value += values[p]; p++; queue.add(new Node(nowBagNode.weight, nowBagNode.value)); maxValue = nowBagNode.value > maxValue? nowBagNode.value : maxValue; }
完整的代碼我放在下面的實例中了。
假定有N=4件商品,分別用A、B、C、D表示。每件商品的重量分別爲3kg、2kg、5kg和4kg,對應的價值分別爲66元、40元、95元和40元。現有一個揹包,能夠容納的總重量位9kg,問:如何挑選商品,使得揹包裏商品的總價值最大?
2二、20、1九、10
答案:
暴力破解法:
因爲暴力破解法不是咱們本章的重點,因此代碼再此掠過,只留下示意圖
我在寫這篇文章的時候搜遍了網上各類各樣的分支界限法來解決01揹包問題,看各個代碼都要一兩百行,都是優化以後的最優化版分支界限法,這樣是不利於新手進行理解的,因此我在此寫了一個最初級的分支界限法解決01揹包問題,能夠看到要不了50行就能解決01揹包問題。
/** * 結點類,一個結點對象對應着一個當前的揹包狀態 */ class Node { public int weight; // 結點所相應的重量 public int value; // 結點所對應的價值 public Node() { } public Node(int weight, int value) { this.weight = weight; this.value = value; } } public class Bag01 { public int maxWeight = 9; // 揹包的最大容量 public int maxValue = 0; // 揹包內的最大價值總和 /** * 分支界限法 * @param weights 全部物品的重量數組 * @param values 全部物品的價值數組 */ public void f(int[] weights, int[] values) { Queue<Node> queue = new ArrayDeque<>(); Node node = new Node(); // 放入一個初始結點,結點狀態均爲0 queue.add(node); int p = 0; // 物品指針位置 while (!queue.isEmpty()) { // 取出當前結點的揹包狀態 Node nowBagNode = queue.remove(); // 若是物品沒有放完 if (p < weights.length) { // 不放此p號物品的狀態 queue.add(new Node(nowBagNode.weight, nowBagNode.value)); // 放置此p號物品的狀態,若是放入超重了,那就不能放 if (nowBagNode.weight + weights[p] < maxWeight) { nowBagNode.weight += weights[p]; nowBagNode.value += values[p]; p++; queue.add(new Node(nowBagNode.weight, nowBagNode.value)); maxValue = nowBagNode.value > maxValue? nowBagNode.value : maxValue; } } } System.out.println(maxValue); } public static void main(String[] args) { int[] weights = {2, 3, 5, 4}; int[] values = {66, 40, 95, 40}; Bag01 bag01 = new Bag01(); bag01.f(weights, values); } }
程序運行結果:
161
若是你想換一種搜索方式,那麼你能夠把上面的隊列換成堆棧或者優先隊列試試。
我還想看一下他們的代碼,寫一個優化版本。
這裏咱們的上界估算是結合貪心算法的優先隊列(剪枝)
這裏咱們仍是拿01揹包問題來舉例子。
須要注意的是,這裏的優先隊列是一我的爲的概念,你也能夠指定屬於本身的優先級排列方式,只要言之有理,能讓速度加快便可。
結合貪心算法,這裏咱們假設可以只拿物品的一部分把揹包塞滿,每次從隊列中取出上限值最大的一個結點(即 從該結點出發到葉子節點在理想狀況下可能獲得的最大價值)
注意是「可能獲得的最大價值」,真實狀況下因爲每件商品只能總體選擇或者不選,所以價值總和老是小於等於該最大上限值,並且隨着道路的不斷前進,該最優值老是不斷減少,愈來愈接近真實值,當走徹底程考慮完全部商品時,該最優值就變成了真實值。
也就是說,價值上限=節點現有價值+揹包剩餘容量*剩餘物品的最大單位重量價值
如今咱們計算出它們的性價比,咱們每次都選取單位重量下價值最大的那個物品,而且假定咱們能夠只選取物品的一部分。
商品 | 重量 | 價值 | 性價比 |
---|---|---|---|
A | 3 | 66 | 22 |
B | 2 | 40 | 20 |
C | 5 | 95 | 19 |
D | 4 | 40 | 10 |
性價比:A>B>C>D
最大上限值的計算,就拿A結點來舉例好了:
第一步:如實計算已選道路:在此道路中A是必選的
第二步:貪婪算法計算未知道路。
咱們依次選取出性價比最高的物品:
因此A結點的上限爲182。
遍歷方法:
咱們每次都從優先隊列中取出上限值最大的一個結點,依次加入該結點的子節點進行遍歷,直到彈出的上限值(即 最大最優價值)爲某一葉子節點(即 結果),此葉子節點即爲得到揹包最大價值的最優組合方式,由於它比其餘道路最優的狀況還要好,那麼它必定大於其餘道路的真實價值。
物品類:
public class Knapsack implements Comparable<Knapsack> { /*物品重量*/ private int weight; /*物品價值*/ private int value; /*單位重量價值*/ private int unitValue; public Knapsack(int weight, int value){ this.weight = weight; this.value = value; this.unitValue = (weight == 0) ? 0 : value/weight; } public int getWeight(){ return weight; } public void setWeight(int weight){ this.weight = weight; } public int getValue(){ return value; } public void setValue(int value){ this.value = value; } public int getUnitValue(){ return unitValue; } @Override public int compareTo(Knapsack snapsack) { int value = snapsack.unitValue; if (unitValue > value) return 1; if (unitValue < value) return -1; return 0; } }
當前狀態結點:
/*當前操做的節點,放入物品或不放入物品*/ class Node { /*當前放入物品的重量*/ private int currWeight; /*當前放入物品的價值*/ private int currValue; /*不放入當前物品可能獲得的價值上限*/ private int upperLimit; /*當前操做物品的索引*/ private int index; public Node(int currWeight, int currValue, int index) { this.currWeight = currWeight; this.currValue = currValue; this.index = index; } }
實現:
public class ZeroAndOnePackage { /*物品數組*/ private Knapsack[] knapsacks; /*揹包承重量*/ private int totalWeight; /*物品數*/ private int num; /*能夠得到的最大價值*/ private int bestValue; public ZeroAndOnePackage(Knapsack[] knapsacks, int totalWeight) { super(); this.knapsacks = knapsacks; this.totalWeight = totalWeight; this.num = knapsacks.length; /*物品依據單位重量價值進行排序*/ Arrays.sort(knapsacks, Collections.reverseOrder()); } public int getBestValue() { return bestValue; } /*價值上限=節點現有價值+揹包剩餘容量*剩餘物品的最大單位重量價值 *當物品由單位重量的價值從大到小排列時,計算出的價值上限大於全部物 *品的總重量,不然小於物品的總重量當放入揹包的物品愈來愈來越多時, *價值上限也愈來愈接近物品的真實總價值 */ private int getPutValue(Node node) { /*獲取揹包剩餘容量*/ int surplusWeight = totalWeight - node.currWeight; int value = node.currValue; int i = node.index; while (i < this.num && knapsacks[i].getWeight() <= surplusWeight) { surplusWeight -= knapsacks[i].getWeight(); value += knapsacks[i].getValue(); i++; } /*當物品超重沒法放入揹包中時,能夠經過揹包剩餘容量*下個物品單位重量的價值計算出物品的價值上限*/ if (i < this.num) { value += knapsacks[i].getUnitValue() * surplusWeight; } return value; } public void findMaxValue() { LinkedList<Node> nodeList = new LinkedList<Node>(); /*起始節點當前重量和當前價值均爲0*/ nodeList.add(new Node(0, 0, 0)); while (!nodeList.isEmpty()) { /*取出放入隊列中的第一個節點*/ Node node = nodeList.pop(); // 若是當前結點的上限大於等於最大價值而且結點索引小於物品總數,那就能夠進行操做 // 不然,沒啥操做的必要,上限都沒當前最大價值大,何須操做呢 if (node.upperLimit >= bestValue && node.index < num) { /*左節點:該節點表明物品放入揹包中,上個節點的價值+本次物品的價值爲當前價值*/ int leftWeight = node.currWeight + knapsacks[node.index].getWeight(); int leftValue = node.currValue + knapsacks[node.index].getValue(); Node left = new Node(leftWeight, leftValue, node.index + 1); /*放入當前物品後能夠得到的價值上限*/ left.upperLimit = getPutValue(left); /*當物品放入揹包中左節點的判斷條件爲保證不超過揹包的總承重*/ if (left.currWeight <= totalWeight && left.upperLimit > bestValue) { /*將左節點添加到隊列中*/ nodeList.add(left); if (left.currValue > bestValue) { /*物品放入揹包不超重,且當前價值更大,則當前價值爲最大價值*/ bestValue = left.currValue; } } /*右節點:該節點表示物品不放入揹包中,上個節點的價值爲當前價值*/ Node right = new Node(node.currWeight, node.currValue,node.index + 1); /*不放入當前物品後能夠得到的價值上限*/ right.upperLimit = getPutValue(right); if (right.upperLimit >= bestValue) { /*將右節點添加到隊列中*/ nodeList.add(right); } } } } public static void main(String[] args) { Knapsack[] knapsack = new Knapsack[] { new Knapsack(2, 13),new Knapsack(1, 10), new Knapsack(3, 24), new Knapsack(2, 15), new Knapsack(4, 28), new Knapsack(5, 33), new Knapsack(3, 20),new Knapsack(1, 8)}; int totalWeight = 12; ZeroAndOnePackage zeroAndOnePackage = new ZeroAndOnePackage(knapsack, totalWeight); zeroAndOnePackage.findMaxValue(); System.out.println("最大價值爲:"+zeroAndOnePackage.getBestValue()); } }
設有n個物體和一個揹包,物體i的重量爲wi價值爲pi ,揹包的載荷爲M, 若將物體i(1<= i <=n)裝入揹包,則有價值爲pi . 目標是找到一個方案, 使得能放入揹包的物體總價值最高.
設N=3, W=(16,15,15), P=(45,25,25), C=30(揹包容量)
能夠經過畫分支限界法狀態空間樹的搜索圖來理解具體思想和流程
每一層按順序對應一個物品放入揹包(1)仍是不放入揹包(0)
步驟:
用一個隊列存儲活結點表,初始爲空
A爲當前擴展結點,其兒子結點B和C均爲可行結點,將其按從左到右順序加入活結點隊列,並捨棄A。
按FIFO原則,下一擴展結點爲B,其兒子結點D不可行,捨棄;E可行,加入。捨棄B
C爲當前擴展結點,兒子結點F、G均爲可行結點,加入活結點表,捨棄C
擴展結點E的兒子結點J不可行而捨棄;K爲可行的葉結點,是問題的一個可行解,價值爲45
當前活結點隊列的隊首爲F, 兒子結點L、M爲可行葉結點,價值爲50、25
G爲最後一個擴展結點,兒子結點N、O均爲可行葉結點,其價值爲25和0
活結點隊列爲空,算法結束,其最優值爲50
注意:活結點就是不可再進行擴展的節點,也就是兩個兒子尚未所有生成的節點
步驟:
用一個極大堆表示活結點表的優先隊列,其優先級定義爲活結點所得到的價值。初始爲空。
由A開始搜索解空間樹,其兒子結點B、C爲可行結點,加入堆中,捨棄A。
B得到價值45,C爲0. B爲堆中價值最大元素,併成爲下一擴展結點。
B的兒子結點D是不可行結點,捨棄。E是可行結點,加入到堆中。捨棄B。
E的價值爲45,是堆中最大元素,爲當前擴展結點。
E的兒子J是不可行葉結點,捨棄。K是可行葉結點,爲問題的一個可行解價值爲45。
繼續擴展堆中惟一活結點C,直至存儲活結點的堆爲空,算法結束。
算法搜索獲得最優值爲50,最優解爲從根結點A到葉結點L的路徑(0,1,1)。
應用貪心法求得近似解爲(1, 0, 0, 0),得到的價值爲40,這能夠做爲0/1揹包問題的下界。
如何求得0/1揹包問題的一個合理的上界呢?考慮最好狀況,揹包中裝入的所有是第1個物品且能夠將揹包裝滿,則能夠獲得一個很是簡單的上界的計算方法:
b=W×(v1/w1)=10×10=100。因而,獲得了目標函數的界[40, 100]。
因此咱們定義限界函數爲:
\[ub=v+(W+w)*(v_{i+1}/w_{i+1})\]
再來畫狀態空間樹的搜索圖:
步驟:
在根結點1,沒有將任何物品裝入揹包,所以,揹包的重量和得到的價值均爲0,根據限界函數計算結點1的目標函數值爲10×10=100;
在結點2,將物品1裝入揹包,所以,揹包的重量爲4,得到的價值爲40,目標函數值爲40 + (10-4)×6=76,將結點2加入待處理結點表PT中;在結點3,沒有將物品1裝入揹包,所以,揹包的重量和得到的價值仍爲0,目標函數值爲10×6=60,將結點3加入表PT中;
在表PT中選取目標函數值取得極大的結點2優先進行搜索;
在結點4,將物品2裝入揹包,所以,揹包的重量爲11,不知足約束條件,將結點4丟棄;在結點5,沒有將物品2裝入揹包,所以,揹包的重量和得到的價值與結點2相同,目標函數值爲40 + (10-4)×5=70,將結點5加入表PT中;
在表PT中選取目標函數值取得極大的結點5優先進行搜索;
在結點6,將物品3裝入揹包,所以,揹包的重量爲9,得到的價值爲65,目標函數值爲65 + (10-9)×4=69,將結點6加入表PT中;在結點7,沒有將物品3裝入揹包,所以,揹包的重量和得到的價值與結點5相同,目標函數值爲40 + (10-4)×4=64,將結點6加入表PT中;
在表PT中選取目標函數值取得極大的結點6優先進行搜索;
在結點8,將物品4裝入揹包,所以,揹包的重量爲12,不知足約束條件,將結點8丟棄;在結點9,沒有將物品4裝入揹包,所以,揹包的重量和得到的價值與結點6相同,目標函數值爲65;
因爲結點9是葉子結點,同時結點9的目標函數值是表PT中的極大值,因此,結點9對應的解便是問題的最優解,搜索結束。
總結:
剪枝函數給出每一個可行結點相應的子樹可能得到的最大價值的上界。
如這個上界不會比當前最優值更大,則能夠剪去相應的子樹。
也可將上界函數肯定的每一個結點的上界值做爲優先級,以該優先級的非增序抽取當前擴展結點。由此可快速得到最優解。
分支界限法一直是我比較喜歡的算法思想,咱們的人生不就是這樣一棵二叉樹嗎?從出生開始最終走向終點,不一樣的道路決定不一樣的終點,在每一個分岔口不妨試着用分支界限法的思想幫助咱們作出判斷,快速走向最美好的人生。對於每次選擇,咱們不妨先算一算它的最優和最差結果,對於最優結果,咱們能夠想想它值不值得咱們付出精力去作,對於最差結果,咱們想想能不能承擔的了,或許不須要結果,在計算的過程當中忽然就有了答案。。。