回溯算法基础

回溯算法理论基础

1、什么是回溯法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。

回溯是递归的副产品,只要有递归就会有回溯。

回溯函数也就是递归函数,指的都是一个函数

2、回溯法的效率

回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

3、回溯法解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

4、回溯的模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

组合问题

1、77. 组合

题意:给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

思路:回溯法

  • 递归函数返回值和参数,返回值为空,参数传入n和k,以及每次递归开始位置startIndex,存储过程的集合path、存储结果的集合result
void combine(int n, int k, int startIndex, List<Integer> path, List<List<Integer>> result) 
  • 确定终止条件,当过程集合path的大小等于k时,就可以终止,并将过程集合存储到结果集合中
if (path.size() == k) {
    result.add(new ArrayList<>(path));
    return;
}
  • 确定单次递归的条件
    • for循环条件,i从startIndex开始,当i小于等于n时停止
    • 循环体中,首先将当前i存储到过程集合中,然后递归下一行,递归结束后移除过程集合中的值
  • 剪枝优化:
    • 剪枝优化主要在for循环的终止条件上,比如说4,4,第一层只能等于1,再往后遍历没有意义。
    • 因此可以将for循环的终止条件修改为i <= n - (k - path.size()) + 1
    • image-20220609085756954
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
    path.add(i);
    combine(n, k, startIndex + 1, path, result);
    path.remove(path.size() - 1);
}

整体代码如下:

package com.yzu.lee.recall;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName: Combine
 * @Description:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
 * @author: Leekuangyew
 * @date: 2022/6/2 20:13
 */
public class Combine {

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

    private void combine(int n, int k, int startIndex, List<Integer> path, List<List<Integer>> result) {
        if (path.size() == k) {
            result.add(new ArrayList<>(path));
            return;
        }

        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
            path.add(i);
            combine(n, k, startIndex + 1, path, result);
            path.remove(path.size() - 1);
        }
    }
}

2、216.组合总和III

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

说明:

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

示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]

示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]

思路:回溯法

  • 确定递归函数返回值和参数,参数传入k和n,传入count用来记录当前值是否已经满足和为n的要求,startIndex为每次递归开始的条件,路径集合path和结果集合result
void combinationSum3(int k, int n, int count, int startIndex, List<Integer> path, List<List<Integer>> result)
  • 确定递归结束的条件,如果当前路径集合的大小等于k,则执行判断count是否等于0,如果等于0,则将路径集合传入到结果集合中,最终返回
if (path.size() == k) {
    if (count == 0) {
        result.add(new ArrayList<>(path));
    }
    return;
}
  • 确定单次递归逻辑,当i小于等于9时,一直循环遍历递归
for (int i = startIndex; i <= 9; i++) {
    path.add(i);
    count -= i;
    combinationSum3(k, n, count, i + 1, path, result);
    path.remove(path.size() - 1);
    count += i;
}
  • 剪枝优化,主要有两处,一处为循环结束条件,当i <= 9 - (k - path.size()) + 1时就结束循环,第二处在循环体中判断count-i如果小于0,则该数层不再继续往后遍历,也不再加树枝
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
    if(count - i < 0) break;
    path.add(i);
    count -= i;
    System.out.println(count);
    combinationSum3(k, n, count, i + 1, path, result);
    path.remove(path.size() - 1);
    count += i;
}

17.电话号码的字母组合

题意:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

image-20220609093505778

示例: 输入:“23” 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].

说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

思路:回溯法

  • 数字与字母的映射关系
String[] strings = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
  • 确定递归函数的返回值和参数,参数传过来按键数字字符串,按键数字与字符串的映射关系数组,以及num用于记录已经统计了几个路径
void letterCombinations(String digits, String[] strings, int num)
  • 确定终止条件,如果按键数字字符串的长度与num相等时,将字符串存入结果集合中
if (digits.length() == num) {
    result.add(sb.toString());
    return;
}
  • 确定单次递归的逻辑,首先根据题目的按键字符串在num位置的字符-’0‘,根据这个值作为索引,去找到映射数组中对应的字符串,然后遍历字符串,递归时需要将num+1;
