本文内容来源链接:https://blog.csdn.net/yinlili2010/article/details/39313035
本博主做了应当编辑工作,并对代码做了注释
分支定界算法的原理
1 广度优先搜索
介绍分支界定法之前需要了解一下广度优先搜索breadth-First-search(BFS)。
1、从图中某个顶点V0出发,并访问此顶点;以层为顺序,一层一层往下遍历。
2、从V0出发,访问V0的各个未曾访问的邻接点W1,W2,…,Wk;然后,依次从W1,W2,…,Wk出发访问各自未被访问的邻接点。扩展标签。
宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图(即枚举),直到找到结果为止。
BFS,其英文全称是Breadth First Search。 BFS并不使用经验法则算法。从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的队列中。一般的实验里,其邻居节点尚未被检验过的节点会被放置在一个被称为 open 的容器中(例如队列或是链表)list, vector或者是最优值的小根堆大根堆,而被检验过的节点则被放置在被称为 closed 的容器中。(open-closed表)
广度搜索的判断重复如果直接判断十分耗时,我们一般借助哈希表来优化时间复杂度。
2 分支定界算法
分支界定法一般是广度优先搜索的优化
分支界定算法是一种在问题的解空间树上搜索问题的解方法。一般如果有n层, 那么解空间树的叶节点有2 power n, 那么分支界定法就是使用限定条件,不断对这颗解空间树进行剪枝, 直到最后一层。
分支界定法采用广度优先或者最小耗费优先的方法搜索解空间树。 并且,在分解界定法中, 每一个活结点只有一次机会会成为扩展节点。他的搜索策略是:
1.产生当前扩展节点的所有孩子节点, 并且在活结点链表中删除当前拓展节点;对于一般的求最优值和资源问题是二叉树, 对于求最短路径这样的图论问题, 是一般的树。
2. 在产生的孩子节点中,剪掉那些不可能产生可行解(或最优解)的节点;(使用限定条件,即约束条件,如比当前最优上界还大的下界)。
3.将其余所有的孩子节点加入活节点链表;
4.从活结点表中选择下一个活结点作为新的扩展节点;
5. 遇到层结束标记(采取不同的数据结构实现可能不同, 加入新的结束标记)
如此循环,直到找到问题的可行解(或最优解)或者活结点表为空。
分支界定法的思想是:首先确定目标值的上下界, 边搜索边减掉搜索树的某些枝, 提高搜索效率。
分支界定法在两个方面加速了算法的搜索效率, 一是在选择扩展节点时, 总是选择一个最小成本的节点, 尽可能早的进入最有可能成为最优解的分支;二是扩展节点的过程,舍弃导致不可行解或导致非最优解的子节点。
3 算例及代码
使用C++实现FIFO的分支界定算法,
3.1 集装箱问题(装箱问题1,求极值)
3.1.1 问题描述和程序
现在有一个集装箱, 能装30的容量, 有分别为10, 15, 20的货物, 问如何能最大化集装箱的载重货物??
我们知道答案为30
#include <iostream>
#include <list>
using namespace std;
#define MAX 3//一共三个货物
const int CAP = 30;//最大容量
const int box[MAX] = {10,15,20};//三箱货物
int main()
{
int temp = 0, level = -1, best = 0;//best是最优解
int curVal = 0, parentVal = 0, expectVal = 0;//parent是队列种pop出来的父节点,他能产生两个子节点。
//curVal是左子树的目标值,expectVal是右子树的目标值,表示预期目标值。curVal和expectValue都能用来剪枝。
list<int> queue; //队列
queue.push_back(-1);//-1 表示一层分层用
queue.push_back( parentVal ); //当前点,父节点
do{
parentVal = queue.front();//先进先出,当前分的点
queue.pop_front();//根据先进先出原则取第一元素,并且删除。
if( parentVal != -1){//表示在层间, 每一个parentVal可以分两个点left child 和right child
//left child,若加入该box[level], 加入box[level]为true
curVal = parentVal + box[level];//根据约束条件1(最大容量)剪枝, 可以不满足约束条件的没有再进行分支
if( curVal > best && curVal <= CAP ){//如果当前最优值比全局最优值好,并且满足约束条件,则更新全局最优解
best = curVal;
std::cout <<"Current BestValue:\t" << best << endl;
if(level < MAX - 1){//如果不是最后节点,更新后的子节点加入队列,可以继续进行分支
queue.push_back(curVal);
}//end if level
}//end if CurVal 左节点结束,左子树的值为CurVal
//right child
temp = 0;
curVal = parentVal;
for(int i = level + 1; i < MAX; i++){//不包括 i = level,级不加入box[level],即加入box[level为 false
temp += box[i];
}
expectVal = curVal + temp;//期望值为不加上当前 level的值,剩下的可能最大值, 计算右子树的值 expectVal
std::cout << "Expect Value:\t" << expectVal << endl;
//预计最大值若大于当前最佳值,则加入队列;否则不加入。即剪枝
if(expectVal > best && level < MAX - 1){
queue.push_back(curVal);
}
for(list<int>::iterator ite = queue.begin(); ite != queue.end(); ite++){//打印当前列
std::cout<<"\t"<<*ite;
}
std::cout << endl;
}//end if parentVal
else{//表示层结束,加入层标志。层标志主要是用来区分left child 和 right child
if(level < MAX-1)
{
queue.push_back(-1);
}
level++;
}
}while(level != MAX && queue.empty() != true);//到最后一层或者是队列为空结束//end do
std::cout <<"Final BestValue:\t" << best << endl;
system("pause");
return 0;
}
3.1.2 程序分析
我们分析一下该程序。
根据程序设计概率的基本概念:程序 = 数据结构 + 算法。我们从数据结构和算法两个角度分析一下该分支定界算法。
3.1.2.1 数据结构
分支定界的数据结构是一个二叉树。二叉树的遍历采用了一个list来实现。按照先进先出的顺序遍历各节点。
还可以直接用queue来实现。
然后需要定义两类数据:1 分层标志(-1);2 节点(curVal , parentVal, expectVal);3 目标值(lower bound, best)。节点又分为父节点(parentVal),左子点(curVal),右子点(expectVal)。
每个点有一下属性:目标值,剪枝条件。
对左子点 curVal来说 level 和 CAP是其剪枝条件, curVal是下界。
对又子点expectVal来说 level和 best 是其剪枝条件, expectVal是上界。
在本程序中所谓剪枝即不在分枝,不往队列中加新的点。
3.1.2.2 算法
初始化分支约束条件:已装箱子的个数(level)和 全局最优解(best),3个结点(父,左子,右子)
初始化队列
do{
从队列中取出一个点,该点为父节点。
if 该点是还未分的点
生成左子点。
计算左子点目标值。(最优解的下界)
if 左子点还可再分(未触发剪枝条件,容量限制 和 箱子数量)
左子点加入到队尾
生成右子点。
计算右子点目标值。(最优解的上界)
if 右子点还可再分(未触发剪枝条件,潜在最大目标值比左子树当前目标值大,即剪掉了上界比下界还小的枝)
右子点加入到队尾
else
if 箱子还没有装完
在队尾加入新的分层
}while(箱子个数已达到或对队列为空)
3.2 装船问题(装箱问题2,求可行解)
有两艘船,n个货箱。第一艘船的载重量是c1,第二艘船的载重量是c2,wi是货箱 i 的重量,且 w1 + w2 +……+ wn ≤ c1 + c2。 确定是否有一种可将所有n 个货箱全部装船的方法。若有的话,找出该方法。
3.2.1 问题分析
我们先将一个求可行解问题转化为求极值问题。将所有货箱装到两艘船上的一种可行方法是一艘船尽量装到最满,剩下的货物装到另一艘船上。因此,问题转化为,使船 i 装到最大的方案。
3.2.2 代码
#include <iostream>
#include <queue>
using namespace std;
int w[100]; //最大箱子数量为100
int n; //箱子数量
int bestw = 0;
queue<int> Queue;//存储当前活节点,即需要分支的节点
void AddLiveNode(int wt, int i)//wt 当前已装箱的货物量
{
if (i == n)//是叶子, 需要结束
{
if (wt > bestw)
bestw = wt;
}
else//不是叶子
{
Queue.push(wt);
}
}
int MaxLoading(int c)
{
// 初始化活结点队列,标记分层
Queue.push(-1);
int level = 0;
int currentw = 0;
while(!Queue.empty())
{
if(currentw != -1)
{
if(currentw + w[level] <= c) //左节点,限定条件为current + wi < c1
{
AddLiveNode(currentw + w[level], level); //物品i可以装载
}
AddLiveNode(currentw, level); //右孩子总是可行的,不装载物品i
//取下一个E-结点
currentw = Queue.front();
Queue.pop();
cout<<"current:\t"<<currentw<<endl;
}
else //到达层的尾部
{
if (Queue.empty())
{
cout<<"best:\t"<< bestw <<endl;
return bestw;
}
//添加分层标记
Queue.push(-1);
//取下一个E-结点
currentw = Queue.front();
Queue.pop();
cout<<"current next level:\t"<< currentw <<endl;
level++;
}//ew的
}
}
void main( )
{
int c1, c2; int sum=0;
scanf("%d", &c1);
scanf("%d", &c2);
scanf("%d", &n);
cout<<"n:\t"<< n<< endl;
for(int i = 0; i<n; i++)
{
scanf("%d", w+i);
sum = sum + w[i];
}
if (sum<=c1 || sum<=c2)
{
cout<<"need only one ship "<<endl;
}
else if (sum > c1+c2)
{
cout<<"no solution. "<<endl;
}
else{
MaxLoading(c1);
if ((sum-bestw) <= c2)
{
cout<<"The first ship loading:\t"<< bestw <<endl;
cout<<"The second ship loading:\t"<< sum-bestw <<endl;
}
else
{
cout<<"no solution!"<<endl;
}
}
system("pause");
}
3.2.2.3 算法分析
改代码稍微有点复杂。主要分为两部分,主函数和子函数。我们适用金字塔方式层层将代码进行分析。
主函数部分就是把问题转化为极值问题。
主函数
void main{
对所有货物的重量进行求和
if(货物总重 <= 船1的载重量 || 货物总重 <= 船2的载重量)
一定优解。即用一艘船装完所有货物。
else if(货物总重 > 两艘船的总载重量)
一定无解。
else{
需要判断是否有解。
将在船1中装入尽可能多的货物(这是背包问题)。
if(剩余货物重量 < 船2的装载量)
有解。解为船1中装的货物,船2中装入剩下的货物。
else
无解。
}
}
在一堆货物中挑选货物使将船1装的货物量最大
void KnapsackProblem(c1){ // c1为背包的容量
初始化分支约束条件:已装箱子的个数(level)和 全局最优解(best),
3个结点(父,左子,右子)//在本例中,3个结点用一个节点currentw表示了。因为他们是线性关系
初始化队列
do{
从队列中取出一个点,该点为父节点。
if 该点是还未分的点,即活节点。
生成左子点。
计算左子点目标值。(最优解的下界)
if 左子点还可再分(未触发剪枝条件,容量限制 和 箱子数量)
左子点加入到队尾
生成右子点。
计算右子点目标值。(最优解的上界)
if 右子点还可再分(未触发剪枝条件,潜在最大目标值比左子树当前目标值大,即剪掉了上界比下界还小的枝)
右子点加入到队尾
else
if 箱子还没有装完
在队尾加入新的分层
}while(箱子个数已达到或对队列为空)
}
————————————————
版权声明:本文为CSDN博主「Diehard_Yin」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yinlili2010/article/details/39313035