目录
回溯(恢复现场)要点:
回溯所做的一切操作,都是针对于本层的数据(恢复的都是本层的数据),本次调用结束之后,会将数据恢复到刚进入本次调用的初始状态。也就是说,恢复这个数据在本层所做的一切操作,也就是将这个数据还原成刚调用这个函数时候的样子,所以执行完这一层,其实,没有对这个数据有任何的影响和改变。
1、电话号码的字母组合
1.1 算法原理
- 画出决策树,越详细越好
- 设计代码 -> 全局变量: List<String> ret;//返回值 StringBuffer path;//路径 String[] reflect;//digits中元素所映射的字符串
- 设计代码 -> 函数头:dfs(digits, pos); //pos为下一个要添加进path中的digits元素的下标
- 设计代码 -> 函数体 ...
- 设计代码 -> 函数出口:path.length() == digtis.length();
- 设计代码 -> 回溯 -> 恢复现场:path.deleteChatAt(最后一个元素);
- 设计代码 -> 剪枝:无
1.2 算法代码
class Solution {
List<String> ret;
StringBuffer path;
String[] reflect = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
ret = new ArrayList<>();
if(digits.equals("")) return ret;
path = new StringBuffer();
dfs(digits, 0);
return ret;
}
public void dfs(String digits, int pos) {
if(path.length() == digits.length()) {
ret.add(path.toString());
return;
}
int index = digits.charAt(pos) - '0';
for(int i = 0; i < reflect[index].length(); i++) {
path.append(reflect[index].charAt(i));
dfs(digits, pos + 1);
//回溯 -> 恢复现场
path.deleteCharAt(path.toString().length()-1);
}
}
}
2、括号生成
2.1 算法原理
合法的字符串:
①:"(" 的数量始终大于等于 ")" 的数量
②:"(" 的个数 <= n
③:最终,( 和 ) 的总数为2n,且各数量相等
代码设计:
- 全局变量:left //记录"("数量;right //记录")"数量;ret //返回值;path //路径
- 函数体:dfs(n)
- 剪枝:当left < n时 -> 可添加"(";当right < left时 -> 可添加")"。
- 回溯(恢复现场):①:path.delete(最后一个元素);②:left--/right--;
- 函数出口:left + right == 2*n 或者 right == n
2.2 算法代码
class Solution {
List<String> ret;
StringBuffer path;
int left, right;
public List<String> generateParenthesis(int n) {
ret = new ArrayList<>();
path = new StringBuffer();
dfs(n);
return ret;
}
public void dfs(int n) {
if ((left + right) == (2 * n)) {
ret.add(path.toString());
return;
}
//剪枝
if (left < n) {
path.append("(");
left++;
dfs(n);
//回溯 -> 恢复现场
path.deleteCharAt(path.length() - 1);
left--;
}
//剪枝
if(right < left) {
path.append(")");
right++;
dfs(n);
//回溯 -> 恢复现场
path.deleteCharAt(path.length() - 1);
right--;
}
}
}
3、组合
3.1 算法原理
- 画决策树
- 设计代码
- 函数头:dfs(n, k)
- 函数体:添加当前元素,接着dfs当前元素的下一层(当前位置的下一个位置)
- 函数出口:path.size()==k
- 回溯:将path恢复现场
- 剪枝:从当前位置的下一个位置开始枚举 --> 自动剪枝
3.2 算法代码
class Solution {
List<List<Integer>> ret;
List<Integer> path;
public List<List<Integer>> combine(int n, int k) {
ret = new ArrayList<>();
path = new ArrayList<>();
dfs(n, k, 1);
return ret;
}
public void dfs(int n, int k, int pos) {
//函数出口
if (path.size() == k) {
ret.add(new ArrayList<>(path));
return;
}
//自动剪枝 -> 从当前元素的下一个位置开始枚举
for (int i = pos; i <= n; i++) {
path.add(i);
dfs(n, k, i + 1);
//回溯(恢复现场)
path.remove(path.size() - 1);
}
}
}
4、目标和
4.1 算法原理
- 决策树
- 设计代码
- 全局变量:ret //返回值;path //记录路径和
- 注意:当path为整形时,也可形参传入(处理简单且函数将自动回溯)。但是全局变量形式和形参形式并无好坏之分,两者均可。
- 函数头:dfs(pos,...)//pos为要进行操作的元素的下标(nums中)
- 函数体:分左右两分支,分别进行nums中元素的加减运算
- 函数出口:pos==nums.length,在出口时判断path是否等于target
- 回溯:恢复path
- 剪枝:无
4.2 算法代码【path-->全局变量形式】
class Solution {
int ret;
int pathOfSum;
public int findTargetSumWays(int[] nums, int target) {
dfs(nums, target, 0);
return ret;
}
public void dfs(int[] nums, int target,int pos) {
if(pos == nums.length) {
if(pathOfSum == target) ret++;
return;
}
//加
pathOfSum += nums[pos];
dfs(nums, target, pos + 1);
//回溯
pathOfSum -= nums[pos];
//减
pathOfSum -= nums[pos];
dfs(nums, target, pos + 1);
//回溯
pathOfSum += nums[pos];
}
}
4.3 算法代码【path-->dfs参数形式】
class Solution {
int ret;
public int findTargetSumWays(int[] nums, int target) {
dfs(nums, target, 0, 0);
return ret;
}
public void dfs(int[] nums, int target,int pos, int pathOfSum) {
if(pos == nums.length) {
if(pathOfSum == target) ret++;
return;
}
//加 //参数形式->函数自动回溯
dfs(nums, target, pos + 1, pathOfSum + nums[pos]);
//减 //参数形式->函数自动回溯
dfs(nums, target, pos + 1, pathOfSum - nums[pos]);
}
}
注意:全局变量形式和形参形式并无好坏之分,两者均可。
5、组合总和
5.1 算法原理【策略一】
- 画决策树【决策树并非只有一种!!!】
- 算法思想:DFS回溯搜索即暴力枚举。
注意:为避免结果中数据的重复,每次枚举都应从pos位置开始向后枚举(包含pos位置,因为每一个元素可以重复使用)。
解释:因为2节点得到的[2,3,...]和3节点得到的[3,2,...]的数据组,属于重复枚举,
在选2节点时就已经完成了[2,3,...]的选择,
故在选3节点时,可直接从下一层的3节点处开始选择,即[3,3,...],
故"每次枚举都应从pos位置开始向后枚举"。
5.2 算法代码【策略一】
class Solution {
List<List<Integer>> ret;
int sum;
List<Integer> path;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
ret = new ArrayList<>();
path = new ArrayList<>();
dfs(candidates, target, 0);
return ret;
}
public void dfs(int[] candidates, int target, int pos) {
if(sum >= target) {
if(sum == target) ret.add(new ArrayList<>(path));
return;
}
for(int i = pos; i < candidates.length; i++) {
sum += candidates[i];
path.add(candidates[i]);
//从当前位置开始枚举(因为一个数据可以多次),不能枚举前面的元素,否则重复
dfs(candidates, target, i);
//回溯 -> 清理现场
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
}
5.3 算法原理【策略二】
- 策略二思想:从一个位置开始枚举,然后1个、2个、....的疯狂枚举,只要 <= target,就继续枚举,当 == target时,就添加这个结果,然后递归下层。
- 细节问题:回溯。注意回溯时,本层数据枚举完成后,再进行恢复现场操作。
5.4 算法代码【策略二】
class Solution {
List<List<Integer>> ret;
List<Integer> path;
int sum;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
ret = new ArrayList<>();
path = new ArrayList<>();
dfs(candidates, target, 0);
return ret;
}
public void dfs(int[] candidates, int target, int pos) {
if(sum >= target || pos >= candidates.length) {
if(sum == target) ret.add(new ArrayList<>(path));
return;
}
for(int k = 0; candidates[pos] * k <= target; k++) {
if(k != 0) {
path.add(candidates[pos]);
sum += candidates[pos];
}
dfs(candidates, target, pos + 1);
}
//回溯 -> 恢复现场
for(int k = 1; candidates[pos] * k <= target; k++) {
sum -= candidates[pos];
path.remove(path.size() - 1);
}
}
}
END