算法分析与设计(五)回溯法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SakuraMashiro/article/details/78942876

回溯法的基本思想

回溯法有“通用的解题法”之称。该方法系统地搜索一个问题的所有解或任一解。

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

回溯法通常将问题解空间组织成“树”结构,通过采用系统的方法搜索解空间树,从而得到问题解。

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

搜索策略:深度优先、广度优先、函数优先、广度深度结合等。

结点分支判定条件:
满足约束条件:分支扩展解向量
不满足约束条件:回溯到当前结点的父结点

结点状态:
白结点(尚未访问)
灰结点(正在访问以该结点为根的子树)
黑结点(以该结点为根的子树遍历完成)
存储:当前路径

回溯法在搜索解空间时,通常采用两种策略(剪枝函数)避免无效搜索:
约束函数:在扩展结点处剪去不满足约束条件的子树
界限函数:在扩展结点处剪去得不到最优解的子树

回溯法的设计要素

  1. 针对问题定义解空间:
    问题解向量
    解向量分量取值集合
    构造解空间树

  2. 判断问题是否满足多米诺性质

  3. 搜索解空间树,确定剪枝函数
  4. 确定存储搜索路径的数据结构

两类典型的解空间树

  • 子集树:当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。子集树通常有2n个叶结点
  • 排列树:当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有n!个叶结点。

回溯法的程序结构

  • 递归
void backtrack (int t)
{
       if (t>n) output(x);
       else
         for (int i=f(n,t);i<=g(n,t);i++) {
           x[t]=h(i);
           if (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--;
    }
}

回溯法求解问题实例分析
一、装载问题
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,装载问题要求确定是否有一个合理的装载方案可将这个集装箱装上这2艘轮船。如果有,找出一种装载方案。

例如:当n=3, c1=c2=50,且w=[10,40,40]时,可将1和2装上第一艘轮船,3装入第二艘轮船;若w=[20,40,40],则无法将这三个集装箱都装上轮船。

分析
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和与第一艘轮船载重量最接近。由此可知,装载问题等价于特殊的0-1背包问题。

解空间:子集树
可行性约束函数(选择当前元素):cw+w[i]<=capacity (之前的载重量加上当前元素的重量小于等于轮船最大载重)
上界函数(不选择当前元素):
当前载重量cw+剩余集装箱的重量r <= 当前最优载重量bestw ( 即如果当前载重量加上剩余子树中所有物品的重量都没有最优载重大,则当前元素及其子树可以不再搜索(剪枝),因为及时到达叶节点也得不到最优解。)

实例计算过程
W = <90, 80, 40, 30, 20, 12, 10>,c1=152,c2=130。

这里写图片描述

代码实现

/**
 * 装载问题
 *
 * @author luwei
 * @version
 */

 /**
  * 回溯法实现装载类
  * 
  */
 final class Loading{

    //集装箱数量
    private int gNum;

    //集装箱重量数组
    private int[] weights;

    //最大载重
    private int capacity;

    //当前解
    private int[] x;

    //当前最优解
    private int[] bestx;

    //当前载重
    private int cw;

    //当前最优载重
    private int bestw;

    //剩余物品重量
    private int rWeight = 0;

    /**
     * 构造函数
     *
     * @param w 物品数组
     * @param c 最大载重
     */
    public Loading(int[] w, int c){

        this.gNum = w.length;
        this.weights = w;
        this.capacity = c;
        this.cw = 0;
        this.bestw = 0;
        this.x = new int[gNum];
        this.bestx = new int[gNum];

        this.rWeight = 0;
        for(int i=0; i<gNum; i++)
            rWeight += weights[i];
    }

    /**
     * 递归回溯函数
     *
     * @param i 子集树求解深度
     */
    public void backtrack(int i){

        //搜索第i层结点
        if(i == gNum ){//到达叶结点
            if(cw > bestw){//找到一个更优值,当采用右剪枝情况下,可以不用判断
                for(int j=0; j<gNum; j++){
                    bestx[j] = x[j];//更新记录最优解
                }
                bestw = cw;//更新最优解  
            }
            return;
        }

        //搜索子树
        rWeight -= weights[i];

        if(cw+weights[i] <= capacity){//搜索左子树x[i]=1
            x[i] = 1;
            cw += weights[i];
            backtrack(i+1);
            cw -= weights[i];
        }

        if(cw + rWeight > bestw){//搜索右子树x[i]=0
            x[i] = 0;
            backtrack(i+1);
        }

        //返回父节点
        rWeight += weights[i];
    }

    /**
     * 获取最大装载量
     *
     * @return 最大装载量
     */
    public int getMaxLoad() {

        return bestw;
    }

    /**
     * 获取装载方案
     *
     * @return 装载方案,物品装载与否向量
     */
    public int[] getLoadingPlan(){

        return bestx;
    }
}


/**
 * 求解装载问题类
 */
public final class Load{

    /**
     * 求解最大装载量和装载方案方法
     *
     * @param w 物品重量数组
     * @param c 最大容量
     * @param loadingPlan 返回参数,装载方案,物品装载与否向量
     * @return 最大装载量
     */
    public static int maxLoading(int[] w, int c, int[] loadingPlan){

        Loading ld = new Loading(w, c);

        ld.backtrack(0);

        int[] p = ld.getLoadingPlan();

        for(int i=0; i<w.length; i++){

            loadingPlan[i] = p[i];
        }

        return ld.getMaxLoad();
    }

    public static void main(String[] args){

        int[] w = {90, 80, 40, 30, 20, 12, 10};
//      int[] w = {90, 80, 90};
        int c = 152;

        int[] plan = new int[w.length];

        System.out.print("weights: " + w[0]);
        for(int i=1; i<w.length; i++){
            System.out.print(", " + w[i]);
        }
        System.out.println();

        System.out.println("capacity: " + c);

        System.out.println("maxLoad: " + maxLoading(w, c, plan));

        System.out.print("Loading plan: " + plan[0]);
        for(int i=1; i<w.length; i++){
            System.out.print(", " + plan[i]);
        }
        System.out.println();
    }
}

二、N皇后问题

问题描述
在n×n的棋盘中放置n个皇后,使得任何两个皇后之间不能相互攻击(不在同行同列或者对角线),试给出所有的放置方法。

分析
1)问题解向量:(x1, x2, … , xn)
2)显约束:xi=1,2, … ,n
3)隐约束:
(1)不同列:xixj;
(2)不处于同一正、反对角线:|i-j||xi-xj|

