Leetcode回溯法题解

文章介绍了如何使用回溯法解决电话号码的字母组合及全排列问题。在电话号码问题中,通过将数字映射到字母,用递归函数穷举所有可能的组合。在全排列问题中,递归遍历数组,同时使用used数组记录已使用的元素,确保排列的唯一性。这两个问题都利用了递归和回溯策略来生成所有可能的解决方案。
摘要由CSDN通过智能技术生成

第一题

17. 电话号码的字母组合

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

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

示例1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

思路

我们可以把这道题转换成另一种更好理解的方式。想象一下,现在我们眼前有编号为“0”和“1”的两个箱子。“0”号箱子可以放入“a”,“b”,“c”三张牌,“1”号箱子可以放入“d”,“e”,“f”三张牌。问,一共有几种搭配方案?

穷举法

在这里插入图片描述
一个很简单的思路,我们可以进行两层for循环,穷举每一种可能的组合。第一层for循环遍历“0”号箱子的每一种可能,也就是从‘a’到‘c’。第二层for循环遍历“1”号箱子的每一种可能,也就是从‘d’到‘f’。

List<String> result = new ArrayList<>();
StringBuilder path = new StringBuilder<>();

for(char s1 = 'a'; s1 <= 'c'; s1++){
    path.append(s1);
    for(char s2 = 'd'; s2 <= 'f'; s2++){
        path.append(s2);
        result.add(path.toString());
    }
}

return result;

我们现在已经知道了示例1的每一位是什么,自然可以写出上面的代码。但是,实际上digits是不确定的,我们无法知道它的具体长度和每一位,那怎么知道要如何写for循环?

回溯法

一个更好的思路,是用递归函数来代替多层for循环,每一层递归就相当于一层for循环。在递归完digits最后一个元素后,结束递归。

class Solution {
    private List<String> result = new ArrayList<String>();
    private StringBuilder path = new StringBuilder();
    private String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    
    /**
     * 返回digits能表示的字母组合
     * @param String digits,digits的每一位对应numString中的一个String
     * @return List<String> result,digits能表示的字母组合
     */
    public List<String> letterCombinations(String digits) {
        if(digits.length() == 0 || digits == null){
            return result;
        }
        // 从digits[0]开始搜索,也是首次递归
        dfs(0, digits);
        return result;
    }
    
    /**
     * 递归搜索digits的每一位
     * @param int step,step代表digits的第几位,同时也代表了递归的第几层
     * @param String digits,digits的每一位对应numString中的一个字符串num
     */
    private void dfs(int step, String digits){
        // 如果递归完digits最后一个元素,那么保存结果,结束递归
        if(step == digits.length()){
            result.add(path.toString());
            return;
        }
        // 获取digits的第step位元素
        char idx = digits.charAt(step);
        // 获取digits的第step位元素对应的字符串
        String num = numString[idx - '0'];
        // 对digits的第step位元素对应的字符串num进行“树层遍历”
        for(int i = 0; i < num.length(); i++){
            char item = num.charAt(i);
            // 放入第step位元素对应的字符
            path.append(item);
            // 进行“树枝遍历”,继续递归遍历第step + 1位元素对应的字符
            dfs(step + 1, digits);
            // 清空第step位元素对应的字符
            path.deleteCharAt(path.length() - 1);
        }
        // 如果对这一层完成了“树层遍历”,那么返回上一层
        return;
    }
}

结合示例1来看。在递归的第0层,首先遍历digits[0],‘2’。'2’对应的num为"abc",接下来就对num进行“树层遍历”。在第0层递归的“树层遍历”中,第一次选择将‘a’放入“0”号箱子中,然后进入下一层递归进行“树枝遍历”,选择放入“1”号箱子中的字符。同样的,在第1层递归的“树层遍历”中,第一次选择将‘d’放入“1”号箱子中,然后进入下一层递归进行“树枝遍历”。当到达第2层递归时,已经递归完digits最后一个元素,那么保存结果,结束递归。

在这里插入图片描述

现在,回到了第1层递归的“树层遍历”中。第一次我们选择将‘d’放入“1”号箱子中,现在我们要将‘d’从“1”号箱子中取出(path.deleteCharAt(path.length() - 1)),选择将‘e’放入“1”号箱子中。

当我们把“def”都选择过一遍之后,返回第0层递归的“树层遍历”中。第一次我们选择将‘a’放入“0”号箱子中,现在我们要将‘a’从“0”号箱子中取出(path.deleteCharAt(path.length() - 1)),选择将‘b’放入“0”号箱子中。

回溯法通过递归函数,可以实现多次for循环,遍历所有可能的结果。

总结

