目录
前言
A.建议
1.学习算法最重要的是理解算法的每一步,而不是记住算法。
2.建议读者学习算法的时候,自己手动一步一步地运行算法。
B.简介
分支界定算法(Branch and Bound, B&B)是一个用于解决整数和混合整数规划问题的搜索算法。
一 代码实现
以下是一个简化的分支界定算法在C语言中的实现,我将以一个0-1背包问题为例来说明:
#include <stdio.h>
#include <stdbool.h>
#include <limits.h>
// 定义物品结构体
typedef struct Item {
int weight;
int value;
} Item;
// 定义背包状态结构体
typedef struct State {
int remainingCapacity; // 剩余容量
int currentValue; // 当前价值
bool* chosenItems; // 记录哪些物品被选择(true表示选中)
} State;
// 计算下界函数:使用贪心策略计算当前状态的下界
double calculateLowerBound(State state, const Item *items, int numItems, int capacity) {
double lowerBound = 0;
for (int i = 0; i < numItems; ++i) {
if (!state.chosenItems[i]) { // 只考虑未选择的物品
double ratio = (double) items[i].value / items[i].weight;
int canFit = min(capacity - state.remainingCapacity, items[i].weight);
lowerBound += ratio * canFit;
}
}
return lowerBound + state.currentValue;
}
// 检查是否满足约束条件
bool isFeasible(State state, const Item *items, int numItems, int capacity) {
int totalWeight = 0;
for (int i = 0; i < numItems; ++i) {
if (state.chosenItems[i]) {
totalWeight += items[i].weight;
}
}
return totalWeight <= state.remainingCapacity;
}
// 创建子节点
void createChildState(State parent, int itemIndex, State *child, const Item *items, int capacity) {
*child = parent;
child->remainingCapacity -= items[itemIndex].weight;
child->currentValue += items[itemIndex].value;
child->chosenItems[itemIndex] = true;
}
// 分支界定主函数
double branchAndBound(const Item items[], int numItems, int capacity) {
// 初始化根节点
State root;
root.remainingCapacity = capacity;
root.currentValue = 0;
root.chosenItems = (bool*) malloc(numItems * sizeof(bool));
memset(root.chosenItems, false, numItems * sizeof(bool));
// 初始化最优解变量
double bestValue = 0.0;
// 使用队列存储待处理的节点
Queue *queue = initializeQueue();
enqueue(queue, &root);
while (!isEmpty(queue)) {
State currentState = dequeue(queue);
// 如果找到一个可行解并且价值大于已知的最佳解,则更新最佳解
if (isFeasible(currentState, items, numItems, capacity) && currentState.currentValue > bestValue) {
bestValue = currentState.currentValue;
}
// 如果所有物品都已经检查过,并且剩余容量为0,那么可以结束搜索
bool allItemsChecked = true;
for (int i = 0; i < numItems; ++i) {
if (!currentState.chosenItems[i]) {
allItemsChecked = false;
break;
}
}
if (allItemsChecked && currentState.remainingCapacity == 0) {
break;
}
// 生成子节点并检查是否值得加入队列
for (int i = 0; i < numItems; ++i) {
if (!currentState.chosenItems[i]) {
State childState;
createChildState(currentState, i, &childState, items, capacity);
// 使用下界剪枝
double lb = calculateLowerBound(childState, items, numItems, capacity);
if (lb >= bestValue) continue; // 若下界小于等于当前最佳解,则剪枝
enqueue(queue, &childState);
}
}
}
free(root.chosenItems);
destroyQueue(queue);
return bestValue;
}
int main() {
// 初始化物品数据
Item items[] = {{2, 6}, {3, 10}, {4, 12}, ...}; // 假设有多组物品重量和价值
int numItems = sizeof(items) / sizeof(items[0]);
int capacity = 5; // 背包最大容量
double result = branchAndBound(items, numItems, capacity);
printf("Optimal value: %.2f\n", result);
return 0;
}
上述代码仅提供了概念性的框架,具体的initializeQueue
, enqueue
, dequeue
, isEmpty
, 和destroyQueue
函数需要根据实际数据结构进行实现,这里假设它们定义了一个FIFO队列用于存储待处理的状态节点。
在这个例子中,我们通过递归地创建子节点,并利用下界剪枝避免搜索无效分支,从而有效地寻找背包问题的最大价值。
二 时空复杂度
A.时间复杂度:
calculateLowerBound
函数的时间复杂度是 ,因为它遍历了所有未选择的物品。isFeasible
函数的时间复杂度也是 ,因为它遍历了所有已选择的物品计算总重量。- 主循环在每次迭代中都会检查每个物品是否可以加入到当前状态下,并生成子节点,对于每个节点进行可行性判断和下界计算。最坏情况下,需要遍历 numItems 个物品并将它们全部加入背包(即树的高度为 numItems),因此总的迭代次数是指数级别的。
- 但是由于引入了下界剪枝,在实际执行过程中很多节点会被提前剪枝掉,从而降低搜索空间。
综合考虑,该算法的时间复杂度理论上可能达到 ,其中 是物品的数量,但实际取决于物品价值与重量比例以及背包容量导致的有效剪枝程度,可能会远低于这个理论上限。
B.空间复杂度:
- 需要存储队列中的节点,每个节点包含一个布尔数组(大小为 numItems)和其他几个变量,所以单个节点的空间复杂度是 。
- 在最坏情况下,若不进行有效剪枝,则队列中的节点数量最多可达 。
- 然而,由于进行了剪枝操作,实际使用的节点数会大大减少,但在一般讨论中仍然假设空间复杂度为 表示潜在的最大需求。
C.总结
总结:尽管在实践中,通过下界剪枝,分支界定法能够有效地减少搜索空间,但在最坏情况下的时间复杂度仍然是指数级的,空间复杂度同样取决于剪枝的效果,通常假定为指数级。实际上,算法性能主要依赖于剪枝策略的有效性,对于好的实例,表现可以非常好。
三 优缺点
A.优点:
-
分支界定算法 (Branch and Bound):此代码实现了一个使用贪心策略计算下界的分支界定法来解决0-1背包问题。该方法在穷举搜索的基础上增加了剪枝操作,通过计算每个节点的下界来避免不必要的搜索空间,从而大大提高了求解效率。
-
结构化数据表示:代码中定义了
Item
和State
结构体,清晰地表示了物品的重量和价值以及背包当前状态(剩余容量、当前价值和已选择物品列表),使得问题的状态易于理解和管理。 -
下界函数优化:
calculateLowerBound
函数利用贪心策略为当前状态提供一个可能的最优解的下界,有助于快速判断子节点是否有可能包含更好的解。 -
可行性检查:
isFeasible
函数用于检查当前背包状态是否满足约束条件,确保解的有效性。 -
动态调整搜索空间:采用队列数据结构存储待处理节点,并根据下界剪枝策略决定哪些子节点需要加入队列,这样可以随着搜索过程动态改变搜索范围。
B.缺点:
-
指数时间复杂度:尽管采用了下界剪枝优化,但在最坏情况下,当无法有效剪枝时,分支界定法的时间复杂度仍可能是指数级别的 O(2^n),其中 n 是物品的数量。这意味着对于大规模问题可能会耗时较长。
-
内存消耗:每生成一个子节点都需要分配额外的空间来存储新状态。由于每个状态包含一个与物品数量等长的布尔数组
chosenItems
,因此在最坏情况下,空间复杂度也是指数级的。 -
对实例依赖性较大:算法性能高度依赖于问题实例的特性,尤其是物品价值与其重量的比例关系,以及能否通过下界剪枝有效地减少搜索空间。
-
未进行进一步优化:代码仅实现了基础的分支界定算法,没有包括其他可能的优化技术,如启发式排序物品以优先处理更可能产生更好解的节点,或者使用其他更高效的搜索策略(例如迭代加深搜索)。
四 现实中的应用
-
旅行商问题(Traveling Salesman Problem, TSP):寻找访问给定城市的最短可能路线时,需要求解一个NP-hard问题。分支界定结合其他策略(如最近插入或最远插入等)可以帮助找到近似最优解或精确解。
-
车辆路径问题(Vehicle Routing Problem, VRP):在物流和配送领域中,如何安排多辆车的行驶路线以服务多个客户点并最小化总行驶距离或时间成本。分支界定法可以处理带有约束条件(如容量限制、时间窗限制等)的复杂VRP变种。
-
生产计划与排程(Production Planning and Scheduling):工厂生产过程中需要决定何时生产何种产品以及每种产品的数量,同时还要考虑机器能力和订单交货期等因素,这种问题可以用混合整数规划模型表示,分支界定是其有效求解工具。
-
设施选址问题(Facility Location Problem):在选择开设新仓库、商店或其他设施的位置时,要综合考虑覆盖客户群的成本、客户需求量以及建设维护设施的成本,这是一个典型的整数规划问题。
-
资源调度(Resource Allocation):在项目管理或者云计算环境中,合理分配有限资源(例如机器、人力资源)到不同的任务或项目上,以实现最大效益或最小成本,可以采用分支界定方法来解决。
-
投资组合优化(Portfolio Optimization):在金融领域,投资者希望确定最佳的投资组合,即在风险控制下最大化收益。虽然原始问题通常被表述为连续优化问题,但在包含离散投资决策的情况下,也可以借助分支界定技术。
-
电路设计(Circuit Design):在电子工程中,布线层的设计、逻辑门的选择与布局等问题,可以通过整数规划模型描述,并用分支界定算法找到满足特定性能指标下的设计方案。
-
集合覆盖问题(Set Cover Problem):选择最少数量的集合使得它们的并集包含所有元素,在新闻推荐系统、广告投放等领域有实际应用,分支界定算法能帮助寻优。
-
二次分配问题(Quadratic Assignment Problem, QAP):在工业布局、计算机科学等领域,QAP要求将一组设施分配到相应位置,使得设施之间的交互成本最低,分支界定算法可用于解决此类高度非线性且复杂的整数优化问题。