趣学算法系列-分支限界法

趣学算法系列-分支限界法

声明:本系列为趣学算法一书学习总结内容,在此推荐大家看这本算法书籍作为算法入门,
原作者博客链接,本书暂无免费电子版资源,请大家支持正版

第六章 分支限界法

在树搜索法中,从上到下为纵,从左向右为横,纵向搜索是深度优先,而横向搜索是广
度优先。前面讲的回溯法就是一种深度优先的算法。分支限界法就是以广(宽)度优先的树搜索方法
- 分支限界法的算法思想
*从根开始,常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
首先将根结点加入活结点表(用于存放活结点的数据结构)
*接着从活结点表中取出根结点,使其成为当前扩展结点,一次性生成其所有孩子结点,判断孩子结点是舍 弃还是保留,舍弃那些导致不可行解或导致非最优解的孩子结点,其余的被保留在活结点表中。
*再从活结点表中取出一个活结点作为当前扩展结点,重复上述扩展过程,直到找到所需的解或活结点表为
空时为止。由此可见,每一个活结点最多只有一次机会成为扩展结点。
*活结点表的实现通常有两种形式:一是普通的队列,即先进先出队列;一种是优先级队
列,按照某种优先级决定哪个结点为当前扩展结点
- 分支限界法算法步骤
(1)定义解空间 确定解空间包括解的组织形式和显约束(范围限定)
(2)确定解空间的组织结构 通常用解空间树形象的表达(只是辅助理解并不是真的树)
(3)搜索解空间 按照广度优先搜索,根据限制条件,搜索问题的解
- 分支限界法解题秘籍
(1)定义解空间 确定解空间包括解的组织形式和显约束(范围限定)
(2)确定解空间的组织结构 通常用解空间树形象的表达(只是辅助理解并不是真的树)
(3)搜索解空间 按照深度优先搜索,根据限制条件,搜索问题的解
- 回溯法的典型应用
        树的广度优先遍历
        旅行商问题


  • 实际案例分析-旅行商问题

  • 问题描述

    终于有一个盼望已久的假期!立马拿出地图,标出最想去的 n 个
    景点,以及两个景点之间的距离 dij,为了节省时间,我们希望在最短
    的时间内看遍所有的景点,而且同一个景点只经过一次。怎么计划行
    程,才能在最短的时间内不重复地旅游完所有景点回到家呢?
    趣学算法系列-分支限界法_第1张图片

  • 问题分析

    现在我们从 1 号景点出发, 游览其他 3 个景点, 先给景点编号 1~4,每个景点用一个结点表示,可以直接到达的景点有连线,连线上的数字代表两个景点之间的路程(时间)。那么要去的景点地图就转化成了一个无向带权图,如图 6-34 所示。
    趣学算法系列-分支限界法_第2张图片
    在无向带权图 G=( V, E)中,结点代表景点,连线上的数字代表景点之间的路径长度

  • 算法设计

    (1)定义问题的解空间
    奇妙之旅问题解的形式为 n 元组: {x1, x2,…, xi,…, xn},分量 xi 表示第 i 个要去的旅
    游景点编号,景点的集合为 S={1, 2,…, n}。景点不可重复走,xi 的取值为 S−{x1, x2,…, xi−1}, i=1, 2,…, n。
    (2)解空间的组织结构
    问题解空间是一棵排列树,树的深度为n=4,如图 6-35 所示。
    趣学算法系列-分支限界法_第3张图片
    (3)搜索解空间
    约束条件
    用二维数组 g[][]存储无向带权图的邻接矩阵,如果 g[i][j]≠∞表示城市 i 和城市 j 有边相连,能走通
    限界条件
    cl这里写图片描述bestl, cl 的初始值为 0, bestl 始值为+∞。
    cl:当前已走过的城市所用的路径长度。
    bestl:表示当前找到的最短路径的路径长度。
    搜索过程
    如果采用普通队列(先进先出)式的分支限界法,那么除了最后一层外,所有的结点都
    会生成,这显然不是我们想要的
    可以使用优先队列式分支限界法,加速算法的搜索速度
    设置优先级:当前已走过的城市所用的路径长度 cl。 cl 越小,优先级越高

  • 完美图解

    (1)数据结构
    设置地图的带权邻接矩阵为 g[][],即如果从顶点 i 到顶点 j 有边,就让 g[i][j]等于这里写图片描述i, j>
    的权值,否则 g[i][j]=∞(无穷大),如图 6-37 所示。
    趣学算法系列-分支限界法_第4张图片
    (2)初始化
    当前已走过的路径长度 cl=0,当前最优值 bestl=∞。初始化解向量 x[i]和最优解 bestx[i],
    如图 6-38 和图 6-39 所示。
    趣学算法系列-分支限界法_第5张图片
    (3)创建 A 节点
    A0 作为初始结点,因为我们是从 1 号结点出发,因此 x[1]=1, 生成 A 结点。 创建 A 结点 Node( cl, id),cl=0, id=2; cl 表示当前已走过的城市所用的路径长度,id 表示层号;解向量 x[]=( 1, 2, 3, 4), A 加入优先队列 q 中,如图 6-40 所示。
    趣学算法系列-分支限界法_第6张图片
    (此处的优先队列不是普通的队列,是队列和堆结构实现的)优先队列中按自定义的优先规则排序
    具体查看[优先队列和堆]http://blog.csdn.net/ahafg/article/details/60581286
    (4)扩展 A 结点
    队头元素 A 出队,一次性生成 A 结点的所有孩子,用 t 记录 A 结点的 id, t=2。
    搜索 A 结点的所有分支, for(j=t; j这里写图片描述=n; j++)。对每一个 j,判断 x[t−1]结点和 x[j]结点是否有
    边相连,且 cl+g[x[t−1]][[x[j]]这里写图片描述bestl,即判定是否满足约束条件和限界条件。如果满足则生成新
    结点 Node(cl, id),新结点的 cl=cl+g[x[t−1]][[x[j]],新结点的 id=t+1,复制父结点 A 的解向量,
    并执行交换操作 swap(x[t], x[j]),刚生成的新结点加入优先队列;如果不满足,则舍弃。
    • j=2:因为 x[1]结点和 x[2]结点有边相连,且 cl+g[1][2]=0+15=15这里写图片描述bestl=∞,满足约束
    条件和限界条件,生成 B 结点 Node( 15, 3)。复制父结点 A 的解向量 x[]=( 1, 2,
    3, 4),并执行交换操作 swap( x[t], x[j]),即 x[2]和 x[2]交换,解向量 x[]=( 1, 2,
    3, 4)。 B 加入优先队列。
    • j=3:因为 x[1]结点和 x[3]结点有边相连,且 cl+g[1][3]=0+30=30这里写图片描述bestl=∞,满足约束
    条件和限界条件,生成 C 结点 Node( 30, 3)。复制父结点 A 的解向量 x[]=( 1, 2,
    3, 4),并执行交换操作 swap( x[t], x[j]),即 x[2]和 x[3]交换,解向量 x[]=( 1, 3,
    2, 4)。 C 加入优先队列。
    • j=4:因为 x[1]结点和 x[4]结点有边相连,且 cl+g[1][4]=0+5=5这里写图片描述bestl=∞,满足约束条
    件和限界条件,生成 D 结点 Node( 8, 3)。复制父结点 A 的解向量 x[]=( 1, 2, 3,
    4),并执行交换操作 swap( x[t], x[j]),即 x[2]和 x[4]交换,解向量 x[]=( 1, 4, 3,
    2)。 D 加入优先队列
    趣学算法系列-分支限界法_第7张图片
    后续过程略
    趣学算法系列-分支限界法_第8张图片
    趣学算法系列-分支限界法_第9张图片

  • 伪代码详解

    (1)定义结点结构体

    struct Node //定义结点,记录当前结点的解信息
    {
    double cl; //当前已走过的路径长度
    int id; //景点序号
    int x[N]; //记录当前路径
    Node() {}
    Node(double _cl,int _id)
    {
    cl = _cl;
    id = _id;
    }
    };

    (2)优先队列式分支限界法搜索函数

    //Travelingbfs 为优先队列式分支限界法搜索
    double Travelingbfs()
    {
    int t; //当前处理的景点序号 t
    Node livenode,newnode; //定义当前扩展结点 livenode,生成新结点 newnode
    priority_queue q; //创建优先队列,优先级为已经走过的路径长度cl, cl 值越小,越优先
    newnode=Node(0,2); //创建根节点
    for(int i=1;i<=n;i++)
    {
    newnode.x[i]=i; //初时化根结点的解向量
    }
    q.push(newnode); //根结点加入优先队列
    while(!q.empty())
    {
    livenode=q.top(); //取出队头元素作为当前扩展结点 livenode
    q.pop(); //队头元素出队
    t=livenode.id; //当前处理的景点序号
    // 搜到倒数第 2 个结点时个景点的时候不需要往下搜索
    if(t==n) //立即判断是否更新最优解,
    //例如当前找到一个路径(1243),到达 4 号结点时,立即判断 g[4][3]和 g[3][1]是
    否有边相连,如果有边则判断当前路径长度 cl+g[4][3]+g[3][1]//说明找到了一条更好的路径,记录相关信息
    if(g[livenode.x[n-1]][livenode.x[n]]!=INF&&g[livenode.x[n]][1]!=INF)
    if(livenode.cl+g[livenode.x[n-1]][livenode.x[n]]+g[livenode
    .x[n]][1]1]][livenode.x[n]]+g[l
    ivenode.x[n]][1];
    for(int i=1;i<=n;i++)
    {
    bestx[i]=livenode.x[i];//记录最优解
    }
    }
    continue;
    }
    //判断当前结点是否满足限界条件,如果不满足不再扩展
    if(livenode.cl>=bestl)
    continue;
    //扩展
    //没有到达叶子结点
    for(int j=t; j<=n; j++)//搜索扩展结点的所有分支
    {
    if(g[livenode.x[t-1]][livenode.x[j]]!=INF)//如果 x[t-1]景点与 x[j]
    景点有边相连
    {
    double cl=livenode.cl+g[livenode.x[t-1]][livenode.x[j]];
    if(cl//有可能得到更短的路线
    {
    newnode=Node(cl,t+1);
    for(int i=1;i<=n;i++)
    {
    newnode.x[i]=livenode.x[i];//复制以前的解向量
    }
    swap(newnode.x[t], newnode.x[j]);//交换 x[t]、 x[j]两个
    元素的值
    q.push(newnode);//新结点入队
    }
    }
    }
    }
    return bestl; //返回最优值
    }
  • 实战演练
    //program 6-2
    
    #include 
    
    
    #include 
    
    
    #include 
    
    
    #include 
    
    
    #include 
    
    using namespace std;
    const int INF=1e7; //设置无穷大的值为 10^7
    const int N=100;
    double g[N][N]; //景点地图邻接矩阵
    int bestx[N]; //记录当前最优路径
    double bestl; //当前最优路径长度
    int n,m; //景点个数 n,边数 m
    struct Node //定义结点,记录当前结点的解信息
    {
    double cl; //当前已走过的路径长度
    int id; //景点序号
    int x[N]; //记录当前路径
    Node() {}
    Node(double _cl,int _id)
    {
    cl = _cl;
    id = _id;
    }
    };
    //定义队列的优先级。以 cl 为优先级, cl 值越小,越优先
    bool operator <(const Node &a, const Node &b)
    {
    return a.cl>b.cl;
    }
    //Travelingbfs 为优先队列式分支限界法搜索
    double Travelingbfs()
    {
    int t; //当前处理的景点序号 t
    Node livenode,newnode; //定义当前扩展结点 livenode,生成新结点 newnode
    priority_queue q; //创建一个优先队列,优先级为已经走过的路径长度 cl, cl 值
    越小,越优先
    newnode=Node(0,2); //创建根节点
    for(int i=1;i<=n;i++)
    {
    newnode.x[i]=i; //初时化根结点的解向量
    }
    q.push(newnode); //根结点加入优先队列
    while(!q.empty())
    {
    livenode=q.top(); //取出队头元素作为当前扩展结点 livenode
    q.pop(); //队头元素出队
    t=livenode.id; //当前处理的景点序号
    // 搜到倒数第 2 个结点时个景点的时候不需要往下搜索
    if(t==n) //立即判断是否更新最优解
    //例如当前找到一个路径(1243),到达 4 号结点时,立即判断 g[4][3]和 g[3][1]是
    否有边相连,如果有边则判断当前路径长度 cl+g[4][3]+g[3][1]//说明找到了一条更好的路径,记录相关信息
    if(g[livenode.x[n-1]][livenode.x[n]]!=INF&&g[livenode.x[n]][1]!=INF)
    if(livenode.cl+g[livenode.x[n-1]][livenode.x[n]]+g[livenod
    e.x[n]][1]1]][livenode.x[n]]+g[l
    ivenode.x[n]][1];
    cout<//记录当前最优的解向量
    for(int i=1;i<=n;i++)
    {
    bestx[i]=livenode.x[i];
    }
    }
    continue;
    }
    //判断当前结点是否满足限界条件,如果不满足不再扩展
    if(livenode.cl>=bestl)
    continue;
    //扩展
    //没有到达叶子结点
    for(int j=t; j<=n; j++)//搜索扩展结点的所有分支
    {
    if(g[livenode.x[t-1]][livenode.x[j]]!=INF)//如果 x[t-1]景点与 x[j]
    景点有边相连
    {
    double cl=livenode.cl+g[livenode.x[t-1]][livenode.x[j]];
    if(cl//有可能得到更短的路线
    {
    newnode=Node(cl,t+1);
    for(int i=1;i<=n;i++)
    {
    newnode.x[i]=livenode.x[i];//复制以前的解向量
    }
    swap(newnode.x[t], newnode.x[j]);//交换 x[t]、 x[j]两个
    元素的值
    q.push(newnode);//新结点入队
    }
    }
    }
    }
    return bestl;//返回最优值
    }
    void init()//初始化
    {
    bestl=INF;
    for(int i=0; i<=n; i++)
    {
    bestx[i]=0;
    }
    for(int i=1;i<=n;i++)
    for(int j=i;j<=n;j++)
    g[i][j]=g[j][i]=INF;//表示路径不可达
    }
    void print()//打印路径
    {
    cout<cout<<"最短路径: ";
    for(int i=1;i<=n;i++)
    cout<"--->";
    cout<<"1"<cout<<"最短路径长度: "<int main()
    {
    int u, v, w;//u,v 代表城市, w 代表 u 和 v 城市之间路的长度
    cout << "请输入景点数 n(结点数): ";
    cin >> n;
    init();
    cout << "请输入景点之间的连线数(边数): ";
    cin >> m;
    cout << "请依次输入两个景点 u 和 v 之间的距离 w,格式:景点 u 景点 v 距离 w: "<for(int i=1;i<=m;i++)
    {
    cin>>u>>v>>w;
    g[u][v]=g[v][u]=w;
    }
    Travelingbfs();
    print();
    return 0;
    }
  • 算法时间复杂度分析

    (1)时间复杂度:最坏情况下,如图 6-46 所示。时间复杂度为 O(n!)。
    趣学算法系列-分支限界法_第10张图片
    (2)空间复杂度:程序中我们设置了每个结点都要记录当前的解向量 x[]数组,占用空间为 O(n),结点的
    个数最坏为 O(n!),所以该算法的空间复杂度为 O(n*n!)。

  • 你可能感兴趣的:(算法分析)