leecode学习笔记-回溯的去重(组合排列问题)

回溯可以看成是一个树的结构,for循环是树层(即树的宽度遍历),递归函数是进行树枝(即树的深度遍历)。如果给的数组有重复的数据,那么就会涉及到去重。
树层去重代码:为了防止结果上同一个位置的值相同,这样可能会造成相同的结果[[1,1],[1,1]],树层表示的就是数组当前位置的值,树枝就是沿着数组往后走。

    if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {//事先需要对数组进行排序
        continue;
    }

树枝去重:防止得到的结果中存在重复[1,1,2]

    if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == true) {//事先需要对数组进行排序
        continue;
    }

组合总和(无重复)

class Solution {
class Solution {
    
    List<List<Integer>> res = new LinkedList<>();
    int t;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        t = target;
        List<Integer> path = new LinkedList<>();
        backTracking(0, candidates, path, 0);
        return res;

    }
    void backTracking(int startIndex, int[] nums, List<Integer> path, int sum) {
        if (sum > t) return;
        if (sum == t) {
            res.add(new LinkedList<>(path));
            return;
        }


        for (int i = startIndex; i < nums.length; i++) {
            path.add(nums[i]);
            sum += nums[i];
            backTracking(i, nums, path, sum);//这里不用i + 1,因此此题要求,每个元素可以无限次数使用
            sum -= nums[i];
            path.remove(path.size() - 1);
        }

    }

}

组合总和(有重复)

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    List<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        boolean[] used = new boolean[candidates.length];
        backTracking(0, 0, candidates, used, target);
        return res;
    }

    //确定 回溯函数返回值 
    void backTracking(int sum, int startIndex, int[] nums, boolean[] used, int target) {
        //确定终止条件
        if (sum > target) return;
        if (sum == target) {
            res.add(new LinkedList<>(path));
            return;
        }

        for (int i = startIndex; i < nums.length && sum + nums[i] <= target; i++) {
            
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
                continue;
            }

            sum += nums[i];
            path.add(nums[i]);
            used[i] = true;
            backTracking(sum, i + 1, nums, used, target); 
            sum -= nums[i];
            used[i] = false;
            path.remove(path.size() - 1);
        }
    }
}

全排列(无重复)

全排列和组合的区别就是,每次startIndex从0开始,并且用used[i]记录某个数据是否北方问过,如果访问过就跳过

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    List<Integer> path = new LinkedList<>(); 
    public List<List<Integer>> permute(int[] nums) {
        boolean[] used = new boolean[nums.length];
        backTracking(nums, used);
        return res;
    }

    void backTracking(int[] nums, boolean[] used) {
        if (nums.length == path.size()) {
            res.add(new LinkedList(path));
            return;
        }


        for (int i = 0; i < nums.length; i++) {
            if (used[i] == true) continue;
            used[i] = true;
            path.add(nums[i]);
            backTracking(nums, used);
            path.remove(path.size() - 1);
            used[i] = false;
        }
        
    }


}

全排列有重复

class Solution {
    List<Integer> path = new ArrayList<>();
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        boolean[] used = new boolean[nums.length];
        backTracking(nums, used);
        return res;
    }
    //确定回溯函数的返回值 和 待定参数 
    void backTracking(int[] nums, boolean[] used) {
        //确定终止条件
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            //去重
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue; //树层去重

            if (used[i] == false) {
                used[i] = true;
                path.add(nums[i]);
                backTracking(nums, used);
                path.remove(path.size() - 1);
                used[i] = false;
            }
        }
    }
}

分割问题

分割问题和组合问题相似,不同的地方在于分割问题有一个分割点起始点问题,即startIndex会作为分割起始点,然后遍历的值假设x,那么x-startIndex这一段就是我们要分割的目标,递归函数就是在剩余的数据中找新的起始分割点。而组合就是每次只选取1个,for循环的作用,就是不断改变当前位置的值。

class Solution {
    List<String> path = new LinkedList<>();
    List<List<String>> res = new LinkedList<>();
    public List<List<String>> partition(String s) {
            backTracking(s, 0);
            return res;
    }

    //确定 回溯函数的返回值
    void backTracking(String s, int startIndex) {
        //确定终止条件
        if (startIndex >= s.length()) {
            res.add(new LinkedList<>(path));
            return;
        }

        for (int i = startIndex; i < s.length(); i++) {
            if (isPalindrome(s, startIndex, i) == true) {
                path.add(s.substring(startIndex, i + 1));
            } else {
                continue;
            }

            backTracking(s, i + 1);
            path.remove(path.size() - 1);
        }
    }

    boolean isPalindrome(String s, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            if (s.charAt(i) != s.charAt(j)) return false; 
        }
        return true;
    }

}

复原Ip(分割)

class Solution {
    List<String> res = new LinkedList<>();
    public List<String> restoreIpAddresses(String s) {
        if (s == null || s.length() == 0) return res;
        StringBuilder sb = new StringBuilder(s);
        backTracking(sb, 0, 0);
        return res;
    }
    //用"."分割IP串
    void backTracking(StringBuilder sb, int startIndex, int sum) {
        if (sum == 3) {
            if (checkIp(sb, startIndex, sb.length() - 1)) {
                res.add( sb.toString());
                return;
            }
        }

        for (int i = startIndex; i < sb.length(); i++) {
            if (checkIp(sb, startIndex, i)) {
                sb.insert(i + 1, ".");
                sum++;
                backTracking(sb, i + 2, sum);
                sum--;
                sb.deleteCharAt(i + 1);
            } else {
                break;  //一旦不满足Ip说明在往后走还是会大于255,所以用break
            }
        }
    }

    boolean checkIp(StringBuilder sb, int start, int end) {
        if (start > end) 
            return false;
        
        if (sb.charAt(start) == '0' && start != end) 
            return false;
        int num = 0;
        for (int i = start; i <= end; i++) {
            if (!Character.isDigit(sb.charAt(i)))
                return false;
            num = num * 10 + sb.charAt(i) - '0';
            if (num > 255) 
                return false;
        }
        return true; 
    }


}

深搜和回溯

由从新安排行程问题有以下思考,重新安排行程就是每一个起点对应一个集合,集合中的每个地点又对应另一个集合,所以可以看成是一个深搜问题,但是深搜问题也可以用回溯的方式写,因为深搜的实现本身就是回溯。
个人的理解:回溯是一种思想,深搜是一种搜索方式,深搜的实现用到了回溯的思想,对于一些组合,排序,分割数组,子集问题都有自己对应的模板,相当于直接利用回溯思想解题,这种问题往往是从集合中选元素的问题,他们还不能称为深搜,只是用回溯的思想解题。回溯问题可以看成是一个树的枝干问题。深搜也是当前的位置到下一个位置有几条路径,然后分别去走这几条路,达到条件就回退,这里就隐含回溯的意思,并且一般要带一些变量来判读是否经过这个位置的变量,这个在回溯解题问题中也存在(去重,树层去重,树枝去重,排列问题防止重复元素)。

用不用startIndex?

startIndex:代码随想录中说对于组合问题,一个集合就要有startIndex,不同集合就不用startIndex(手机号码问题,不同数字对应不同的字母集合)。自己想了想,说的真对。。。。,对于排列问题,每次选的时候,所有的元素都可以选,就可以等价于,每次都是新的集合和之前不相关,只是随着树枝的深入,判断某个值是否取过就可以(不同于树层去重和树枝去重,因为排列组合也可以出现[1,1,2]的情况)。此外,还有N皇后问题,看着是棋盘问题,但是解决的思路确是确实从每一层进行深入,每一层的数据都可以看成是不同的集合,所以每次层的树层遍历都是i=0开始的。

数读

一个纸老虎题

class Solution {
    /**
        1. 棋盘格本身就是去重的作用,以及本身就是一个结束条件(都满了)
        2. 每次递归时,就要判断哪个棋盘格是空的,虽然每次进函数都是一个双重遍历,但是也只是找到下一个“落脚点”而已,不要怕!
        3. 在二位遍历中如果遇到可以落脚的“.”处,就遍历1-9,看哪个可以放到当前位置,如果都不能说明,当前的方案不行,就要return false。
        4.如果能就一直递归,当棋盘格满了以后,其实就自动出来了,return true
        5. 总之很巧妙。。。和之前的套路都不太一样,外面的两层for循环是纸老虎,实际干活的是里面的1-9的遍历递归,这里是符合回溯的套路的。
     */
    public void solveSudoku(char[][] board) {
        backTracking(board);
    }


    boolean backTracking(char[][] board) {
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                if (board[i][j] != '.') //这里如果棋盘满了,那就结束,相当于结束条件
                    continue;
                for (char k = '1'; k <= '9'; k++) {
                    if (isValid(i, j, board, k)) 
                        {
                            board[i][j] = k;
                            if (backTracking(board)) return true;
                            board[i][j] = '.';
                        }
                }
                return false;//这里就是如果当前的位置从1-9都不满足条件,那么就需要回退了所以返回false
            }
        }
        return true;
    }
    

    boolean isValid(int row, int col, char[][] board, char k) {
        //检查行 
        for (int i = 0; i < 9; i++) {
            if (board[row][i] == k) 
                return false;
        }
        //检查列
        for (int i = 0; i < 9; i++) {
            if (board[i][col] == k) 
                return false;
        }

        //检查九宫格
        
        int startRow = (row / 3) * 3;
        int startCol = (col / 3) * 3;
        for (int i = startRow; i < startRow + 3; i++) {
            for (int j = startCol; j < startCol + 3; j++) {
                if (board[i][j] == k) 
                    return false;
            }
        }
        return true;
    }

    

    
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值