回溯法概述

回溯法在问题的解空间树中,按深度优先策略,从根 结点出发搜索解空间树。算法搜索至解空间树的任意 一点时,先判断该结点是否包含问题的解。如果肯定 不包含,则跳过对该结点为根的子树的搜索,逐层向 其祖先结点回溯;否则,进入该子树,继续按深度优 先策略搜索。

回溯法希望一个问题的解能够表示为一个n元式的样子(x1,x2,…,xn)

  • 显约束:对分量的取值限定
  • 隐约束:为满足问题的解而对不同分量之间施加的约束
  • 解空间:解向量满足所有显式约束条件的多元组,构成一个解空间

生成问题状态的基本方法

扩展结点:一个正在产生儿子的结点称为扩展结点

活结点:一个自身已经生成但是其儿子还没有全部生成的结点

死结点:儿子已经全部产生的结点

深度优先的问题状态生成法:如果对一个扩展结点R,一旦 产生了它的一个儿子C,就把C当做新的扩展结点。在完成 对子树C(以C为根的子树)的穷尽搜索之后,将R重新变 成扩展结点,继续生成R的下一个儿子(如果存在)

宽度优先的问题状态生成法:在一个扩展结点变成死结点 之前,它一直是扩展结点

回溯法:为了避免生成那些不可能产生最佳解的问题状态, 要不断地利用限界函数(bounding function)来处死那些实际 上不可能产生所需解的活结点,以减少问题的计算量。

具 有限界函数的深度优先生成法称为回溯法

回溯法基本思想

  1. 针对所给问题,定义问题的解空间
  2. 确定易于搜索的解空间结构
  3. 以深度优先方式搜索解空间,并在搜索过程中使用剪枝函数避免无效搜索

用约束函数在扩展结点处剪去不满足约束的子树; 用限界函数剪去得不到最优解的子树。

在任何时刻,算法只保存从根结点到当前扩展结点的 路径。如果解空间树中从根结点到叶结点的最长路径的长度为 h(n),则回溯法所需的计算空间通常为O(h(n))。

递归回溯

void backtrack (int t)//t为递归深度
{
    if (t>n) output(x);//算法已搜索至叶结点
    else
    {
        for (int i=f(n,t); i<=g(n,t); i++)//f,g 表示当前扩展节点处未搜索过的子树的起始编号和终止编号
        {
            x[t]=h(i);//h(i)表示在当前扩展结点处x[t]的第i个可选值;
            if (constraint(t)&&bound(t))//constraint(t)和bound(t))分别是当前扩展结点处的约束函数和限界函数。
                backtrack(t+1);
        }
    }
}
解释一下就是从自己的第一个子节点开始进行深度优先遍历,如果当前子节点满足约束函数和限界函数,继续向下递归,否则就不管改子节点,称为剪枝

迭代回溯

采用树的非递归深度优先遍历算法,可将回溯法表示为一个非 递归迭代过程。

void iterativeBacktrack ()
{
    int t=1;
    while (t>0) {
        if (f(n,t)<=g(n,t))
          for (int i=f(n,t);i<=g(n,t);i++) {
            x[t]=h(i);
            if (constraint(t)&&bound(t)) {
                if (solution(t)) output(x);
                else t++;}
              }
            else t--;//如果改节点不满足c和b,回溯
         }
}

涉及到回溯算法大部分的时间复杂度都为O(n2^n) 

1.装载问题

有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其 中集装箱i的重量为wi,且

,装载问题要求确定是否有一个合理的装载方案可将这个集装箱 装上这2艘轮船。如果有,找出一种装载方案。

问题可等价转换为

  1. 首先将第一艘轮船尽可能装满;
  2. 将剩余的集装箱装上第二艘轮船。

将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集, 使该子集中集装箱重量之和最接近。由此可知,装载问题等价 于以下特殊的0-1背包问题。

void backtrack (int i)
    {// 搜索第i层结点
        if (i > n) // 到达叶结点
            {更新最优解bestx,bestw;return;}//bestx为最优路径,bestw为最优载重量
        r -= w[i];//r为剩余重量
        if (cw + w[i] <= c) {// 搜索左子树:当前载重量cw+剩余集装箱的重量r<=当前最优载重量bestw
            x[i] = 1;
            cw += w[i];
            backtrack(i + 1);
            cw -= w[i]; }
        if (cw + r > bestw) {
            x[i] = 0; // 搜索右子树
            backtrack(i + 1); }
        r += w[i];//如果没有选择该子树,回溯时要将质量加回去
}

代码如下

函数类定义
template <class Type>
class Loading
{
    friend Type MaxLoading(Type [],Type,int);//定义友元函数
    private:
        void Backtrack(int i);
        int n;
        Type * w,
        c,cw,bestw,
        r;//剩余集装箱重量  
};
template <class Type>
void Loading<Type>::Backtrack(int i)//这里对类内函数实现
{
    if(i>n)
        {if(cw>bestw) bestw = cw;
            return;}
    r-=w[i];//计算剩余(未考察)的集装箱的重量,减去当前考察过的对象的重量
    if (cw + w[i] <= c) {// 搜索左子树:当前载重量cw+当前集装箱重量<=当前最优载重量bestw
            x[i] = 1;
            cw += w[i];
            backtrack(i + 1);
            cw -= w[i]; }
    if (cw + r > bestw) {//如果当前重量加上剩余集装箱质量大于当前最优载重量,那么就需要继续向下遍历,进行集装箱的挑选
            x[i] = 0; // 搜索右子树
            Backtrack(i + 1); }
    r += w[i];//如果没有选择该子树,回溯时要将质量加回去
}

template <class Type>
Type MaxLoading(Type w[],Type c,int n)
{
    Loading<Type> X; //初始化
    X.w = w;
    X.c = c;
    X.n = n;
    X.bestw = 0;
    X.cw = 0;
    X.r = 0; //初始化r
    for(int i=1;i<=n;i++) //计算总共的剩余(当前为考察过的)集装箱重量
        X.r += w[i];//r初始情况下为所有集装箱的质量
    X.Backtrack(1);
    return X.bestw;
}

2.批处理作业调度

给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处 理,然后由机器2处理。作业Ji需要机器j的处理时间为tji。对 于一个确定的作业调度,设Fji是作业i在机器j上完成处理的时 间。所有作业在机器2上完成处理的时间和称为该作业调度的 完成时间和。 批处理作业调度问题要求对于给定的n个作业,制定最佳作业 调度方案,使其完成时间和达到最小。

代码如下

void Flowshop::Backtrack(int i)
{
    if (i > n) {//更改当前最优作业调度
        for (int j = 1; j <= n; j++)
            bestx[j] = x[j];
        bestf = f;
    else
        for (int j = i; j <= n; j++) {
            f1+=M[x[j]][1]; //作业j在M1上的加工时间
            f2[i]=((f2[i-1]>f1)?f2[i-1]:f1)+M[x[j]][2]; 
            f+=f2[i]; //作业1…j完成的加工时间之和
            if (f < bestf) {//在i层找到更优的作业调度
                Swap(x[i], x[j]);
                Backtrack(i+1);
                Swap(x[i], x[j]);
            }
            f1- =M[x[j]][1];
            f- =f2[i];
}


//对于这个代码的解释在于x内存放的为当前作业调度,
//每次遍历到最末节点,都会更新bestx(最优路径),
//而向下遍历的条件在于当前时间小于之前的最优时间,
//保证可以遍历到底部的为新的最优路径,
//而又由于只存储一条路径,所以通过交换来进行路径选择

3.符号三角形问题

下图是由14个“+”和14个“-”组成的符号三角形。2个同号下 面都是“+” ,2个异号下面都是“-” 。

 在一般情况下,符号三角形的第一行有n个符号。符号三角形 问题要求对于给定的n,计算有多少个不同的符号三角形,使 其所含的“+”和“-”的个数相同。

void Triangle::Backtrack(int t)
{
   if ((count>half) || (t*(t-1)/2-count>half)) return;//如果加号减号相同,那么就不会有一个的数量大于一半n*(n+1)/4
   if (t>n) sum++;//sum代表共有多少符号三角形,所以到达n+1的时候++
   else//当深度t没有到达底部,选择下一个符号为+还是-
    for (int i=0;i<2;i++)
    {//以0表示-,以1表示+
        p[1][t]=i;
        count+=i;
        for (int j=2; j<=t; j++)
        {
            p[j][t-j+1]=p[j-1][t-j+1]^p[j-1][t-j+2];
            count+=p[j][t-j+1];
        }
        Backtrack(t+1);
        for (int j=2;j<=t;j++){ count-=p[j][t-j+1]; }
        count-=i;
     
}

4.n后问题

在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象 棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线 上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任 何2个皇后不放在同一行或同一列或同一斜线上。

 设第i行的皇后,放置在Xi列上。则n后问题就是发现所有 可能的序列(x1, x2, … , xn)。

要使得n个皇后放置均不在同一行、同一列,同一个对角线上, 则只需要满足如下条件:

bool Queen::Place(int k)
{//第i行的皇后,放置在Xi列上
    for (int j=1; j<k; j++)
        if ((abs(k-j)==abs(x[j]-x[k])) || (x[j]==x[k]))//保证任意两个不在同一列和斜线
            return false;
    return true;
}


void Queen::Backtrack(int t)
{//递归回溯
    if (t>n) sum++; //输出结果x[0:n]
    else
        for (int i=1;i<=n;i++)
        {
            x[t]=i;
            if (Place(t)) Backtrack(t+1);
        }
}

---------------------------------------------------------------------------------
//迭代回溯
public static long Queen::nQueen(int nn)
{
    n=nn;
    sum=0;
    x=new int[n+1];
    for (i=0;i<=n; i++) x[i]=0;
    backtrack(1);
    return sum++;
}

private static void nQueen:backtrack()
{// k表示当前第k层,x[k]表示第k行皇后的列号
    x[1]=0;
    int k=1;
    while (k>0)
    {
        x[k]+=1;
        while ( (x[k]<=n && !(Place(k) ) x[k]+=1;
        if (x[k]<=n)
        {//可以放置在k位置
            if (k==n)
            { //最后一个皇后
                sum++;
                //得到一个解,输出x[1:n]
            }
            else
            {//不是最后一个皇后
                k++; x[k]=0; //放置下一个(层)皇后
            }
        }
        else
        k--; //不可以放置,回退到上一层(回溯)
    }
}

5.0-1背包问题

double c; //背包容量
int n; //物品数
double []w; //物品重量数组
double []p; //物品价值数组
double cw; //当前重量
double cp; //当前价值
double bestp; //当前最优价值
Bound()计算后半段最大价值的时候,使
用的是一个贪心算法。尽管切割的情况是不
被同意的,可是能够用这个结果来进行估算


Private static double Bound(int i)//这个是进入右子树的条件
{// 计算上界
    double cleft = c - cw; // 剩余容量
    double b = cp;
    // 以物品单位重量价值递减序装入物品
    while (i <= n && w[i] <= cleft) {
        cleft -= w[i];
        b += p[i];
        i++;
        }
    // 装满背包
    if (i <= n) b += p[i]/w[i] * cleft;
    return b;
}


//递归回溯
private static double Backtrack(int i) {
    if(i>n)
    {//到达叶结点
        bestp=cp;
        return
    }
//搜素子树
    if (cw+w[i]<=c)
    {//进入左子树
        cw+=w[i];
        cp+=p[i];
        Backtrack(i+1);
        cw-=w[i];
        cp-=p[i];
    }
    if (Bound(i+1)>bestp)
    Backtrack(i+1); //进入右子树:预测价值大于最大价值
    }

6.最大团问题

对于最大团的理解:顶点i到已选入的顶点集中每一个顶点都有边相连。

先画出G杠(包含G的所有顶点,以及G中不存在的边),然后找寻最大独立集(没有互相连接的边的点) 

下面代码的变量包括:

void Clique::Backtrack(int i)
{// 计算最大团
    if (i > n) {// 到达叶结点
        for (int j = 1; j <= n; j++) bestx[j] = x[j];
        bestn = cn; return;}
    // 检查顶点 i 与当前团的连接
    int OK = 1;
    for (int j = 1; j < i; j++)
        if (x[j]!=0 && a[i][j] ) {
        //j是最大团中的节点,且 i与j不相连
            OK = 0; break;}
    if (OK)
    {//节点i满足最大团, 进入左子树
        x[i] = 1; cn++;
        Backtrack(i+1);
        x[i] = 0; cn--; //恢复状态,为回溯做准备
    }
    if (cn + n - i > bestn) {// 进入右子树
        x[i] = 0;
        Backtrack(i+1);}
}

 

7.图的m着色问题

给定无向连通图G和m种不同的颜色。用这些颜色为图 G的各顶点着色,每个顶点着一种颜色。是否有一种 着色法使G中每条边的2个顶点着不同颜色。这个问题 是图的m可着色判定问题。若一个图最少需要m种颜色 才能使图中每条边连接的2个顶点着不同颜色,则称 这个数m为该图的色数。求一个图的色数m的问题称为 图的m可着色优化问题。

void Color::Backtrack(int t)
{
    if (t>n) {
        sum++;
        for (int i=1; i<=n; i++)
            cout << x[i] << ' ';
            cout << endl;
            }
    else
        for (int i=1;i<=m;i++) {
            x[t]=i;
            if (Ok(t)) Backtrack(t+1);
            }
}

bool Color::Ok(int k)
{// 检查颜色可用性
    for (int j=1;j<k; j++) //化前j<=n
        if ((a[k][j]==1)&&(x[j]==x[k])) return false;
    return true;
}

 

8.旅行售货员问题

旅行售货员问题(travelling salesman problem)是一类 组合最优化问题,设有一个售货员从城市1出发,到城市2, 3,…,n去推销货物,最后回到城市1.假定任意两个城市 i,j间的距离dij(dij=dji)是已知的,问他应沿着什么样的 路线走,才能使走过的路线最短(总旅费最小)?

 旅行售货员问题就是在一个完全网络中,找出一个具有最 小权的哈密顿圈,寻求旅行售货员问题的有效算法似乎是 没有希望的,它属于NP完全类,一个可行的办法是首先求 一个哈密顿圈,然后适当修改,以得到具较小权的另一个 哈密顿圈。

用CC来记录当前路径X[1:i]的费用

template<class Type>
void Traveling<Type>::Backtrack(int i)
{//float a[][]为邻接矩阵,若a[i][j]为MAX_VALUE则城市i与城市j之间旅费极大
    if (i == n) {
        if (a[x[n-1]][x[n]] <MAX_VALUE && a[x[n]][1] < MAX_VALUE &&(cc + a[x[n-1]][x[n]] + a[x[n]][1] < bestc || bestc>MAX_VALUE)) {
        for (int j = 1; j <= n; j++) bestx[j] = x[j];
        bestc = cc + a[x[n-1]][x[n]] + a[x[n]][1];}
        }
    else {
        for (int j = i; j <= n; j++)
    // 是否可进入x[j]子树?
        if ( a[x[i-1]][x[j]]<MAX_VALUE &&(cc + a[x[i-1]][x[i]] < bestc || bestc==MAX_VALUE)) {
    // 搜索子树
            Swap(x[i], x[j]);
            cc += a[x[i-1]][x[i]];
            Backtrack(i+1);
            cc -= a[x[i-1]][x[i]];
            Swap(x[i], x[j]);}
         }
}

​​​​​​​

9.圆排列问题

float Circle::Center(int t)
{// 计算当前所选择圆的圆心横坐标
    float temp=0;
    for (int j=1;j<t;j++) {
        float valuex=x[j]+2.0*sqrt(r[t]*r[j]);
        if (valuex>temp) temp=valuex;
        }
    return temp;
}


void Circle::Compute(void)
{// 计算当前圆排列的长度
    float low=0, high=0;
    for (int i=1;i<=n;i++) {
        if (x[i]-r[i]<low) low=x[i]-r[i];
        if (x[i]+r[i]>high) high=x[i]+r[i];
    }
    if (high-low<min) min=high-low;
}

void Circle::Backtrack(int t)
{
    if (t>n) Compute();
    else
        for (int j = t; j <= n; j++) {
            Swap(r[t], r[j]);
            float centerx=Center(t);
            if (centerx+r[t]+r[1]<min) {//下界约束
                x[t]=centerx;
                Backtrack(t+1);
            }
        Swap(r[t], r[j]);
    }
}

10.连续邮资问题

假设国家发行了n种不同面值的邮票,并且规定每张信 封上最多只允许贴m张邮票。连续邮资问题要求对于给 定的n和m的值,给出邮票面值的最佳设计,在1张信封 上可贴出从邮资1开始,增量为1的最大连续邮资区间。

对于连续邮资的问题,由于实验开始是仅给出面值的数量,而面值的具体值是未知的。 但是由于邮资需要从1开始,因此,面值具体值中必然有1。可以建立一个数组用于存储具体 的面值。X[1:n]表示从小到大存储具体面值。

当面值为1时,可形成的连续邮资区间为1-m;在此基础上,若要增加面值,为保证区 间连续,第二个面值必然要在2到m+1中取(第二个面值不能为1,并且若为m+2或者更大时, 只用一个就变为m+2,此时不连续);第三个面值则需要根据前两个面值能达到的最大值来 确定。假设第i个面值x[i]的连续区间若为1~r时,则第x[i+1]的个的取值必然为x[i]+1到 r+1。则从第一个面值开始,接下来的各个面值的取值都由上一个面值以及能形成的最大值 来取。

为了求解最大连续区间,需要将面值的所有情况进行考虑,则对面值可能取值的语法 树进行递归遍历,即回溯。递归到叶子节点时,将当前情况的最大值进行记录并与之前的进 行比较,若更大则保存最大值,之后回溯到上一层继续求解,直到将所有情况计算完成。

解向量:用n元组x[1:n]表示n种不同的邮票面值,并约定它 们从小到大排列。x[1]=1是唯一的选择。

可行性约束函数:已选定x[1:i-1],最大连续邮资区间是[1:r], 接下来x[i]的可取值范围是[x[i-1]+1:r+1]。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值