回溯算法
T77-组合
LeetCode第77题组合
题目描述
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
我的思路
- 回溯算法就是一棵棵决策树的生成过程,最重要的就是根节点与子节点的确定
- 谁可以做根节点:
1~n - 谁可以做子节点:
i + 1 ~ n
- 谁可以做根节点:
- 回溯的过程,需要明确以下几点:
- 明确回溯方法的返回条件是什么?
- 一定要先插入值,再判断返回条件,有效避免重复的问题
- 临时列表,插入元素之后,最后要清除元素
private List<List<Integer>> resList = new ArrayList<>();
/**
* 组合
* @param n
* @param k
* @return
*/
public List<List<Integer>> combine(int n, int k) {
List<Integer> temp = new ArrayList<>();
// 根节点元素可能为
for (int i = 1; i <= n; i++) {
backtrace(i, n, k, temp);
}
return resList;
}
/**
* 回溯算法构建结果集
* @param i
* @param k
* @param temp
*/
private void backtrace(int i, int n, int k, List<Integer> temp) {
// 先加入当前节点
temp.add(i);
// 判断返回条件
if (temp.size() == k) {
resList.add(new ArrayList<>(temp));
// 移除元素
temp.remove(temp.size() - 1);
return;
}
// 遍历孩子节点
for (int j = i + 1; j <= n; j++) {
backtrace(j, n, k, temp);
}
// 将当前节点删除
temp.remove(temp.size() - 1);
}
时间复杂度:
T216-组合总和III
题目描述
找出所有相加之和为 n 的 k个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
我的思路
- 实例变量
curSum记录当前的总和 - 回溯算法返回条件:
curSum == n && temp.size() == k - 每个数字只能用1次,可以剪枝
实现代码
private List<List<Integer>> resList = new ArrayList<>();
private int curSum = 0;
/**
* 找出所有相加之和为 n 的 k 个数的组合, 每个数字只能用一次
* @param k
* @param n
* @return
*/
public List<List<Integer>> combinationSum3(int k, int n) {
int maxBound = Math.min(9, n);
List<Integer> path = new ArrayList<>();
for (int i = 1; i <= maxBound; i++) {
backtrace(i, k, n, path);
}
return resList;
}
private void backtrace(int i, int k, int n, List<Integer> path) {
if (curSum > n || curSum + i > n) {
return;
}
// 将当前的节点加入路径
path.add(i);
curSum += i;
// 判断是否应该返回
if (curSum == n && path.size() == k) {
resList.add(new ArrayList<>(path));
// 移除当前节点
path.remove(path.size() -1);
curSum -= i;
return;
}
for (int j = i + 1; j <= 9; j++) {
backtrace(j, k, n, path);
}
// 将当前节点移除路径
path.remove(path.size() - 1);
curSum -= i;
}
T17-电话号码的字母组合
见LeetCode第17题电话号码的字母组合
题目描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
我的思路
- 典型的回溯算法
- 每个节点,他能做的决策性就是该数字对应的字母
- 考虑使用
Map存储数字到字母的映射
/**
*
* @param start digit 的下表
* @param i 映射的第 i 个字母
* @param digits
* @param sb
*/
private void backtrace(int start, int i, String digits, StringBuilder sb) {
if (start >= digits.length()) return;
// 添加当前字符
sb.append(codebook.get(digits.charAt(start)).get(i));
// 如果当前字符是最后一个字符
if (start == digits.length() - 1) {
resList.add(new String(sb));
sb.deleteCharAt(sb.length() - 1);
return;
}
for (int j = 0; j < codebook.get(digits.charAt(start + 1)).size(); j++) {
backtrace(start + 1, j, digits, sb);
}
sb.deleteCharAt(sb.length() - 1);
}
T39-组合总和
见LeetCode第39题组合总和
题目描述
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 **不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入: candidates = [2,3,6,7], target = 7
输出: [[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
我的思路
- 排序,首先将数组进行排序
- 索引
curIndex记录当前遍历的节点,遍历范围为[curIndex, length - 1] - 如果当前和值
curSum > target,进行剪枝操作 - 如果当前和值
curSum == target,将路径放入到结果集中 - 如何去重复呢?
- 剪枝操作,如果两个兄弟节点相同,就可以剪枝了
- 在遍历孩子节点的时候,遍历之前进行剪枝
- 剪枝条件为:
preIndex >= startIndex && candidate[preIndex] == candidate[startIndex],直接continue - 孩子节点回溯之后,需要更新前一个索引,将
preIndex = startIndex
实现代码
private final List<List<Integer>> resList = new ArrayList<>();
private int curSum = 0;
private int preIndex = -1;
/**
* 组合总和
* @param candidates
* @param target
* @return
*/
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0) return resList;
List<Integer> path = new ArrayList<>();
for (int i = 0; i < candidates.length; i++) {
if (preIndex >= i && candidates[preIndex] == candidates[i]) {
continue;
}
backtrace(i, candidates, target, path);
preIndex = i;
}
return resList;
}
/**
* 回溯求解
* @param i 当前索引
* @param candidates 目标数组
* @param target 目标值
* @param path 路径
*/
private void backtrace(int i, int[] candidates, int target, List<Integer> path) {
if (i >= candidates.length || curSum + candidates[i] > target) return;
// 将当前数字添加到路径中
path.add(candidates[i]);
curSum += candidates[i];
// 判读是否达到条件
if (curSum == target) {
resList.add(new ArrayList<>(path));
// 将当前节点移除
path.remove(path.size() - 1);
curSum -= candidates[i];
return;
}
// 遍历当子节点,加入路径
for (int j = i; j < candidates.length; j++) {
// 剪枝操作
if (preIndex >= j && candidates[preIndex] == candidates[j]) {
continue;
}
backtrace(j, candidates, target, path);
preIndex = j;
}
// 将当前的节点移除
path.remove(path.size() - 1);
curSum -= candidates[i];
}
T40-组合总和II
见Leetcode第40题组合总和II
题目描述
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意: 解集不能包含重复的组合。
我的思路
- 相较于第39题,这一题元素是只能用1次的。遍历决策树的时候,子节点一定会要明确
- 因为元素不能重复,因此同样应该,先排序,再剪枝
- 如何记录同级的前一个节点?
preIndex应该使用局部变量还是全局变量?- 若想正确对同一层相同元素的剪枝,需要使用局部变量来记录前一个索引
我的题解
private List<List<Integer>> resList = new ArrayList<>();
private int curSum = 0;
private int preIndex = -1;
/**
* 组合总和
* @param candidates
* @param target
* @return
*/
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if (candidates == null || candidates.length == 0) return resList;
Arrays.sort(candidates);
List<Integer> path = new ArrayList<>();
// 根节点决策
for (int i = 0; i < candidates.length; i++) {
// 剪枝去重
if (preIndex >= 0 && candidates[i] == candidates[preIndex]) continue;
backtrace(i, candidates, target, path, preIndex);
preIndex = i;
}
return resList;
}
private void backtrace(int i, int[] candidates, int target, List<Integer> path, int preIndex) {
if (i >= candidates.length || curSum + candidates[i] > target) return;
// 将当前元素加入到路径中
path.add(candidates[i]);
curSum += candidates[i];
// 判断是否满足条件
if (target == curSum) {
resList.add(new ArrayList<>(path));
// 将当前元素从路径中移除
path.remove(path.size() - 1);
curSum -= candidates[i];
return;
}
// 递归遍历子节点
for (int j = i + 1; j < candidates.length; j++) {
// 剪枝
if (preIndex >= i + 1 && candidates[j] == candidates[preIndex]) continue;
backtrace(j, candidates, target, path, preIndex);
preIndex = j;
}
// 将当前节点删除
path.remove(path.size() - 1);
curSum -= candidates[i];
}
T131-分割回文串
见LeetCode第131题分割回文串
题目描述
给你一个字符串 s,请你将 **s **分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入: s = "aab"
输出: [["a","a","b"],["aa","b"]]
我的思路
- 根据题意,这些子串是可以完全组合成字符串
s的,也就是说,不能丢弃任何的字符 - 先找根节点的人选,那些可以做根节点呢?
- 理论上讲,根节点是以
0索引所在字符为前缀的所有的子串 - 通过判断是否为回文串进行剪枝
- 理论上讲,根节点是以
- 回文串的判断,暂时使用
start和curIndex进行遍历比较来判断,暴力解法
我的题解
private List<List<String>> resList = new ArrayList<>();
/**
* 分割回文串
* @param s
* @return
*/
public List<List<String>> partition(String s) {
List<String> temp = new ArrayList<>();
if (s == null || s.length() <= 1) {
temp.add(s);
resList.add(temp);
return resList;
}
int start = 0;
// 遍历根节点,根节点为[start, i)之间的字符串
for (int i = 1; i <= s.length(); i++) {
// 剪枝逻辑
if (isPalindrome(s, start, i)) {
// 回溯寻找解
backtrace(start, i, s, temp);
}
}
return resList;
}
/**
* 回溯寻找解
* @param start
* @param i
* @param s
* @param temp
*/
private void backtrace(int start, int i, String s, List<String> temp) {
// 首先将子串添加进来
temp.add(s.substring(start, i));
// 递归结束的条件为:i == s.length()
if (i == s.length()) {
// 临时结果添加到结果中
resList.add(new ArrayList<>(temp));
// 回溯
temp.remove(temp.size() - 1);
return;
}
start = i;
// 遍历子节点
for (int j = start + 1 ; j <= s.length(); j++) {
if (isPalindrome(s, start, j)) {
backtrace(start, j, s, temp);
}
}
// 将结果移除
temp.remove(temp.size() - 1);
}
/**
* 判断处于区间[start, i)之间的字符串是否为回文串
* @param s
* @param start
* @param i
* @return
*/
private boolean isPalindrome(String s, int start, int i) {
if (i - start <= 1) return true;
while (start < i) {
if (s.charAt(start++) != s.charAt(--i)) {
return false;
}
}
return true;
}
T93-复原IP地址
见LeetCode第93题复原IP地址
题目描述
给定一个只包含数字的字符串,请你用.分割出所有可能得IP地址。
我的题解
private List<String> resList = new ArrayList<>();
/**
* 根据数字字符串 S 复原出所有可能的IP地址
* @param s
* @return
*/
public List<String> restoreIpAddresses(String s) {
if (s == null || s.length() < 4) return resList;
List<String> temp = new ArrayList<>();
int start = 0;
// 遍历所有可能的根节点
for (int i = 1; i <= 3; i++) {
// 判断左闭右开的区间[start, i)的数是否为合法的IP范围
if (isValid(s, start, i)) {
backtrace(start, i, s, temp);
}
}
return resList;
}
/**
* 递归寻找可能的 IP 组
* @param start
* @param i
* @param s
* @param temp
*/
private void backtrace(int start, int i, String s, List<String> temp) {
// 将 [start, i) 之间的数字添加到temp
temp.add(s.substring(start, i));
// 判断返回条件:temp.size() == 4
if (temp.size() > 4 || temp.size() == 4 && i != s.length()) {
// 将当前数字移除
temp.remove(temp.size() - 1);
return;
};
if (temp.size() == 4) {
// 将temp 使用 . 连接为一个IP
StringBuilder sb = new StringBuilder();
for (String str : temp) {
sb.append(str).append('.');
}
resList.add(sb.substring(0, sb.length() - 1)); // 去掉末尾的 .
// 将当前数字移除
temp.remove(temp.size() - 1);
return;
}
start = i;
for (int j = start + 1; j <= s.length(); j++) {
if (isValid(s, start, j)) {
backtrace(start, j, s, temp);
}
}
temp.remove(temp.size() - 1);
}
/**
* 判断区间 [start, i) 之间的数字是否为合法的IP
* @param s
* @param start
* @param i
* @return
*/
private boolean isValid(String s, int start, int i) {
if (start >= i || i - start > 3 || (s.charAt(start) == '0' && i - start > 1)) return false;
int num = Integer.parseInt(s.substring(start, i));
if (num >= 0 && num <= 255) {
return true;
}
return false;
}
注意
-
判断IP是否合法的时候,需要考虑 0 开头的IP段,例如
023 -
返回条件有两个:
- 满足要求:
temp.size() == 4并且结束索引正好在字符串的末尾i == s.length() - 不满足要求:
temp.size() > 4 || temp.size() == 4 && i < s.length()
- 满足要求: