目录
基本思想
分支限界法是一种用于求解组合优化问题的启发式搜索算法,核心是通过"分支"遍历问题的解空间,同时通过"限界"剪枝掉不可能包含最优解的子空间,从而高效找到最优解(或可行解)。其核心思想为以下几点:
-
解空间树构建
将问题的所有可能解组织成一棵解空间树,每个节点代表问题的一个部分解,分支则表示对部分解的扩展(如选择某个决策、添加某个元素)。解空间树的叶子节点对应问题的完整解,算法通过遍历这棵树寻找最优解。
-
分支策略:广度优先/优先队列搜索
分支限界法通常采用广度优先搜索(BFS) 或优先队列(堆) 进行节点扩展:
- 普通队列(FIFO):按节点生成顺序依次扩展,适合求解可行解或简单优化问题。
- 优先队列(如最大堆/最小堆):根据节点的界值 (预估的目标函数值)排序,优先扩展更可能产生最优解的节点,是最常见的分支策略,能更快逼近最优解。
-
限界核心:剪枝无效子空间
对每个节点计算界值 (基于问题的目标函数,预估该节点子树中可能的最优解值),并与当前已找到的最优解上界/下界对比:
- 若节点的界值表明其后续分支不可能产生更优的解,则剪枝该节点,不再扩展其分支。
- 若节点的界值更优,则继续分支扩展,同时更新当前最优解的边界。
-
最优解的确定
当解空间树中所有可能的节点都被处理(剪枝或扩展)后,最终保留的最优界值对应的解即为问题的最优解。
它与回溯法的核心区别在于:回溯法侧重深度优先搜索并剪枝,主要用于求解所有可行解 ;分支限界法侧重广度/优先队列搜索并限界,主要用于求解最优解。
案例:0-1背包问题
【题目】
经典原题
给定两个整数组A和B,大小为N,分别代表与N个项相关的值和权重。也给定一个整数C,代表背包容量。求出A的最大值子集,使得该子集权重之和小于或等于C。
注意:你不能破坏物品,要么选择完整物品,要么不选择(0-1属性)
【问题约束】
1 <= N <= 103
1 <= C <= 103
1 <= A[i], B[i] <= 103
【输入格式】
第一个参数是一个大小为 N 的整数组 A,表示 N 个项的值。
第二个参数是一个大小为 N 的整数组 B,表示 N 项的权重。
第三个参数是一个整数C,表示背包容量。
【输出格式】
返回一个整数,表示A的最大值子集,使得该子集权重之和小于或等于C。
【示例输入】
输入1:
A = [60, 100, 120]
B = [10, 20, 30]
C = 50
输入2:
A = [10, 20, 30, 40]
B = [12, 13, 15, 19]
C = 10
【示例输出】
输出1:
220
输出2:
0