最后,我们可以总结出回溯法使用递归函数实现多层循环的代码模版。

void dfs(){
    if(达到了递归条件){
        保存结果
        返回
    }
    
    for(进行“树层循环”){
        添加元素
        dfs(进行“树枝循环”)
        移除元素
    }
}

第二题

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]]

思路

这道题可以和第一题一样,转换成往箱子里放牌。想象一下,数组nums的长度为3,我们就是要遍历编号为“0”,“1”,“2”的三个箱子。“0”号箱子可以放入“1”,“2”,“3”三张牌,“1”号箱子也可以放入“1”,“2”,“3”三张牌。但是,和第一题不一样的地方在于,这道题里用过的牌不可以再用。当“0”号箱子可以放入“1”时,“1”号箱子就不可以放入“1”了,只能放“2”或“3”两张牌。同理,当“0”号箱子可以放入“2”时,“1”号箱子就不可以放入“2”了,只能放“1”或“3”两张牌。

回溯法

同样的,我们使用递归函数来遍历数组nums的长度,进行“树枝循环”。为了实现用过的牌不可以再用,我们需要一个used数组记录哪些牌已经被用过了

class Solution {
    private List<List<Integer>> result = new ArrayList<>();
    private List<Integer> path = new LinkedList<>();

    /**
     * 返回nums所有可能的全排列
     * @param int[] nums,nums的每一位是一个数字
     * @return List<List<Integer>> result,nums所有可能的全排列
     */
    public List<List<Integer>> permute(int[] nums) {
        // used数组表示nums对应位置的数字是否已经被使用,默认为false
        boolean[] used = new boolean[nums.length];
        dfs(0, nums, used);
        return result;
    }


    /**
     * 递归搜索nums的每一位
     * @param int step,step代表第几个箱子,同时也代表了递归的第几层
     * @param int[] nums,nums的每一位是一个数字
     * @param boolean[] used,表示nums对应位置的数字是否已经被使用
     */
    private void dfs(int step, int[] nums, boolean[] used){
        // 如果递归完nums最后一个元素,那么保存结果,结束递归
        if(step == nums.length){
            result.add(new ArrayList<>(path));
            return;
        }
        // 第step个箱子对nums的元素进行“树层遍历”
        for(int i = 0; i < nums.length; i++){
            // 如果该元素没有放入step之前的箱子里,那么就可以放入第step个箱子
            if(!used[i]){
                // 将nums的元素放入第step个箱子
                path.add(nums[i]);
                // 记录该元素已经被使用过
                used[i] = true;
                // 进行“树枝遍历”,继续递归遍历第step + 1个箱子
                dfs(step + 1, nums, used);
                // 将第step个箱子的元素取出
                path.remove(path.size() - 1);
                // 记录该元素没有被使用过
                used[i] = false;
            }
        }
        return;
    }
}

在这里,nums数组的长度就是箱子的个数。在第0个箱子,对nums的元素进行“树层遍历”,可以放入“1”,“2”,“3”三张牌。第一次选择将‘1’放入“0”号箱子中,然后进入下一层递归进行“树枝遍历”,选择放入“1”号箱子中的数字。同样的,在第1层递归的“树层遍历”中,此时“1”已经被用过了,可以选择的数字只有“2”和“3”。第一次选择将‘2’放入“1”号箱子中,然后进入下一层递归进行“树枝遍历”。当到达第2层递归时,只剩下“3”可以用,放入“2”号箱子中,进行下一层递归。在第3层递归中,已经遍历完nums数组的长度,保存答案并返回。
在这里插入图片描述
现在,回到了第2层递归的“树层遍历”中。因为第0层循环用了‘1’,第1层循环用了‘2’,所以第2层循环只有‘3’可以用。现在结束第2层循环,回到第1层循环。

现在,回到了第1层递归的“树层遍历”中。第一次我们选择将‘2’放入“1”号箱子中,现在我们要将‘2’从“1”号箱子中取出并记录该元素没有被使用过,选择将‘3’放入“1”号箱子中。

当我们把‘2’,‘3’都选择过一遍之后,返回第0层递归的“树层遍历”中。第一次我们选择将‘1’放入“0”号箱子中,现在我们要将‘1’从“0”号箱子中取出并记录该元素没有被使用过,选择将‘2’放入“0”号箱子中。

通过这种回溯的方式,遍历所有的可能。

总结

首先,我们要知道我们递归的是什么。在这里,nums数组的长度就是箱子的个数,所以我们对nums数组的长度进行递归。

其次,和第一题不一样的地方在于,这道题用过的牌不可以再用,所以需要有一个used数组来记录元素的使用情况。

参考:
代码随想录
啊哈!算法

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值