String string = strings[digits.charAt(num) - '0'];
for (int i = 0; i < string.length(); i++) {
    sb.append(string.charAt(i));
    letterCombinations(digits, strings, num + 1);
    sb.deleteCharAt(sb.length() - 1);
}

整体代码如下:

package com.yzu.lee.recall;

import com.sun.org.apache.bcel.internal.generic.NEW;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName: LetterCombinations
 * @Description:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
 * 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
 * @author: Leekuangyew
 * @date: 2022/6/5 15:05
 */
public class LetterCombinations {
    List<String> result = new ArrayList<>();
    StringBuilder sb = new StringBuilder();

    public List<String> letterCombinations(String digits) {
        if (digits.length() == 0) return result;

        String[] strings = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        letterCombinations(digits, strings, 0);
        return result;
    }

    private void letterCombinations(String digits, String[] strings, int num) {
        if (digits.length() == num) {
            result.add(sb.toString());
            return;
        }

        String string = strings[digits.charAt(num) - '0'];
        for (int i = 0; i < string.length(); i++) {
            sb.append(string.charAt(i));
            letterCombinations(digits, strings, num + 1);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

39. 组合总和

题意:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

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

示例 1: 输入:candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]

示例 2: 输入:candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]

思路:回溯法

  • 确定递归函数返回值和参数,参数传入candidates和target,传入中间集合list和结果集合result,起始位置startIndex
void combinationSum(int[] candidates, int target, List<Integer> list, List<List<Integer>> result, int startIndex)
  • 确定终止条件,当target等于0时,将中间集合存入到结果集合中,并直接返回
if (target == 0) {
    result.add(new ArrayList<>(list));
    return;
}
  • 确定单次递归的逻辑,由于同一个元素可以重复使用,所以递归时还是传入i,剪枝优化首先对数组进行排序,然后当taeget - candidates[i] < 0时,直接break,即该树层不再继续往后遍历,也不再往下遍历
for (int i = startIndex; i < candidates.length ; i++) {
    if(taeget - candidates[i] < 0) break;
    target -= candidates[i];//剪枝优化
    list.add(candidates[i]);
    combinationSum(candidates, target, list, result, i);
    list.remove(list.size() - 1);
    target += candidates[i];
}

整体代码如下:

package com.yzu.lee.recall;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @ClassName: CombinationSum
 * @Description:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
 * candidates 中的数字可以无限制重复被选取。
 * @author: Leekuangyew
 * @date: 2022/6/6  15:55
 */
public class CombinationSum {

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        Arrays.sort(candidates);//剪枝优化
        combinationSum(candidates, target, list, result, 0);
        return result;
    }

    private void combinationSum(int[] candidates, int target, List<Integer> list, List<List<Integer>> result, int startIndex) {
        if (target < 0) return;

        if (target == 0) {
            result.add(new ArrayList<>(list));
            return;
        }

        for (int i = startIndex; i < candidates.length ; i++) {
            target -= candidates[i];//剪枝优化
            list.add(candidates[i]);
            combinationSum(candidates, target, list, result, i);
            list.remove(list.size() - 1);
            target += candidates[i];
        }
    }
}

40.组合总和II

题意:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明: 所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。

示例 1: 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]

示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]

思路:回溯法

  • 确定递归函数返回值和参数,参数传入candidates和target,每次递归开始位置startIndex,中间集合list和结果集合result
void combinationSum2(int[] candidates, int target, int startIndex, List<Integer> list, List<List<Integer>> result)
  • 确定终止条件,如果target等于0,将中间集合添加到结果集合中
if (target == 0) {
    result.add(new ArrayList<>(list));
    return;
}
  • 确定单次递归的逻辑
    • 正常for循环,终止条件为i < candidates.length,i从startIndex开始
    • 剪枝优化,如果target - candidates[i] < 0,直接break
    • 剪枝优化,如果i > startIndex && candidates[i] == candidates[i - 1],直接continue
    • 接下来就是正常的递归回溯
