算法分析与设计:分支限界法

分支限界法

1. 基本思想

分支是使用广度优先策略,依次生成扩展结点的所有分支
限界是在结点扩展过程中,计算结点的上界,搜索的同时剪掉某些分支
分支限界法就是把问题的可行解展开,再由各个分支寻找最佳解。
与回溯法类似,分支限界法也是在解空间中搜索得到解;
不同的是,分支限界法会生成所有扩展结点,并舍弃不可能通向最优解的结点,然后根据广度优先/最小耗费优先,从活结点中选择一个作为扩展结点,使搜索向解空间上有最优解的分支推进。

2. 搜索策略

分支限界法首先生成当前扩展结点的所有分支,然后再从所有活结点中选择一个作为扩展结点。每一个活结点都要计算限界,根据限界情况判断是否剪枝,或选择最有利的结点。
分支限界法有两种不同的搜索空间树方式,分别为广度优先最小耗费优先,它们对应两种不同的方法:

  1. 队列式分支限界法(FIFO)
    常规的广度优先策略。按照先进先出的原则选取下一个扩展结点,以队列储存活结点。
  2. 优先队列式分支限界法/最小耗费优先分支限界法(LC)
    按照优先队列中指定的优先级,选取优先级最高的结点作为下一个扩展结点,以优先队列储存。

分支限界法的具体搜索策略如下:

  1. 根结点入队;
  2. 根据使用的方法(FIFO或LC),令一个活结点出队,作为扩展结点
  3. 对扩展结点,生成所有的分支;使用约束条件舍弃不可行的结点/不可能为最优解的结点,剩余的结点入队;
  4. 重复2和3,直到找到要求的解队列为空
方法搜索策略存储结点常用结构结点存储特性应用问题
回溯法深度优先结点可以多次成为扩展结点,所有可行子结点都遍历后才弹出找出满足条件的所有解
分支限界法广度/LC优先队列/优先队列结点只能成为一次扩展结点,剪枝或扩展后立刻出队找出条件下的某个/最优解
3. 分支结点选择

所有界限满足上界/下界的结点都可以作为扩展结点。因此,必须有一个分支选择策略,FIFO法和LC法对应两种策略:
● 按顺序选择结点作为下一次的扩展结点。优点是节省空间,缺点是需要计算的分支数较多,时间花费大;
● 每次计算完限界后,找出限界最优的结点,作为下一次的扩展结点。优点是计算的分支数少,缺点是需要额外空间。

4. 限界函数

限界函数很大程度上决定了算法的效率。同一问题可以设计不同的限界函数。
FIFO分支限界法中,常以约束条件作为限界函数,满足约束条件才可入队,不满足约束条件的舍弃。
LC分支限界法中,还可以设计一个启发函数作为限界函数。
对于有约束的问题,FIFO法和LC法均可以求解;对于无约束问题, 宜使用LC法。

例题:单源最短路径

1. 问题描述

给定带权有向图G,每边的权值是一个正实数,表示点到点的路径距离。给定图中的一个源点V,求图G中所有点到源点V的最短路径。
带权有向图

2. 问题分析

除了用Dijkstra算法(贪心)解决该问题外,也可以使用分支限界法。由于要求的是最短的路径,我们考虑使用优先队列式分支限界法,以减少计算的分支数。显然,我们的限界就是源到目的点的路径长度:若源到同一个顶点有多条路径,将长路径的分支全部舍弃,而保存更短路径的分支。并且由于题目的贪心选择性质,每次从优先队列中取最短路径,最终得到的解也必然是最优的。

为了避免出队列可能造成的异常,并能更有规律地处理优先队列,我们为最小堆构造一个长度等同于顶点个数的结点数组。数组元素的下标对应顶点的编号;数组元素的编号为-1时,代表该结点被删除(出队列)。
解空间树

3. 算法设计
  1. 生成根节点的所有分支,全部入队列并记录路径;
  2. 在队列中选择路径最短的分支作为扩展结点
  3. 逐个生成分支,并判断分支的路径是否小于记录的最短路径;
  4. 若不小于,舍弃该分支;
  5. 若小于,该分支入队列;
  6. 生成所有分支后,回到2;
  7. 当队列为空时,算法结束。
4. 算法实现
//单源最短路径

class Graph{	//带权有向图
private:
    int n;			//顶点个数
    int **c;		//邻接矩阵
    int *dist;		//记录路径
public:
    void shortestPaths(int);
    Graph();         //根据情况构造图
};

