算法识途|回溯问题

题目

正文

1、 46. 全排列

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

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

    private void dfs(List<List<Integer>> res, Deque<Integer> path, int[] nums, boolean[] used) {
        if (nums.length == path.size()) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i])
                continue;
            used[i] = true;
            path.addFirst(nums[i]);
            dfs(res, path, nums, used);
            used[i] = false;
            path.removeFirst();
        }
    }

2、47. 全排列 II

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

public List<List<Integer>> permuteUnique(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    int len = nums.length;
    if (len == 0)
        return res;
    boolean[] used = new boolean[len];
    Arrays.sort(nums);
    dfs(res, new ArrayDeque<>(), nums, used);
    return res;
}

private void dfs(List<List<Integer>> res, Deque<Integer> path, int[] nums, boolean used[]) {
    if (path.size() == nums.length) {
        res.add(new ArrayList<>(path));
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        if (used[i])
            continue;
        if (i > 0 && !used[i - 1] && nums[i] == nums[i - 1])
            continue;
        path.push(nums[i]);
        used[i] = true;
        dfs(res, path, nums, used);
        used[i] = false;
        path.poll();
    }
}

我们对比46、47两道题目,发现题目的区别在于,题47中会出现重复数字,假设46就是朴素回溯的话,那么47就是需要加上判断条件的回溯。
所以,问题就在于,怎么样子在回溯过程中加条件,可以让重复数字不被使用,即【对于数组[1,1’,2]而言,[1,1’]和[1’,1]只有前者可以选取成功。】
关键代码在于i > 0 && !used[i - 1] && nums[i] == nums[i - 1],此处使用了反向思维,即所有重复数字序列[…,a,b,…],只有a已经在path中,才能使用b。

3、39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。

public List<List<Integer>> combinationSum(int[] c, int target) {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new ArrayDeque<>();
    Arrays.sort(c);
    dfs(res, path, c, target, 0);
    return res;
}
private void dfs(List<List<Integer>> res, Deque<Integer> path, int[] c, int curSum, int start) {
    if (curSum < 0) {
        // 没有解了
        return;
    }
    if (curSum == 0) {
        res.add(new ArrayList<>(path));
        return;
    }
    for (int i = start; i < c.length; i++) {
        curSum -= c[i];
        path.push(c[i]);
        dfs(res, path, c, curSum, i);
        path.poll();
        curSum += c[i];
    }
}

问题:这个start起到了什么作用?

什么时候使用 used 数组,什么时候使用 begin 变量?

有些朋友可能会疑惑什么时候使用 used 数组,什么时候使用 begin 变量。这里为大家简单总结一下:
排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组;
组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/combination-sum/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

4、40. 组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次
**注意:**解集不能包含重复的组合。

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList();
        int len = candidates.length;
        Deque<Integer> path = new ArrayDeque<>();
        Arrays.sort(candidates);
        dfs(res, candidates, path, target, 0);
        return res;
    }
    private void dfs(List<List<Integer>> res, int[] arr, Deque<Integer> path, int curSum, int start) {
        if (curSum < 0)
            return;
        if (curSum == 0) {
            res.add(new ArrayList(path));
            return;
        }
        for (int i = start; i < arr.length; i++) {
            if (i > start && arr[i] == arr[i - 1])
                continue;
            curSum -= arr[i];
            path.push(arr[i]);
            dfs(res, arr, path, curSum, i + 1);
            path.poll();
            curSum += arr[i];
        }
    }
}

不能选重复数字,加一层ab相同判断即可

5、77. 组合

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

public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new ArrayDeque();
    dfs(res, path, n, k, 1);
    return res;
}

private void dfs(List<List<Integer>> res, Deque<Integer> path, int n, int k, int start) {
    if (path.size() == k)
        res.add(new ArrayList(path));
    for (int i = start; i <= n; i++) {
        path.push(i);
        dfs(res, path, n, k, i + 1);
        path.poll();
    }
}

