五大常用算法:回溯算法

回溯法(back tracking)(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。回溯法可以理解为通过选择不同的岔路口寻找目的地,一个岔路口一个岔路口的去尝试找到目的地。如果走错了路,继续返回来找到岔路口的另一条路,直到找到目的地。

回溯法经典题——八皇后:

在国际象棋中,皇后是最强大的一枚棋子,可以吃掉与其在同一行、列和斜线的敌方棋子。比中国象棋里的车强几百倍,比她那没用的老公更是强的飞起(国王只能前后左右斜线走一格)。上图右边高大的棋子即为皇后。

   八皇后问题是这样一个问题:将八个皇后摆在一张8*8的国际象棋棋盘上,使每个皇后都无法吃掉别的皇后,一共有多少种摆法?下面以4*4的棋盘为例,解释思路。

步骤1
尝试先放置第一枚皇后,被涂黑的地方是不能放皇后

步骤2
第二行的皇后只能放在第三格或第四格,比方我们放第三格,则:

步骤3
第三行全部锁死了,第三位皇后无论放哪里都难逃被吃掉的厄运。于是在第一个皇后位于1号,第二个皇后位于3号的情况下问题无解。我们只能返回上一步来,给2号皇后换个位置。

步骤4
虽然是能放置第三个皇后,但是第四个皇后又无路可走了。返回上层调用(3号皇后),而3号也别无可去,继续回溯上层调用(2号),2号已然无路可去,继续回溯上层(1号),于是1号皇后改变位置如下,继续回溯。

这就是回溯算法的精髓,根据这个算法,最终能够把四位皇后放在4x4的棋盘里。也能用同样的方法解决了八皇后问题。下面上八皇后问题的代码。

改编自刘汝佳《算法竞赛入门经典》,见过的实现8皇后问题最简洁的代码
void queen(int row){
    if(row==n)
        total++;
    else
        for(int col=0;col!=n;col++){
            c[row]=col;
            if(is_ok(row))
                queen(row+1);
        }        
}

算法是逐行安排皇后的,其参数row为现在正执行到第几行。n是皇后数,在八皇后问题里当然就是8。第2行,如果程序当前能正常执行到第8行,那自然是找到了一种解法,于是八皇后问题解法数加1。

完整版java代码如下,详细解释清参见《回溯法与八皇后问题

public class EightQueen {
    private static int n = 8;
    private static int total = 0;
    private static int[] c = new int[n];
 
    public static void main(String[] args) {
        EightQueen eq = new EightQueen();
        System.out.println(total);
        eq.queen(0);
        System.out.println(total);
    }
 
    boolean isOk(int row) {
        for (int j = 0; j != row; j++) {
            if (c[row] == c[j] || row - c[row] == j - c[j] || row + c[row] == j + c[j]) {
                return false;
            }
        }
        return true;
    }
 
    void queen(int row) {
        if (row == n) {
            total++;
        } else {
            for (int col = 0; col != n; col++) {
                c[row] = col;
                if (isOk(row)) {
                    queen(row + 1);
                }
            }
        }
    }
}

运行结果:

0
92

回溯法经典题——0-1背包问题:

给定n种物品和一个容量为c的背包,物品i的重量为Wi,其价值为Pi,0-1背包问题是如何选择装入背包的物品(物品不可分割),使得装入背包的物品的价值为最大

求解步骤

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

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

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

问题的解空间

用回溯法解问题时,应明确定义问题的解空间。问题的解空间至少包含问题的一个(最优)解。对于 n=3 时的 0/1 背包问题,可用一棵完全二叉树表示解空间,如图所示:

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

回溯法对解空间做深度优先搜索时,有递归回溯和迭代回溯(非递归)两种方法,但一般情况下用递归方法实现回溯法。

完整版java代码如下,详细解释清参见《回溯法与0-1背包问题》:

public class KnapsackProblem {
    static int n, c, bestp;//物品的个数,背包的容量,最大价值
    static int[] p = new int[10000], w = new int[10000], x = new int[10000], bestx = new int[10000];//物品的价值,物品的重量,x[i]暂存物品的选中情况,物品的 选中情况

    public static void main(String[] args) {
        bestp = 0;
        KnapsackProblem kp = new KnapsackProblem();
        //用例1
        c = 10;
        n = 10;
        w = new int[]{2, 2, 6, 5, 4, 4, 3, 4, 6, 3};
        p = new int[]{6, 3, 5, 4, 6, 2, 8, 3, 1, 7};
        kp.dfsByBackTraching(0, 0, 0);
        System.out.println(bestp);
        for (int s = 0; s < 10; s++) {
            System.out.print(bestx[s] + "  ");
        }
        System.out.println();
        //用例2
        c = 5;
        n = 4;
        w = new int[]{1, 2, 3, 4};
        p = new int[]{2, 4, 4, 5};
        bestp = 0;
        Arrays.fill(x, 0);
        Arrays.fill(bestx, 0);
        KnapsackProblem kp1 = new KnapsackProblem();
        kp1.dfsByBackTraching(0, 0, 0);
        System.out.println(bestp);
        for (int s = 0; s < 4; s++) {
            System.out.print(bestx[s] + "  ");
        }
    }


    void dfsByBackTraching(int i, int cp, int cw) { //cw当前包内物品重量,cp当前包内物品价值
        int j;
        if (i >= n) {//回溯结束
            if (cp > bestp) { //是否超过了最大价值
                bestp = cp; //更新最大价值
                for (i = 0; i < n; i++) {
                    bestx[i] = x[i]; //得到选中的物品,bestx[i]值为1的元素对应的下表即为选中的物品编号
                }
            }
        } else {
            for (j = 0; j <= 1; j++) {
                x[i] = j;
                if (cw + x[i] * w[i] <= c) { //满足约束,继续向子节点探索
                    cw += w[i] * x[i];
                    cp += p[i] * x[i];
                    dfsByBackTraching(i + 1, cp, cw);
                    //回溯上一层物体的选择情况
                    cw -= w[i] * x[i];
                    cp -= p[i] * x[i];
                }
            }
        }

    }
}

运行结果:

24
1  1  0  0  0  0  1  0  0  1  
8
0  1  1  0  

回溯法经典题——求解数独:

数独(Sūdoku),一种数学游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫内的数字均含1-9,不重复。

数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次。

完整版java代码如下:

public class Sudoku {
    static int[][] arr = new int[9][9];

    public static void main(String[] args) {
        //示例1
/*        arr[0][0] = 9;
        arr[0][1] = 1;
        arr[0][7] = 4;
        arr[2][3] = 5;
        arr[2][5] = 3;
        arr[3][0] = 5;
        arr[3][2] = 6;
        arr[3][5] = 8;
        arr[3][6] = 3;
        arr[5][4] = 1;
        arr[5][7] = 2;
        arr[5][8] = 4;
        arr[6][0] = 8;
        arr[6][2] = 5;
        arr[7][2] = 3;
        arr[8][4] = 4;
        arr[8][8] = 1;*/
        //示例2
        arr[0][0] = 5;
        arr[0][1] = 3;
        arr[0][4] = 7;
        arr[1][0] = 6;
        arr[1][3] = 1;
        arr[1][4] = 9;
        arr[1][5] = 5;
        arr[2][1] = 9;
        arr[2][2] = 8;
        arr[2][7] = 6;
        arr[3][0] = 8;
        arr[3][4] = 6;
        arr[3][8] = 3;
        arr[4][0] = 4;
        arr[4][3] = 8;
        arr[4][5] = 3;
        arr[4][8] = 1;
        arr[5][0] = 7;
        arr[5][4] = 2;
        arr[5][8] = 6;
        arr[6][1] = 6;
        arr[6][6] = 2;
        arr[6][7] = 8;
        arr[7][3] = 4;
        arr[7][4] = 1;
        arr[7][5] = 9;
        arr[7][8] = 5;
        arr[8][4] = 8;
        arr[8][7] = 7;
        arr[8][8] = 9;

        Sudoku sd = new Sudoku();
        sd.sudokuByBackTracking(0);
    }

    public void sudokuByBackTracking(int count) {
        if (81 == count) {
            print();
            return;
        }

        int i = count / 9;
        int j = count % 9;
        if (arr[i][j] == 0) {
            for (int n = 1; n <= 9; n++) {
                arr[i][j] = n;
                if (IsValid(count)) {
                    sudokuByBackTracking(count + 1);
                }
            }
            arr[i][j] = 0;
        } else {
            sudokuByBackTracking(count + 1);
        }
    }

    public boolean IsValid(int count) {
        int i = count / 9;
        int j = count % 9;
        //检测行
        for (int iter = 0; iter < 9; iter++) {
            if (iter == j)
                continue;
            if (arr[i][iter] == arr[i][j]) {
                return false;
            }
        }
        //检测列
        for (int iter = 0; iter < 9; iter++) {
            if (iter == i)
                continue;
            if (arr[iter][j] == arr[i][j]) {
                return false;
            }
        }
        //检测小九宫格
        for (int p = i / 3 * 3; p < (i / 3 + 1) * 3; p++) {
            for (int q = j / 3 * 3; q < (j / 3 + 1) * 3; q++) {
                if (p == i && j == q) {
                    continue;
                }
                if (arr[p][q] == arr[i][j]) {
                    return false;
                }
            }
        }
        return true;
    }

    public void print() {
        System.out.println("数独的解集为:");
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                System.out.print(arr[i][j] + " ");
            }
            System.out.println();
        }
    }
}

示例1运行结果:

数独的解集为:
9 1 7 6 8 2 5 4 3 
3 5 2 1 9 4 7 6 8 
6 8 4 5 7 3 1 9 2 
5 9 6 4 2 8 3 1 7 
4 2 1 7 3 6 9 8 5 
7 3 8 9 1 5 6 2 4 
8 7 5 2 6 1 4 3 9 
1 4 3 8 5 9 2 7 6 
2 6 9 3 4 7 8 5 1 

示例2运行结果:

数独的解集为:
5 3 4 6 7 8 9 1 2 
6 7 2 1 9 5 3 4 8 
1 9 8 3 4 2 5 6 7 
8 5 9 7 6 1 4 2 3 
4 2 6 8 5 3 7 9 1 
7 1 3 9 2 4 8 5 6 
9 6 1 5 3 7 2 8 4 
2 8 7 4 1 9 6 3 5 
3 4 5 2 8 6 1 7 9 

猜你喜欢

转载自blog.csdn.net/daobuxinzi/article/details/109528663