LeetCode - 7. 回溯

刷题顺序来自:代码随想录

组合

77. 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。

回溯

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

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

private void backtracking(int n, int k, int index) {
    if(path.size() == k) {
        res.add(new LinkedList<Integer>(path));
        return;
    }
    // for第一层1, 2, 3, 4
    for(int i = index; i <= n; i++) {
        path.add(i);
        backtracking(n, k, i+1);  // 往第二层或者更深
        path.removeLast();  // 回溯
    }
}

回溯与剪枝

for(int i = index; i <= n - (k - path.size()) + 1; i++)

39. 组合总和

给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。

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

回溯

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

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

private void backtracking(int[] candidates, int target, int index) {
    if(sum >= target) {
        if(sum == target) {
            res.add(new LinkedList<Integer>(path));
        }
        return;
    }

    for(int i = index; i < candidates.length; i++) {
        path.add(candidates[i]);
        sum += candidates[i];
        backtracking(candidates, target, i);
        path.removeLast();
        sum -= candidates[i];
    }
}

剪枝

由于数组是无序的,想要剪枝,必须先排序。

Arrays.sort(candidates);

for(int i = index; i < candidates.length && sum + candidates[i] <= target; i++)

40. 组合总和 II

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。

这一题与I和III的区别是需要去重,去重需要去掉当前层已经使用过的元素。

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates);
    backtracking(candidates, target, 0);
    return res;
}

ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
int last;
private void backtracking(int[] candidates, int target, int index) {
    if(sum == target) {
        res.add(new LinkedList<Integer>(path));
        return;
    }
    for(int i = index; i < candidates.length && sum + candidates[i] <= target; i++) {
        // 去除当前层已经使用过的元素
        if(i > index && candidates[i] == candidates[i-1]) {
            continue;
        }
        sum += candidates[i];
        path.add(candidates[i]);
        backtracking(candidates, target, i+1);
        sum -= candidates[i];
        path.removeLast();
    }
}

216. 组合总和 III

找出所有相加之和为 nk 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。

回溯

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

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

private void backtracking(int k, int n, int index) {
    if(path.size() == k) {
        if(sum == n) {
            res.add(new LinkedList(path));
        }
        return;
    }
	// 注意剪枝
    for(int i = index; i <= 9 - (k - path.size()) + 1; i++) {
        path.add(i);
        sum += i;
        backtracking(k, n, i+1);
        path.removeLast();
        sum -= i;
    }
}

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

public List<String> letterCombinations(String digits) {
    // 数字与字母的映射
    HashMap<Character, Character[]> map = new HashMap<>();
    map.put('2', new Character[]{'a', 'b', 'c'});
    map.put('3', new Character[]{'d', 'e', 'f'});
    map.put('4', new Character[]{'g', 'h', 'i'});
    map.put('5', new Character[]{'j', 'k', 'l'});
    map.put('6', new Character[]{'m', 'n', 'o'});
    map.put('7', new Character[]{'p', 'q', 'r', 's'});
    map.put('8', new Character[]{'t', 'u', 'v'});
    map.put('9', new Character[]{'w', 'x', 'y', 'z'});

    backtracking(digits, map, 0);
    return res;
}

ArrayList<String> res = new ArrayList<>();
StringBuilder builder = new StringBuilder();

private void backtracking(String digits, HashMap<Character, Character[]> map, int index) {
    if(digits.length() == 0) {
        return;
    }

    if(builder.length() == digits.length()) {
        res.add(builder.toString());
    }

    for(int i = index; i < digits.length(); i++) {
        char digit = digits.charAt(index);
        Character[] chars = map.get(digit);
        for(Character ch: chars) {
            builder.append(ch);
            backtracking(digits, map, i+1);
            builder.deleteCharAt(builder.length()-1);
        }

    }
}

131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

与上面的组合不同,每次循环只有2种可能,即在/不在当前索引切割。

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

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

private void backtracking(String s, int index, int start) {
    if(index >= s.length()) {
        // 需要字符串被完整地切割,才收集此结果
        if(index == start) {
            res.add(new LinkedList<String>(path));
        }
        return;
    }
    // 不在当前索引切割
    backtracking(s, index + 1, start);

    // 在当前索引切割,则切割下来的字符串需要是回文串,否则剪枝
    if(isPalindrome(s, start, index)) {
        path.add(s.substring(start, index + 1));
        backtracking(s, index + 1, index + 1);
        path.removeLast();
    }
}