for (int i = startIndex; i < candidates.length; i++) {
    if (target - candidates[i] < 0) break;
    if (i > startIndex && candidates[i] == candidates[i - 1]) continue;
    target -= candidates[i];
    list.add(candidates[i]);
    combinationSum2(candidates, target, i + 1, list, result);
    target += candidates[i];
    list.remove(list.size() - 1);
}

整体代码如下:

package com.yzu.lee.recall;

import com.sun.crypto.provider.PBEWithMD5AndDESCipher;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @ClassName: CombinationSum2
 * @Description:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
 * candidates 中的每个数字在每个组合中只能使用一次。
 * @author: Leekuangyew
 * @date: 2022/6/6  17:49
 */
public class CombinationSum2 {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        Arrays.sort(candidates);
        combinationSum2(candidates, target, 0, list, result);
        return result;
    }

    private void combinationSum2(int[] candidates, int target, int startIndex, List<Integer> list, List<List<Integer>> result) {
        if (target < 0) return;
        if (target == 0) {
            result.add(new ArrayList<>(list));
            return;
        }

        for (int i = startIndex; i < candidates.length; i++) {
            if (target - candidates[i] < 0) break;
            if (i > startIndex && candidates[i] == candidates[i - 1]) continue;
            target -= candidates[i];
            list.add(candidates[i]);
            combinationSum2(candidates, target, i + 1, list, result);
            target += candidates[i];
            list.remove(list.size() - 1);
        }
    }
}

重点::使用used数组进行剪枝操作

image-20220609144516067

分割问题

131.分割回文串

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]

思路:回溯法

  • 确定递归函数返回值和参数,参数传入s,递归起始位置startIndex,中间集合list和结果集合result
void partition(String s, int startIndex, List<String> list, List<List<String>> result)
  • 确定终止条件,如果起始位置大于等于字符串s的长度,就直接将中间集合添加到结果中
if (startIndex >= s.length()) {
    result.add(new ArrayList<>(list));
    return;
}
  • 确定单次递归的逻辑
    • 正常for循环,开始i = startIndex,循环结束条件 i < s.length()
    • 判断s的startIndex到i之间是否为回文字串,如果是,则将字符串截断存放到中间集合中,否则直接continue
    • 接下来就是正常的递归
for (int i = startIndex; i < s.length(); i++) {
    if (isPalindrome(s, startIndex, i)) {
        String substring = s.substring(startIndex, i + 1);
        list.add(new String(substring));
    } else continue;
    System.out.println(list);
    partition(s, i + 1, list, result);
    list.remove(list.size() - 1);
}
  • 判断是否为回文子串,利用双指针法进行判断
private boolean isPalindrome(String s, int startIndex, int endIndex) {
    for (int i = startIndex, j = endIndex; i < j; i++, j--) {
        if (s.charAt(i) != s.charAt(j)) return false;
    }
    return true;
}

整体代码如下:

package com.yzu.lee.recall;

import org.hamcrest.core.Is;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName: Partition
 * @Description:给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
 * 返回 s 所有可能的分割方案。
 * @author: Leekuangyew
 * @date: 2022/6/6  18:16
 */
public class Partition {
    public List<List<String>> partition(String s) {
        List<List<String>> result = new ArrayList<>();
        List<String> list = new ArrayList<>();
        partition(s, 0, list, result);
        return result;
    }

    private void partition(String s, int startIndex, List<String> list, List<List<String>> result) {
        if (startIndex >= s.length()) {
            result.add(new ArrayList<>(list));
            return;
        }

        for (int i = startIndex; i < s.length(); i++) {
            if (isPalindrome(s, startIndex, i)) {
                String substring = s.substring(startIndex, i + 1);
                list.add(new String(substring));
            } else continue;
            System.out.println(list);
            partition(s, i + 1, list, result);
            list.remove(list.size() - 1);
        }

    }

    private boolean isPalindrome(String s, int startIndex, int endIndex) {
        for (int i = startIndex, j = endIndex; i < j; i++, j--) {
            System.out.println(j);
            if (s.charAt(i) != s.charAt(j)) return false;
        }
        return true;
    }
}

