算法四 递归、回溯与分治(Java实现)

640 阅读4分钟

刷题时间:2021.7.26-2021.7.28

回溯法

回溯法又称为试探法,但当探索到某一步时,发现原先选择达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为 回溯法。

分治算法(归并排序)

将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解后进行合并,就可得到原问题的解。

一般步骤:1.分解,将要解决的问题划分成若干规模较小的同类问题;

2.求解,当子问题划分得足够小时,用较简单的方法解决;

3.合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

1、LeetCode78子集

方法一:回溯法

思路:利用回溯方法生成子集,即对于每个元素,都有试探放入或不放入集合中的两个选择:

选择放入该元素,递归的进行后续元素的选择,完成放入该元素后续所有元素的试探;之后将其拿出,即再进行一次选择不放入该元素,递归的进行后续元素的选择,完成不放入该元素后续所有元素的试探。

本来选择放入,再选择一次不放入的这个过程,称为回溯试探法

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();//存储最终结果
        List<Integer> item = new ArrayList<Integer>();//回溯时,产生各个子集的数组
        generate(0,nums,item,result);//计算各个子集
        return result;
    }
    public void generate(int i,int[] nums,List<Integer> item,List<List<Integer>> result){
        //找到所有以item开头的所有子集
        result.add(new ArrayList<Integer>(item));
        //因为java传的是引用,如果直接把item添加进去,之后item变化添加进去的对象也会发生变化
        for(int j=i;j<nums.length;j++){
            item.add(nums[j]);
            generate(j+1,nums,item,result);
            item.remove(item.size()-1);//删除最后一个元素
        }
    }   
}

若输入为[1,2,3],则输出为[[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]。

方法二:位运算法

image.png

思路:若一个集合有三个元素A, B, C,则3个元素有2^3 = 8种组成集合的方式,用0-7表示这些集合。

A元素为100=4;B元素为010=2; C元素为001= 1。如构造某一集合,即使用A,B,C对应的三个整数与该集合对应的整数做&运算,当为真时,将该元素push进入集合。

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<Integer> item = new ArrayList<Integer>();
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        int all_set = 1 << nums.length;//1<<n即为2^n
        for (int i = 0; i < all_set; i++) {//遍历所有集合
            item.clear();
            for (int j = 0; j < nums.length; j++) {
                if ((i & (1 << j)) != 0) {//(1<<j)即为构造nums数组的第j个元素,若(i&(1<<j)为真则nums[j]放入item
                    item.add(nums[j]);
                }
            }
            result.add(new ArrayList<Integer>(item));
        }
        return result;
    }  
}

若输入为[1,2,3],则输出为[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]。

2、LeetCode90子集 II

LeetCode官方题解

在递归时,若发现没有选择上一个数,且当前数字与上一个数相同,则可以跳过当前生成的子集。

class Solution {
    List<Integer> item = new ArrayList<Integer>();
    List<List<Integer>> result = new ArrayList<List<Integer>>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        dfs(false, 0, nums);
        return result;
    }
    public void dfs(boolean choosePre, int cur, int[] nums) {
        if (cur == nums.length) {
            result.add(new ArrayList<Integer>(item));
            return;
        }
        dfs(false, cur + 1, nums);
        if (!choosePre && cur > 0 && nums[cur - 1] == nums[cur]) {
            return;
        }
        item.add(nums[cur]);
        dfs(true, cur + 1, nums);
        item.remove(item.size() - 1);
    }
}

若输入为[1,2,2],则输出为[[],[2],[2,2],[1],[1,2],[1,2,2]]。

3、LeetCode40组合总和 II

思路:在搜索回溯过程中进行剪枝操作:递归调用时,计算已选择元素的和sum,若sum >target,不再进行更深的搜索,直接返回。

LeetCode官方题解

public class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        int len = candidates.length;
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }
        Arrays.sort(candidates);
        Deque<Integer> path = new ArrayDeque<>(len);
        dfs(candidates, len, 0, target, path, res);
        return res;
    }
    //begin从候选数组的 begin 位置开始搜索,target表示剩余,这个值一开始等于 target,path从根结点到叶子结点的路径
    private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) {
        if (target == 0) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = begin; i < len; i++) {
            // 大剪枝:减去candidates[i]小于0,减去后面的candidates[i+1]、candidates[i+2]肯定也小于0,因此用 break
            if (target - candidates[i] < 0) {
                break;
            }
            // 小剪枝:同一层相同数值的结点,从第2个开始,候选数更少,结果一定发生重复,因此跳过,用continue
            if (i > begin && candidates[i] == candidates[i - 1]) {
                continue;
            }
            path.addLast(candidates[i]);
            // 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
            dfs(candidates, len, i + 1, target - candidates[i], path, res);
            path.removeLast();
        }
    }
}

4、LeetCode22括号生成

思路:递归生成所有可能,递归需要限制条件:

1.左括号与右括号的数量,最多放置n个。