常见题型
【问题描述】有n个重量分别为 { w 1 , w 2 , ... , w n } \{w_1,w_2,...,w_n\} {w1,w2,...,wn}的物品,它们的价值分别为 { v 1 , v 2 , ... , v n } \{v_1,v_2,...,v_n\} {v1,v2,...,vn},给定一个容量为 W W W的背包。设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么不选中,要求选中的物品不仅能够放到背包中,而且重量和为 W W W具有最大的价值。
假设一个0/1背包问题,物品个数为n=4,重量为w=(8,3,4,2),价值为v=(8,6,8,10),背包限重为W=15,解向量为x=(x1,x2,x3,x4)。
| 编号 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 重量 | 8 | 3 | 4 | 2 |
| 价值 | 8 | 6 | 8 | 10 |
分析
采用优先队列式分支限界法求解,核心思路:
- 分支:将解空间树划分为"选第i个物品"(左分支)和"不选第i个物品"(右分支),逐层扩展节点;
- 限界:为每个节点计算"能达到的最大价值上界",若上界≤当前已知最优解,则剪枝;
- 优先队列:用大根堆(按上界降序)选择下一个扩展节点,优先处理"潜力更大"的节点,加速找到最优解。
1. 解空间树构建
0-1背包的解空间是一棵完全二叉树,每个节点对应"前i个物品的选择状态":
- 根节点(i=0):无物品被选择,重量/价值均为0;
- 第i层节点:处理完前i个物品的选择,左孩子=选第i+1个物品,右孩子=不选第i+1个物品;
- 叶子节点(i=n):所有物品处理完毕,对应一个完整的解。
2. 上界计算
bound() 函数是限界的核心,采用部分背包贪心策略估算当前节点能达到的最大价值:
- 上界是"当前节点能达到的理论最大价值",实际0-1背包的价值≤该上界;
- 若节点上界≤当前已知最优解(
maxv),则该节点的所有子节点都不可能更优,直接剪枝。
3. 剪枝策略
两处关键剪枝:
- 节点扩展前剪枝 :取出队列节点时,若
e.ub <= maxv,直接跳过,该节点无扩展价值; - 右孩子入队前剪枝 :仅当
e2.ub > maxv时,右孩子才入队(左孩子因重量合法,默认入队); - 左孩子剪枝:仅当
e.w + w[e.i+1] <= W时,才生成左孩子(重量超限则直接跳过)。
算法执行流程
- 初始化:根节点入队(无物品选择,重量/价值为0,计算上界);
- 循环扩展节点 :
- 取出队列中
ub最大的节点; - 若节点上界≤
maxv,剪枝; - 否则,生成左孩子(选当前物品,重量合法则入队)和右孩子(不选当前物品,上界>maxv则入队);
- 取出队列中
- 更新最优解 :叶子节点若价值>当前
maxv,更新maxv和bestx(最优解向量); - 终止条件:队列为空,所有有潜力的节点均处理完毕。
代码实现:
cpp
#include <iostream>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
//定义最大物品数量
const int MAXN = 100;
//全局变量
int maxv = -9999; //最大价值,初始为极小值
int bestx[MAXN]; //最优解向量
int total = 1; //解空间中节点数累计
int n, W; //物品数量、背包容量
int w[MAXN], v[MAXN]; //物品的重量、价值数组
//队列中的节点类型
struct NodeType {
int no; //节点编号
int i; //当前节点在搜索空间中的层次
int w; //当前节点的总重量
int v; //当前节点的总价值
int x[MAXN]; //当前节点包含的解向量
double ub; //上界,该节点能达到的最大价值估算
//重载<运算符:优先队列是大根堆,ub越大越优先出队
bool operator<(const NodeType &s)const {
return ub < s.ub;
}
};
//计算分支节点e的上界e.ub
void bound(NodeType &e) {
int i = e.i + 1; //从当前节点的下一个物品开始考虑
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) * (double)v[i] / w[i];
}
else
{
e.ub = sumv; //剩余物品已全部装入
}
}
//节点进队操作
void EnQueue(NodeType e,priority_queue<NodeType> &qu) {
if (e.i == n) { //到达叶子结点,所有物品处理完毕
if (e.v>maxv) { //找到更大价值的解
maxv = e.v;
for (int j = 1;j <= n;j++) {
bestx[j] = e.x[j];
}
}
}
else {
qu.push(e); //非叶子节点入队
}
}
//分支限界法求解0-1背包问题
void bfs() {
NodeType e, e1, e2;
//定义优先队列(大根堆,按ub降序出队)
priority_queue<NodeType> qu;
//初始化根节点
e.no = total++;
e.i = 0;
e.w = 0;
e.v = 0;
for (int j = 1;j <= n;j++) {
e.x[j] = 0; //初始解向量全为0,没有选择任何物品
}
bound(e); //计算根节点的上界
qu.push(e); //根节点入队
//队列非空时循环
while (!qu.empty()) {
//取出队首节点,ub最大的结点
e = qu.top();
qu.pop();
//剪枝:若当前节点的上界<=已知最大价值,无需继续搜索
if (e.ub <= maxv) {
continue;
}
//处理左孩子节点,选择第e.i+1个物品
if (e.w + w[e.i + 1] <= W) { //重量不超过背包容量
e1.no = total++;
e1.i = e.i + 1; //层次加一,处理下一个物品
e1.w = e.w + w[e1.i]; //累计重量
e1.v = e.v + v[e1.i]; //累计价值
//复制解向量
for (int j = 1;j <= n;j++) {
e1.x[j] = e.x[j];
}
e1.x[e1.i] = 1; //标记选择第e1.i个物品
bound(e1); //计算左孩子上界
EnQueue(e1, qu); //左孩子入队, 左孩子.ub 大概率 > maxv,直接入队
}
//处理右孩子节点,不选择第e.i+1个物品
e2.no = total++;
e2.i = e.i + 1; //层次+1
e2.w = e.w; //重量不变
e2.v = e.v; //价值不变
//复制解向量
for (int j = 1;j <= n;j++) {
e2.x[j] = e.x[j];
}
e2.x[e2.i] = 0; //标记不选择第e2.i个物品
bound(e2); //计算右孩子的上界
//剪枝:若右孩子的上界>已知最大价值,才入队
if (e2.ub > maxv) {
EnQueue(e2, qu);
}
}
}
int main() {
//输入物品数量和背包容量
cout << "请输入物品数量n:";
cin >> n;
cout << "请输入背包容量W:";
cin >> W;
//输入每个物品的重量和价值(从编号1开始)
cout << "请依次输入每个物品的重量和价值(空格分隔):" << endl;
for (int i = 1;i <= n;i++) {
cout << "物品" << i << ":";
cin >> w[i] >> v[i];
}
//按照"价值/重量"降序排列,提升剪枝效率
vector<pair<double, pair<int, int>>> items; //存储性价比、重量、价值
for (int i = 1;i <= n;i++) {
double ratio = (double)v[i] / w[i];
items.emplace_back(-ratio, make_pair(w[i], v[i])); //默认升序排列,这里给ratio加上负号,实现降序排列
}
sort(items.begin(), items.end());
//排序后,赋值回w,v数组
for (int i = 1;i <= n;i++) {
w[i] = items[i - 1].second.first;
v[i] = items[i - 1].second.second;
}
//分支限界法求解
bfs();
//输出结果
cout << "\n 0-1背包问题的最优解:" << endl;
cout << "最大价值为:" << maxv << endl;
cout << "选择的物品编号(从1开始):";
for (int j = 1;j <= n;j++) {
if (bestx[j] == 1) {
cout << j << " ";
}
}
cout << endl;
return 0;
}
结果:

| 指标 | 说明 |
|---|---|
| 时间复杂度 | 最坏O(2ⁿ)(无剪枝),实际因剪枝大幅降低;优先队列操作增加O(logn)系数 |
| 空间复杂度 | O(n)(存储节点、解向量)+ 队列空间(取决于剪枝效果) |
| 优势 | 相比回溯法(深度优先),能更早找到最优解,剪枝更高效 |
| 局限性 | 依赖上界估算精度,若上界偏大,剪枝效果差;优先队列有额外开销 |
| 适用场景 | 物品数量n适中,需快速找到最优解的0-1背包问题 |
cpu处理调度问题
【题目】
问题描述:有若干个任务需要在一台机器上运行。它们之间没有依赖关系,因此 可以被按照任意顺序执行。
该机器有两个CPU和一个GPU。对于每个任务,你可以为它分配不同的硬件资源:
- 在单个CPU上运行。
- 在两个CPU上同时运行。
- 在单个CPU和GPU上同时运行。
- 在两个CPU和GPU上同时运行。
一个任务开始执行以后,将会独占它所用到的所有硬件资源,不得中断,直到执行结束为止。第i个任务用单个CPU,两个CPU,单个CPU加GPU,两个CPU加GPU运行所消耗的时间分别为ai,bi,ci 和 di。
现在需要你计算出至少需要花多少时间可以把所有给定的任务完成。
【输入格式】
输入的第一行只有一个正整数 n n n(1 ≤ n ≤ 40), 是总共需要执行的任 务个数。接下来的 n n n 行每行有四个正整数 a i , b i , c i , d i ai,bi,ci,di ai,bi,ci,di ( a i , b i , c i , d i ai,bi,ci,di ai,bi,ci,di 均不超过10),以空格隔开。
【输出格式】
输出只有一个整数,即完成给定的所有任务所需的最少时间。
【样例输入】
3
4 4 2 2
7 4 7 4
3 3 3 3
【样例输出】
7
样例说明:
有很多种调度方案可以在7个时间单位里完成给定的三个任务,以下是其中的一种方案:同时运行第一个任务(单CPU加上GPU)和第三个任务(单CPU),它们分别在时刻2和时刻3完成。在时刻3开始双CPU运行任务2,在时刻7完成。
代码实现
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <algorithm>
using namespace std;
struct Task {
int a, b, c, d;
int min_cost; // 每个任务的最小耗时
};
// 优先队列中的状态节点
struct State {
int task_idx; // 已处理的任务数
int cpu1; // CPU1的空闲时间
int cpu2; // CPU2的空闲时间
int gpu; // GPU的空闲时间
int current_max; // 当前已用时间(max(cpu1,cpu2,gpu))
int lower_bound; // 当前状态的下界(current_max + 剩余任务下界)
// 优先级队列:下界越小,越优先探索
bool operator<(const State& other) const {
return lower_bound > other.lower_bound;
}
};
int n;
vector<Task> tasks;
int total_min_sum; // 所有任务min_cost的总和
int best_time = INT_MAX; // 全局最优解
// 计算剩余任务的下界(从task_idx到n-1)
int calc_lower_bound(int task_idx) {
if (task_idx >= n) return 0;
// 剩余任务的最小耗时总和 ÷ 3
int remain_min = total_min_sum;
for (int i = 0; i < task_idx; i++) {
remain_min -= tasks[i].min_cost;
}
return (remain_min + 2) / 3;
}
int branch_and_bound() {
// 初始化优先级队列
priority_queue<State> pq;
int initial_lb = calc_lower_bound(0);
pq.push({ 0, 0, 0, 0, 0, initial_lb });
while (!pq.empty()) {
State curr = pq.top();
pq.pop();
// 剪枝1:当前下界 ≥ 已知最优解,无需探索
if (curr.lower_bound >= best_time) {
continue;
}
// 所有任务处理完成,更新最优解
if (curr.task_idx == n) {
if (curr.current_max < best_time) {
best_time = curr.current_max;
}
continue;
}
Task t = tasks[curr.task_idx];
int next_idx = curr.task_idx + 1;
// 尝试4种运行方式
// 方式1:单CPU
int new_cpu1 = curr.cpu1, new_cpu2 = curr.cpu2;
if (curr.cpu1 <= curr.cpu2) {
new_cpu1 = curr.cpu1 + t.a;
}
else {
new_cpu2 = curr.cpu2 + t.a;
}
int new_max1 = max({ new_cpu1, new_cpu2, curr.gpu });
int lb1 = new_max1 + calc_lower_bound(next_idx);
if (lb1 < best_time) { // 剪枝2:新下界 < 最优解,才入队
pq.push({ next_idx, new_cpu1, new_cpu2, curr.gpu, new_max1, lb1 });
}
// 方式2:双CPU(等两个CPU空闲)
int cpu_both = max(curr.cpu1, curr.cpu2);
int end_both = cpu_both + t.b;
int new_max2 = max(end_both, curr.gpu);
int lb2 = new_max2 + calc_lower_bound(next_idx);
if (lb2 < best_time) {
pq.push({ next_idx, end_both, end_both, curr.gpu, new_max2, lb2 });
}
// 方式3:单CPU+GPU(等CPU和GPU空闲)
int cpu_free = min(curr.cpu1, curr.cpu2);
int start3 = max(cpu_free, curr.gpu);
int end3 = start3 + t.c;
int nc1 = (curr.cpu1 <= curr.cpu2) ? end3 : curr.cpu1;
int nc2 = (curr.cpu1 > curr.cpu2) ? end3 : curr.cpu2;
int new_max3 = max({ nc1, nc2, end3 });
int lb3 = new_max3 + calc_lower_bound(next_idx);
if (lb3 < best_time) {
pq.push({ next_idx, nc1, nc2, end3, new_max3, lb3 });
}
// 方式4:双CPU+GPU(等所有资源空闲)
int start4 = max(cpu_both, curr.gpu);
int end4 = start4 + t.d;
int new_max4 = end4;
int lb4 = new_max4 + calc_lower_bound(next_idx);
if (lb4 < best_time) {
pq.push({ next_idx, end4, end4, end4, new_max4, lb4 });
}
}
return best_time;
}
int main() {
cin >> n;
tasks.resize(n);
total_min_sum = 0;
for (int i = 0; i < n; i++) {
cin >> tasks[i].a >> tasks[i].b >> tasks[i].c >> tasks[i].d;
tasks[i].min_cost = min({ tasks[i].a, tasks[i].b, tasks[i].c, tasks[i].d });
total_min_sum += tasks[i].min_cost;
}
cout << branch_and_bound() << endl;
return 0;
}