分枝限界法类似于回溯法,也是一种在问题的解空间树上搜索问题解的算法。
但在一般情况下,分枝限界法与回溯法的求解目标不同。回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分枝限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
所谓“分枝”就是采用广度优先的策略,依次搜索活结点的所有分枝,也就是所有相邻结点。
求最优解时,选择哪一个子结点?
采用一个限界函数,计算限界函数值,选择一个最有利的子结点作为扩展结点,使搜索朝着解空间树上有最优解的分枝推进,以便尽快地找出一个最优解。
分枝限界法与回溯法的主要区别
方法 | 解空间搜索方式 | 存储结点的数据结构 | 结点存储特性 | 常用应用 |
---|---|---|---|---|
回溯法 | 深度优先 | 栈 | 活结点的所有可行子结点被遍历后才从栈中出栈 | 找出满足条件的所有解 |
分枝限界法 | 广度优先 | 队列,优先队列 | 每个结点只有一次成为活结点的机会 | 找出满足条件一个解或者特定意义的最优解 |
在搜索解空间树时,每个活结点可能有很多孩子结点,其中有些孩子结点搜索下去是不可能产生问题解或最优解的。
可以设计好的限界函数在扩展时删除这些不必要的孩子结点,从而提高搜索效率。
假设活结点si有4个孩子结点,而满足限界函数的孩子结点只有2个,可以删除这2个不满足限界函数的孩子结点,使得从si出发的搜索效率提高一倍。
限界函数设计难以找出通用的方法,需根据具体问题来分析。一般地,先要确定问题解的特性:
目标函数是求最大值:则设计上界限界函数ub(根结点的ub值通常大于或等于最优解的ub值),若si是sj的双亲结点,应满足ub(si)≥ub(sj),当找到一个可行解ub(sk)后,将所有小于ub(sk)的结点剪枝。
目标函数是求最小值:则设计下界限界函数lb(根结点的lb值一定要小于或等于最优解的lb值),若si是sj的双亲结点,应满足lb(si)≤lb(sj),当找到一个可行解lb(sk)后,将所有大于lb(sk)的结点剪枝。
根据选择下一个扩展结点的方式来组织活结点表,不同的活结点表对应不同的分枝搜索方式。
队列式分枝限界法
优先队列式分枝限界法
(1)队列式分枝限界法
队列式分枝限界法将活结点表组织成一个队列,并按照队列先进先出(FIFO)原则选取下一个结点为扩展结点。步骤如下:
①将根结点加入活结点队列。
②从活结点队中取出队头结点,作为当前扩展结点。
③对当前扩展结点,先从左到右地产生它的所有孩子结点,用约束条件检查,把所有满足约束条件的孩子结点加入活结点队列。
④重复步骤②和③,直到找到一个解或活结点队列为空为止。
(2)优先队列式分枝限界法
优先队列式分枝限界法的主要特点是将活结点表组组成一个优先队列,并选取优先级最高的活结点成为当前扩展结点。步骤如下:
①计算起始结点(根结点)的优先级并加入优先队列(与特定问题相关的信息的函数值决定优先级)。
②从优先队列中取出优先级最高的结点作为当前扩展结点,使搜索朝着解空间树上可能有最优解的分枝推进,以便尽快地找出一个最优解。
③对当前扩展结点,先从左到右地产生它的所有孩子结点,然后用约束条件检查,对所有满足约束条件的孩子结点计算优先级并加入优先队列。
④重复步骤②和③,直到找到一个解或优先队列为空为止。
分枝限界法在搜索解空间树时,结点的处理是跳跃式的,回溯也不是单纯地沿着双亲结点一层一层地向上回溯,因此当搜索到某个叶子结点且该结点对应一个可行解时,如何得到对应的解向量呢?
两种方法:
① 对每个扩展结点保存从根结点到该结点的路径。
每个结点带有一个可能的解向量。这种做法比较浪费空间,但实现起来简单,后面的示例均采用这种方式。
② 在搜索过程中构建搜索经过的树结构。
每个结点带有一个双亲结点指针,当找到最优解时,通过双亲指针找到对应的最优解向量。这种做法需保存搜索经过的树结构,每个结点增加一个指向双亲结点的指针。
采用分枝限界法求解的3个关键问题如下:
(1)如何确定合适的限界函数。
(2)如何组织待处理结点的活结点表。
(3)如何确定解向量的各个分量。
【问题描述】有n个重量分别为{w1,w2,…,wn}的物品,它们的价值分别为{v1,v2,…,vn},给定一个容量为W的背包。
设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么不选中,要求选中的物品不仅能够放到背包中,而且重量和为W具有最大的价值。
设计限界函数,为了简便,设根结点为第0层,然后各层依次递增,显然i=n时表示是叶子结点层。
由于该问题是求装入背包的最大价值,属求最大值问题,采用上界设计方式。
对于第i层的某个结点e,用e.w表示结点e时已装入的总重量,用e.v表示已装入的总价值:
如果所有剩余的物品都能装入背包,那么价值的上界e.ub=e.v+ (v[i+1]+…+v[n])
如果所有剩余的物品不能全部装入背包,那么价值的上界e.ub=e.v+ (v[i+1]+…+v[k])+(物品k+1装入的部分重量)×物品k+1的单位价值
bound(i) 物品按单位价值降序排序!(否则会剪掉不该剪的)
下面直接给出优先队列的代码
2. 01背包问题 - AcWing题库
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;
public class Main {
static int N; //n种物品
int W; //限制重量
int w[]; //存放n个物品重量,不用下标0元素
int v[]; //存放n个物品价值,不用下标0元素
int best = Integer.MIN_VALUE;
int[] bestX;
int total;
PriorityQueue<NodeType> queue = new PriorityQueue<>(new Comparator<NodeType>() {
@Override
public int compare(NodeType o1, NodeType o2) {
return (o1.ub - o2.ub) >= 0 ? -1 : 1;
}
});
//结点e进队qu
void EnQueue(NodeType e) {
if (e.i >= N) //到达叶子结点
{
if (e.v > best) //找到更大价值的解
{
best = e.v;
for (int j = 1; j <= N; j++) {
bestX[j] = e.x[j];
}
}
} else {
queue.add(e); //非叶子结点进队
}
}
void bfs() {
int j;
NodeType e, e1, e2; //定义3个结点
e = new NodeType();
e.i = -1; //根结点置初值,其层次计为0
e.w = 0;
e.v = 0;
e.no = total++;
for (j = 1; j <= N; j++) {
e.x[j] = 0;
}
bound(e);
queue.add(e);
while (!queue.isEmpty()) //队不空循环
{
e = queue.peek();
queue.poll(); //出队结点e
if (e.w + w[e.i + 1] <= W) //剪枝:检查左孩子结点
{
e1 = new NodeType();
e1.no = total++;
e1.i = e.i + 1; //建立左孩子结点
e1.w = e.w + w[e1.i];
e1.v = e.v + v[e1.i];
for (j = 1; j <= N; j++) //复制解向量
{
e1.x[j] = e.x[j];
}
e1.x[e1.i] = 1;
bound(e1); //求左孩子结点的上界
EnQueue(e1); //左孩子结点进队操作
}
e2 = new NodeType();
e2.no = total++; //建立右孩子结点
e2.i = e.i + 1;
e2.w = e.w;
e2.v = e.v;
for (j = 1; j <= N; j++) //复制解向量
{
e2.x[j] = e.x[j];
}
e2.x[e2.i] = 0;
bound(e2); //求右孩子结点的上界
if (e2.ub > best) //若右孩子结点可行,则进队,否则被剪枝
{
EnQueue(e2);
}
}
}
void bound(NodeType e) { //计算分枝结点e的上界
int i = e.i + 1; //考虑结点e的余下物品
int sumw = e.w; //求已装入的总重量
double sumv = e.v; //求已装入的总价值
while (i <= N && (sumw + w[i] <= W)) {
sumw += w[i]; //计算背包已装入载重
sumv += v[i]; //计算背包已装入价值
i++;
}
if (i <= N) { //余下物品只能部分装入
e.ub = sumv + (W - sumw) * v[i] / (w[i] + 0.0);
} else { //余下物品全部可以装入
e.ub = sumv;
}
}
public static void main(String[] args) {
Main bag = new Main();
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int v = scanner.nextInt();
int[][] ints = new int[n][2];
Item[] items = new Item[n];
for (int i = 0; i < n; i++) {
items[i] = new Item();
items[i].w = scanner.nextInt();
items[i].v = scanner.nextInt();
items[i].p = (double) items[i].v / items[i].w;
}
//将所有物品以单位重量价值递减排列
Arrays.sort(items, (o1, o2) -> {
return (o1.p - o2.p) > 0 ? -1 : 1;
});
bag.w = new int[n + 1];
bag.v = new int[n + 1];
bag.bestX = new int[n + 1];
bag.W = v;
Main.N = n;
for (int i = 0; i < n; i++) {
bag.w[i + 1] = items[i].w;
bag.v[i + 1] = items[i].v;
}
bag.bfs();
System.out.println(bag.best);
}
}
class Item {
int w;
int v;
double p;
}
class NodeType { //队列中的结点类型
int no; //结点编号,从1开始
int i; //当前结点在搜索空间中的层次
int w; //当前结点的总重量
int v; //当前结点的总价值
int[] x; //当前结点包含的解向量
double ub; //上界
public NodeType() {
x = new int[Main.N + 1];
}
}
【算法分析】无论采用队列式分枝限界法还是优先队列式分枝限界法求解0/1背包问题,最坏情况下要搜索整个解空间树,所以最坏时间和空间复杂度均为O(2^n),其中n为物品个数。
【问题描述】给定一个带权有向图G=(V,E),其中每条边的权是一个正整数。
另外,还给定V中的一个顶点v,称为源点。计算从源点到其他所有顶点的最短路径长度。这里的长度是指路上各边权之和。
若有环?
P3371 【模板】单源最短路径(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
package review;
import java.util.PriorityQueue;
import java.util.Scanner;
import learn.Graph;
public class main {
static int n;
int[][] graph;
int[] dist;
int[] pre;
PriorityQueue<NodeType> queue = new PriorityQueue<>((o1, o2) -> {
return o1.length - o2.length;
});
void bfs(int s) {
NodeType e, e1;
e = new NodeType();
e.vno = s;
e.length = 0;
queue.add(e);
dist[s] = 0;
while (!queue.isEmpty()) {
e = queue.peek();
queue.poll();
for (int i = 0; i < n; i++) {
if (graph[e.vno][i + 1] < Integer.MAX_VALUE && e.length + graph[e.vno][i + 1] < dist[i
+ 1]) {
dist[i + 1] = e.length + graph[e.vno][i + 1];
pre[i + 1] = e.vno;
e1 = new NodeType();
e1.vno = i + 1;
e1.length = dist[i + 1];
queue.add(e1);
}
}
}
}
public static void main(String[] args) {
main main = new main();
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int m = scanner.nextInt();
int s = scanner.nextInt();
main.dist = new int[n + 1];
for (int i = 0; i < n; i++) {
main.dist[i + 1] = Integer.MAX_VALUE;
}
main.pre = new int[n + 1];
main.graph = new int[n + 1][n + 1];
for (int i = 0; i < main.graph.length; i++) {
for (int j = 0; j < main.graph[0].length; j++) {
main.graph[i][j] = Integer.MAX_VALUE;
}
}
for (int i = 0; i < m; i++) {
int x = scanner.nextInt();
int y = scanner.nextInt();
int d = scanner.nextInt();
main.graph[x][y] = Math.min(d,main.graph[x][y]);
}
main.bfs(s);
main.dist[s] = 0;
for (int i = 1; i <= n; i++) {
System.out.print(main.dist[i] + " ");
}
}
}
class NodeType {
//队列结点类型
int vno; //顶点编号
int length; //路径长度
}
【问题描述】有n(n≥1)个任务需要分配给n个人执行,每个任务只能分配给一个人,每个人只能执行一个任务。
第i个人执行第j个任务的成本是c[i][j](1≤i,j≤n)。求出总成本最小的分配方案。
4个人员、4个任务的信息
人员 | 任务1 | 任务2 | 任务3 | 任务4 |
---|---|---|---|---|
1 | 9 | 2 | 7 | 8 |
2 | 6 | 4 | 3 | 7 |
3 | 5 | 8 | 1 | 8 |
4 | 7 | 6 | 9 | 4 |
【问题求解】这里采用优先队列式分枝限界法求解。
优先队列结点的类型
class NodeType {
int no; //结点编号
int i; //人员编号
int x[]; //x[i]为人员i分配的任务编号
boolean worker[]; //worker[i]=true表示任务i已经分配
int cost; //已经分配任务所需要的成本
int lb; //下界
public NodeType(int n) {
this.x = new int[n + 1];
this.worker = new boolean[n + 1];
}
}
下界限界函数设计
lb为当前结点对应分配方案的成本下界。
例如对于结点e:x[]=[2,1,0,0],表示第1个人员分配任务2,第2个人员分配任务1,第3、4个人员没有分配任务;
相对应有worker[]=[true,true,false,false],表示任务1和2已经分配,而任务3、4还没有分配。此时计算结果是:e.cost=c[1][2]+c[2][1]=2+6=8。
下一步最好的情况是在数组c中第3行和第4行中找到非第1、2列(因为任务1、2已经分配)中最小元素和,显然为1+4=5,即其e.lb=e.cost+5=13。
void bound(NodeType e) {
int minsum = 0;
for (int i1 = e.i + 1; i1 <= n; i1++) //求c[e.i+1..n]行中最小元素和
{
int minc = Integer.MIN_VALUE;
for (int j1 = 1; j1 <= n; j1++) //各列中仅仅考虑没有分配的任务
{
if (e.worker[j1] == false && c[i1][j1] < minc) {
minc = c[i1][j1];
}
}
minsum += minc;
}
e.lb = e.cost + minsum;
}
剪枝操作
用bestx[MAXN]存放最优分配方案, mincost(初始值为∞)存放最优成本。
显然一个结点的lb>mincost,则不可能从其子结点中找到最优解,进行剪枝。仅仅扩展lb≤mincost的结点。
int n = 4;
int[][] c = {{0, 0, 0, 0, 0}, {0, 9, 2, 7, 8}, {0, 6, 4, 3, 7},
{0, 5, 8, 1, 8}, {0, 7, 6, 9, 4}};
int[] x = new int[5]; //临时解
int cost = 0; //临时解的成本
int[] bestx = new int[5]; //最优解
int mincost = Integer.MAX_VALUE; //最优解的成本
boolean[] worker = new boolean[5]; //worker[j]表示任务j是否已经分配人员
PriorityQueue<NodeType> queue = new PriorityQueue<>(new Comparator<NodeType>() {
@Override
public int compare(NodeType o1, NodeType o2) {
return (o1.lb - o2.lb) > 0 ? 1 : -1;
}
});
void bfs() {
NodeType e = new NodeType(n);
NodeType e1;
e.no = 1;
e.i = 0;
bound(e);
queue.add(e);
while (!queue.isEmpty()) {
e = queue.peek();
queue.poll();
if (e.i == n) { //达到叶子结点
if (e.cost < mincost) {//比较求最优解
mincost = e.cost;
for (int j = 1; j <= n; j++) {
bestx[j] = e.x[j];
}
}
}
for (int i = 1; i <= n; i++) {
if (e.worker[i]) {
continue;
}
e1 = new NodeType(n);
e1.i = e.i + 1;
for (int j = 0; j < n; j++) {
e1.x[j + 1] = e.x[j + 1];
e1.worker[j + 1] = e.worker[j + 1];
}
e1.x[e1.i] = i;
e1.cost=e.cost+c[e1.i][i];
e1.worker[i] = true;
bound(e1);
if (e1.lb < mincost) {
queue.add(e1);
}
}
}
}
【问题描述】有n个作业(编号为1~n)要在由两台机器M1和M2组成的流水线上完成加工。每个作业加工的顺序都是先在M1上加工,然后在M2上加工。M1和M2加工作业i所需的时间分别为ai和bi(1≤i≤n)。
流水作业调度问题要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少。可以假定任何作业一旦开始加工,就不允许被中断,直到该作业被完成,即非优先调度。
【问题求解】作业编号为1到n,调度方案的执行步骤为1到n,解空间每一层对应一个步骤的作业分配。
根结点对应步骤0(虚),依次为步骤1、2、…、n分配任务,叶子结点对应步骤n。
对于按1~n顺序执行的某种调度方案,f1表示在M1上执行完当前第i步的作业对应的总时间,f2数组表示在M2上执行完当前第i步的作业的总时间。
若第i步执行作业j,计算公式如下:
f1=f1+a[j];
f2[i]=max(f1,f2[i-1])+b[j]
对应的求结点e的lb的算法同回溯法所推导:
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Scanner;
public class Main {
int n;
int[] x; //解向量 1~n 排列树
int[] a; //a车间所需时间
int[] b; //b车间...
int[] bestX; //最优解向量
int bestF = Integer.MAX_VALUE; //最优调度时间
int f1; //f1[i]表示第i步执行的作业x[i]在M1上执行完的总时间(含前面作业的执行时间)
int[] f2; //f2[i]表示第i步执行的作业x[i]在M2上执行完的总时间(含前面作业的执行时间)
PriorityQueue<Node> queue = new PriorityQueue<>(new Comparator<Node>() {
@Override
public int compare(Node o1, Node o2) {
return (o1.lb - o2.lb) > 0 ? 1 : -1;
}
});
void bfs() {
Node e = new Node(n);
Node e1;
e.i = 0;
e.f1 = 0;
e.f2 = 0;
bound(e);
queue.add(e);
while (!queue.isEmpty()) {
e = queue.peek();
queue.poll();
for (int i = 1; i <= n; i++) {
if (e.y[i] == 1) {
continue;
}
e1 = new Node(n);
e1.i = e.i + 1;
for (int j = 0; j < n; j++) {
e1.x[j + 1] = e.x[j + 1];
e1.y[j + 1] = e.y[j + 1];
}
e1.x[e1.i] = i;
e1.y[i] = 1;
e1.f1 = e.f1 + a[i];
e1.f2 = Math.max(e.f2, e1.f1) + b[i];
bound(e1);
if (e1.i == n) {
if (e1.f2 < bestF) {
bestF = e1.f2;
for (int i1 = 0; i1 < n; i++) {
bestX[i1 + 1] = e1.x[i1 + 1];
}
}
} else if (e1.lb < bestF) {
queue.add(e1);
}
}
}
}
void bound(Node e) {
int sum = 0;
for (int i = 1; i <= n; i++) { //扫描所有作业
if (e.y[i] == 0) {
sum += b[i];
}
}
//仅累计e.x中还没有分配的作业的b时间
e.lb = e.f2 + sum;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
Main product = new Main();
int i = scanner.nextInt();
product.n = i;
product.a = new int[i + 1];
product.b = new int[i + 1];
product.x = new int[i + 1];
for (int j = 0; j < i; j++) {
product.x[j + 1] = j + 1;
}
product.bestX = new int[i + 1];
product.f2 = new int[i + 1];
for (int j = 0; j < i; j++) {
product.a[j + 1] = scanner.nextInt();
}
for (int j = 0; j < i; j++) {
product.b[j + 1] = scanner.nextInt();
}
// product.dfs(1);
product.bfs();
System.out.println(product.bestF);
for (int j = 0; j < product.n; j++) {
System.out.print(product.bestX[j + 1] + " ");
}
}
}
}
class Node {
int[] x; //解向量 1~n 排列树
int f1; //已经分配作业M1的执行时间
int f2; //已经分配作业M2的执行时间
int[] y; //y[i]=1表示编号为i的作业已经分配
int i; //步骤编号
int lb; //下界
public Node(int n) {
this.x = new int[n + 1];
this.y = new int[n + 1];
}
}
#10003. 「一本通 1.1 例 4」加工生产调度 - 题目 - LibreOJ (loj.ac)
Java超时