// 判断字符串是否是回文串
private boolean isPalindrome(String s, int start, int end) {
    while(start < end) {
        if(s.charAt(start) != s.charAt(end)) {
            return false;
        }
        start++;
        end--;
    }
    return true;
}

占用内存更小的回溯:

List<List<String>> lists = new ArrayList<>();
Deque<String> deque = new LinkedList<>();

public List<List<String>> partition(String s) {
    backTracking(s, 0);
    return lists;
}

private void backTracking(String s, int startIndex) {
    //如果起始位置大于s的大小,说明找到了一组分割方案
    if (startIndex >= s.length()) {
        lists.add(new ArrayList(deque));
        return;
    }
    for (int i = startIndex; i < s.length(); i++) {
        //如果是回文子串,则记录
        if (isPalindrome(s, startIndex, i)) {
            String str = s.substring(startIndex, i + 1);
            deque.addLast(str);
        } else {
            continue;
        }
        //起始位置后移,保证不重复
        backTracking(s, i + 1);
        deque.removeLast();
    }
}

93. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你不能重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

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

ArrayList<String> res = new ArrayList<>();
String path = "";

int count = 0;  // 记录插入点的个数
private void backtracking(String s, int index) {
    if(count == 3) {  // 当已经插入了3个点之后,说明需要开始收集结果了
        if(isValid(s, index, s.length())) {  // 判断剩余子串是否是有效ip
            path += s.substring(index, s.length());
            res.add(path);
            // 收集好结果之后,需要把刚刚加入的剩余第4位ip删除
            path = path.substring(0, path.length()- s.length() + index);            
        }
        return;
    }
    for(int i = index; i < s.length(); i++) {
        if(isValid(s, index, i+1)) {
            path += s.substring(index, i+1) + ".";
            count++;

        }
        else {
            continue;
        }
        backtracking(s, i+1);
        // 在回溯时,需要多删除一位,即添加的点
        path = path.substring(0, path.length() - i + index - 2);
        count--;
    }
}

// 判断ip是否是有效的
private boolean isValid(String s, int start, int end) {
    if(start < 0 || end <= 0 || start >= s.length() || end > s.length() || end <= start || end - start > 3) {
        return false;
    }
    if(end - start == 3) {  // 如果长度为3,则必须介于100和255之间
        if(s.charAt(start) == '1') {
            return true;
        }
        else if(s.charAt(start) == '2') {
            return s.charAt(start + 1) < '5' || (s.charAt(start + 1) == '5' && s.charAt(start + 2) <= '5');
        }
        return false;
    }
    else if(end - start == 2) {  // 如果长度是2,第一位必须大于等于1
        return s.charAt(start) >= '1';
    }
    return true;
}

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

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

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

private void backtracking(int[] nums, int index) {
    res.add(new LinkedList<Integer>(path));  // 需要记录所有结果
    for(int i = index; i < nums.length; i++) {
        path.add(nums[i]);
        backtracking(nums, i+1);
        path.removeLast();
    }
}

90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

public List<List<Integer>> subsetsWithDup(int[] nums) {
    Arrays.sort(nums);  // 由于需要去重,所以先排序
    backtracking(nums, 0);
    return res;
}

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

private void backtracking(int[] nums, int index) {
    res.add(new LinkedList<Integer>(path));
    for(int i = index; i < nums.length; i++) {
        // 去除同一层中相同的节点
        if(i > index && nums[i] == nums[i-1]) {
            continue;
        }
        path.add(nums[i]);
        backtracking(nums, i+1);
        path.removeLast();
    }
}

491. 递增子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

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

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

private void backtracking(int[] nums, int index) {
    if(path.size() >= 2) {  // 有至少2个元素后,收集结果
        res.add(new LinkedList<Integer>(path));
    }
    // 使用set去重
    HashSet<Integer> set = new HashSet<>();
    for(int i = index; i < nums.length; i++) {
        // 去重,每一层相同的元素只收集一次
        if(set.contains(nums[i])) {
            continue;
        }
        set.add(nums[i]);
        if(path.size() == 0 || nums[i] >= path.get(path.size() - 1)) {
            path.add(nums[i]);
            backtracking(nums, i + 1);
            path.removeLast();
        }
    }
}

由于输入-100 <= nums[i] <= 100,可以使用一个大小为201的数组记录数字有没有使用过,占用内存更小。优化之后:

private List<Integer> path = new ArrayList<>();
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
    backtracking(nums,0);
    return res;
}

private void backtracking (int[] nums, int start) {
    if (path.size() > 1) {
        res.add(new ArrayList<>(path));
    }

    int[] used = new int[201];
    for (int i = start; i < nums.length; i++) {
        if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
            (used[nums[i] + 100] == 1)) continue;
        used[nums[i] + 100] = 1;
        path.add(nums[i]);
        backtracking(nums, i + 1);
        path.remove(path.size() - 1);
    }
}

排列

46. 全排列

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

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

ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int[] used;  // 记录已经访问过的元素
private void backtracking(int[] nums) {
    if(path.size() == nums.length) {
        res.add(new LinkedList<>(path));
        return;
    }
    for(int i = 0; i < nums.length; i++) {
        if(used[i] == 0) {
            used[i] = 1;
            path.add(nums[i]);
            backtracking(nums);
            path.removeLast();
            used[i] = 0;
        }
    }
}

47. 全排列 II

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

public List<List<Integer>> permuteUnique(int[] nums) {
    used = new int[nums.length];
    Arrays.sort(nums);  // 排序
    backtracking(nums);
    return res;
}

ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int[] used;

private void backtracking(int[] nums) {
    if(path.size() == nums.length) {
        res.add(new LinkedList<Integer>(path));
        return; 
    }
    for(int i = 0; i < nums.length; i++) {
        if(used[i] == 0) {
            // 在同一层内去重
            if(i > 0 && used[i-1] == 0 && nums[i] == nums[i-1]) {
                continue;
            }
            used[i] = 1;
            path.add(nums[i]);
            backtracking(nums);
            path.removeLast();
            used[i] = 0;
        }
    }
}

332. 重新安排行程

public List<String> findItinerary(List<List<String>> tickets) {
    used = new int[tickets.size()];
    path.add("JFK");
    tickets.sort(new Comparator<List<String>> () {
        public int compare(List<String> l1, List<String> l2) {
            return l1.get(1).compareTo(l2.get(1));
        }
    });
    backtracking(tickets, "JFK");
    return res;
}

LinkedList<String> res = null;
LinkedList<String> path = new LinkedList<>();
int[] used;
boolean find = false;

private void backtracking(List<List<String>> tickets, String target) {
    if(find) {
        return;
    }
    if(path.size() == tickets.size() + 1) {
        res = new LinkedList<String>(path);
        find = true;
        return;
    }

    for(int i = 0; i < tickets.size(); i++) {
        if(!find && used[i] == 0 && tickets.get(i).get(0).equals(target)) {
            // 去重,加了之后效果一般
            // if(i > 0 && used[i-1] == 0 && tickets.get(i-1).get(0).equals(target) && tickets.get(i).get(1).equals(tickets.get(i-1).get(1))) {
            //     continue;
            // }
            used[i] = 1;
            path.add(tickets.get(i).get(1));
            backtracking(tickets, tickets.get(i).get(1));
            path.removeLast();
            used[i] = 0;
        }
    }
}

优化版:

private Deque<String> res;
private Map<String, Map<String, Integer>> map;

private boolean backTracking(int ticketNum){
    if(res.size() == ticketNum + 1){
        return true;
    }
    String last = res.getLast();
    if(map.containsKey(last)){//防止出现null
        for(Map.Entry<String, Integer> target : map.get(last).entrySet()){
            int count = target.getValue();
            if(count > 0){
                res.add(target.getKey());
                target.setValue(count - 1);
                if(backTracking(ticketNum)) return true;
                res.removeLast();
                target.setValue(count);
            }
        }
    }
    return false;
}

public List<String> findItinerary(List<List<String>> tickets) {
    map = new HashMap<String, Map<String, Integer>>();
    res = new LinkedList<>();
    for(List<String> t : tickets){
        Map<String, Integer> temp;
        if(map.containsKey(t.get(0))){
            temp = map.get(t.get(0));
            temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1);
        }else{
            temp = new TreeMap<>();//升序Map
            temp.put(t.get(1), 1);
        }
        map.put(t.get(0), temp);

    }
    res.add("JFK");
    backTracking(tickets.size());
    return new ArrayList<>(res);
}

51. N 皇后

public List<List<String>> solveNQueens(int n) {
    backtracking(n);
    return res;
}

ArrayList<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
StringBuilder builder = new StringBuilder();