2.若左括号的数量<=右括号数量,不可进行放置右括号的递归。

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        generate("",n,n,result);
        return result;
    }
    //生成字符串item,当前还可以放置左括号的数量left,右括号的数量right
    void generate(String item, int left,int right,List<String> result){
        if(left == 0 && right == 0){
            result.add(item);
            return;
        }
        if(left > 0){//左括号还有剩余
            generate(item+'(',left-1,right,result);
        }
        if(left < right){//右括号剩余比左括号多
            generate(item+')',left,right-1,result);
        }
    }
}

5、LeetCode51 N皇后

思路:利用递归对棋盘的每一行放置皇后,放置时,按列顺序寻找可以放置皇后的列,若可以放置皇后,将皇后放置该位置,并更新mark标记数组,递归进行下一行的皇后放置;当该次递归结束后,恢复mark数组,并尝试下一个可能放皇后的列。当递归可以完成N行的N个皇后放置,则将该结果保存并返回。

LeetCode评论代码

class Solution {
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> result = new ArrayList<>();
        char[][] chess = new char[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                chess[i][j] = '.';
            }
        }      
        dfs(0, chess, result);//从第0行开始放置皇后
        return result;
    }
     private static void dfs(int row, char[][] chess, List<List<String>> result) {       
        if (row == chess.length) {//所有行填满,添加结果
            result.add(construct(chess));
            return;
        }        
        for (int column = 0; column < chess.length; column++) {//对当前行尝试放置每一列
            if (valid(chess, row, column)) {               
                chess[row][column] = 'Q';//放置皇后              
                dfs(row + 1, chess, result);//放下一行              
                chess[row][column] = '.';//回溯
            }
        }
    }
    private static boolean valid(char[][] chess, int row, int column) {
        //因为是一行一行往下放,所以只有列和斜对角出现攻击,行不会被攻击
        for (int i = 0; i < row; i++) {//列攻击检查
            if (chess[i][column] == 'Q') {
                return false;
            }
        }
        //看右上角是否被攻击,即从当前位置的上一行,当前列的右边,也就是右上角第一个斜对角位置开始检查。每次在上一个的检查的位置上上移一行,右移一列,也就是下一个斜对角
        for (int i = row -1, j = column+1; i >= 0 && j < chess.length; i--, j++) {          
            if (chess[i][j] == 'Q') {//右上角是否有皇后
                return false;
            }
        }
        //和右上角一样进行左上角的检查。i代表被检查行,j代表被检查的列。i和j组成一个斜对角的坐标
        for (int i = row -1, j = column -1; i >= 0 && j >= 0; i--, j--) {
            if (chess[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
    private static List<String> construct(char[][] chess) {//将数组转换为List<String>来保存最终结果
        List<String> path = new ArrayList<>();
        for (int i = 0; i < chess.length; i++) {
            path.add(new String(chess[i]));
        }
        return path;
    }
}

6、LeetCode315计算右侧小于当前元素的个数

思路:在归并两排序数组时,当需要将前一个数组元素的指针i指向的元素插入时,对应的count[j], 即为指向后一个数组的指针j的值。

LeetCode官方题解

class Solution {
    private int[] index;
    private int[] temp;
    private int[] tempIndex;
    private int[] ans;
    public List<Integer> countSmaller(int[] nums) {
        this.index = new int[nums.length];
        this.temp = new int[nums.length];
        this.tempIndex = new int[nums.length];
        this.ans = new int[nums.length];
        for (int i = 0; i < nums.length; ++i) {
            index[i] = i;
        }
        int l = 0, r = nums.length - 1;
        mergeSort(nums, l, r);
        List<Integer> list = new ArrayList<Integer>();
        for (int num : ans) {
            list.add(num);
        }
        return list;
    }
    public void mergeSort(int[] a, int l, int r) {
        if (l >= r) {
            return;
        }
        int mid = (l + r) >> 1;
        mergeSort(a, l, mid);
        mergeSort(a, mid + 1, r);
        merge(a, l, mid, r);
    }
    public void merge(int[] a, int l, int mid, int r) {
        int i = l, j = mid + 1, p = l;
        while (i <= mid && j <= r) {
            if (a[i] <= a[j]) {
                temp[p] = a[i];
                tempIndex[p] = index[i];
                ans[index[i]] += (j - mid - 1);
                ++i;
                ++p;
            } else {
                temp[p] = a[j];
                tempIndex[p] = index[j];
                ++j;
                ++p;
            }
        }
        while (i <= mid)  {
            temp[p] = a[i];
            tempIndex[p] = index[i];
            ans[index[i]] += (j - mid - 1);
            ++i;
            ++p;
        }
        while (j <= r) {
            temp[p] = a[j];
            tempIndex[p] = index[j];
            ++j;
            ++p;
        }
        for (int k = l; k <= r; ++k) {
            index[k] = tempIndex[k];
            a[k] = temp[k];
        }
    }
}