算法学习——回溯法

回溯法的实质

回溯法可看作穷举法的一种实现方式

 

计算过程

每步只构造一个部分节并立即对此部分解进行评估。若此部分解有可能拓展为“所求解”,则继续扩展;反之此部分解不可能扩展为所求解,则继续尝试其他部分解。直到穷尽一切可能。

 

解空间与解空间树

描述回溯法时,可有两种解空间树选择。一是子集树,一是排列树。

解空间:所有可能的解构成的集合。

解空间树:将解空间组织成树结构。

子集树:每个解(x1,….,xn)的每个分量xi的值取自于一个集合Si,解空间大小为2的n次方;

排列树:每个解(x1,…,xn)都是集合S的全排列。解空间大小为n!

一般情况下,子集树优于排列树,因为n! >> 2的n次方(题目要采取哪种解空间,要具体视情况而定)

 

解题步骤

1)针对所给问题,定义问题的解空间;

2)确定易于搜索的解空间结构;

3)以深度优先方式搜索解空间树,并在搜索过程中用剪枝函数避免无效搜索。

 

框架

t:表示递归深度,即当前扩展节点在解空间树中的深度。

output():纪录或输出结果的函数

constaint():约束函数。返回值=true,则满足约束条件;返回值=false,可剪去相应子树。

bound():限界函数。返回值=true,目标函数未越界,可用backtak(t+1)进一步搜索;返回值=false,可剪去相应子树。

一、子集树

1.递归回溯子集树的一般算法

        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);
                    }
                }
            }
        }

 

2.递归回溯排列树的一般算法

因为是排列树,所以相当于当前的数不断地和后面的数交换,得到新的排列。

        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]);
                }
            }
        }

 

例子

1、n皇后问题

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

 

解法一:

/**
 * 递归版本
 * 子集树来做n后问题
 * 输出所有的排布方案和方案数量
 */
public class Queen1 {

    public int sum;
    /**
     * 这个用1维数组存,设下标为i,i为行号,arr[i]为列号,表示第i行第j列的位置放了皇后
     */
    private int[] arr;
    public StringBuilder res;
    private int n;



    public void run(){
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();

        res = new StringBuilder();
        arr = new int[n];
        backtrack(0);
        }


    /**
     * 回溯法法搜索子集树的一般算法
     * @param layer layer为层数,即树的第x层,从0开始
     */
    public void backtrack(int layer){
        // 如果已经遍历到最后一层了,那说明前面的所有条件都满足,说明这是一个合格的序列,故保存
        if(layer >= n){
            update();
            sum++;
            return;
        }
        // 把第layer层给它从0-(n-1)的所有可能值,看看哪个满足
        for(int i = 0;i < n;i++) {
            arr[layer] = i;
            if (bound(layer)) {
                // 若满足,则继续填下一个
                backtrack(layer + 1);
            }
        }
    }



    
    /**
     * 判断是否满足,不满足则不要,这是限界。
     * @param layer 目前的层数
     * @return
     */
    public boolean bound(int layer){
        // 只有从0-(layer-1)层填了数,这是遍历arr[layer]的所有可能取值,不满足就false
          for(int i = 0;i < layer;i++){
        //for(int i = 0;i < n;i++){
              // 共三个条件,不能在同一行同一列同一斜线
            if(arr[i] == arr[layer]){
                return false;
            }
            // 同一斜线判断用斜率就好
            if((arr[i] - arr[layer] == (i - layer))){
                return false;
            }
            if((arr[i] - arr[layer] == (layer - i))){
                return false;
            }
        }
        return true;
    }


    /**
     * 纪录结果的函数
     */
    public void update(){
        for(int i = 0;i < n;i++){
            res.append(arr[i] + " ");
        }
        res.append("\n");
    }
}

 

解法二:

/**
 * 对此题来说,用全排列好很多
 * 后面的改造成1半也是跟Queen2的constraint函数相同
 * 排列树来做n后问题
 * 输出所有的排布方案和方案数量
 */
public class Queen3 {

    public int sum;
    private int[] arr;
    public StringBuilder res;
    private int n;


    public void run(){
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();

        res = new StringBuilder();
        arr = new int[n];
        // 排列树来做,初始化要为n的有效排列之一,因为后面用的是swap来进行排列组合,如果刚开始不是一个n的一个排列,那后面也不会满足
        for(int i = 0;i < n;i++){
            arr[i] = i;
        }
        backtrack(0);
    }


    /**
     * 回溯法法搜索子集树的一般算法
     * @param layer layer为层数,即树的第x层,从0开始
     */
    public void backtrack(int layer){
        // 如果已经遍历到最后一层了,那说明前面的所有条件都满足,说明这是一个合格的序列,故保存
        if(layer >= n){
            update();
            sum++;
            return;
        }
        // 把layer后面的层一一跟layer层交换,得到全排列。
        for(int i = layer;i < n;i++) {
            swap(layer,i);
            if (bound(layer)) {
                // 若满足,则继续填下一个
                backtrack(layer + 1);
            }
            swap(i,layer);
        }
    }


    public void swap(int index1,int index2){
        int temp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = temp;
    }


    /**
     * 判断是否满足,不满足则不要,这是限界。
     * @param layer 目前的层数
     * @return
     */
    public boolean bound(int layer){
        // 只有从0-(layer-1)层填了数,这是遍历arr[layer]的所有可能取值,不满足就false
        for(int i = 0;i < layer;i++){
            //for(int i = 0;i < n;i++){
            // 共三个条件,不能在同一行同一列同一斜线
            if(arr[i] == arr[layer]){
                return false;
            }
            // 同一斜线判断用斜率就好
            if((arr[i] - arr[layer] == (i - layer))){
                return false;
            }
            if((arr[i] - arr[layer] == (layer - i))){
                return false;
            }
        }
        return true;
    }


    /**
     * 纪录结果的函数
     */
    public void update(){
        for(int i = 0;i < n;i++){
            res.append(arr[i] + " ");
        }
        res.append("\n");
    }
}

 

解法三:

package com.lianup.suanfa.Backtrack;

import java.util.Scanner;

/**
 * 迭代版本
 * 子集树来做n后问题
 * 输出所有的排布方案和方案数量
 */
public class Queen4 {

    public int sum;
    /**
     * 这个用1维数组存,设下标为i,i为行号,arr[i]为列号,表示第i行第j列的位置放了皇后
     */
    private int[] arr;
    public StringBuilder res;
    private int n;



    public static void main(String[] args){
        new Queen4().run();
    }

    public void run(){
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();

        res = new StringBuilder();
        arr = new int[n];
        backtrack();
        System.out.println(sum);

    }


    /**
     * 回溯法法搜索子集树的一般算法
     */
    public void backtrack(){
        int layer = 0;
        arr[layer] = -1;
        // 当layer为-1时,说明不存在满足条件的解了,说明遍历完成
        while(layer > -1) {
            //移到下一列
            arr[layer]+=1;
            // 如果不满足条件
            while (arr[layer] < n && !bound(layer)) {
                arr[layer] += 1;
            }

            // 如果找到了满足条件的值
            if(arr[layer] < n){
                // 若为此层的最后一个皇后,就纪录结果
                if(layer == n - 1){
                    update();
                    sum++;
                }else {
                    // 否则,继续遍历下一个皇后
                    layer++;
                    // 之前这里设置成了0,是错的,因为要为-1,后面才能满足arr[layer]++=1;
                    arr[layer] = -1;
                }
                // 找不到满足条件的值就回溯
            }else {
                layer--;
            }
        }
    }




    /**
     * 判断是否满足,不满足则不要,这是限界。
     * @param layer 目前的层数
     * @return
     */
    public boolean bound(int layer){
        // 只有从0-(layer-1)层填了数,这是遍历arr[layer]的所有可能取值,不满足就false
        for(int i = 0;i < layer;i++){
            //for(int i = 0;i < n;i++){
            // 共三个条件,不能在同一行同一列同一斜线
            if(arr[i] == arr[layer]){
                return false;
            }
            // 同一斜线判断用斜率就好
            if((arr[i] - arr[layer] == (i - layer))){
                return false;
            }
            if((arr[i] - arr[layer] == (layer - i))){
                return false;
            }
        }
        return true;
    }


    /**
     * 纪录结果的函数
     */
    public void update(){
        for(int i = 0;i < n;i++){
            res.append(arr[i] + " ");
        }
        res.append("\n");
    }
}

 

2、0-1背包问题

public class Package1 {


    int[][] element; // 物品,element[i][0]为重量,1为价值
    int n;
    int[] choose; //是否选择该物品
    int c; // 背包容量
    boolean hasRes; // 是否有结果
    int bestV; // 最优值(上界)
    int curW; //目前的已装入背包的重量
    int curV; // 目前装入背包的所有物品总价值

    public static void main(String[] args){
        new Package1().run();
    }

    public void run(){
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        c = scanner.nextInt();
        choose = new int[n];
        element = new int[n][n];
        curW = 0;
        curV = 0;
        hasRes = false;
        bestV = 0;
        for(int i = 0;i < n;i++){
            int w= scanner.nextInt();
            int v = scanner.nextInt();
            element[i][0] = w;
            element[i][1] = v;
        }

        // 把其按单价从高到低排序
        Arrays.sort(element, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                double a = o1[1]/(double)o1[0];
                double b = o2[1]/(double)o2[0];
                if(b > a){
                    return 1;
                }
                if(b == a){
                    return 0;
                }

                return -1;

            }
        });

        backtrack(0);
        System.out.println("all value:" + bestV);
        System.out.println(Arrays.toString(choose));


    }


    public void backtrack(int t){

        if(t >= n){
            hasRes = true;
            bestV = curV;
            return;
        }

        if(curW + element[t][0] <= c){
            // 回溯解空间树的左子树
            curW += element[t][0];
            curV += element[t][1];
            choose[t] = 1;
            backtrack(t+1);
            curW -= element[t][0];
            curV -= element[t][1];
        }

        // 若其最大可能值大于上一次回溯的最优值,那么就让它继续回溯
        // 即,右子树可能包含最优解时,才进入,否则剪枝
        if(constraint(t+1) > bestV){
            backtrack(t+1);
        }
    }


    public int constraint(int t){
        int leftw = c - curW;
        int b = curV;
        while(t < n && element[t][0] <= leftw){
            leftw += element[t][0];
            b += element[t][1];
            t++;
        }
        return b;
        }

    }

 

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