private void backtracking(int n) {
    if(path.size() == n) {
        res.add(new LinkedList<String>(path));
        return;
    }

    int layer = path.size(); // 当前层数

    // 根据前面若干层的位置,找出当前层有哪些位置是不能放的
    HashSet<Integer> invalid = new HashSet<Integer>();
    for(int i = 0; i < path.size(); i++) {
        // 根据字符串中Q的位置判断第i层的棋子位置
        int pos = 0;
        for(int j = 0; j < path.get(i).length(); j++) {
            if(path.get(i).charAt(j) == 'Q') {
                pos = j;
                break;
            }
        }

        // 根据第i层的棋子位置pos,找出哪些位置是当前层不能放的
        invalid.add(pos);
        if(pos - (layer - i) >= 0) {
            invalid.add(pos - (layer - i));
        }
        if(pos + (layer - i) < n) {
            invalid.add(pos + (layer - i));
        }
    }
	
    // 如果当前层所有位置都不能放,说明这种情况失败了,直接return
    if(invalid.size() >= n) {
        return;
    }

    builder.setLength(0);  // 清空字符串
    for(int i = 0; i < n; i++) {
        if(!invalid.contains(i)) {
            builder.append('Q');
            while(builder.length() < n) {
                builder.append('.');
            }
            path.add(builder.toString());
            backtracking(n);
            path.removeLast();
            builder.setLength(i);  // 切割字符串,回溯
        }
        builder.append('.');
    }
}

优化版:

List<List<String>> res = new ArrayList<>();

public List<List<String>> solveNQueens(int n) {
    char[][] chessboard = new char[n][n];
    for (char[] c : chessboard) {
        Arrays.fill(c, '.');
    }
    backTrack(n, 0, chessboard);
    return res;
}


public void backTrack(int n, int row, char[][] chessboard) {
    if (row == n) {
        res.add(Array2List(chessboard));
        return;
    }

    for (int col = 0;col < n; ++col) {
        if (isValid (row, col, n, chessboard)) {
            chessboard[row][col] = 'Q';
            backTrack(n, row+1, chessboard);
            chessboard[row][col] = '.';
        }
    }
}


public List Array2List(char[][] chessboard) {
    List<String> list = new ArrayList<>();
    for (char[] c : chessboard) {
        list.add(String.copyValueOf(c));
    }
    return list;
}


public boolean isValid(int row, int col, int n, char[][] chessboard) {
    // 检查列
    for (int i=0; i<row; ++i) { // 相当于剪枝
        if (chessboard[i][col] == 'Q') {
            return false;
        }
    }

    // 检查45度对角线
    for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
        if (chessboard[i][j] == 'Q') {
            return false;
        }
    }

    // 检查135度对角线
    for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
        if (chessboard[i][j] == 'Q') {
            return false;
        }
    }
    return true;
}

37. 解数独

public void solveSudoku(char[][] board) {
    backtracking(board, 0);
}

// 每次递归从row开始
private boolean backtracking(char[][] board, int row) {
    for(int i = row; i < 9; i++) {
        for(int j = 0; j < 9; j++) {
            // 结束条件:已经找到了一组可行解
            if(i == 8 && j == 8 && board[i][j] != '.') {
                return true;
            }
            if(board[i][j] == '.') {
                // 分别填入1-9
                for(char k = '1'; k <= '9'; k++) {
                    if(isValid(board, i, j, k)) {                            
                        board[i][j] = k;
                        if(backtracking(board, i)) {
                            return true;
                        }
                        board[i][j] = '.';
                    }
                }
                return false;  // 每次递归只填写一个数字
            }
        }
    }
    return false;
}

// 判断在[row][col]填写val是否可行
private boolean isValid(char[][] board, int row, int col, char val) {
    // 当前行不能有重复
    for(int j = 0; j < 9; j++) {
        if(board[row][j] == val) {
            return false;
        }
    }

    // 当前列不能有重复
    for(int i = 0; i < 9; i++) {
        if(board[i][col] == val) {
            return false;
        }
    }

    // 当前九宫格不能有重复
    int firstRow = row / 3 * 3;
    int firstCol = col / 3 * 3;
    for(int i = 0; i < 3; i++) {
        for(int j = 0; j < 3; j++) {
            if(board[firstRow+i][firstCol+j] == val) {
                return false;
            }
        }
    }

    return true;
}
posted @ 2021-12-22 11:01  lv6laserlotus  阅读(35)  评论(0)    收藏  举报