蓝桥杯备考随手记: 回溯算法

1. 什么是回溯

回溯算法是一种常见的解决问题的算法思想,它通过尝试不同的选择,逐步构建可能的解,并在遇到无效选择时回退到上一步进行另一种选择,直到找到问题的解或者穷尽所有可能。

回溯算法可以看作是一种深度优先搜索,在搜索的过程中,通过剪枝操作来减少不必要的搜索。回溯算法通常通过递归的方式实现,每一层递归代表一种选择,通过参数传递的方式记录已经选择的路径,并将问题缩小为规模更小的子问题。

2. 一般框架

回溯算法的一般框架如下:

  1. 定义问题的解空间:确定问题的解空间,选择适合的搜索方式。

  2. 定义解的表示方法:选择合适的数据结构来存储已选择的路径。

  3. 定义结束条件:判断当前路径是否为有效解。

  4. 搜索过程:在解空间中进行搜索,对于每个选择,判断是否满足约束条件。如果满足,则将其添加到路径中,递归地进入下一层搜索。

  5. 处理有效解:如果在搜索过程中得到一个有效解,则存储或进行相关操作。

  6. 回溯过程:回溯到上一层搜索,撤销当前路径的选择,尝试其他选择。

回溯算法的伪代码如下所示:

function backtrack(选择列表, 路径):
    if 满足结束条件:
        将路径添加到结果集
        return
    
    for 选择 in 选择列表:
        做出选择
        backtrack(新的选择列表, 新的路径)
        撤销选择

3. 代码示例 

下面是一个使用Java实现回溯算法的示例,解决一个简单的组合求和问题:

import java.util.*;

public class BacktrackingExample {

    // 回溯算法的实现
    public void backtrack(List<Integer> choices, List<Integer> path, List<List<Integer>> result) {
        // 如果路径长度等于选择列表长度,将路径添加到结果集中
        if (path.size() == choices.size()) {
            result.add(new ArrayList<>(path));
            return;
        }
        
        // 遍历选择列表
        for (int i = 0; i < choices.size(); i++) {
            // 避免重复选择
            if (path.contains(choices.get(i))) {
                continue;
            }
            
            // 做出选择
            path.add(choices.get(i));
            // 递归调用回溯算法
            backtrack(choices, path, result);
            // 撤销选择
            path.remove(path.size() - 1);
        }
    }

    public static void main(String[] args) {
        // 初始化选择列表和结果集
        List<Integer> choices = Arrays.asList(1, 2, 3);
        List<List<Integer>> result = new ArrayList<>();
        
        // 创建示例对象
        BacktrackingExample example = new BacktrackingExample();
        // 调用回溯算法
        example.backtrack(choices, new ArrayList<>(), result);
        
        // 输出结果集中的所有路径
        for (List<Integer> list : result) {
            System.out.println(list);
        }
    }
}

执行以上代码,可以得到排列问题的所有解。每个解以列表形式展示,每个列表代表一种排列。

