回溯算法总结篇

组合问题:N个数里面按一定规则找出k个数的集合

如果题目要求的是组合的具体信息,则只能使用回溯算法,如果题目只是要求组合的某些最值,个数等信息,则使用动态规划(比如求组合中元素最少的组合,求组合的个数等)

77. 组合

List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();

public List<List<Integer>> combine(int n, int k) {
    dfs(1, n, k);
    return res;
}

private void dfs(int start, int n, int k) {
    if (path.size() == k) {
        res.add(new ArrayList<>(path));
        return;
    }
    // for循环的作用,固定第一个值,比如 1~4,分别固定1~4,如果start为2,则不会遍历到1
    for (int i = start; i <= n; i++) {
        path.push(i); // 记录
        dfs(i + 1, n, k);
        path.pop(); // 回溯
    }
}

39. 组合总和

List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    dfs(0, candidates, target);
    return res;
}

private void dfs(int start, int[] candidates, int target) {
    // 如果target为0,说明找到解
    if (0 == target) {
        res.add(new ArrayList<>(path));
        return;
    }
    for (int i = start; i < candidates.length; i++) {
        int candidate = candidates[i];
        if (target < candidate) {
            continue;   // 剪枝
        }
        path.push(candidate);
        dfs(i, candidates, target - candidate); //  // 关键点:不用i+1了,表示可以重复读取当前的数
        path.pop();
    }
}

40. 组合总和 II

List<List<Integer>> result = new LinkedList<>();
LinkedList<Integer> stack = new LinkedList<>();
boolean[] visited;

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates); // 排序,将相同的元素相邻放置
    visited = new boolean[candidates.length];
    dfs(0, candidates, target);
    return result;
}

public void dfs(int start, int[] candidates, int target) {
    if (target == 0) {
        result.add(new LinkedList<>(stack));
        return;
    }
    for (int i = start; i < candidates.length; i++) {
        int candidate = candidates[i];
        if (target < candidate) {
            continue; // 减枝
        }
        // 如果相邻的两个值相等,且前一个值还没有被访问,则跳过循环
        // (相邻的值只有一个可以被先访问,防止组合结果出现重复)比如:【521,512】
        if (i > 0 && candidate == candidates[i - 1] && !visited[i - 1]) {
            continue;
        }
        visited[i] = true;
        stack.push(candidate);
        // 不同点,从当前遍历到的元素后面开始递归,start = i+1;排除自身(使得结果中一个元素只能出现一次)
        dfs(i + 1, candidates, target - candidate);
        stack.pop();
        visited[i] = false;
    }
}

216. 组合总和 III

List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();

public List<List<Integer>> combinationSum3(int k, int n) {
    dfs(1, k, n);
    return res;
}

private void dfs(int start, int k, int target) {
    if (target == 0 && path.size() == k) {
        res.add(new ArrayList<>(path));
        return;
    }
    for (int i = start; i <= 9; i++) {
        if (target < i) {
            continue; // 剪枝
        }
        if (path.size() == k) {
            continue; // 剪枝
        }
        path.push(i);
        dfs(i + 1, k, target - i);
        path.pop();
    }
}

17. 电话号码的字母组合

List<String> res = new ArrayList<>(); // 存放最终的结果集合
StringBuilder temp = new StringBuilder(); // 每个结果

public List<String> letterCombinations(String digits) {
    if (digits == null || digits.isEmpty()) {
        return res;
    }
    // 数字和字符串的映射关系,比如数组下标为2时,numString[2] == "abc"
    String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    // 递归
    dfs(digits, numString, 0);
    return res;
}

/**
 *
 * @param digits 题目的参数,比如”23”
 * @param numString 数字和字符串的映射关系
 * @param i 当前正在处理的字符串的索引 比如0,则表示正在处理 digits的第一个字符串,也是就2对应的字符串‘abc’
 */
