前面咱们介绍了一下回溯法的使用。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对应的解便是问题的最优解,搜索结束。
总结:
剪枝函数给出每一个可行结点相应的子树可能得到的最大价值的上界。
如这个上界不会比当前最优值更大,则能够剪去相应的子树。
也可将上界函数肯定的每一个结点的上界值做为优先级,以该优先级的非增序抽取当前扩展结点。由此可快速得到最优解。
分支界限法一直是我比较喜欢的算法思想,咱们的人生不就是这样一棵二叉树吗?从出生开始最终走向终点,不一样的道路决定不一样的终点,在每一个分岔口不妨试着用分支界限法的思想帮助咱们作出判断,快速走向最美好的人生。对于每次选择,咱们不妨先算一算它的最优和最差结果,对于最优结果,咱们能够想想它值不值得咱们付出精力去作,对于最差结果,咱们想想能不能承担的了,或许不须要结果,在计算的过程当中忽然就有了答案。。。