赞
踩
排列问题
46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
排列问题,采用回溯算法解决,首先将选择过程想成树型结构,并通过回溯,得到所有的结果。
排列问题需要考虑顺序,此类回溯通用解法,设一个二维数组res保存所有的结果,一个数组path保存当前搜索的路径,depth表示当前树中的深度,对于排列问题,设置一个used数组,记录目前数字是否使用,避免重复使用。
class Solution { public List<List<Integer>> permute(int[] nums) { int len=nums.length; List<List<Integer>> res=new ArrayList<>(); if(len==0)return res; boolean[] used=new boolean[len]; List<Integer> path=new ArrayList<>(); dfs(nums,len,0,path,used,res); return res; } public void dfs(int nums[],int len,int depth,List<Integer> path,boolean[] used,List<List<Integer>> res){ if(depth==len){ //res.add(path); //为什么会出现空了。实际上path也确实被加进去了,但是由于Java的特性,下次递归的时候,实际上是又在修改这个path,以及相应的res,致使在最后一次递归复原的时候,把path加入的东西全吐出来了 res.add(new ArrayList<>(path));//达到最深处,将当前路径添加到结果中 return; } for(int i=0;i<len;i++){ if(!used[i]){ path.add(nums[i]); used[i]=true; dfs(nums,len,depth+1,path,used,res);//递归的执行下一层。 used[i]=false;//回溯过程,退回一个。 path.remove(path.size()-1); } } } }
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
与上一题的区别是,这个nums中包含重复的数字,要处理掉重复的结果,进行剪枝。
先把数字排序,在有序的条件下
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1])
因为数组是有序的,nums[i-1]是前一个撤销的数字,如果目前的值和前一个撤销的数字相同,即可进行剪枝(cntinue)。 例如:当前路径中为1,2,前一个撤销的数字为3;说明1,2,3开头的排列都已经找完了,如果nums[i]还等于3,即可剪枝跳过,去掉重复的值。
class Solution { public List<List<Integer>> permuteUnique(int[] nums) { int len=nums.length; List<List<Integer>> res=new ArrayList<>(); if(len==0)return res; Arrays.sort(nums);//先经过排序,后续剪枝的num[i-1]才能保证是上一步撤销的值。 boolean[] used=new boolean[len]; // 使用 Deque 是 Java 官方 Stack 类的建议 Deque<Integer> path = new ArrayDeque<>(len); dfs(nums,len,0,path,used,res); return res; } public void dfs(int nums[],int len,int depth,Deque<Integer> path,boolean[] used,List<List<Integer>> res){ if(depth==len){ res.add(new ArrayList<>(path)); return; } for(int i=0;i<len;i++){ if (used[i]) { continue; } // 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义 // 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择 if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { continue; } path.addLast(nums[i]); used[i] = true; dfs(nums, len, depth + 1, path, used, res); // 回溯部分的代码,和 dfs 之前的代码是对称的 used[i] = false; path.removeLast(); } }}
组合问题
39. 组合总和
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。
对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
示例 1:
输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]
组合问题区别于排列,不使用used[]数组,如果使用used数组,当选择了1后,再选择2;如果选择了2,used[1]还是0,可以被选择,所以会产生两个重复的组合。
所以组合不使用used数组,而是用begin,在数组有序的情况下,不会选择前面的数字。
例如path[1,2],begin=2,不会再去寻找前面的值
和为7,根节点为7,尝试减去给定数组的各个值,如果最后在叶子节点中能够减为0,则该路径上的值的和为target。
class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { int len=candidates.length; List<List<Integer>> res=new ArrayList<>(); Deque<Integer> path=new ArrayDeque<>(len); Arrays.sort(candidates);//一定要排序 if(len==0)return res; dfs(candidates,len,0,path,res,target);//初始begin起始点为0 return res; } public void dfs(int[] nums, int len, int begin, Deque<Integer> path, List<List<Integer>> res,int target){ if(target==0){ res.add(new ArrayList<>(path)); return; } for(int i=begin;i<len;i++){ if(target-nums[i]<0){//剪枝条件 break; } path.addLast(nums[i]); dfs(nums,len,i,path,res,target-nums[i]); path.removeLast(); } } }
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
与上一题的区别是每个数字只能使用一次。
递归的dfs中,传递的begin值为i+1,因为每个元素只能使用一次。
public class Solution { public List<List<Integer>> combinationSum2(int[] candidates, int target) { int len = candidates.length; List<List<Integer>> res = new ArrayList<>(); if (len == 0) { return res; } // 关键步骤 Arrays.sort(candidates); Deque<Integer> path = new ArrayDeque<>(len); dfs(candidates, len, 0, target, path, res); return res; } private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) { if (target == 0) { res.add(new ArrayList<>(path)); return; } for (int i = begin; i < len; i++) { if (target - candidates[i] < 0) { break; } if (i > begin && candidates[i] == candidates[i - 1]) { continue; }//数组中有多个相同数字,保证不产生重复的结果。 path.addLast(candidates[i]); // 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i dfs(candidates, len, i + 1, target - candidates[i], path, res); path.removeLast(); } } }
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
也是一个不能重复选择的组合,多了个参数k,找到指定的个数的即可。
class Solution { public List<List<Integer>> combine(int n, int k) { int[] candidates=new int[n+1]; for(int i=1;i<=n;i++){ candidates[i]=i; } int len = candidates.length; List<List<Integer>> res = new ArrayList<>(); if (len == 0) { return res; } Deque<Integer> path = new ArrayDeque<>(len); dfs(candidates, len, 1, path, res,k,0); return res; } private void dfs(int[] candidates, int len, int begin,Deque<Integer> path, List<List<Integer>> res,int k,int depth) { if(depth==k){ res.add(new ArrayList<>(path)); return; } for(int i=begin;i<len;i++){ path.addLast(candidates[i]); dfs(candidates, len, i + 1, path, res,k,depth+1); path.removeLast(); } } }
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
本质也是组合,不过函数每次执行都把当前路径加到结果中,保证子集都考虑。
class Solution { public List<List<Integer>> subsets(int[] nums) { int len = nums.length; List<List<Integer>> res = new ArrayList<>(); // 关键步骤 Arrays.sort(nums); Deque<Integer> path = new ArrayDeque<>(len); dfs(res,nums,path,0,len); return res; } private void dfs(List<List<Integer>> res,int[] nums,Deque<Integer> path,int begin,int length) { res.add(new ArrayList<>(path)); for(int i=begin;i<length;i++){ path.addLast(nums[i]); dfs(res,nums,path,i+1,length); path.removeLast(); } } }
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
与上一道题的区别是,nums中可能包含重复元素。与之前的组合类似,需要进行剪枝,如果nums[i]==nums[i-1]就进行剪枝。
class Solution { public List<List<Integer>> subsetsWithDup(int[] nums) { int len = nums.length; List<List<Integer>> res = new ArrayList<>(); // 关键步骤 Arrays.sort(nums); Deque<Integer> path = new ArrayDeque<>(len); dfs(res,nums,path,0,len); return res; } private void dfs(List<List<Integer>> res,int[] nums,Deque<Integer> path,int begin,int length) { res.add(new ArrayList<>(path)); for(int i=begin;i<length;i++){ if(i>begin&&nums[i]==nums[i-1]){ continue; } path.addLast(nums[i]); dfs(res,nums,path,i+1,length); path.removeLast(); } } }
给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
"123"
"132"
"213"
"231"
"312"
"321"
给定 n 和 k,返回第 k 个排列。
如果采用回溯法解决,很容易超时,所以要采用正确的剪枝方法,我的做法是,本题求第k个,当搜索到第k个之后就直接结束回溯,不在继续后面的,但是这样也很浪费时间。
把递归函数设置为布尔型,把递归函数的执行放到if条件判断中,保证执行
class Solution { boolean[] def; StringBuilder res; int count; int k; int n; public String getPermutation(int n, int k) { this.k = k; this.n = n; def = new boolean[n+1]; // 哈希表做访问标记 count++; res = new StringBuilder(); // 可变res dfs(0); return res.toString(); } boolean dfs(int depth){ if(depth == n){ if(count == k){ // 说明已经找到了第 k 个,直接返回true,方便剪枝 return true; } else{ ++count; // 还没找到第 k 个,所以count自增,并且返回false。 return false; } } for(int i = 1;i <= n;++i){ if(def[i]) continue; // 已访问元素直接略过,i从1开始保证了k个结果的大小顺序 def[i] = true; // 未访问元素先做标记 res.append(i); if(dfs(depth+1)) return true; // 大剪枝,也就是说找到了元素,此后就不必再找了 res.deleteCharAt(res.length()-1); // 每次删除res的最后一个字符,回溯。 def[i] = false; // 恢复未访问标记 } return false; } }
看了回溯的高效解法,当搜索进行到某一层时,先查看该点下面一共有多少种可能,如果小于给定的k,可以直接进行剪枝,因为就算把下面的路径都搜索出来,也达不到k。
import java.util.Arrays; public class Solution { private boolean[] used; /** * 阶乘数组 */ private int[] factorial; private int n; private int k; public String getPermutation(int n, int k) { this.n = n; this.k = k; calculateFactorial(n); used = new boolean[n + 1]; Arrays.fill(used, false); StringBuilder path = new StringBuilder(); dfs(0, path); return path.toString(); } /** * @param index 在这一步之前已经选择了几个数字,其值恰好等于这一步需要确定的下标位置 * @param path */ private void dfs(int index, StringBuilder path) { if (index == n) { return; } // 计算还未确定的数字的全排列的个数,第 1 次进入的时候是 n - 1 int cnt = factorial[n - 1 - index]; for (int i = 1; i <= n; i++) { if (used[i]) { continue; } if (cnt < k) { k -= cnt;//当前点之下可能的结果达不到k,直接换下一个,并从k中减去。 continue; } path.append(i); used[i] = true; dfs(index + 1, path); // 注意 1:不可以回溯(重置变量),算法设计是「一下子来到叶子结点」,没有回头的过程 // 注意 2:这里要加 return,后面的数没有必要遍历去尝试了 return; } } /** * 计算阶乘数组 * * @param n */ private void calculateFactorial(int n) { factorial = new int[n + 1]; factorial[0] = 1; for (int i = 1; i <= n; i++) { factorial[i] = factorial[i - 1] * i; } } }
给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。
示例:
输入:S = “a1b2”
输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]
输入:S = “3z4”
输出:[“3z4”, “3Z4”]
输入:S = “12345”
输出:[“12345”]
此题虽说是字符串的排列,但实际上字母的顺序并不能改变,不能用used数组,应该用begin设置初始值。
遇到数字,直接进行递归回溯,如果是字母,先将其大写字母进行递归回溯,再将其小写字母递归回溯即可。
class Solution { private List<String> res = new ArrayList<>(); public List<String> letterCasePermutation(String s) { StringBuilder sb = new StringBuilder(); backtrack(s, 0, sb); return res; } public void backtrack(String s, int start, StringBuilder sb) { if (sb.length() == s.length()) { res.add(sb.toString()); return ; } for (int i=start;i<s.length();i++) { char c = s.charAt(i); if (Character.isDigit(c)) { sb.append(c); backtrack(s, i + 1, sb); sb.deleteCharAt(sb.length() - 1); // 撤销选择 } else { char A = Character.toUpperCase(c);//大写执行一遍 sb.append(A); backtrack(s, i + 1, sb); sb.deleteCharAt(sb.length() - 1); char a = Character.toLowerCase(c);//小写执行一遍 sb.append(a); backtrack(s, i + 1, sb); sb.deleteCharAt(sb.length() - 1); } } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。