private void dfs(String digits, String[] numString, int i) {
    if (digits.length() == temp.length()) {
        // 将结果加入集合
        res.add(temp.toString());
        return;
    }
    // 取当前应该处理的字符串
    // digits.charAt(i)表示取参数中的数字,比如“2”  -'0'操作将其转化为2~9范围内的int类型
    // numString[digits.charAt(i) - '0']; 则表示在映射关系中取当前数字对应的字符串
    char charAt = digits.charAt(i);
    String str = numString[charAt - '0'];
    for (int j = 0; j < str.length(); j++) {
        temp.append(str.charAt(j));
        dfs(digits, numString, i + 1); // 取下一个字符串一个一个处理
        // 比如“digits”为“23”,则先取2对应的字符串“abc”,然后固定'a',递归处理3对应的字符串'def',固定'd',则返回'ad'
        temp.deleteCharAt(temp.length() - 1);
    }
}

切割问题:一个字符串按一定规则有几种切割方式

  • for (int i = startIndex; i < s.size(); i++)循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。

131. 分割回文串

class Solution {
    //保持前几题一贯的格式
    List<List<String>> res = new ArrayList<>();
    List<String> cur = new ArrayList<>();

    public List<List<String>> partition(String s) {
        backtracking(s, 0, new StringBuilder());
        return res;
    }

    private void backtracking(String s, int start, StringBuilder sb) {
        //因为是起始位置一个一个加的,所以结束时start一定等于s.length,因为进入backtracking时一定末尾也是回文,所以cur是满足条件的
        if (start == s.length()) {
            //注意创建一个新的copy
            res.add(new ArrayList<>(cur));
            return;
        }
        //像前两题一样从前往后搜索,如果发现回文,进入backtracking,起始位置后移一位,循环结束照例移除cur的末位
        for (int i = start; i < s.length(); i++) {
            sb.append(s.charAt(i));
            if (check(sb)) {
                cur.add(sb.toString());
                backtracking(s, i + 1, new StringBuilder());
                cur.remove(cur.size() - 1);
            }
        }
    }

    //helper method, 检查是否是回文,前后指针指向的元素不同,则不是回文
    private boolean check(StringBuilder sb) {
        for (int i = 0; i < sb.length() / 2; i++) {
            if (sb.charAt(i) != sb.charAt(sb.length() - 1 - i)) {
                return false;
            }
        }
        return true;
    }

}

93. 复原 IP 地址❤️

class Solution {
        LinkedList<String> res = new LinkedList<>();

        public List<String> restoreIpAddresses(String s) {
            if (s.length() > 12) {
                return res;
            }
            backTrack(s, 0, 0);
            return res;
        }

        private void backTrack(String s, int startIndex, int pointNum) {
            if (pointNum == 3) {
                if (isValid(s, startIndex, s.length() - 1)) {
                    res.add(s);
                }
                return;
            }
            for (int i = startIndex; i < s.length(); i++) {
                if (isValid(s, startIndex, i)) {
                    s = s.substring(0, i + 1) + '.' + s.substring(i + 1);
                    pointNum++;
                    backTrack(s, i + 2, pointNum);
                    pointNum--;
                    s = s.substring(0, i + 1) + s.substring(i + 2);
                } else {
                    break;
                }
            }
        }

        /**
         *  验证当前子串是否符合下面三个条件
         *  1.子串的第一个字符不为0
         *  2.子串总数不大于255
         *  3.子串为正整数
         */
        private boolean isValid(String s, int start, int end) {
            if (start > end) {
                return false;
            }
            if (s.charAt(start) == '0' && start != end) {
                return false;
            }
            int num = 0;
            for (int i = start; i <= end; i++) {
                num = num * 10 + s.charAt(i) - '0';
                if (num > 255) {
                    return false;
                }
            }
            return true;
        }
    }

子集问题:一个N个数的集合里有多少符合条件的子集

求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树。

78. 子集❤️

List<List<Integer>> list = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();

public List<List<Integer>> subsets(int[] nums) {
    dfs(0, nums);
    return list;
}

private void dfs(int start, int[] nums) {
    // 子集是收集树形结构中树的所有节点的结果。
    list.add(new ArrayList<>(path));
    for (int i = start; i < nums.length; i++) {
        int num = nums[i];
        path.push(num);
        dfs(i + 1, nums);
        path.pop();
    }
}

90. 子集 II

