前言
如果说回溯法是使用深度优先遍历算法,那么分支界限法就是使用广度优先遍历算法。
分支界限法是类似于广度优先的搜索过程,也就是地毯式搜索,主要是在搜索过程中寻找问题的解,当发现已不满足求解条件时,就舍弃该分身,不管了。
它是一种选优搜索法,按选优条件向前广度优先搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就放弃该分身,不进行下一步退回,这种走不通就放弃分身的技术称为分支界限法。
所谓“分支”就是采用广度优先的策略,依次搜索E-结点的所有分支,也就是所有相邻结点,抛弃不满足约束条件的结点,其余结点加入活结点表。然后从表中选择一个结点作为下一个E-结点,继续搜索。
选择下一个E-结点的方式不同,则会有几种不同的分支搜索方式:
- FIFO搜索(使用队列实现):按照先进先出原则选取下一个节点为扩展节点。 活结点表是先进先出队列。
- LIFO搜索(使用栈实现):活结点表是堆栈。
- 优先队列式搜索(使用优先队列实现):按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。活结点表是优先权队列,LC分支限界法将选取具有最高优先级的活结点出队列,成为新的E-结点。
基本思想
在包含问题的所有解的解空间树中,按照广度优先搜索的策略,从根结点出发广度地毯式探索解空间树。对于不同的分支搜索方式要使用不同的数据结构来实现。
当探索到某一结点时,要先判断该结点是否包含问题的解:
- 如果包含,就将该结点的延伸结点加入队列,以便之后遍历;
- 如果该结点不包含问题的解,则直接剪枝放弃该结点的延伸结点。(其实分子界限法就是对隐式图的广度优先搜索算法)
结束条件:
- 若用分子界限法求问题的所有解时,根结点的所有可行的子树都要已被搜索遍才结束。
- 若使用分子界限法求任一个解时,只要搜索到问题的一个解就可以结束。
适用场景
分支界限法一般使用在问题可以树形化表示时的场景。
当你发现你的问题需要用到多重循环,具体几重循环你又没办法确定,那么就可以使用我们的分支界限算法来将循环一层一层的进行遍历。
就像这样:
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);
}
}
}
这样层次遍历的话,无论多少重循环我们都可以满足。
分支界限三步走
由于上述步骤太抽象,所以在这里总结了分支界限三步走:
1、建立状态结点
分支界限法中需要广度优先遍历整个分支树,所以其结点都需要记录下当前的状态,否则到需要进行遍历时我们不能得知此结点的状态,无法进行操作。
与此相对的就是回溯法,回溯法由于是一条路走到底,所以并不需要使用结点记录下当前的状态。
类比:做作业
回溯法:先做完数学作业再做英语作业,我们的思路是完整的,是一步一步顺着来的,不会被遗忘。
分支界限法:做一会数学作业,再做一会英语作业,这样我们为了保证之前做的思路不会遗忘,我们要使用结点记录下当前的状态。
在01背包问题中,我们需要记录的状态是此时背包内物品的重量与价值。所以我们的状态结点为:
/**
* 结点类,一个结点对象对应着一个当前的背包状态
*/
class Node {
public int weight; // 结点所相应的重量
public int value; // 结点所对应的价值
public Node() {
}
public Node(int weight, int value) {
this.weight = weight;
this.value = value;
}
}
2、明确所有分支(选择)
这个构思路径最好用树形图表示。
例如:走迷宫有上下左右四个方向,也就是说我们站在一个点处有四种选择,我们可以画成无限向下延伸的四叉树。直到向下延伸到叶子节点,那里便是出口;从根节点到叶子节点沿途所经过的节点就是我们满足题目条件的选择。
在01背包问题中,每个物品都有2个选择,0不放入背包,1放入背包,两条路,二叉树。
3、寻找界限条件
每一个分支都需要进行判断,判断是否到达了界限,如果到达界限那么我们就无需再进行下去了,直接剪枝放弃该分支。
比如说,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;
}
总结
分支界限法是一个比较有意思的算法思想,我们的人生不就是这样一棵二叉树吗?从出生开始最终走向终点,不同的道路决定不同的终点,在每个分岔口不妨试着用分支界限法的思想帮助我们做出判断,快速走向最美好的人生。对于每次选择,我们不妨先算一算它的最优和最差结果,对于最优结果,我们可以想一想它值不值得我们付出精力去做,对于最差结果,我们想一想能不能承担得了,或许不需要结果,在计算的过程中突然就有了答案。
写在最后
最后,欢迎大家扫码关注下面公众号,回复“C100”可获得一份神秘技术资料!