93.复原IP地址

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。

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

例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效的 IP 地址。

示例 1:

  • 输入:s = “25525511135”
  • 输出:[“255.255.11.135”,“255.255.111.35”]

示例 2:

  • 输入:s = “0000”
  • 输出:[“0.0.0.0”]

思路:回溯法

  • 确定递归参数的返回值和参数,参数传入s,以及递归起始位置startIndex,中间集合result和小数点数量pointNum
void backtracking(String s, int startIndex, List<String> result, int pointNum)
  • 确定终止条件,当小数点数量等于3时,判断字符串s的startIndex到s.length() - 1之间是否合法,合法的话就添加到中间节点,并直接返回
if (pointNum == 3) {
    if (isValid(s, startIndex, s.length() - 1)) {
        result.add(s);
    }
    return;
}
  • 确定单次递归的逻辑
    • 正常循环递归
    • 如果当前截取的一部分合法,就直接对s进行处理,截取0到i+1的部分并拼接小数点,和s剩下来的部分
    • 递归时需要将i+2
    • 回溯时记得删除小数点
    • 如果不合法,就直接break
for (int i = startIndex; i < s.length(); i++) {
    if (isValid(s, startIndex, i)) {
        s = s.substring(0, i + 1) + "." + s.substring(i + 1);
        System.out.println(s);
        pointNum++;
        backtracking(s, i + 2, result, pointNum);
        pointNum--;
        s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯删掉逗点
    } else break;
}
  • 判断当前截取的字符串是否满足条件
    • 如果startIndex大于endIndex,直接返回false
    • 如果startIndex不等于endIndex并且startIndex位置的字符串等于0,直接返回false
    • startIndex, endIndex + 1截取字符串
    • 如果这个ip范围在合法范围就直接返回true
private boolean isValid(String s, int startIndex, int endIndex) {
        if (startIndex > endIndex) return false;
        if (startIndex != endIndex && s.charAt(startIndex) == '0') return false;

        String substring = s.substring(startIndex, endIndex + 1);
//      int ip = Integer.parseInt(substring);
        long ip = Long.parseLong(substring);
        if (ip >= 0 && ip <= 255) return true;
        else return false;
    }

子集问题

78.子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]

思路:回溯法

  • 确定递归函数的返回值和参数,参数传入nums,递归起始位置startIndex,中间集合path和结果结合result
void backtracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> result)
  • 确定终止条件,不需要终止条件,循环结束就可以
  • 确定单次递归的逻辑
    • 每次递归开始,首先将中间集合添加到结果集合中
    • for循环中,正常递归回溯
result.add(new ArrayList<>(path));


for (int i = startIndex; i < nums.length; i++) {
    path.add(nums[i]);
    backtracking(nums, i + 1, path, result);
    path.remove(path.size() - 1);
}

整体代码如下:

package com.yzu.lee.recall;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName: Subsets
 * @Description:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
 * 说明:解集不能包含重复的子集。
 * @author: Leekuangyew
 * @date: 2022/6/6  21:39
 */
public class Subsets {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        if (nums.length == 0) return result;
        List<Integer> path = new ArrayList<>();
        backtracking(nums, 0, path, result);
        return result;
    }

    private void backtracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> result) {

        result.add(new ArrayList<>(path));


        for (int i = startIndex; i < nums.length; i++) {
            path.add(nums[i]);
            backtracking(nums, i + 1, path, result);
            path.remove(path.size() - 1);
        }
    }
}

90.子集II

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

  • 输入: [1,2,2]
  • 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]

思路:回溯法

  • 确定递归函数的返回值和参数
void backtracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> result, boolean[] used)
  • 确定终止条件,其实不需要
if (startIndex >= nums.length) return;
  • 确定单次递归的逻辑
    • 与上一题类似
    • 增加了去重的过程
      • i > 0 && nums[i] == nums[i - 1] && !used[i - 1]时,直接continue
result.add(new ArrayList<>(path));

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

排列问题

46.全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

  • 输入: [1,2,3]
  • 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

思路:回溯法

  • 确定递归函数的返回值和参数
void backtracking(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result)
  • 确定终止条件,中间集合的大小等于数组的大小,就将中间集合添加到结果集合中,并直接返回
if (path.size() == nums.length) {
    result.add(new ArrayList<>(path));
    return;
}
  • 确定单次递归的逻辑
    • for循环开始,起始位置都是0,终止条件都是i < nums.length
    • 如果used[i]为真,则直接continue,为了给同一个树枝进行去重
    • 其余zjhe
for (int i = 0; i < nums.length; i++) {
    if (used[i]) continue;
    used[i] = true;
    path.add(nums[i]);
    backtracking(nums, used, path, result);
    path.remove(path.size() - 1);
    used[i] = false;
}

整体代码如下:

package com.yzu.lee.recall;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName: Permute
 * @Description:给定一个 没有重复 数字的序列,返回其所有可能的全排列。
 * 示例:
 * 输入: [1,2,3]
 * 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
 * @author: Leekuangyew
 * @date: 2022/6/8 9:43
 */
public class Permute {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        if (nums.length == 0) return result;
        List<Integer> path = new ArrayList<>();
        boolean[] used = new boolean[nums.length];
        backtracking(nums, used, path, result);
        return result;
    }

    private void backtracking(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result) {
        if (path.size() == nums.length) {
            result.add(new ArrayList<>(path));
            return;
        }

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

47.全排列 II

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

示例 1:

  • 输入:nums = [1,1,2]
  • 输出: [[1,1,2], [1,2,1], [2,1,1]]

思路:回溯法

  • 确定递归函数的返回值和参数
void backtracking(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result)
  • 确定终止条件
if (path.size() == nums.length) {
    result.add(new ArrayList<>(path));
}
  • 确定单次递归的逻辑
    • 同一树层去重,if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
    • 同一树枝去重,if (used[i]) continue;
for (int i = 0; i < nums.length; i++) {
    if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
    if (used[i]) continue;
    path.add(nums[i]);
    used[i] = true;
    backtracking(nums, used, path, result);
    path.remove(path.size() - 1);
    used[i] = false;
}

整体代码如下:

package com.yzu.lee.recall;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @ClassName: PermuteUnique
 * @Description:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
 * @author: Leekuangyew
 * @date: 2022/6/8  11:49
 */
public class PermuteUnique {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        if (nums.length == 0) return result;
        List<Integer> path = new ArrayList<>();
        boolean[] used = new boolean[nums.length];
        Arrays.sort(nums);
        backtracking(nums, used, path, result);
        return result;

    }

    private void backtracking(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result) {
        if (path.size() == nums.length) {
            result.add(new ArrayList<>(path));
        }

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

其他问题

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

示例:

  • 输入: [4, 6, 7, 7]
  • 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

说明:

  • 给定数组的长度不会超过15。
  • 数组中的整数范围是 [-100,100]。
  • 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

思路:回溯法

  • 确定递归函数的返回值和参数
void backtracking(int[] nums, int startIndex, List<Integer> path, List<List<Integer>> result)
  • 确定终止条件,当中间集合的大小大于等于2时,将中间集合添加到结果集合中
if (path.size() >= 2) {
    result.add(new ArrayList<>(path));
}
  • 确定单次递归的逻辑
    • 每层新建一个map
    • 如果!path.isEmpty() && path.get(path.size() - 1) > nums[i],直接continue
    • 如果map.get(nums[i]) != null && map.get(nums[i]) >= 1,说明同一树层已经有了,直接continue
Map<Integer, Integer> map = new HashMap<>();
for (int i = startIndex; i < nums.length; i++) {
    if (!path.isEmpty() && path.get(path.size() - 1) > nums[i]) continue;
    //因为数组不是递增的,所以不能使用这种方式直接判断
    //if (i>startIndex && nums[i] == nums[i-1]) continue;
    if (map.get(nums[i]) != null && map.get(nums[i]) >= 1) continue;
    map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
    path.add(nums[i]);
    backtracking(nums, i + 1, path, result);
    path.remove(path.size() - 1);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leekuangyee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值