回溯法-这次我真的会了

334 阅读3分钟

基础概念

回溯法是一种基于搜索的算法,常用于解决组合问题和排列问题等。它基于“试错”的思想,通过不断尝试可能的解决方案来找到一个正确的解。

基本思路

将问题分解成多个子问题,然后对每个子问题进行尝试。如果在某个子问题中发现无法找到解决方案,则返回上一个子问题,重新尝试其它解决方案,直到找到正确的解决方案或者所有的解决方案都已尝试完毕。

function backtrack(问题, 解决方案, 结果集合) {
    if (满足结束条件) {
        结果集合.add(解决方案); // 将当前解决方案添加到结果集合中
        return;
    }
    for (每个可行解) {
        if (满足约束条件) {
            执行操作,得到新的解决方案;
            backtrack(问题, 新的解决方案, 结果集合); // 递归调用回溯法,处理下一个子问题
            恢复状态; // 回溯到上一个状态,开始处理下一个可行解
        }
    }
}

其中, 问题 表示当前需要解决的问题, 解决方案 表示当前已经找到的解决方案, 结果集合 表示所有满足条件的解决方案集合。在算法流程中,我们首先判断是否满足结束条件,如果满足则将当前解决方案添加到结果集合中;否则,我们遍历所有可行解,如果满足约束条件,则执行操作得到新的解决方案,然后递归调用回溯法,处理下一个子问题。在递归调用结束后,我们需要恢复状态,回溯到上一个状态,开始处理下一个可行解。 需要注意的是,在回溯法中, 约束条件恢复状态 这两个步骤非常重要。约束条件可以帮助我们判断当前解决方案是否满足条件,如果不满足则不必继续处理,直接跳过即可。恢复状态可以帮助我们回溯到上一个状态,继续尝试其它可行解。同时,在编写代码时,我们需要注意避免出现死循环和重复计算的问题。

使用场景

回溯法的使用场景非常广泛,例如:求解数独、N皇后问题、八皇后问题等。这些问题都有一个共同的特征,就是需要找到一组满足特定条件的解决方案。在这些问题中,回溯法可以非常有效地找到正确的解决方案。

问题实战

回溯法的核心是递归,因此在编写回溯法时需要注意一些问题。例如:如何设计递归函数、如何设置退出条件、如何剪枝等。

全排列

不重复数字的全排列

问题描述: 给定一组不同的数字,返回它们的所有排列方式。

解决思路

  1. 我们使用一个列表来存储当前已经找到的解决方案,如果列表中的数字数量等于原始数组的长度,则说明已经找到一个完整的解决方案。
  2. 在遍历所有可行解时,我们需要判断当前数字是否已经包含在当前解决方案中,如果包含则需要跳过。
  3. 最后,我们需要在搜索完所有情况后,恢复状态,回溯到上一个状态,继续处理下一个可行解,以找到所有的解决方案。

代码实现

package algo;

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

public class BackTracking {
    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        List<List<Integer>> res = new ArrayList<>();
        backtrack(nums, new ArrayList<>(), res);
        System.out.println(res);
    }

    /**
     * 回溯
     * @param nums 问题
     * @param temp 解决方案
     * @param res 结果合集
     */
    public static void backtrack(int[] nums, List<Integer> temp, List<List<Integer>> res) {
        // 将当前解决方案添加到结果集合中
        if (temp.size() == nums.length) {
            res.add(new ArrayList<>(temp));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            // 重复跳过
            if (temp.contains(nums[i])) {
                continue;
            }
            temp.add(nums[i]);
            // 递归调用回溯法,处理下一个子问题
            backtrack(nums, temp, res);
            // 回溯到上一个状态,开始处理下一个可行解
            temp.remove(temp.size() - 1);
        }
    }
}

可重复数字的全排列

问题描述: 给定一组可重复的数字,返回它们的所有排列方式。

解决思路

  1. 我们使用一个列表来存储当前已经找到的解决方案,如果列表中的数字数量等于原始数组的长度,则说明已经找到一个完整的解决方案。
  2. 排序后的数组,在遍历所有可行解时,我们需要判断当前数字和下一个数字是否重复,重复跳过,同时,为了解决是否访问的问题,需要用k,v存储数组中每个数字和数字出现次数的映射关系。
  3. 最后,我们需要在搜索完所有情况后,恢复状态,回溯到上一个状态,继续处理下一个可行解,以找到所有的解决方案。

代码实现

package leetcode;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class QuanPailie47 {
    public static void main(String[] args) {
        int[] nums = {1, 2, 1};
        System.out.println(permuteUnique(nums));
    }
    public static List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        Map<Integer, Long> map = Arrays.stream(nums).boxed()
                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

        backtrack(nums, new ArrayList<>(), map, res);
        return res;
    }

    /**
     * 回溯
     * @param nums 问题
     * @param temp 解决方案
     * @param map 每个数组元素及出现的次数,用于判断元素是否用过
     * @param res 结果合集
     */
    public static void backtrack(int[] nums, List<Integer> temp, Map<Integer, Long> map, List<List<Integer>> res) {
        if (temp.size() == nums.length) {
            res.add(new ArrayList<>(temp));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            long count = map.get(nums[i]);
            if (count > 0) {
                map.put(nums[i], count - 1);
                temp.add(nums[i]);
                backtrack(nums, temp, map, res);
                temp.remove(temp.size() - 1);
                map.put(nums[i], count);
            }
        }
    }
}