回溯算法

概述

回溯算法是一种组织搜索的一般技术,它常常可以避免搜索所有的可能性,适用于求解那些有潜在的大量解但是有限个数的解已经检查过的问题。

3着色问题

问题描述

给出一个无向图G=(V,E),需要用三种颜色之一为V中的每个顶点着色,要求没有两个相邻的顶点有相同的颜色。

算法思想

我们称没有两个邻接顶点有同样颜色的着色方案为合法的,反之成为非法的。如果不考虑合法性的要求,给出n个顶点的无向图,将其用三种颜色着色,共有n^3种不同的方法,因为没一个顶点都有三种不同的着色方案,这就构成了一颗三叉树,如图:
《回溯算法》
在该三叉树中,从根到叶子节点的每一条路径代表一种着色方法(合法的和不合法的),我们需要做的就是选出一条合法的从根到叶子的路径即可。所以我们从跟节点开始向叶子节点走,这时有两种情况:

  • 从根到当前节点的路径对应一个合法的着色:
    • 当前路径长度小于n:过程终止(除非希望找到不止一种着色方案)
    • 当前路径长度等于n:生成当前节点的一个子节点,并将生成的当前节点的子节点标记为新的当前节点
  • 从根到当前节点的路径对应一个非法的着色:回溯到当前节点的父节点即将当前节点的父节点标记为新的当前节点

代码

使用数组c[1…n]代表图的顶点集合,判断合法性只需判断与当前有联系的点中是否存在与之涂色相同的点即可,此处为节省时间省略该部分代码的实现。

迭代法

void ColorItre(int *c) {//c[1...n]
    for (int i = 1; i <= n; ++i) {
        c[i] = 0;
    }
    bool flag = false;
    int k = 1;
    while (k >= 1) {
        while (c[k] <= 2) {
            c[k] = c[k] + 1;
            if (c[k]为合法的){
                if (k == n) {
                    flag = true;
                    break;
                } else {
                    k++;
                }

            }
        }

        if (flag) {
            break;
        }

        //如果第二个循环跳出执行到这里,则说明当前节点k试遍了三种颜色仍然没有找到合法的着色,则将k--进行回溯,注意要将c[k]置为初始值0
        c[k] = 0;
        k--;
    }

    if (flag) {
        cout << "success" << endl;
    } else {
        cout << "no solution" << endl;
    }

}

递归法

void ColorRec(int *c) {//c[1...n]
    for (int i = 1; i <= n; ++i) {
        c[i] = 0;
    }

    bool flag = false;
    flag = graphcolor(c, 1);
    if (flag) {
        cout << "success" << endl;
    } else {
        cout << "no solution" << endl;
    }
}

bool graphcolor(int *c, int i) {
    for (int color = 1; color <= 3; ++color) {
        c[i] = color;
        if (c[i]是合法的){
            if (i < n) {
                graphcolor(c, i + 1);
            } else {
                return true;
            }
        }
    }
    //如果执行到这里说明当前节点不存在合法着色,需要回溯,返回false即可激活前一次递归(即让前一层for循环的color加一),以此达到回溯的目的
    return false;
}

复杂度分析

这两种实现方式在最坏情况下生成了O(3^n)个节点,对于每个生成的节点,判断当前节点的合法性(合法、部分、二者都不是)需要O(n)的工作来检查,因此,最坏情况下的运行时间是O(n3^n)。

回溯法的特点

  • 节点是使用深度优先搜索算法生成的
  • 不需要存储整棵搜索树,只需存储根到当前活动节点的路径

8皇后问题

代码见两种不同方式解决八皇后问题,此处主要介绍算法思路。

问题描述

八皇后问题是一个以国际象棋为背景的问题:如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n×n,而皇后个数也变成n。当且仅当 n = 1 或 n ≥ 4 时问题有解。

代码

代码和着色问题代码几乎一样,这里不做过多介绍,给出参考代码:

void eight_Queens() {
    int c[9];
    for (int i = 1; i <= 8; ++i) {
        c[i] = 0;
    }

    bool flag = false;
    int k = 1;
    while (k >= 1) {
        while (c[k] <= 7) {
            c[k]++;
            if (c[k]为合法着色){
                if (k == 8) {
                    flag = true;
                    break;
                } else {
                    k++;
                }
            }
        }
        if (flag) {
            break;
        }
        c[k] = 0;
        k--;
    }

    if (flag) {
        cout << "success" << endl;
    } else {
        cout << "no solution" << endl;
    }
}

复杂度分析

回溯法在最坏情况下需要O(n^2)的运行时间。
但是需要注意,虽然蛮力搜索法的最坏情况也需要 O(n^2)的时间,但是根据经验回溯法的有效性远远超过蛮力法。(鬼知道这是谁的经验,反正只要知道考试用蛮力法肯定会xx)

一般回溯方法

什么是一般回溯法

在回溯法中,解向量中每个xi都属于一个有限的线序集Xi,因此,算法最初从空向量开始,然后选择X1中最小的元素作为x1,如果(x1)是一个部分解,算法从X2中找出最小的元素作为x2继续,如果(x1,x2)是一个部分解,则从X3中找出最小元素作为x3,否则跳过x2寻找下一个。一般地,假如算法已经找到部分解(x1,x2,x3…,xj),在判断v=(x1,x2,x3…,xj,x_j+1)时有以下情况:

  • v是最终解:记录下当前v作为一组解,如果只想求得一组解则算法结束,否则继续寻找其他解
  • v是一组部分解:从X_j+2中寻找新的最小元素继续向前走
  • v既不是最终解也不是部分解:
    • X_j+1中还有其他元素可供选择:在X_j+1中寻找下一个元素
    • X_j+1中没有其他元素可供选择:将x_j置为X_j中的下一个元素回溯,如果X_j中仍然没有其他元素可供选择,则照此方法继续向前回溯。

分支限界法

算法介绍

分支限界法(branch and bound method)是求解纯整数规划混合整数规划问题的经典方法,在上世纪六十年代由Land Doig和Dakin等人提出。这种方法灵活且便于用计算机求解,目前已经成功运用于求解生产进度问题、旅行推销员问题、工厂选址问题、背包问题及分配问题等。算法基本思想如下:

  • 按宽度优先策略遍历解空间树;
  • 在遍历过程中,对处理的每个结点vi,根据界限函数,估计沿该结点向下搜索所可能达到的完全解的目标函数的可能取值范围—界限bound(vi)=[downi, upi];
  • 从中选择使目标函数取的极值(最大、最小)的结点优先进行宽度优先搜索,从而不断调整搜索方向,尽快找到问题解。

各结点的界限函数bound(vi)=[downi, upi]是解决问题的关键,通常依据具体问题而定。常见的两种分支限界法是队列式分支限界法和优先队列式分支限界法,它们分别按照队列先进先出的原则和优先队列中规定的优先级选取下一个节点为扩展节点。

分支限界法与回溯法的区别

求解目标不同

  • 回溯法的求解目标是找出解空间树中满足约束条件的所有解

  • 分支限界法的求解目标则是尽快找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解

  • 分支限界法通常用于解决离散值的最优化问题

搜索方式不同

  • 回溯法以深度优先的方式(遍历结点)搜索解空间树
  • 分支限界法以广度优先最小耗费优先的方式搜索解空间树

对扩展结点的扩展方式不同

  • 分支限界法中,每一个活结点只有一次机会成为扩展结点,活结点一旦成为扩展结点,就一次性产生其所有儿子结点
  • 重复上述结点扩展过程,直至到找到所需的解或活结点表为空时为止

Demo——旅行商问题求解

参考:这里

问题描述

给出一个城市的集合和一个定义在每一对城市之间的耗费函数,找出耗费最小的旅行。

解决思路

考虑下图所示的情况及其代价矩阵,假定起始城市为1号城市:

《回溯算法》
《回溯算法》
注意代价矩阵的特点,每条满足要求的回路在代价矩阵中的每一行每一列有且只有1个元素与之对应。据此,我们可以用贪心算法计算问题的上界:
以起始城市作为出发城市,每次从当前出发城市发出的多条边中,选择没有遍历过的最短边连接的城市,作为下一步达到城市。在这个问题中,从城市1出发,途经1→3→5→4→2→1,路径长度1+2+3+7+3=16作为上界,即最短路径长度<=16。
对于下界,一个简单的办法是直接将矩阵中每一行的最小元素相加,在这个问题中,路径长度1+3+1+3+2=10作为下界,即最短路径长度>=10。更优的计算方式是将矩阵中每一行最小的2个元素相加除以2并向上取整。因为在一条路径上,每个城市有2条邻接边:进入该城市、离开该城市。对每一步经过的城市j,从最近的上一个城市i来,再到下一个最近城市k去,即i→j→k。在这个问题中,路径长度{(1+3)+(3+6)+(1+2)+(3+4)+(2+3)}/2向上取整等于14作为下界,即最短路径长度>=14。因此,以最短路径长度dist作为TSP问题目标函数,则dist的界为[14,16]。在问题求解过程中,如果1个部分解的目标函数dist下界超出此界限,则该部分解对应了死结点,可剪枝。对于1条正在生成的路径/部分解,设已经确定的顶点(已经经过/遍历的城市)集合为U=(r1, r2, …, rk),则该部分解的目标函数的下界为(已经经过的路径的总长的2倍+从起点到最近未遍历城市的距离+从终点到最近未遍历城市的距离+进入/离开未遍历城市时各未遍历城市带来的最小路径成本)除以2并向上取整。假设正在生成的路径/部分解为1→4,U={1,4},未遍历城市={2,3,5},该部分解下界为{2*5+1+3+(3+6)+(1+2)+(2+3)}/2向上取整等于16:
《回溯算法》

    原文作者:分支限界法
    原文地址: https://blog.csdn.net/qq_36982160/article/details/80205879
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