回溯法

1. 回溯法

回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。

基本思想:

  1. 回溯法在问题的解空间树中进行深度优先搜索,从根结点出发当探索到某一结点时,要先判断该结点是否包含问题的解
    • 如果包含,就从该结点出发继续按深度优先策略搜索
    • 否则逐层向其祖先结点回溯(退回一步重新选择)
    • 满足回溯条件的某个状态的点称为“回溯点”
  2. 算法结束条件:
    • 求所有解:回溯到根,且根的所有子树均已搜索完成
    • 求任一解:只要搜索到问题的一个解就可以结束
  3. 问题的解空间
    • 问题的解空间应至少包含该问题的一个(最优)解
    • 例如:对于有n种备选物品的0/1背包问题而言,解空间可以由长度为n的向量来表示,显然:该解空间包含了对该问题所有可能的解法
    • 定义了问题的解空间后,可以将其组织成树或图的形式。例如:n = 3 的0/1背包问题,解空间可用一棵完全二叉树表示,从根到任一叶结点的路径表示解空间的一个元素

图片名称

生成问题状态的基本方法:

  1. 基本概念
    • 扩展结点:一个正在产生子结点的结点称为扩展结点
    • 活结点:一个自身已生成但其子结点尚未全部生成的结点
    • 死结点:一个所有子结点已经产生的结点称做死结点
  2. 深度优先的问题状态生成法
    • 对一个扩展结点R,一旦产生了它的一个子结点C,则将其作为新扩展结点,并对以C为根的子树进行穷尽搜索;在完成对子树C的穷尽搜索后,将R重新变成扩展结点;继续生成R的下一个子结点,若存在,则对其进行穷尽搜索
  3. 宽度优先的问题状态生成法
    • 在一个扩展结点变成死结点之前,它一直是扩展结点

回溯法的解题思路:

  1. 针对所给问题,定义问题的解空间
  2. 确定易于搜索的解空间结构
  3. 从根结点开始深度优先搜索解空间(利用剪枝避免无效搜索)
    • 此时:根结点成为活结点,并成为当前的扩展结点
    • 进一步的搜索从当前扩展结点开始,向纵深方向移至一个新结点,该新结点成为新的活结点,并成为当前扩展结点
    • 若在当前扩展结点处不能再向纵深方向移动,则当前扩展结点变为死结点,此时应回溯至最近的活结点,将其作为当前扩展结点
  4. 回溯法以这种方式递归地在解空间中搜索
    • 直至找到所要求的解,或者解空间中已经没有活结点为止

通用算法框架:

void backtrack (int t)  
{  
    if (t>n)   
        output(x); //已到叶子结点,输出结果  
    else  
        // f(n,t),g(n,t)表示当前扩展结点处未搜索过的子树的起始编号和终止编号
        for (int i=f(n,t);i<=g(n,t);i++) {  
            x[t]=h(i); // h(i):表示在当前扩展结点处x[t]的第i个可选值
            //constraint(t)为true表示在当前扩展结点处x[1:t]的取值满足问题的约束条件
            //bound(t)为true表示在当前扩展结点处x[1:t]的取值尚未导致目标函数越界
            if (constraint(t)&&bound(t))  
                backtrack(t+1);  
        }  
}

用回溯法解题时常用到两种典型的解空间树:子集树与排列树

  • 第一类解空间树:子集树
    • 当问题是:从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树
    • 例如n个物品的0/1背包问题,这类子集树通常有 2n 个叶结点,解空间树的结点总数为 2n+11 ,遍历子集树的算法需Ω( 2n )计算时间
    • 子集树回溯算法框架:
void backtrack (int t) {
    if (t > n){
        output(x);
    }
    else{
    // 对当前扩展结点的所有可能取值进行枚举
        for (int i = 0; i <= 1; i++) {
            x[t] = i;
            if (constraint(t) && bound(t)) backtrack(t+1);
        }
    }
}// 执行时,从Backtrack(1)开始
  • 第二类解空间树:排列树
    • 当问题是:确定n个元素满足某种性质的排列时,相应的解空间树称为排列树
    • 例如旅行商问题,排列树通常有n!个叶结点,因此遍历排列树需要Ω(n!)计算时间
    • 排列树回溯算法框架:
void backtrack (int t) {
    if (t > n){
        output(x);
    }
    else{
        for (int i = t; i <= n; i++) {
            swap(x[t], x[i]);
            if (constraint(t) && bound(t)) backtrack(t+1);
            swap(x[t], x[i]);
        }
    }
} // 调用Backtrack(1)前,首先将数组x初始化为单位排列[1,2, ..., n]

2. 示例: 旅行商问题

问题描述:

  1. 某推销员要去若干城市推销商品,已知各城市间的开销(路程或旅费),要求选择一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使总开销最小
  2. 形式化描述如下:给定带权图G=(V,E),已知边的权重为正数,图中的一条周游路线是包括V中每个顶点的一条回路,一条周游路线的开销是这条路线上所有边的权重之和,要求在图G中找出一条具有最小开销的周游路线

求解思路:

  1. 利用排列生成问题的回溯算法Backtrack()
  2. 解空间为X={12341, 12431, 13241, 13421, 14231, 14321}
  3. 构造解空间树
    • 从根结点到任一叶结点的路径定义了图G的一条周游路线,例如:A->L 对应周游路线(1, 2, 3, 4, 1)
    • 解空间树中的每个叶结点恰好对应于图G的每一条周游路线,解空间树中的叶结点个数为(n-1)!

图片名称

backtrack(int i){
    if (i>n){// 输出可行解,与当前最优解比较
        if (sum + A[x[n]][1] < m || m = ∞ ) {
            m = sum + A[x[n]][1];
            for( k=1 ; k <= n; k++) S[k] = x[k];
        }
    }//sum记录(x[1],x[2]),…, (x[i-2],x[i-1])的距离和
    else{
        for( k = i ; k<=n; k++ )// 依次处理当前扩展结点的分支
            if ( sum + A[x[i-1]][x[k]] < m || m = ∞ ){
                swap( x[i], x[k]);
                sum += A[x[i-1]][x[i]];
                backtrack(i+1);
                sum -= A[x[i-1]][x[i]];
                swap(x[i],x[j]);
            }
    }
}// 初始调用:Backtrack(2

3. 示例: 0/1背包问题

问题描述:
设:n = 4,c = 7,p = [9, 10, 7, 4],w = [3, 5, 2, 1]

怎样计算价值上界?

  1. 易求得这四个物品的单位重量价值分别为:[3, 2, 3.5, 4]
  2. 按物品单位重量价值递减的顺序装入物品
  3. 依次装入物品4、3、1之后,剩余背包容量为1
    • 所以只能容纳物品2的20%
    • 得到解向量x = [1, 1, 0.2, 1],相应价值为22
  4. 虽然x并不是0/1背包问题的可行解,但它提供了一个最优的价值上界(最优值不超过22)
  5. 为便于计算上界函数,可先对物品按单位价值从大到小排序,对每个扩展结点,只需按顺序考查排在其后的物品即可

0/1背包问题的回溯算法:

backtrack(int i){
    if( i > n) { // vc当前背包价值,m当前最优价值
        m = ( m < vc )? vc : m; output(x);
    } else { // wc当前背包重量
        if ( wc + w[i] <= C ) { // 左子树(将 i 放入背包)
            x[i]= 1; wc += w[i]; vc += v[i];
            backtrack(i+1);
            x[i]= 0; wc -= w[i]; vc -= v[i];
        }
        if(Bound(i+1) > m){ // 右子树(拿出物品i)
            backtrack(i+1);
        }
    }
}

限界函数的实现:

// 根据当前背包内物品情况求出:当前可行解的价值上界
// w[i]和v[i]均已按物品单位价值递减顺序排好序
int Bound(int i){
    int wr = c - wc; // 背包剩余容量
    int vb = vc; // vc为当前背包价值
    // 按单位价值递减顺序装入物品
    while(i <= n && w[i] <= wr){
        wr -= w[i]; vb += v[i]; ++i;
    }
    if(i <= n) vb += (v[i]/w[i])*wr; // 继续装满背包
    return vb; // 返回背包价值上界
}

4. 示例: 装载问题

问题描述:

  1. 有n个集装箱要装上2艘载重量分别为 c1 和 c2 的轮船,其中集装箱 i 的重量为 wi,且 ni=1wic1+c2
  2. 装载问题要求:确定是否有一个合理的装载方案可将这n个集装箱装上这2艘轮船?如果有,找出一种装载方案。
  3. 示例:n=3,c1=c2=50
    • 若:w=[10, 40, 40],则可以将集装箱1和2装到第一艘船上,将3号集装箱装到第二艘船上
    • 若:w=[20,40,40],则无法将这三个集装箱全部装船

问题分析:

  1. 若给定问题有解
    • 首先将第一艘轮船尽可能装满
    • 将剩余的集装箱装上第二艘轮船
  2. 将第一艘轮船尽可能装满等价于
    • 选取全体集装箱的一个子集
    • 使该子集中集装箱重量之和最接近 c1
    • 由此可知,装载问题等价于特殊的0-1背包问题

算法设计:

  1. 解空间的表达:子集树
  2. 剪枝函数(1)
    • 约束函数: ni=1wixic1 ,减去不满足该约束的子树
  3. 剪枝函数(2): 限界函数(用于剪去不含最优解的子树)
    • 设:R是解空间树第k层上的当前扩展结点
    • 设:wc表示当前结点对应的的装载重量 wc=ki=1wixi
    • 设:wm表示当前的最优载重量
    • 设:wr表示剩余集装箱的重量 wr=ni=k+1wi
    • 定义限界函数为: w=wc+wr
    • 以R为根的子树中任一叶结点对应的载重量均不会超过w
    • 因此当 w≤wm 时,可将R的右子树剪去

代码:

void backtrack (int i) {
    if (i > n){
        if(wc > wm) wm = wc; return;
    }
    wr -= w[i];
    if (wc + w[i] <= c){ // x[i] = 1; 搜索左子树
    wc += w[i];
    backtrack(i+1);
    wc -= w[i];
    }
    if (wc + wr > wm){ // x[i] = 0; 搜索右子树
        backtrack(i+1);
    }
    wr += w[i];
}

5. 示例: n-皇后问题

问题描述:

  1. 根据国际象棋的规则,皇后可以攻击与之处在同一行同一列同一斜线上的棋子
  2. n-皇后问题
    • 在n×n的棋盘上放置彼此不受攻击的n个皇后
    • 即:任何2个皇后不放在同一行或同一列或同一斜线上

图片名称

问题分析:

  1. 问题的解向量:(x1, x2, … , xn)
    • 采用数组下标 i 表示皇后所在的行号
    • 采用数组元素x[i]表示皇后 i 的列号
  2. 采用解空间树:子集树
  3. 剪枝函数
    • 显约束(对解向量的直接约束):xi =1, 2, … , n
    • 隐约束1:任意两个皇后不同列 :xi ≠ xj
    • 隐约束2:任意两个皇后不处于同一对角线,即 |i-j| ≠ |xi - xj|

代码:

bool Bound(int k){
    for (int i = 1; i < k; i++){
        if ((abs(k-i)==abs(x[i]-x[k]))||(x[i]==x[k]))
            return false;
    }
    return true;
}

void Backtrack(int t){
    if (t>n) output(x);
    else {
        for (int i = 1; i <= n; i++) {
            x[t] = i;
            if (Bound(t)) Backtrack(t+1);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值