摘在前面(作者:liweiwei1419):
回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
1.找到一个可能存在的正确的答案;
2.在尝试了所有可能的分步方法后宣告该问题没有答案。
其实应该按照:
这个顺序来学习:
这篇涉及到去重的题目要注意:
详细地看看随想录这里的解读:代码随想录 (programmercarl.com)
1.46. 全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2:
输入:nums = [0,1] 输出:[[0,1],[1,0]]示例 3:
输入:nums = [1] 输出:[[1]]提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums
中的所有整数 互不相同
思路:
代码:
class Solution {
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
// 使用一个动态数组保存所有可能的全排列
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
boolean[] used = new boolean[len];
Deque<Integer> path = new ArrayDeque<>(len);
dfs(nums, len, 0, path, used, res);
return res;
}
private void dfs(int[] nums, int len, int depth,
Deque<Integer> path, boolean[] used,
List<List<Integer>> res) {
if (depth == len) {//全程path只有一份,因此需要在这里做一次拷贝添加进去
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < len; i++) {
if (!used[i]) {//如果没有访问过
path.addLast(nums[i]);
used[i] = true;
//递归之前
dfs(nums, len, depth + 1, path, used, res);
//回退以后,清除位置
used[i] = false;
//去除刚刚加进来的最后一个元素
path.removeLast();
//递归之后
}
}
}
}
全排列扩展:47全排列Ⅱ
题目:
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]示例 2:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
思考:
这里因为是排列,就涉及到去重,还要再强调一遍:“去重一定要先排序”;
下文的for循环内部呢,就是同一层的选择,back(nums,path,res,used)那一块则是对下一层的遍历,而本问题的去重则是对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。
!!!
至于代码中提到的used[i-1]或者not used[i-1]都是可以的,是因为这是两种考虑方式,就是去对树枝进行分析,还是对数层进行去重,但无论如何,都需要保持一致性,不能一会是true,一会是false,这样是不可以的,详细见随想录:代码随想录 (programmercarl.com)
代码:
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
used = [False]*9
def back(nums,path,res,used):
if len(path)==len(nums):
res.append(path[:])
return
for i in range(len(nums)):
if i>0 and nums[i]==nums[i-1] and used[i-1]:
#去重的关键步骤,其实这里,not used[i-1]也可以过
# used[i - 1] == true,说明同一树枝nums[i - 1]使用过
# used[i - 1] == false,说明同一树层nums[i - 1]使用过
# 如果同一树层nums[i - 1]使用过则直接跳过
continue
if not used[i]:
used[i]=True
path.append(nums[i])
back(nums,path,res,used)
path.pop()
used[i] = False
nums.sort()
back(nums,path,res,used)
return res
2.78. 子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]示例 2:
输入:nums = [0] 输出:[[],[0]]提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums
中的所有元素 互不相同
思路:
代码:
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
public List<List<Integer>> subsets(int[] nums) {
subsetsHelper(nums, 0);
return result;
}
private void subsetsHelper(int[] nums, int startIndex){
result.add(new ArrayList<>(path));//「遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合」。
if (startIndex >= nums.length){ //终止条件可不加
return;
}
for (int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
subsetsHelper(nums, i + 1);
path.removeLast();
}
}
}
3.17. 电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]示例 2:
输入:digits = "" 输出:[]示例 3:
输入:digits = "2" 输出:["a","b","c"]提示:
0 <= digits.length <= 4
digits[i]
是范围['2', '9']
的一个数字。
思路:
- 这是个排列组合问题,可以用树的形式表示出来;
- 当给定了输入字符串,比如:"23",那么整棵树就构建完成了,如下:
代码:
class Solution {
List<String> result = new ArrayList<>();
StringBuilder temp = new StringBuilder();
Map<Character, List<Character>> map = new HashMap<Character, List<Character>>() {
{
put('2', Arrays.asList('a', 'b', 'c'));
put('3', Arrays.asList('d', 'e', 'f'));
put('4', Arrays.asList('g', 'h', 'i'));
put('5', Arrays.asList('j', 'k', 'l'));
put('6', Arrays.asList('m', 'n', 'o'));
put('7', Arrays.asList('p', 'q', 'r', 's'));
put('8', Arrays.asList('t', 'u', 'v'));
put('9', Arrays.asList('w', 'x', 'y', 'z'));
}
};
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return result;
}
backTracking(digits, 0);
return result;
}
private void backTracking(String digits, int num) {
if (digits.length() == num) {
result.add(temp.toString());
return;
}
List<Character> list = map.get(digits.charAt(num));
if (list == null) {
return;
}
for (int i = 0; i < list.size(); i++) {
Character cur = list.get(i);
temp.append(cur);
backTracking(digits, num + 1);
temp.deleteCharAt(num);
}
}
}
用熟悉的套路来完成:
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
dict1={2:"abc",3:"def",4:"ghi",5:"jkl",6:"mno",7:"pqrs",8:"tuv",9:"wxyz"}
k=len(digits)
res=[]
path=[]
def back(digits,index):#参数里有一个是必须的,那就是比对值,比如总和、拨号键的内容
if index==len(digits):
res.append(''.join(path))
return
#终止条件
digit = ord(digits[index]) - ord('0')
letters = dict1[digit]
for i in range(len(letters)):
path.append(letters[i])
back(digits,index + 1)#注意index+1,一下层要处理下一个数字了
path.pop()
if len(digits)==0:
return res
back(digits,0)
return res
注释是需要看懂的,在不同的回溯运用中,函数的传参都不同,但是思想一致,都包含待比对的数据。