概念
分支限界法类似于回溯法,是一种在问题的解空间树上搜索问题解的算法。但在一般情况下,分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出满足约束条件的所有解,采用的是深度优先搜索堆栈活结点的所有可行子结点。而分支限界法的求解目标则是采用广度优先策略找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
算法应用
所谓“分支”就是采用广度优先的策略,依次搜索E-结点的所有分支,也就是所有相邻结点,抛弃不满足约束条件的结点,其余结点加入活结点表。 然后从表中选择一个结点作为下一个E-结点,继续搜索。选择下一个E-结点有几种不同的分支搜索方式:
1. 队列式(FIFO)搜索:
按照队列先进先出(FIFO)原则选取下一个结点为扩展结点。
2. 优先队列式搜索:
按照优先队列中规定的优先级选取优先级最高的结点成为当前扩展结点。一般选定一个上界函数确定的每个结点的上界值作为优先级,以该优先级的非增序抽取当前扩展结点。由此可快速获得最优解。
算法步骤
-
求解目标:分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。
-
搜索方式:以广度优先或以最小耗费优先的方式搜索解空间树。分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
-
在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。
-
计算一个函数值(限界) : 为了加速搜索的过程,在每一个活结点处,计算一个函数值,并根据函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使得搜索朝着解空间上最优解的分支进行推进,尽快一遍找出一个最优解。
-
此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。
-
一般分支限界法的解题步骤:
(1)定义问题的解空间
(2)确定问题的解空间组织结构(数或图)
(3)搜索解空间,搜索前要定义判断标准(约束函数或限界函数),如果选用优先队列分支限界法,则必须确定优先级。 -
具体:
(1)对每一活结点计算一个优先级(某些信息的函数值)。根据这些优先级从当前活结点表中优先选择一个优先级最高(最有利)的结点作为扩展结点,使搜索朝着解空间树上有最优解的分支推进,以便尽快地找出一个最优解。
(2)对当前扩展结点,先从左到右地产生它的所有儿子,用约束条件检查,把所有满足约束函数的儿子加入活结点队列中。
(3)再从活结点表中下一个优先级别最高的结点为当前扩展结点,重复上述过程,直到找到一个解或活结点队列为空为止。
算法示例
- 01背包问题:n个物体和一个背包。对物体i,其价值为value,重量为weight,背包的容量为W。如何选取物品装入背包,使背包中所装入的物品总价值最大?
/*有4个物品,其重量分别为(4,7,5,3),价值分别为(40,42,25,12),背包容量为W=13。已知每个物品不可再分割
限界函数:nCurrentValue为当前背包中物品价值,nMaxValue之前背包中装入的最大价值的物品。
nP = bound(i + 1): 第 i个物品之后的物品可装入背包的最大价值。要求:nP +nCurrentValue > nMaxValue.
*/
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
struct Res {
int weight, value;
bool used = false;
};
struct Bags {
vector< Res*> bags;
int nweigt, nvalue = 0, ub;
Bags operator+(Res*& res) {
Bags bag = *this;
bag.bags.push_back(res);
bag.nweigt -= res->weight; bag.nvalue += res->value;
return bag;
}
};
int main() {
Res thing[] = { {4, 40}, { 7,42 }, { 5,25 }, { 3,12 } };
vector<Res*> res;
vector<Bags> result;
for (auto& it : thing)
res.push_back(&it);
int W = 13;
sort(res.begin(), res.end(), [](Res* & a, Res* & b) {return a->value / a->weight > b->value / b->weight; }); //优先排序
Bags start;
int up = W * res.front()->value / res.front()->weight, down = 0;
for (int i = 0, w = W; i < res.size(); i++) { //贪心获取下界
if (res[i]->weight < w) {
down += res[i]->value;
w -= res[i]->weight;
}
}
start.ub = up; start.nweigt = W;
int size, count = 0;
queue<Bags> q;
q.push(start);
while (!q.empty() && count < res.size()) {
size = q.size(); //记录每层的节点数,进行层处理
for (int i = 0; i < size; i++) {
//记录每层的节点数,进行层处理
Bags left;
Bags right = q.front();
if (right.nweigt - res[count]->weight >= 0) {
left = right + res[count]; //左节点
left.ub = left.nvalue + (count + 1 < res.size() ? left.nweigt * (res[count + 1]->value / res[count + 1]->weight) : 0); //无法访问最后一层的count+1,值为0
//q.push(left);
}
right.ub = right.nvalue + (count + 1 < res.size() ? right.nweigt * (res[count + 1]->value / res[count + 1]->weight) : 0); //修改右结点
if ((count+1) == res.size())
{
result.push_back(left);
result.push_back(right);
q.pop();
continue;
}
if (left.ub == 0)
{
q.push(right);
}
else if (right.ub > left.ub) {
q.push(right);
q.push(left);
}
else
{
q.push(left);
q.push(right);
}
q.pop();
}
count++;
}
Bags* minBag = nullptr;
for (int i = 0; i < result.size(); i++) { //在表中寻找最优方案
if (minBag == nullptr || result[i].nvalue > minBag->nvalue)
minBag = &result[i];
}
for (auto& it : minBag->bags)
it->used = true;
cout << "分支限界法求得最大价值为:" << minBag->nvalue << endl;
for (int i = 0; i < res.size(); i++)
cout << "物品" << i + 1 << "是否被选:" << thing[i].used<< endl;
getchar();
return 0;
}
- 单源最短路径问题(Dijkstra最短路径算法算法): 给定一个带权有向图G=(V,E),其中每条边的权是非负数。给定V中的一个顶点,成为源。现在要计算从源到所有其他个顶点的最短路径长度,这里路径长度指的是各边权之和。这个问题通常被称作单源最短路径问题。
分析:
(1)先找一条路径并记录路径长度。在算法扩展结点的过程中,一旦发现一个结点的下界不小于当前找到的最短路长,则算法剪去以该结点为根的子树。
(2)在算法中,利用结点间的控制关系进行剪枝。从源顶点s出发,2条不同路径到达图G的同一顶点。由于两条路径的路长不同,因此可以将路长长的路径所对应的树中的结点为根的子树剪去。
(3)算法思想解单源最短路径问题的优先队列式分支限界法用一极小堆来存储活结点表。其优先级是结点所对应的当前路长。算法从图G的源顶点s和空优先队列开始。结点s被扩展后,它的儿子结点被依次插入堆中。此后,算法从堆中取出具有最小当前路长的结点作为当前扩展结点,并依次检查与当前扩展结点相邻的所有顶点。如果从当前扩展结点i到顶点j有边可达,且从源出发,途经顶点i再到顶点j的所相应的路径的长度小于当前最优路径长度,则将该顶点作为活结点插入到活结点优先队列中。这个结点的扩展过程一直继续到活结点优先队列为空时为止。
#include <iostream>
#include <queue>
using namespace std;
#define MAX 9999
#define N 60
int n, dist[N], graph[N][N]; //graph[i][j]为图中i到j点的权值,dist[i]为原点到i点的路径距离。
class HeapNode
{
public:
int i, length;
HeapNode() { }
HeapNode(int nodei, int l)
{
i = nodei;
length = l;
}
bool operator<(const HeapNode& node)const
{
return length < node.length;
}
};
void shorest(int v)
{
priority_queue<HeapNode> heap;
HeapNode enode(v, 0);//活结点
for (int i = 1; i <= n; i++) dist[i] = MAX;
dist[v] = 0;
while (1)
{
//graph[i][j]为图中i到j点的权值,dist[i]为原点到i点的路径距离。heapNode{nodei,length}为原点到i的距离长度length.
for (int j = 1; j <= n; j++) //顶点encode.i和j间有边,且此路径长小于原先从原点到j的路径长
if (graph[enode.i][j] < MAX && enode.length + graph[enode.i][j] < dist[j])
{
dist[j] = enode.length + graph[enode.i][j];
HeapNode node(j, dist[j]);
heap.push(node);
}
if (heap.empty()) break;
else
{
enode = heap.top();
heap.pop();
}
}
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
{
cin >> graph[i][j];
if (graph[i][j] == -1) graph[i][j] = MAX;
}
shorest(1);
for (int i = 2; i < n; i++) cout << dist[i] << " ";
cout << dist[n] << endl;
return 0;
}