6、78. 子集

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

public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new ArrayDeque<>();
    dfs(res, path, 0, nums);
    return res;
}

private void dfs(List<List<Integer>> res, Deque<Integer> path, int start, int arr[]) {
    res.add(new ArrayList(path));
    for (int i = start; i < arr.length; i++) {
        path.push(arr[i]);
        dfs(res, path, i + 1, arr);
        path.poll();
    }
}

怎样选出边界条件?
不用选,可以发现,这个子集记录的就是回溯的过程,直接放入容器即可

7、90. 子集 II

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

public List<List<Integer>> subsetsWithDup(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new ArrayDeque<>();
    Arrays.sort(nums);
    dfs(res, path, 0, nums);
    return res;
}

private void dfs(List<List<Integer>> res, Deque<Integer> path, int start, int arr[]) {
    res.add(new ArrayList(path));
    for (int i = start; i < arr.length; i++) {
        if (i > start && arr[i] == arr[i - 1])
            continue;
        path.push(arr[i]);
        dfs(res, path, i + 1, arr);
        path.poll();
    }
}

8、60. 排列序列

给出集合 [1,2,3,...,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

  1. "123"
  2. "132"
  3. "213"
  4. "231"
  5. "312"
  6. "321"
    给定 nk,返回第 k 个排列。
public String getPermutation(int n, int k) {
    int[] factorial = new int[n + 1];
    factorial[0] = 1;
    for (int i = 1; i <= n; i++) {
        factorial[i] = i * factorial[i - 1];
    }
    List<Integer> numList = new ArrayList<>();
    for (int i = 1; i <= n; i++)
        numList.add(i);
    StringBuilder res = new StringBuilder();
    k--;
    for (int i = n - 1; i >= 0; i--) {
        int index = k / factorial[i];
        res.append(numList.remove(index));
        k -= index * factorial[i];
    }
    return res.toString();
}

其实是数学题。

9、93. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
    给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你不能重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
public List<String> restoreIpAddresses(String s) {
    List<String> res = new ArrayList();
    if (s.length() < 4 || s.length() > 12)
        return res;
    int len = s.length();
    Deque<String> path = new ArrayDeque();
    dfs(s, res, 0, 0, path, len);
    return res;
}

private int validateIP(String s, int left, int right) {
    int len = right - left + 1;
    if (len > 1 && s.charAt(left) == '0')
        return -1;
    if (right >= s.length())
        return -1;
    int res = 0;
    for (int i = left; i <= right; i++) {
        res = res * 10 + s.charAt(i) - '0';
    }
    if (res > 255)
        return -1;
    return res;
}

private void dfs(String s, List<String> res, int begin, int splitTimes, Deque<String> path, int len) {
    if (begin == len) {
        if (splitTimes == 4) {
            res.add(String.join(".", path));
        }
        return;
    }
    if (len - begin > (4 - splitTimes) * 3 || len - begin < 4 - splitTimes)
        return;
    for (int i = 0; i < 3; i++) {
        int cur = validateIP(s, begin, begin + i);
        if (cur == -1)
            break;
        path.addLast(cur + "");
        splitTimes++;
        dfs(s, res, begin + i + 1, splitTimes, path, len);
        path.removeLast();
        splitTimes--;
    }
}

算是系列里面较为复杂的题目,之前固有的模型是不不好做的。
思路:
1、如何回溯:从0到2,作为从当前位置开始选取数字的step
2、判断条件:多个数字不能有前导0
3、剪枝:当后面的数字不够或者过多的时候
4、模拟结构问题,使用Deque的数据结构,可以直接用String.join(".",path)拼接

总结

回溯题掌握:
1、排列OR组合?
排列需要记住数字是否使用过【used[i]】
组合需要保留回溯的位置
2、是否需要排除重复?
中途添加i > 0 && arr[i] > arr[i - 1]的判断

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值