当前位置: 首页 > news >正文

算法之分支定界

分支定界

  • 分支定界
    • 概述
    • 核心思想与步骤
    • 常见变体
    • 复杂度分析
    • 案例分析
      • 1. 0-1背包问题
      • 2. 最短路径问题(分支定界法)
      • 3. 旅行商问题(TSP)

分支定界

概述

分支定界(Branch and Bound)是一种用于解决组合优化问题的算法设计范式。其核心思想是通过系统枚举所有可能解,并利用上下界策略剪枝,丢弃不可能产生最优解的子问题,从而有效减少搜索空间。

对于最小化问题(如最短路径、TSP),下界表示完成任务的最小可能代价;对于最大化问题(如0-1背包),上界表示可能获得的最大收益。通过比较这些界限与当前已知最优解,算法可以智能地剪枝,避免无谓的搜索。

分支定界法特别适用于NP难问题,如旅行商问题(TSP)、0-1背包问题、整数规划等。

核心思想与步骤

  1. 初始化:创建一个优先队列(或堆)存储待处理子问题,将初始问题加入队列。
  2. 迭代处理:每次从队列中取出一个最有希望的子问题(通常是下界最小或上界最大),进行如下操作:
    • 界定:计算该子问题的上下界。上界代表该子问题可能达到的最优解,下界代表该子问题的最小代价。
    • 可行性检查:若下界大于当前最优解,直接剪枝丢弃该子问题(对于最小化问题)。
    • 完整性检查:若找到完整解,且优于当前最优解,则更新最优解。
    • 分支:将子问题分解为更小子问题,加入队列。
  3. 终止条件:队列为空时,当前最优解即为全局最优解。

:分支定界算法的效率高度依赖于上下界的定义和剪枝策略,合理设计可大幅减少搜索空间。

常见变体

  • 最小优先队列(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:

  1. 状态表示:使用部分路径表示当前状态,包括已访问的城市和当前所在城市。
  2. 分支策略:从当前城市出发,选择下一个未访问的城市。
  3. 界定函数:使用最小生成树或者最小匹配等方法计算下界(对于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;

}


相关文章:

  • 多语言虚拟币海外游戏娱乐平台源码详解(整合篇)
  • 架构-系统可靠性分析与设计
  • 复杂性决策-思维训练
  • JAVA设计模式——(五)享元模式(Flyweight Pattern)
  • 缓存与数据库数据一致性:旁路缓存、读写穿透和异步写入模式解析
  • ArrayList与顺序表详解
  • C# 综合示例 库存管理系统4 classMod类
  • 力扣面试150题--基本计算器
  • 移动零--LeetCode
  • 切割PDF使用python,库PyPDF2
  • 区块链技术:深入共识算法、智能合约与DApps的架构奥秘
  • 【GIT】github中的仓库如何删除?
  • Langchain+RAG+向量数据库
  • vue-study(1)
  • java面向对象编程【基础篇】之基础语法
  • Day11(回溯法)——LeetCode79.单词搜索
  • 日语学习-日语知识点小记-构建基础-JLPT-N4阶段(10): つもり 计划/打算
  • Jenkins:开启高效软件开发的魔法之门
  • Java面试:从Spring Boot到微服务的全面考核
  • 【Leetcode 每日一题】2799. 统计完全子数组的数目
  • 牛市早报|商务部:目前中美之间未进行任何经贸谈判
  • 广东东莞调整普通住宅价格标准:一类镇街上浮300余元/平方米
  • 中国工程院院士、歼八Ⅱ飞机系统工程副总设计师温俊峰逝世
  • 宝龙地产:委任中金国际为境外债务重组新的独家财务顾问
  • GDP十强省份“一季报”出炉,湖北领跑
  • 美国国务院:鲁比奥将不参加在伦敦举行的乌克兰问题会谈