【算法设计】分支限界法

目录

基本思想

分支限界法是一种用于求解组合优化问题的启发式搜索算法,核心是通过"分支"遍历问题的解空间,同时通过"限界"剪枝掉不可能包含最优解的子空间,从而高效找到最优解(或可行解)。其核心思想为以下几点:

  1. 解空间树构建

    将问题的所有可能解组织成一棵解空间树,每个节点代表问题的一个部分解,分支则表示对部分解的扩展(如选择某个决策、添加某个元素)。解空间树的叶子节点对应问题的完整解,算法通过遍历这棵树寻找最优解。

  2. 分支策略:广度优先/优先队列搜索

    分支限界法通常采用广度优先搜索(BFS)优先队列(堆) 进行节点扩展:

    • 普通队列(FIFO):按节点生成顺序依次扩展,适合求解可行解或简单优化问题。
    • 优先队列(如最大堆/最小堆):根据节点的界值 (预估的目标函数值)排序,优先扩展更可能产生最优解的节点,是最常见的分支策略,能更快逼近最优解。
  3. 限界核心:剪枝无效子空间

    对每个节点计算界值 (基于问题的目标函数,预估该节点子树中可能的最优解值),并与当前已找到的最优解上界/下界对比:

    • 若节点的界值表明其后续分支不可能产生更优的解,则剪枝该节点,不再扩展其分支。
    • 若节点的界值更优,则继续分支扩展,同时更新当前最优解的边界。
  4. 最优解的确定

    当解空间树中所有可能的节点都被处理(剪枝或扩展)后,最终保留的最优界值对应的解即为问题的最优解。

它与回溯法的核心区别在于:回溯法侧重深度优先搜索并剪枝,主要用于求解所有可行解 ;分支限界法侧重广度/优先队列搜索并限界,主要用于求解最优解

案例: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


0-1背包问题(经典原题-interviewbit)

常见题型

【问题描述】有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

分析

采用优先队列式分支限界法求解,核心思路:

  1. 分支:将解空间树划分为"选第i个物品"(左分支)和"不选第i个物品"(右分支),逐层扩展节点;
  2. 限界:为每个节点计算"能达到的最大价值上界",若上界≤当前已知最优解,则剪枝;
  3. 优先队列:用大根堆(按上界降序)选择下一个扩展节点,优先处理"潜力更大"的节点,加速找到最优解。
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 时,才生成左孩子(重量超限则直接跳过)。

算法执行流程

  1. 初始化:根节点入队(无物品选择,重量/价值为0,计算上界);
  2. 循环扩展节点
    • 取出队列中 ub 最大的节点;
    • 若节点上界≤maxv,剪枝;
    • 否则,生成左孩子(选当前物品,重量合法则入队)和右孩子(不选当前物品,上界>maxv则入队);
  3. 更新最优解 :叶子节点若价值>当前 maxv,更新 maxvbestx(最优解向量);
  4. 终止条件:队列为空,所有有潜力的节点均处理完毕。

代码实现:

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。对于每个任务,你可以为它分配不同的硬件资源:

  1. 在单个CPU上运行。
  2. 在两个CPU上同时运行。
  3. 在单个CPU和GPU上同时运行。
  4. 在两个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;
}
相关推荐
茶猫_1 天前
C++学习记录-旧题新做-链表求和
数据结构·c++·学习·算法·leetcode·链表
yuniko-n1 天前
【牛客面试 TOP 101】链表篇(一)
数据结构·算法·链表·面试·职场和发展
2501_941805311 天前
从微服务网关到统一安全治理的互联网工程语法实践与多语言探索
前端·python·算法
源代码•宸1 天前
Leetcode—1161. 最大层内元素和【中等】
经验分享·算法·leetcode·golang
CodeByV1 天前
【算法题】模拟
算法
s09071361 天前
FPGA加速:Harris角点检测全解析
图像处理·算法·fpga开发·角点检测
前端程序猿之路1 天前
30天大模型学习之Day 2:Prompt 工程基础系统
大数据·人工智能·学习·算法·语言模型·prompt·ai编程
星火开发设计1 天前
堆排序原理与C++实现详解
java·数据结构·c++·学习·算法·排序算法
2501_941803621 天前
在柏林智能城市照明场景中构建实时调控与高并发能耗数据分析平台的工程设计实践经验分享
算法