回溯法

(写于20200520)

回溯法

1、基本概念与关键理解

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

(2)回溯法的基本做法是搜索:或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法

搜索是算法设计的一大核心,搜索最简单的做法就是穷举。分治法、动态规划、回溯法、分支限界法等等都可以看作对穷举的“优化”,这些算法本质上都是在考虑如何有“规律”的组织每一次“随机”。

(3)问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,X2,…,xm)的形式。
显约束:对分量x;的取值限定。
隐约束:为满足问题的解而对不同分量之间施加的约束。
解空间:对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。

(4)回溯法的重点与难点在于剪枝函数的设计。

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

2、基于递归的回溯法伪代码模板

void backtrack(int t){ //t为递归深度 
    if(t > n) output(x); //已经搜索到叶节点 
    else{
        for(int i=f(n,t); i<=g(n,t); i++){
            //f(n,t)、g(n,t)分别表示在当前扩展结点处未搜索过的子树的起始编号和终止编号
            x[t] = h(i);//h(i)表示当前扩展结点处x[t]的第i个可选值 
            if(constraint(t) && bound(t))
                backtrack(t+1);
        }
    }
}

3、子集树、排列树

子集树:当所给的问题是从n个元素的集合S中找出S满足某种性质的子树时,相应的解空间树称为子集树。 
例如:0-1背包问题所对应的解空间是一颗子集树,这类子集树通常有2n个叶子结点,其结点总数为2n+1-1,遍历子集树 需要Ω(2n)

排列树:当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有n!个叶子结点,因此遍历排列树的需要2(n!)。
例如:旅行商问题的解空间是一颗排列树。

排列树的伪代码:

void backtrack (int t){
    if (t>n) output(x);
    else{
        for (int i=t;i<=n;i++){
            swap(x[t],x[i]);
            if(legal(t)) 
                backtrack(t+1);  
            swap(x[t],x[i]);
        }
    }
}

4、装载问题

问题描述:有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其 中集装箱i的重量为,且: n个集装箱重量之和小于c1+c2.装载问题要求确定是否有一个合理的装载方案可将这n个集装 箱装上这2艘轮船。如果有,找出一种装载方案。

代码及解释如下:

#include<stdio.h>
const int MAXN = 100;
int n,c,cw,r,bestw,w[MAXN],x[MAXN];
/*---------
n为集装箱数量 
c为第一艘船的最大容量 
cw为当前已装入第一艘船的重量 
r为剩余未装入第一艘船的重量 
bestw为目前为止最好的装载方案其对应的重量 
w[i]集装箱i的重量 
x[i]集装箱i是否装入第一艘船 
*/
void backtrack(int i){
    if(i > n){ //reach the leaves
        if(cw > bestw) bestw = cw;
        return ;
    }
    r -= w[i];
    if(cw + w[i] <= c){//constraint function for searching left
        x[i] = 1;
        cw += w[i];
        backtrack(i+1);
        cw -= w[i];
    }
    /*-------- 
    关键在于理解下面这个限界函数,目的是剪去得不到最优解的子树。
    这个时候我们正在处理树的第i层,即正在决定第i个集装箱是否装入
    如果说上面的约束函数是对左子树(装入i)的情况剪枝,那么该函数就是对右子树剪枝。 
    
    对该函数的理解:如果我把集装箱i抛弃掉,这个时候
    如果已经装入的重量+未装入的重量比目前的最优解bestw还要差,
    那么说明这种情况不可能得到最优解 
    也就是说如果把不装入集装箱i,肯定的不到最优解,后面的情况不用考虑了
    
    而如果当前载重量cw + 剩余集装箱的重量r>当前最优载重量 best,
    那么我们就要去尝试不装入它,因为有可能得到最优解 
    */
    if(cw + r > bestw){ //bounding function for searching right
        x[i] = 0;
        backtrack(i+1);
    }
    r += w[i];
}

int main(){
    scanf("%d %d",&n,&c);
    for(int i=1;i<=n;i++){
        scanf("%d",&w[i]);
        r += w[i]; //r初始化为所有集装箱重量之和 
    } 
    backtrack(1);
    printf("%d\n",bestw);
    for(int i=1;i<=n;i++) 
        if(x[i] == 1) 
            printf("%d ",i);
    return 0;
}
/*---TEST-----
5 16
2 1 8 4 2
*/   

 

posted @ 2021-02-22 20:51  icodes  阅读(743)  评论(0)    收藏  举报