回溯函数

void Backtrack(int t) {
    if (t>n) sum++;
        else
            for (int i=1;i<=n;i++) {
                x[t]=i;
                if (Place(t)) Backtrack(t+1);
            }
}

判断是否可以放置

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

算法时间复杂度

(1)搜索1+n+n^2+…+n^n=(n^(n+1)-1)/n-1≤2n^n;
(2)每个节点判断是否符合规则,最多要判断3n个位置(列方向、主与副对角线方向是否有皇后)
故最坏情况下时间复杂度O(3n×2n^n)=O(n^(n+1))

代码实现

/**
 * 皇后问题
 *
 * @author lsj
 * @version 
 */
 final class Placing{

    //皇后数量
    private int n;

    //当前可行解
    private int[] x;

    //可行解数量
    private int sum;

    /**
     * 构造函数
     *
     * @parem n 皇后数量(问题规模)
     */
    public Placing(int n){

        this.n = n;
        this.sum = 0;
        this.x = new int[n];
    }

    /**
     * 递归回溯函数
     *
     * @param d 子集树求解深度
     */
    public void backtrack(int d){
        //到达棋盘最后一行
        if(d == n){
            //解的数量加一
            sum++;
            //输出可行解
            outputX();
        }
        else{
            for(int i=0; i<n; i++){
                x[d] = i;
                //如果该位置可以放置皇后,继续向下试探搜索
                if(placeOk(d))  backtrack(d+1);
            }
        }
    }

    /**
     * 判断位置可行函数,约束函数
     *
     * @param p 棋盘列位置
     */
    private boolean placeOk(int p){

        for(int j=0; j<p; j++){
            if(Math.abs(p-j) == Math.abs(x[p]-x[j]) || x[p] == x[j])
                return false;           
        }
        return true;
    }

    private void outputX(){

        for(int i=0; i<x.length; i++){
            System.out.print(x[i]);
        }
        System.out.println();
    }

    /**
     * 获取可行解数量
     *
     * @return 可行解数量
     */
    public int getPlacingNum() {        
        return sum;
    }
}

public final class Queen{

    public static int nQueen(int queenNum){

        Placing plc = new Placing(queenNum);

        plc.backtrack(0);

        return plc.getPlacingNum();
    }

    public static void main(String[] args){

        final int n = 4;

        int num = nQueen(n);

        System.out.println("The number of placing: " + num);
    }
}

猜你喜欢

转载自blog.csdn.net/SakuraMashiro/article/details/78942876