算法之分支定界
分支定界
- 分支定界
- 概述
- 核心思想与步骤
- 常见变体
- 复杂度分析
- 案例分析
- 1. 0-1背包问题
- 2. 最短路径问题(分支定界法)
- 3. 旅行商问题(TSP)
分支定界
概述
分支定界(Branch and Bound)是一种用于解决组合优化问题的算法设计范式。其核心思想是通过系统枚举所有可能解,并利用上下界策略剪枝,丢弃不可能产生最优解的子问题,从而有效减少搜索空间。
对于最小化问题(如最短路径、TSP),下界表示完成任务的最小可能代价;对于最大化问题(如0-1背包),上界表示可能获得的最大收益。通过比较这些界限与当前已知最优解,算法可以智能地剪枝,避免无谓的搜索。
分支定界法特别适用于NP难问题,如旅行商问题(TSP)、0-1背包问题、整数规划等。
核心思想与步骤
- 初始化:创建一个优先队列(或堆)存储待处理子问题,将初始问题加入队列。
- 迭代处理:每次从队列中取出一个最有希望的子问题(通常是下界最小或上界最大),进行如下操作:
- 界定:计算该子问题的上下界。上界代表该子问题可能达到的最优解,下界代表该子问题的最小代价。
- 可行性检查:若下界大于当前最优解,直接剪枝丢弃该子问题(对于最小化问题)。
- 完整性检查:若找到完整解,且优于当前最优解,则更新最优解。
- 分支:将子问题分解为更小子问题,加入队列。
- 终止条件:队列为空时,当前最优解即为全局最优解。
注:分支定界算法的效率高度依赖于上下界的定义和剪枝策略,合理设计可大幅减少搜索空间。
常见变体
- 最小优先队列(LC法):优先扩展下界最小的节点。
- 最佳优先搜索(Best-First):依据评估函数选择最有希望的节点。
- 深度优先分支定界(DFS B&B):结合深度优先策略,优先扩展深层节点。
- 宽度优先分支定界(BFS B&B):结合宽度优先策略,按层次扩展节点。
复杂度分析
- 时间复杂度:最坏情况下需枚举所有解,复杂度为O(2^n)或更高(对于n个决策变量的问题),但实际中通过有效的剪枝策略可大幅降低。
- 空间复杂度:主要取决于优先队列中节点数,最坏O(2^n),实际远小于此。
注:分支定界法可用于解决最大化问题(如0-1背包)和最小化问题(如最短路径、TSP)。对于最大化问题,上界表示可能达到的最大值;对于最小化问题,下界表示可能达到的最小值。剪枝策略也相应调整:最大化问题在上界≤当前最优解时剪枝,最小化问题在下界≥当前最优解时剪枝。
案例分析
1. 0-1背包问题
问题描述:有一个容量为W的背包,n种物品,每种物品有重量和价值。目标是在不超过背包容量的前提下,选择若干物品使总价值最大。每种物品只能选或不选(0-1选择)。
分支定界解法思路:
- 每个节点表示当前已选物品集合。
- 分支:选/不选当前物品。
- 上界:用贪心法估算当前节点能达到的最大价值(对于背包问题,我们求最大值,所以上界是可能达到的最大价值)。
- 剪枝:若上界不优于当前最优解则不再扩展(即上界≤当前最优解时剪枝)。
代码示例:
#include <stdio.h>// 物品结构体
struct Item {int weight; // 重量int value; // 价值
};int best_value = 0; // 当前最优解// 计算上界(贪心估计)
int calculate_bound(int n, int W, struct Item items[], int current_weight, int current_value, int index) {int bound = current_value;int remaining_weight = W - current_weight;// 贪心:尽量装满背包,若遇到装不下的物品则取部分价值// 这里计算的是上界,即该节点能达到的最大价值估计for (int i = index; i < n; i++) {if (items[i].weight <= remaining_weight) {bound += items[i].value;remaining_weight -= items[i].weight;} else {bound += (int)((double)items[i].value / items[i].weight * remaining_weight);break;}}return bound;
}// 分支定界主过程
void branch_and_bound(int n, int W, struct Item items[], int current_weight, int current_value, int index) {if (index == n) { // 所有物品已考虑if (current_value > best_value) {best_value = current_value;}return;}int upper_bound = calculate_bound(n, W, items, current_weight, current_value, index);if (upper_bound <= best_value) return; // 剪枝:如果上界不优于当前最优解,则不再扩展// 不选当前物品branch_and_bound(n, W, items, current_weight, current_value, index + 1);// 选当前物品(需判断容量)if (current_weight + items[index].weight <= W) {branch_and_bound(n, W, items, current_weight + items[index].weight,current_value + items[index].value, index + 1);}
}int main() {struct Item items[] = { {2, 40}, {3, 60}, {4, 80}, {5, 100} };int n = sizeof(items) / sizeof(items[0]);int W = 7;branch_and_bound(n, W, items, 0, 0, 0);printf("Maximum value obtainable: %d\n", best_value);return 0;
}
2. 最短路径问题(分支定界法)
问题描述:给定有向图,求从起点到终点的最短路径。
分支定界思路:
- 每个节点表示当前到达某顶点的路径。
- 分支:从当前顶点扩展到所有可达顶点。
- 下界:当前路径长度(对于最短路径问题,我们求最小值,所以下界是已经确定的路径长度)。
- 剪枝:若当前路径长度已大于已知最短路径则不再扩展。
注意:下面的代码实际上是Dijkstra算法的一种实现,它可以看作是分支定界法的一个特例,使用优先队列(最小堆)来选择下一个要扩展的节点。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.h>
#include <assert.h>#define NO 0xffffff
typedef struct Arc
{int index;int weight;
}Arc;bool compare(Arc one, Arc two)
{return one.weight < two.weight;
}typedef Arc DataType;
typedef struct Heap
{DataType* data;int maxSize;int curSize;
}Heap;//创建堆
Heap* create_heap(int maxSize)
{Heap* heap = (Heap*)malloc(sizeof(Heap));assert(heap);heap->curSize = 0;heap->maxSize = maxSize;heap->data = (DataType*)malloc(sizeof(int) * (heap->maxSize + 1));assert(heap->data);return heap;
}
int size_heap(Heap* heap)
{return heap->curSize;
}
bool empty_heap(Heap* heap)
{return heap->curSize == 0;
}
//调整位置
void move(Heap* heap, int curPos)
{while (curPos > 1){Arc max = heap->data[curPos];//完全二叉树的特点:父节点序号=孩子节点/2;int parentPos = curPos / 2;if (compare(max, heap->data[parentPos])){//和父节点值交换即可heap->data[curPos] = heap->data[parentPos];heap->data[parentPos] = max;//下标改变,继续向上渗透curPos = parentPos;}else{//插入元素小于父节点值,不需要调整break;}}
}
//入堆
void insert_heap(Heap* heap, DataType data)
{if (heap->curSize == heap->maxSize){return;}//heap->data[0] 不存数据,为了保持下表与完全二叉树的序号一致//heap->data[1]=第一个元素 curSize=1heap->data[++heap->curSize] = data;//向上渗透move(heap, heap->curSize);
}
Arc pop_heap(Heap* heap)
{Arc max = heap->data[1]; //最大值int curPos = 1;int childPos = curPos * 2;while (childPos <= heap->curSize){Arc temp = heap->data[childPos]; //左边//childPos childPos+1if (childPos + 1 <= heap->curSize && !compare(temp,heap->data[childPos + 1])){temp = heap->data[++childPos];//childPos = childPos + 1;}heap->data[curPos] = temp;curPos = childPos;childPos *= 2;}//最后一个元素覆盖删除元素heap->data[curPos] = heap->data[heap->curSize];//一定要调整堆move(heap, curPos);heap->curSize--;return max;
}
void init_graph(int(*graph)[11], int size)
{for (int i = 0; i < size; i++){for (int j = 0; j < size; j++){graph[i][j] = -1;}}graph[0][1] = 2;graph[0][2] = 3;graph[0][3] = 4;graph[1][2] = 3;graph[1][5] = 2;graph[1][4] = 7;graph[2][5] = 9;graph[2][6] = 2;graph[3][6] = 2;graph[4][7] = 3;graph[4][8] = 3;graph[5][8] = 3;graph[5][6] = 1;graph[6][8] = 5;graph[6][9] = 1;graph[7][10] = 3;graph[8][10] = 2;graph[9][8] = 2;graph[9][10] = 2;
}void get_short_path(int(*graph)[11],int size, int edge, int end)
{int shortpath = 0;Arc* path = (Arc*)malloc(sizeof(Arc) * size);int* pathIndex = (int*)malloc(sizeof(int) * size);assert(pathIndex);assert(path);for (int i = 0; i < size; i++) {(path + i)->index = 0;(path + i)->weight = NO;}Heap* minHeap = create_heap(size);insert_heap(minHeap, (Arc) { 0, 0 });while (true) {Arc top = pop_heap(minHeap);if (top.index == end) {break;}for (int i = 0; i < size; i++) {if(graph[top.index][i]!=edge&&top.weight+graph[top.index][i]<(path+i)->weight){insert_heap(minHeap, (Arc) { i, top.weight + graph[top.index][i] });(path + i)->index = top.index;(path + i)->weight = top.weight + graph[top.index][i];}}if (empty_heap(minHeap)) {break;}}shortpath = path[end].weight;int index = end;int count = 0;pathIndex[count++] = index;while (true){index = (path + index)->index;*(pathIndex+count) = index;count++;if (index == 0)break;}printf("最短路径:%d\n", shortpath);printf("最短路径:");for (int i = count-1; i >=0; i--) {printf("%d--->",*(pathIndex+i));}
}int main()
{int graph[11][11];int size = 11;init_graph(graph, size);get_short_path(graph, 11, -1, 10);return 0;
}
3. 旅行商问题(TSP)
旅行商问题是一个经典的组合优化问题:给定一组城市和每对城市之间的距离,求解访问每个城市恰好一次并返回起点城市的最短路径。
在分支定界算法中,可以通过以下方式解决TSP:
- 状态表示:使用部分路径表示当前状态,包括已访问的城市和当前所在城市。
- 分支策略:从当前城市出发,选择下一个未访问的城市。
- 界定函数:使用最小生成树或者最小匹配等方法计算下界(对于TSP问题,下界是完成旅行的最小可能距离)。这种下界估计可以有效剪枝,减少搜索空间。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <limits.h>#define MAX_CITIES 20// 城市间距离矩阵
int dist[MAX_CITIES][MAX_CITIES];
// 记录最优路径
int best_path[MAX_CITIES];
// 记录当前路径
int curr_path[MAX_CITIES];
// 记录城市是否已访问
bool visited[MAX_CITIES];
// 最优路径长度
int best_length = INT_MAX;// 计算下界函数
int calculate_bound(int n, int curr_length, int curr_city, int level) {// 计算下界:当前已走过的路径长度加上所有未访问城市到最近城市的最小距离// 这是一个乐观估计,实际完成旅行的距离一定不会小于这个值int bound = curr_length;// 如果已经访问了所有城市,加上返回起点的距离if (level == n) {bound += dist[curr_city][0];return bound;}// 对于每个未访问的城市,找到连接它的最小边for (int i = 0; i < n; i++) {if (!visited[i]) {int min_edge = INT_MAX;for (int j = 0; j < n; j++) {if (i != j && (visited[j] || j == 0)) {if (dist[i][j] < min_edge) {min_edge = dist[i][j];}}}bound += min_edge;}}return bound;
}// 分支定界算法解决TSP
void tsp_branch_and_bound(int n, int curr_length, int curr_city, int level) {// 如果访问了所有城市if (level == n) {// 加上返回起点的距离curr_length += dist[curr_city][0];// 更新最优解if (curr_length < best_length) {best_length = curr_length;for (int i = 0; i < n; i++) {best_path[i] = curr_path[i];}}return;}// 计算当前状态的下界int bound = calculate_bound(n, curr_length, curr_city, level);// 如果下界大于当前最优解,剪枝if (bound >= best_length) {return;}// 尝试访问每个未访问的城市for (int i = 0; i < n; i++) {if (!visited[i]) {// 标记为已访问visited[i] = true;curr_path[level] = i;// 递归处理下一个城市tsp_branch_and_bound(n, curr_length + dist[curr_city][i], i, level + 1);// 回溯visited[i] = false;}}
}int main() {int n = 4; // 城市数量// 初始化距离矩阵(示例)int distances[4][4] = {{0, 10, 15, 20},{10, 0, 35, 25},{15, 35, 0, 30},{20, 25, 30, 0}};// 复制距离矩阵for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {dist[i][j] = distances[i][j];}}// 初始化for (int i = 0; i < n; i++) {visited[i] = false;}// 从城市0开始curr_path[0] = 0;visited[0] = true;// 运行分支定界算法tsp_branch_and_bound(n, 0, 0, 1);// 输出结果printf("最短路径长度: %d\n", best_length);printf("最短路径: 0");for (int i = 1; i < n; i++) {printf(" -> %d", best_path[i]);}printf(" -> 0\n");return 0;
}
j] = distances[i][j];
}
}
// 初始化
for (int i = 0; i < n; i++) {visited[i] = false;
}// 从城市0开始
curr_path[0] = 0;
visited[0] = true;// 运行分支定界算法
tsp_branch_and_bound(n, 0, 0, 1);// 输出结果
printf("最短路径长度: %d\n", best_length);
printf("最短路径: 0");
for (int i = 1; i < n; i++) {printf(" -> %d", best_path[i]);
}
printf(" -> 0\n");return 0;
}