[算法]回溯算法

234 阅读5分钟

回溯算法

问题描述

回溯算法是穷举的一种,通过“剪枝”策略,来减少遍历的次数,整体的时间复杂度还是O(n!),但是比纯暴力破解法还是效率高一些。

回溯算法是一种经典的搜索算法,虽然执行效率低,但是能有思路解决问题,已经比无解要好。

属于递归算法的一种,深度优先遍历算法(DFS)就是回溯算法的应用。

回溯法的思路就是在“分叉路口”逐一路径进行试验,发现走错路了,再往回退,只到上一个“正确的路口”,再尝试其他路径。

解题步骤

解决一个回溯问题,实际上就是一个决策树的遍历过程

你只需要思考 3 个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

选择问题的决策树示意图:

img

回溯算法可以简单归结为:

def backTrack(路径,选择列表[维护的状态]){

​ if 满足条件:

​ result.add(路径);

​ return;

​ for 选择 in 选择列表:

​ if 已存在:

​ continue;

​ add_item (做选择,添加到选择列表中);

​ backTrack (路径,选择列表);

​ remove_item (撤销选择,回溯);

}

回溯算法的经典问题

N皇后问题
/****************************************
     * 八皇后问题
     * 可以使用回溯法和动态规划法
     * 引申处理N皇后问题,返回可以实现的结果个数
     */
    public int calNQueens(int n){
        // 计算n皇后问题
        int res = 0;
        int[] queensRes =  new int[n];   // 八皇后问题的最终结果,记录每行元素放置在哪一列,使用全局变量
        res = calQueens(n,queensRes,0,res);
        return res;
    }

    /***********************
     *
     */

    public int calQueens(int N,int[] queens,int row,int res){
//        for(int i = 0; i< queuesRes.length;i ++){
//            System.out.print(queuesRes[i] + ",");
//        }
//        System.out.println();
        // row是行号,每次判断一行的可行值
        if(row == N){
            // 8行已经执行完毕,打印输出
            printQueues(queens);
            res += 1;
            return res;
        }

        for(int col = 0; col < N ; ++ col ){
            if(is_ok(queens,row,col)){
                queens[row] = col;
                res = calQueens(N,queens,row + 1,res);
            }
        }
        return res;
    }
    private boolean is_ok(int[] queens,int row,int col){
        // 判断当前列位置是否ok,N皇后问题的规则是斜着,横竖都不能有重复值
        int left = col - 1, right = col + 1;   // 判断左右斜面是否可行
        for(int i = row - 1;i >= 0; i --){

            if (left == queens[i] && left >= 0){
                return false;
            }
            if (right == queens[i] && right < 8){
                return false;
            }
            if (queens[i] == col){
                return false;
            }
            -- left;
            ++ right;
        }
        return true;
    }

    private void printQueues(int[] res){
        System.out.println("==========================================");
        for(int i = 0; i < res.length; i ++){
            for (int j = 0; j < res.length ;j ++ ){
                if (j == res[i]){
                    System.out.print("@ ");
                } else {
                    System.out.print("* ");
                }
            }
            System.out.println();
        }
    }
全排列问题
子问题:字符串排列

<待填坑>

和数组一样,都属于全排列问题

// 全排列问题,可以扩展为二维数组的排列

   
子问题:数组全排列

LeetCode 46. 全排列

题目描述:

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] 示例 2:

输入:nums = [0,1] 输出:[[0,1],[1,0]]s

    public static List<List<Integer>> permute(int[] nums) {
        int len = nums.length;
        List<List<Integer>> res = new ArrayList();
        if(len == 0){
            return res;
        }

        Deque<Integer> path = new ArrayDeque<Integer>();
        boolean[] flag = new boolean[len];  // default false
        backTrack(nums,len,0,path,flag,res);
        return res;
    }

    private static void backTrack(int[] nums,int len,int depth,Deque<Integer> path,boolean[] flag,List<List<Integer>> res){
        if(depth == len){
            res.add(new ArrayList<Integer>(path));
            return;
        }

        for(int i = 0; i < len ; i++){
            if(flag[i]){
                continue;
            }
            flag[i] = true;
            path.addLast(nums[i]);
            backTrack(nums,len,depth + 1,path,flag,res);
            flag[i] = false;
            path.removeLast();
        }

    }
子问题:组合总和

问题描述:

LeetCode 39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7 输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。 示例 2:

输入: candidates = [2,3,5], target = 8 输出: [[2,2,2,2],[2,3,3],[3,5]]

Code:

    List<List<Integer>> res = new ArrayList();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        cal(0,0,new LinkedList<Integer> (),candidates,target);
        return res;
    }

    private void cal(int idx,int sum,LinkedList<Integer> track,int[] candidates,int target){
        if(idx >= candidates.length || sum > target){
            // 已经遍历到队尾,或者最大值超标,终止回溯
            return ;
        }

        if(sum == target){
            // 筛选完成
            res.add(new ArrayList<Integer>(track));
            return;
        }
        // 使用当前元素
        track.addLast(candidates[idx]);
        cal(idx,sum + candidates[idx],track,candidates,target);

        // 不使用当前元素
        track.removeLast();
        cal(idx + 1,sum,track,candidates,target);
    }
0-1背包问题

可以使用回溯法和动态规划两种方法处理。

背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?它是在1978年由Merkel和Hellman提出的。

问题描述:

给定n个重量为w1,w2,w3,…,wn,价值为v1,v2,v3,…,vn的物品和容量为C的背包 求这个物品中一个最有价值的子集,使得在满足背包的容量的前提下,包内的总价值最大

注意:0-1背包问题指的是每个物品只能使用一次

 /**
     * 0-1背包计算主函数,每个物品只能采用一次
     * @param values:物品清单的价值
     * @param weights:物品的重量
     * @param capacity:
     * @return
     */

    int MAX = 0;
    public int knapsack(int[] values,int[] weights,int capacity){
        boolean[] flag = new boolean[values.length];
        backTrack(values,weights,capacity,
                0,0,flag);
        return MAX;
    }

    // 回溯函数
    private void backTrack(int[] values,int[] weights,int capacity,
                           int w,int v,boolean[] flag){
        // w是当前背包重量
        if (w > capacity){
            return;
        }
        if(v > MAX){
            System.out.println("当前价值:" + v);
            for(int i = 0; i < flag.length; i++) {
                System.out.print("| " + values[i] + " ");
                System.out.print("--> " + flag[i] + " ");
            }
            System.out.println();
            MAX = v;
        }

        for(int i = 0;i < values.length; i ++){
            if (flag[i]){
                continue;
            }
            flag[i] = true;
            backTrack(values,weights,capacity,
                    w + weights[i],v + values[i],flag);
            flag[i] = false;
        }
    }
正则表达式问题

<待填坑>

图的深度优先搜索

<待填坑>