[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

4. 过程描述

当使用回溯算法来找出由数字1、2、3组成的所有排列组合时,可以详细描述每个过程如下:

  1. 初始状态:开始时,我们有一个选择列表 [1, 2, 3],一个空的路径用来存放当前选择的数字,一个空的结果集用来存放所有符合条件的路径。

  2. 选择数字1:首先选择数字1,将1添加到路径中,此时路径为 [1]。然后继续选择下一个数字。

  3. 选择数字2:在剩下的数字 [2, 3] 中选择数字2,将2添加到路径中,路径变为 [1, 2]。然后继续选择下一个数字。

  4. 选择数字3:在剩下的数字 [3] 中只有一个数字3,选择数字3,将3添加到路径中,路径变为 [1, 2, 3]。此时路径长度等于选择列表长度,将路径 [1, 2, 3] 添加到结果集中。

  5. 回溯:现在需要回溯到上一步,撤销选择数字3,路径变为 [1, 2]。然后选择数字3,将3添加到路径中,路径变为 [1, 2, 3]。将路径 [1, 2, 3] 添加到结果集中。

  6. 回溯:再次回溯到上一步,撤销选择数字2,路径变为 [1]。然后选择数字3,将3添加到路径中,路径变为 [1, 3]。然后选择数字2,将2添加到路径中,路径变为 [1, 3, 2]。将路径 [1, 3, 2] 添加到结果集中。

  7. 回溯:最后回溯到初始状态,撤销选择数字1,路径变为空。然后选择数字2,将2添加到路径中,路径变为 [2]

  8. 选择数字1:在剩下的数字 [1, 3] 中选择数字1,将1添加到路径中,路径变为 [2, 1]。然后选择数字3,将3添加到路径中,路径变为 [2, 1, 3]。将路径 [2, 1, 3] 添加到结果集中。

  9. 重复以上步骤,直到所有可能的排列组合都被找出来。

通过以上详细步骤,我们可以清晰地看到回溯算法是如何在每一步中做出选择、尝试不同的可能性,然后回溯撤销选择,直到找出所有符合条件的排列组合。

5. 剪枝优化

回溯算法的时间复杂度通常较高,因为需要尝试所有可能的选择。在实际应用中,可以通过剪枝等方法来优化回溯算法的性能。

假设给定一个正整数数组 nums 和一个目标整数 target,找出 nums 中所有可以使数字和等于 target 的组合。数组中的每个数字在组合中只能使用一次。

在这个问题中,可以通过以下方式进行剪枝:

  1. 当路径的和已经超过目标整数时,可以立即停止当前路径的搜索,因为继续搜索下去不可能得到符合条件的结果。
  2. 当路径的和加上剩余数字的和仍然小于目标整数时,也可以停止当前路径的搜索,因为即使将剩余的数字全部加上,也不可能达到目标整数。
  3. 可以对数组进行排序,然后在搜索过程中,优先选择较小的数字,这样可以更早地发现不符合条件的路径,从而剪枝。

Java代码实现: 

import java.util.*;

public class BacktrackingExample {

    // 主方法,用于找出数组中所有可以使数字和等于目标整数的组合
    public List<List<Integer>> combinationSum(int[] nums, int target) {
        List<List<Integer>> result = new ArrayList<>(); // 存放结果集
        Arrays.sort(nums); // 排序数组,方便后续剪枝和去重
        backtrack(nums, target, 0, new ArrayList<>(), result); // 调用回溯方法
        return result; // 返回结果集
    }

    // 回溯方法,递归地搜索所有可能的组合
    private void backtrack(int[] nums, int remain, int start, List<Integer> path, List<List<Integer>> result) {
        if (remain == 0) { // 如果剩余值为0,说明找到了满足条件的组合
            result.add(new ArrayList<>(path)); // 将当前路径添加到结果集
            return; // 结束当前递归
        }

        for (int i = start; i < nums.length; i++) {
            if (i > start && nums[i] == nums[i - 1]) {
                continue; // 避免重复解,跳过重复的数字
            }
            if (nums[i] > remain) {
                break; // 剪枝,当前数字已经超过剩余值,不可能满足条件
            }

            path.add(nums[i]); // 选择当前数字
            backtrack(nums, remain - nums[i], i + 1, path, result); // 递归调用,更新剩余值和起始索引
            path.remove(path.size() - 1); // 撤销选择,回溯到上一步
        }
    }

    // 主程序入口
    public static void main(String[] args) {
        int[] nums = {10, 1, 2, 7, 6, 1, 5}; // 给定数组
        int target = 8; // 目标整数
        BacktrackingExample example = new BacktrackingExample(); // 创建示例对象
        List<List<Integer>> result = example.combinationSum(nums, target); // 调用方法获取结果集
        for (List<Integer> list : result) {
            System.out.println(list); // 输出每个满足条件的组合
        }
    }
}

执行以上代码,可以得到排列问题的所有解。每个解以列表形式展示,每个列表代表一种排列。

[1, 1, 6]
[1, 2, 5]
[1, 7]
[2, 6]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值