78.子集
class Solution {
List> res = new ArrayList<>();
LinkedList
public List> subsets(int[] nums) {
if(nums.length == 0){
res.add(new ArrayList<>());
return res;
}
backtrack(nums,0);
return res;
}
public void backtrack(int[] nums,int startIndex){
res.add(new ArrayList<>(path));
if(startIndex >= nums.length){ //终止条件
return;
}
for(int i = startIndex; i < nums.length; i++){ //选择路径
path.add(nums[i]); //做选择
backtrack(nums,i+1); //递归进入下一层
path.removeLast(); //撤销选择
}
}
}
90.子集||
class Solution {
List> res = new ArrayList<>();
LinkedList
boolean[] used;
public List> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
used = new boolean[nums.length];
if(nums.length == 0){
res.add(new ArrayList<>());
return res;
}
backtrack(nums,0);
return res;
}
private void backtrack(int[] nums,int startIndex){
res.add(new ArrayList<>(path));
if(startIndex >= nums.length){
return;
}
for(int i = startIndex;i < nums.length;i++){
if(i>0 && nums[i] == nums[i-1] && !used[i-1]){ //还有i>0
continue;
}
path.add(nums[i]); //选择
used[i] = true; //标记
backtrack(nums,i+1); //递归
path.removeLast(); //撤销选择
used[i] = false;
}
}
}
遇上题区别是,要发生剪枝。剪枝条件的判断是难点。
47.全排列 ||
class Solution {
List> res = new ArrayList<>();
List
//boolean used[] ;
public List> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used,false);
Arrays.sort(nums);
backtrack(nums,used);
return res;
}
private void backtrack(int[] nums,boolean[] used){
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i = 0; i < nums.length; i++){
if(i > 0 && nums[i] == nums[i-1] && used[i-1]==false){
continue;
}
if(used[i] == false){
used[i] = true;
path.add(nums[i]);
backtrack(nums,used);
path.remove(path.size()-1);
used[i] = false;
}
}
}
}
与前两题区别是,此题返回的是全排列,即回溯树的叶子节点,此题同样也用到了剪枝。
小结:组合问题和排列问题是在树形结构的叶子节点上收集结果,子集问题是取树上所有节点的结果。
回溯步骤:
39.组合总和
class Solution {
List> res = new ArrayList<>(); //全局变量
List
int sum = 0;
public List> combinationSum(int[] candidates, int target) {
if(candidates.length == 0){
res.add(new ArrayList<>());
return res;
}
Arrays.sort(candidates); //一定要先进行排序
backtrack(candidates,target,0);
return res;
}
public void backtrack(int[] candidates, int target,int start){
if(sum == target){
res.add(new ArrayList<>(path));
return;
}
for(int i = start; i < candidates.length; i++){
if(sum + candidates[i] > target) break; //注意
sum += candidates[i];
path.add(candidates[i]);
backtrack(candidates,target,i); //元素可以重复使用,所以进入下一层依然i
sum -= candidates[i];
path.remove(path.size()-1);
}
}
}
求组合用回溯方法,元素可以重复使用,所以 backtrack(candidates,target,i);传递参数为i。画回溯树时,同层遍历需注意,元素只能向后取。
40.组合总和 ||
class Solution {
List> res = new ArrayList<>();
List
int sum = 0; //定义全局变量可减少方法参数
public List> combinationSum2(int[] candidates, int target) {
if(candidates.length == 0){
res.add(new ArrayList<>());
return res;
}
Arrays.sort(candidates); //一定要进行排序
backtrack(candidates,target,0);
return res;
}
public void backtrack(int[] candidates,int target, int start){
if(sum == target){
res.add(new ArrayList<>(path));
return;
}
for(int i = start;i < candidates.length; i++){
if(i > 0 && i > start && candidates[i] == candidates[i-1]){
continue; //难点剪枝判断,前一位已经取过,不能再取
}
if(sum < target){
sum += candidates[i];
path.add(candidates[i]);
backtrack(candidates,target,i+1); //不能重复用同一元素,所以i+1
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
}
}
与39组合总和基本相同,不同之处是,此题不能重复使用同一元素,所以进入下层递归时传递参数i+1。
17.电话号码的字母组合
class Solution {
List
public List
if(digits.length() == 0){
return res;
}
String[] nums = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
backtrack(digits,nums,0);
return res;
}
StringBuilder temp = new StringBuilder();
public void backtrack(String digits,String[] nums,int start){
if(start == digits.length()){
res.add(temp.toString());
return;
}
String str = nums[digits.charAt(start) - '0'] ; //stirngBuilder用法,进入下一层字母集
for(int i = 0; i < str.length() ; i++){ //在本层遍历
temp.append(str.charAt(i)); //stirngBuilder用法
backtrack(digits,nums,start+1); //start+1,进入下一层,digits的下一个数字
temp.deleteCharAt(temp.length()-1);
}
}
}
回溯问题模板题,难点是建立数字和字母的映射关系,以及递归进入下一层的参数传递。
22.括号生成
class Solution {
List
public List
if (n == 0) {
return res;
}
dfs("", 0, 0, n);
return res;
}
private void dfs(String path, int left, int right, int n){
if (left == n && right == n) {
res.add(path);
return;
}
if(left < right){ //剪枝条件
return;
}
if (left < n) {
dfs(path + "(", left + 1, right, n); //注意递归条件,需加入path
}
if (right < n){
dfs(path + ")", left, right + 1, n);
}
}
}
画出回溯树,可知剪枝条件left < right,之后就是回溯算法的固定模板。
79.单词搜索
class Solution {
boolean flag = false;
public boolean exist(char[][] board, String word) {
int m = board.length;
int n = board[0].length;
boolean[][] vis = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
backtrack(board, word, vis, i, j, 0);
}
}
return flag;
}
public void backtrack(char[][] board, String word, boolean[][] vis, int i, int j, int start) {
if (start == word.length()) {
flag = true;
return;
}
if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || word.charAt(start) != board[i][j] || vis[i][j] ) {
return;
}
vis[i][j] = true;
backtrack(board, word, vis, i + 1, j, start + 1);
backtrack(board, word, vis, i - 1, j, start + 1);
backtrack(board, word, vis, i, j + 1, start + 1);
backtrack(board, word, vis, i, j - 1, start + 1);
vis[i][j] = false;
}
}
回溯法搜索单词,因为有顺序,所以要定义vis矩阵来标记,进入下一层有上、下、左、右四个不同搜索方向,来搜索start + 1个字母。进入下一层搜索的前提是已经搜索到了start个字母。