class MinHeapNode{		//最小堆的结点
    friend Graph;
private:
    int i;				//结点对应的顶点编号
    int length;			//结点记录的最短路径
public:
    int getI(){ return i; }
    void setI(int i){ this->i = i; }
    int getLength(){ return length; }
    void setLength(int length){ this->length = length; }
};

class MinHeap{		//最小堆(虽然叫堆,但其实并不是用堆实现的)
    friend Graph;
private:
    int length;			//最小堆的长度,等同于顶点个数
    MinHeapNode *nodes;		//结点数组
public:
    MinHeap(int n)
    {
        this->length = n;
        nodes = new MinHeapNode[n];
    }
    void deleteMin(MinHeapNode&);	//令当前节点出队列,并给出下一个扩展结点
    void insertNode(MinHeapNode N)		//结点入队列,将原结点的内容替换即可
    {
        nodes[N.getI()].setI(N.getI());
        nodes[N.getI()].setLength(N.getLength());
    }
    bool outOfBounds()		//检查队列为空
    {
        for(int i = 0;i < length;i++)
            if(nodes[i].getI() != -1)
                return false;
        return true;
    }
};

void MinHeap::deleteMin(MinHeapNode &E)
{
    int j = E.getI();
    nodes[j].setI(-1);
    nodes[j].setLength(-1);		//标记为出队列
    int tmp = INT_MAX;
    for(int i = 0;i < length;i++){		//给出路径最短的结点作为扩展结点
        if(nodes[i].getI() != -1 && nodes[i].getLength() < tmp){
            E.setI(i);
            E.setLength(nodes[i].getLength());
            tmp = nodes[i].getLength();
        }
    }
}

void Graph::shortestPaths(int start)
{
    MinHeap heap = MinHeap(n);	
    MinHeapNode E = MinHeapNode();		//别问,一开始还加了new,太久不写C++了
    E.i = start;	
    E.length = 0;
    dist[start] = 0;	//初始为源点V,对应编号start
    while(true){
        for(int j = 0;j < n;j++){	//检查所有邻接顶点
            if(c[E.i][j] != 0){		//是否邻接
                if(E.length + c[E.i][j] < dist[j]){		//是否满足限界,当前路径小于记录的最短路径
                    dist[j] = E.length + c[E.i][j];		//更新
                    if(/*填入判断表达式*/){          //没有邻接顶点的点不入队,按情况调整,没有也可以,但会造成无效开销
                        MinHeapNode N = MinHeapNode();	//创建一个新的结点,并令其入队列
                        N.i = j;
                        N.length = dist[j];
                        heap.insertNode(N);
                    }
                }
            }
        }
        if(heap.outOfBounds())	//队列为空,结束
            break;
        heap.deleteMin(E);	//该结点已经生成全部分支,出队列并取得下一扩展结点
    }
}

为使队列为空,while循环总共需要取n个结点;每个结点要对所有结点都进行检查。因此算法的时间复杂度为O(n2)

分支限界法的套路单一,就只写一道例题了,怎么可能是因为这两天沉迷骑砍呢😀

  • 31
    点赞
  • 83
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
背包问题是一个经典的组合优化问题,它的目标是在给定的一组物品中选择一些物品放入一个容量为W的背包中,使得背包中物品的总价值最大。分支限界法是解决背包问题的一种常用算法,它通过不断地分解问题,将问题空间划分为多个子问题,并对每个子问题进行求解,最终得到原问题的最优解。 下面是背包问题分支限界法的代码算法设计分析: 1. 算法设计 (1)定义节点类Node,包含以下成员变量: - weight:当前节点已经装入背包的物品重量; - value:当前节点已经装入背包的物品价值; - bound:当前节点的价值上界; - level:当前节点所在的层数; - path:当前节点所在的路径。 (2)定义优先队列Q,用于存储待扩展的节点。 (3)初始化根节点,并将其加入队列Q中。 (4)循环执行以下步骤: - 从队列Q中取出一个节点; - 如果该节点的价值上界小于当前最优解,则舍弃该节点; - 如果该节点是叶子节点,则更新当前最优解; - 否则,生成该节点的左右子节点,并将它们加入队列Q中。 (5)输出当前最优解。 2. 算法分析 背包问题分支限界法的时间复杂度为O(2^n),其中n为物品的数量。由于该算法需要对每个节点进行价值上界的计算,因此空间复杂度为O(n)。在实际应用中,该算法效率受到物品数量和背包容量的限制,当物品数量较大或背包容量较小时,该算法效率会受到较大影响。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值