摘要
本文主要介绍了回溯算法的理论基础和回溯法模版,以及LeetCode中77.组合问题的解题思路和代码。
1、回溯算法理论基础
1.1 概念
回溯算法是一种用于解决组合问题、排列问题和搜索问题的常用算法。它基于深度优先搜索的思想,通过不断地尝试各种可能的选择,然后回退到上一步,直到找到问题的解或穷尽所有可能性。
回溯算法的核心思想是构建一棵决策树,每个节点表示在问题中的一个决策点,从根节点出发,逐步向下扩展树,直到找到解或无法继续扩展。如果遇到无法继续扩展的情况,就回退到上一层节点,尝试其他分支。
以下是回溯算法的基本框架和关键要点:
- 路径(Path): 在算法执行过程中,需要维护一个路径,用于记录当前的选择或决策。
- 选择列表(Choices): 每一步都有一个或多个选择,算法需要生成当前状态下的所有候选选择。
- 结束条件(Termination Condition): 定义何时达到问题的解,当满足结束条件时,算法结束。
- 回溯过程(Backtracking): 如果当前路径无法达到解或满足条件,算法需要回退到上一个状态,尝试其他选择。
下面是一个简单的 Java 示例,用于解决组合问题(找到数组中所有可能的组合):
import java.util.ArrayList;
import java.util.List;
public class BacktrackingExample {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
backtrack(result, path, n, k, 1);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> path, int n, int k, int start) {
// 结束条件:已选择的元素数量达到 k
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
// 选择列表:从 start 到 n 中选择一个数加入当前组合
for (int i = start; i <= n; i++) {
path.add(i);
// 递归进入下一层决策树
backtrack(result, path, n, k, i + 1);
// 撤销选择
path.remove(path.size() - 1);
}
}
public static void main(String[] args) {
BacktrackingExample solution = new BacktrackingExample();
int n = 4;
int k = 2;
List<List<Integer>> result = solution.combine(n, k);
for (List<Integer> combination : result) {
System.out.println(combination);
}
}
}
在上述示例中,combine 函数用于生成组合,backtrack 函数用于递归搜索所有可能的组合。回溯算法的关键在于如何维护路径、选择列表、结束条件和回溯过程。
这个示例展示了回溯算法在 Java 中的基本实现方式,你可以根据具体问题的要求来定制路径、选择列表和结束条件,以解决各种类型的组合、排列和搜索问题。
1.2 回溯法模版
回溯法是一种常用于解决组合、排列、搜索等问题的算法。以下是回溯法的通用模板,使用 Java 编程语言表示:
public class BacktrackingTemplate {
public void backtrack(/* 入参 */) {
// 判断是否满足结束条件
if (满足结束条件) {
// 处理结束条件,例如将结果存入结果集
return;
}
// 遍历选择列表
for (选择 : 选择列表) {
// 做出选择
做出选择;
// 进入下一层决策树
backtrack(/* 下一层参数 */);
// 撤销选择
撤销选择;
}
}
}
在这个模板中,你需要根据具体问题的特性来填充模板中的各个部分:
- 满足结束条件: 这部分代码用于判断是否达到问题的解,如果满足结束条件,通常需要将当前的选择存入结果集或进行其他操作。
- 选择列表: 这是问题中的选择集合,需要根据问题的不同进行设置。例如,如果是组合问题,选择列表是从数组中选择元素;如果是排列问题,选择列表是数组中的所有元素;如果是搜索问题,选择列表是所有可能的状态或路径。
- 做出选择和撤销选择: 在算法的每一层递归中,需要做出一个选择,并进入下一层递归。在进入下一层递归之前,需要做出选择(例如,在路径中添加一个元素),然后在递归完成后撤销选择,以便回到上一层状态。
使用这个通用模板,你可以根据具体问题的需求来填充相关的逻辑。回溯算法的核心思想是通过递归实现决策树的遍历,在每个节点上做出选择并继续搜索,直到找到解或穷尽所有可能性。
2、77.组合
2.1 思路
-
递归函数的参数: 在递归函数中,我们需要传递以下参数:
start:表示当前考虑的数字的起始位置。k:表示还需要选择几个数字。n:表示可选数字的范围。
-
满足结束条件: 在递归函数的开头,首先判断是否满足结束条件。结束条件有两个:
- 当
k等于 0 时,表示已经选择了k个数字,将当前的组合加入结果集。 - 当
i > n时,表示已经考虑完了所有可选数字,直接返回。
- 当
-
进行选择和递归: 在递归函数的主体部分,我们需要考虑两种情况:
- 选择当前数字
i,将其添加到当前组合中,然后递归调用函数,传递参数i+ 1和k - 1。 不选择当前数字i,直接递归调用函数,传递参数i+ 1和k。
- 选择当前数字
-
撤销选择: 在每一次递归返回后,需要撤销选择,即将当前数字从组合中移除,以便考虑下一个数字的选择。
-
回溯搜索: 通过不断地进行选择和递归,回溯算法会搜索所有可能的组合。
1、可以剪枝优化吗?
例如,对于给定的参数
n = 4, k = 2,当遍历选择列表时,需要考虑k的值来确定循环的范围。具体来说,当k = 2时,如果n = 3,我们可以继续遍历,但如果n = 4,则不应该继续遍历。因此,我们可以调整循环变量i的取值范围为[start, (n - k + 1) + linked.size()]。这个调整确保了在当前状态下,仅考虑那些可以生成有效组合的数字。这是一个很好的优化,以减少不必要的遍历,加快算法的执行速度。
2.2 代码
未剪枝优化
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> list = new ArrayList<>();
LinkedList<Integer> linked = new LinkedList<>();
combine(list, linked, n, k, 1);
return list;
}
public void combine(List<List<Integer>> list, LinkedList<Integer> linked, int n, int k, int start) {
if(linked.size() == k) {
list.add(new ArrayList<>(linked));
return;
}
for(int i=start; i<=n; i++) {
linked.add(i);
combine(list, linked, n, k, i+1);
linked.removeLast();
}
}
ChatGPT的代码
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> current = new ArrayList<>();
backtrack(result, current, 1, n, k);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> current, int start, int n, int k) {
if (k == 0) {
result.add(new ArrayList<>(current));
return;
}
for (int i = start; i <= n; i++) {
current.add(i);
backtrack(result, current, i + 1, n, k - 1);
current.remove(current.size() - 1);
}
}
}
剪枝优化
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> list = new ArrayList<>();
LinkedList<Integer> linked = new LinkedList<>();
combine(list, linked, n, k, 1);
return list;
}
public void combine(List<List<Integer>> list, LinkedList<Integer> linked, int n, int k, int start) {
if(linked.size() == k) {
list.add(new ArrayList<>(linked));
return;
}
for(int i=start; i<=(n-k+1)+linked.size(); i++) {
linked.add(i);
combine(list, linked, n, k, i+1);
linked.removeLast();
}
}