子集II问题:这种需要回溯解决的题目,组合问题,子集问题,切割问题如果遇到原先给你的集合中有重复元素,而输出的结果中不允许有重复结果的,需要先将原始数组排序,然后使用一个boolean类型的数组,标记每个元素是否被访问,对于重复的元素,如果前一个元素没有被访问,则不允许访问第二个元素

List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used; // 元素是否被访问

public List<List<Integer>> subsetsWithDup(int[] nums) {
    if (nums.length == 0) {
        result.add(path);
        return result;
    }
    Arrays.sort(nums);
    used = new boolean[nums.length];
    dfs(nums, 0);
    return result;
}

private void dfs(int[] nums, int startIndex) {
    result.add(new ArrayList<>(path));
    if (startIndex >= nums.length) { // 终止条件
        return;
    }
    for (int i = startIndex; i < nums.length; i++) {
        // 去重,递归树层去重,如果nums中有重复,则必须先访问前面的元素才能访问后面的元素
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
            // 如果出现重复,前面一个还没访问,则跳出循环
            continue;
        }
        int num = nums[i];
        path.push(num);
        used[i] = true;
        dfs(nums, i + 1);
        path.pop();
        used[i] = false;
    }
}

491. 非递减子序列

List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();

public List<List<Integer>> findSubsequences(int[] nums) {
    dfs(0, nums);
    return res;
}

public void dfs(int start, int[] nums) {
    if (path.size() > 1) {
        res.add(new ArrayList<>(path));
    }
    // 使用数组去重,第一遇到一个值将对应数组下标置位1,如果再次遇到1,则说明已经访问过,跳过
    int[] used = new int[201];
    for (int i = start; i < nums.length; i++) {
        int num = nums[i];
        if (!path.isEmpty() && path.get(path.size() - 1) > num ||
                used[num + 100] == 1) {
            continue;
        }
        used[num + 100] = 1;
        path.add(num);
        dfs(i + 1, nums);
        path.remove(path.size() - 1);
    }
}

排列问题:N个数按一定规则全排列,有几种排列方式

  • 每层都是从0开始搜索而不是startIndex

  • 需要used数组记录path里都放了哪些元素了

46. 全排列❤️❤️

class Solution {
   List<List<Integer>> res = new LinkedList<>();
    List<Integer> path = new LinkedList<>();
    boolean[] used;

    public List<List<Integer>> permute(int[] nums) {
        used = new boolean[nums.length];
        dfs( nums);
        return res;
    }

    private void dfs(int[] nums) {
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if(!used[i]){
                used[i]  =true;
                path.add(nums[i]);
                dfs(nums);
                used[i] = false;
                path.remove(path.size()-1);
            }
        }
    }
}

47. 全排列 II❤️

class Solution {
       List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> path = new LinkedList<>();
    boolean[] used; // 判断一个元素是否被访问

    public List<List<Integer>> permuteUnique(int[] nums) {
        used = new boolean[nums.length];
        // 定义一个集合存放结果
        /******************************和46的区别*******************************/
        Arrays.sort(nums); // 排序
        /******************************和46的区别*******************************/
        doPermute(nums);
        return res;
    }

    private void doPermute(int[] nums) {
        if (path.size() == nums.length) {
            res.add(new LinkedList<>(path));
        }
        for (int i = 0; i < nums.length; i++) {
            /******************************和46的区别*******************************/
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue; // 相邻元素如果相等且前一个元素还没有被处理,则跳过 【两个相邻元素只有一个可以先被处理,排除重复】
            }
            /******************************和46的区别*******************************/
            if (!used[i]) {
                path.push(nums[i]);
                used[i] = true;
                doPermute(nums); // 递归
                path.pop(); // 回溯
                used[i] = false;  // 回溯
            }
        }
    }
}

棋盘问题:N皇后,解数独等等

51. N 皇后

使用三个数组分别标记,列冲突,左斜线冲突,右斜线冲突

根据当前位置的横坐标 X 和纵坐标 Y 计算当前位置所处的左斜线公式:X + Y ,

右斜线公式为 N - 1 - ( X - Y )

