蓝桥杯 Java B 组之全排列与子集生成(掌握回溯模板)

发布于:2025-02-21 ⋅ 阅读:(169) ⋅ 点赞:(0)

Day 2:全排列与子集生成(掌握回溯模板)


📖 一、回溯算法简介

回溯算法(Backtracking Algorithm)是一种通过试探法(递归)来找出所有解法的算法。它是深度优先搜索(DFS)的一个特殊形式。在回溯过程中,我们会不断地选择并尝试每一个可能的解,当发现当前选择不合适时,我们会回退到上一步并尝试其他选项,这就是“回溯”的意义。

回溯算法的核心思想:

  1. 递归构建解空间树:将问题的解空间表示为树状结构,每一层代表一个选择。
  2. 剪枝:在递归过程中,通过判断是否满足某些条件来避免不必要的搜索,减少计算量。
  3. 回溯:当搜索到某个节点时,判断当前路径是否满足目标,如果不满足目标,则“回溯”到上一步进行其他尝试。

回溯算法的模板:

public class Backtracking {
    public void backtrack(List<Integer> tempList, List<List<Integer>> result, int[] nums, int start) {
        // 记录当前选择的路径
        result.add(new ArrayList<>(tempList));
        
        // 从当前位置开始,尝试选择每个元素
        for (int i = start; i < nums.length; i++) {
            // 做选择
            tempList.add(nums[i]);
            
            // 递归进入下一层
            backtrack(tempList, result, nums, i + 1);
            
            // 撤销选择
            tempList.remove(tempList.size() - 1);
        }
    }
}

📖 二、全排列(Permutation)

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

思路与分析:

  1. 对于一个序列,全排列就是把所有元素放在不同的位置,组合所有可能的情况。
  2. 使用回溯算法:每次选择一个数字,剩下的数字进行递归排列,直到遍历完所有数字。

回溯思路:

  1. 递归地交换数组中的每个元素,与当前位置的元素交换。
  2. 当遍历到数组的末尾时,记录当前排列。
  3. 然后“撤销”交换,继续递归其他情况。

代码实现(全排列)

import java.util.*;

public class Permutations {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(result, new ArrayList<>(), nums);
        return result;
    }
    
    // 回溯方法
    private void backtrack(List<List<Integer>> result, List<Integer> tempList, int[] nums) {
        // 如果当前排列的长度等于原始数组的长度,表示完成一个排列
        if (tempList.size() == nums.length) {
            result.add(new ArrayList<>(tempList));
            return;
        }

        // 遍历每个数字,进行选择
        for (int i = 0; i < nums.length; i++) {
            // 剪枝:跳过已经选过的数字
            if (tempList.contains(nums[i])) continue;

            // 做选择
            tempList.add(nums[i]);

            // 递归选择下一个数字
            backtrack(result, tempList, nums);

            // 撤销选择
            tempList.remove(tempList.size() - 1);
        }
    }

    public static void main(String[] args) {
        Permutations p = new Permutations();
        int[] nums = {1, 2, 3};
        System.out.println(p.permute(nums));
    }
}

代码讲解:

  1. permute 方法:接收一个数字数组,返回所有可能的排列。
  2. backtrack 方法
    • 判断当前排列的长度是否与原数组相等,若相等,表示找到一个全排列,加入结果集。
    • 遍历数组中每个数字,如果数字没有被选过,则加入当前排列。
    • 递归调用自己,继续选择下一个数字。
    • 最后撤销当前选择,尝试其他选项。

📖 三、子集生成(Subset Generation)

问题描述: 给定一个整数数组 nums,返回所有可能的子集(幂集)。子集的顺序无关,结果中不能包含重复的子集。

思路与分析:

  1. 递归法生成子集:对于每个元素,我们有两种选择:
    • 包含该元素。
    • 不包含该元素。
  2. 子集的总数为 2^n,其中 n 为数组的长度,因为每个元素都有两种可能的选择。
  3. 使用回溯来生成所有子集,最终将所有可能的子集收集到结果集中。

回溯思路:

  1. 从第一个元素开始,做出选择(包括该元素或不包括)。
  2. 递归地处理剩下的元素,直到遍历完所有元素。
  3. 在每一层递归时,将当前的选择路径加入到结果集。

代码实现(子集生成)

import java.util.*;

public class Subsets {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(result, new ArrayList<>(), nums, 0);
        return result;
    }
    
    // 回溯方法
    private void backtrack(List<List<Integer>> result, List<Integer> tempList, int[] nums, int start) {
        // 每次递归到一个位置,当前的子集都被加入结果
        result.add(new ArrayList<>(tempList));
        
        // 遍历后续元素,递归生成子集
        for (int i = start; i < nums.length; i++) {
            tempList.add(nums[i]); // 做选择
            backtrack(result, tempList, nums, i + 1); // 递归
            tempList.remove(tempList.size() - 1); // 撤销选择
        }
    }

    public static void main(String[] args) {
        Subsets s = new Subsets();
        int[] nums = {1, 2, 3};
        System.out.println(s.subsets(nums));
    }
}

代码讲解:

  1. subsets 方法:接受一个整数数组,返回所有可能的子集。
  2. backtrack 方法
    • 每次递归都会把当前的子集路径(tempList)添加到结果列表。
    • 从当前位置开始,遍历后续元素,尝试将每个元素加入子集。
    • 然后递归处理剩余元素,最终返回所有的子集。

📖 四、总结:

回溯算法的常见问题:

  1. 全排列(Permutation):给定一组元素,生成所有可能的排列。
  2. 子集(Subset)生成:给定一组元素,生成所有可能的子集。
  3. 组合(Combination):选择一定数量的元素,生成所有可能的组合。
  4. 求解数独、八皇后等问题:回溯是求解此类问题的经典算法。

常考点与易错点:

  1. 路径保存:在回溯过程中,确保在递归结束后恢复路径(撤销选择)。
  2. 剪枝:在不可能满足条件时,提前终止递归,避免不必要的计算。
  3. 递归的终止条件:确保每个递归都有明确的结束条件,避免无限递归。
  4. 变量作用域:注意在递归中修改的变量(如路径)需要正确回溯。

练习建议

  1. 全排列问题:练习处理不同长度的排列,确保能处理重复元素的情况。
  2. 子集问题:练习不同集合的子集生成,注意排序和剪枝的技巧。

回溯算法的关键是理解每一步的决策,并在每个递归调用中合理地“做选择”和“撤销选择”。


网站公告

今日签到

点亮在社区的每一天
去签到