class Solution {
    boolean[] currCol;
    boolean[] leftSlash;
    boolean[] rightSlash;
    char[][] chessBoard;
    List<List<String>> res = new ArrayList<>();
    List<String> path = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        currCol = new boolean[n]; // 当前列数组大小和N皇后的N的大小相同
        leftSlash = new boolean[2 * n - 1]; // 左斜线的大小等于2*N-1,因为在N*N大小的棋盘上有2*N-1个对角线
        rightSlash = new boolean[2 * n - 1]; // 右斜线同理
        chessBoard = new char[n][n];
        for (char[] chars : chessBoard) {
            Arrays.fill(chars, '.');
        }
        dfs(0);
        return res;
    }



    private void dfs(int i) {
        int n = currCol.length;
        if (i == n) {
            for (char[] chars : chessBoard) {
                StringBuilder stringBuilder = new StringBuilder();
                for (char c : chars) {
                    stringBuilder.append(c);
                }
                path.add(stringBuilder.toString());
            }
            res.add(new ArrayList<>(path));
            path.clear();
        }
        for (int j = 0; j < n; j++) {
            if (currCol[j] || leftSlash[i + j] || rightSlash[n - 1 - (i - j)]) {
                continue;
            }
            chessBoard[i][j] = 'Q';
            currCol[j] = true;
            leftSlash[i + j] = true;
            rightSlash[n - 1 - (i - j)] = true;
            dfs(i + 1);
            chessBoard[i][j] = '.';
            currCol[j] = false;
            leftSlash[i + j] = false;
            rightSlash[n - 1 - (i - j)] = false;
        }
    }
}

37. 解数独

解数独问题需要使用三个二维数组记录,行冲突,列冲突,九宫格冲突

根据当前位置的横坐标 X 和纵坐标 Y 计算当前位置所处的九宫格公式为:X / 3 * 3 + Y / 3

class Solution {
    boolean[][] ca = new boolean[9][9]; // 行冲突
    boolean[][] cb = new boolean[9][9];// 列冲突
    boolean[][] cc = new boolean[9][9];// 九宫格冲突

    public void solveSudoku(char[][] board) {
        // 记录当前数独中已经存在的数组
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = board[i][j];
                if (c != '.') { // 如果字符不为'.'
                    ca[i][c - '1'] = true; // 将其所在行的数字c设置为冲突(该行不能再添加c数字)
                    cb[j][c - '1'] = true; // 将其所在列的数字c设置为冲突(该列不能再添加c数字)
                    cc[i / 3 * 3 + j / 3][c - '1'] = true;  // 将其所在九宫格的数字c设置为冲突(该九宫格不能再添加c数字)
                }
            }
        }
        // 参数一:正在处理的行,参数二:正在处理的列,参数三:整个二维数组
        dfs(0, 0, board);
    }

    private boolean dfs(int i, int j, char[][] board) {
        // 跳过数独中已经有数字的地方
        while (board[i][j] != '.') {
            // 超出列长,则列长恢复为0,行++
            if (++j >= 9) {
                j = 0;
                i++;
            }
            // i>=9 说明已经遍历完所有数组中的元素
            if (i >= 9) {
                return true;
            }
        }
        // 通过上面的while跳过数独中有数字的地方,找到了没有数字的地方,开始尝试从1~9放入数字
        for (int x = 1; x <= 9; x++) {
            // 如果添加的数字x在当前行,当前列,当前九宫格中有任意一个冲突,则跳过,尝试下一个数字
            if (ca[i][x - 1] || cb[j][x - 1] || cc[i / 3 * 3 + j / 3][x - 1]) {
                continue;
            }
            // 如果没有冲突,向[i,j]节点添加x数字
            board[i][j] = (char) (x + '0');
            // 添加完数字后,更新冲突数组,把当前行,当前列,当前九宫格中的x-1设置为true
            ca[i][x - 1] = cb[j][x - 1] = cc[i / 3 * 3 + j / 3][x - 1] = true;
            boolean dfs = dfs(i, j, board); // 递归
            if (dfs) {
                return true; // 如果找到解,则直接返回true
            }
            board[i][j] = '.'; // 如果没找到解,回溯
            ca[i][x - 1] = cb[j][x - 1] = cc[i / 3 * 3 + j / 3][x - 1] = false; //回溯
        }
        return false;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值