文章目录
前言
递归和回溯续:包括有全排列II 、组合总和、组合总和II、子集、子集II、第 k 个排列、复原IP地址。
一、全排列II
题目链接:https://leetcode-cn.com/problems/permutations-ii/
题目描述:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
仿照全排列(n 叉树)
和之前的全排列相比,这里的区别是数字序列包含了重复元素。一个很无脑的方法是将之间全排列解法中的 List 转换为 Set,去掉最后排列结果中的重复序列即可。参考算法如下:
LinkedList<Integer> tmp=new LinkedList<>();
Set<List<Integer>> ans=new HashSet<>();//改为 Set 存储
boolean[] flag;
public List<List<Integer>> permuteUnique(int[] nums) {
int len=nums.length;
flag=new boolean[len];//由于每次选择一个元素之后,该元素就不能再被选取了,所以需要标记状态
solve(nums,len-1);
return new ArrayList<>(ans);
}
public void solve(int[] nums,int end) {
if(tmp.size()==end+1){//得到一次排列时,保存结果
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=0;i<=end;i++){//依次选择每一个元素,由于有标记数组存在,所以此处循环每次都从下标 0 开始。
if(flag[i]==false){
tmp.addLast(nums[i]);
flag[i]=true;
solve(nums,end);
flag[i]=false;
tmp.removeLast();
}
}
}
剪枝(去掉重复的结果)
以数字序列 【1 1 3】为例,仿照之前的全排列构建回溯过程的选择树如下:
初始集合为空 | ||||||
---|---|---|---|---|---|---|
第一轮选择一个元素 | 1 | 1 | 3 | |||
第二轮选择第二个元素 | 1 1 | 1 3 | 1 1 | 1 3 | 3 1 | 3 1 |
第三轮选择第三个元素 | 1 1 3 | 1 3 1 | 1 1 3 | 1 3 1 | 3 1 1 | 3 1 1 |
可以发现重复的结果出现在:第一轮中选择第二个 1 得到的最终结果和第一个 1 是一模一样的,均为 【1 1 3】和【1 3 1】;在第一轮选择 3 的情况下,第二轮的两次选择都是 1,最终结果也产生了重复。
所以剪枝的条件,也即约束条件为:当数组采用升序排列时,如果出现当前元素和上一个元素相等时,就没有必要继续递归遍历了;但这还不够,这样在第二轮就永远不会出现【1 1】序列了。如果第一个 1 被选取了之后,第二个 1 也被选取,这是重复结果的第一次出现,这种情况应该被允许发生;对应的重复结果的第二次出现就不允许发生了,比如在第一轮选择第二个 1 时,第一个 1 未被选取(状态为 false),由于之后的递归遍历得到的都是重复结果,所以这种情况跳过(剪枝)。
所以给出的剪枝条件(约束条件)为:
if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]==false)
continue;
它表示的含义为:仅允许重复的排列结果第一次出现,后面会重复出现的递归遍历被跳过(剪去)。
参考算法如下:
LinkedList<Integer> tmp=new LinkedList<>();
List<List<Integer>> ans=new LinkedList<>();
boolean[] flag;
public List<List<Integer>> permuteUnique(int[] nums) {
int len=nums.length;
flag=new boolean[len];//由于每次选择一个元素之后,该元素就不能再被选取了,所以需要标记状态
Arrays.sort(nums);
solve(nums,len-1);
return ans;
}
public void solve(int[] nums,int end) {
if(tmp.size()==end+1){//得到一次排列时,保存结果
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=0;i<=end;i++){//依次选择每一个元素,由于有标记数组存在,所以此处循环每次都从下标 0 开始。
if(flag[i]==false){
if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]==false)
continue;
tmp.addLast(nums[i]);
flag[i]=true;
solve(nums,end);
flag[i]=false;
tmp.removeLast();
}
}
}
虽说约束条件改为 if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]) 也是可以的,它保存的是重复排列的最后一次出现结果,它和保存第一次出现结果正好相反,并且也不太好理解,不建议采用这种做法。
二、组合总和
题目链接:https://leetcode-cn.com/problems/combination-sum/
题目描述:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
一、初始解法(n 叉树):
直接干:
List<List<Integer>> ans=new ArrayList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
solve(candidates,target);//先排序
return ans;
}
void solve(int[] candi,int target){
if(target==0){
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=0;i<candi.length;i++){
if(candi[i]>target)
break;//由于已经按照升序排序了,直接break即可。
tmp.add(candi[i]);
solve(candi,target-candi[i]);
tmp.remove(tmp.size()-1);
}
}
然而初始解法的答案存在重复情况,对于示例 candidates = [2,3,6,7], target = 7,此解法的答案为:[2, 2, 3] [2, 3, 2] [3, 2, 2] [7]
,这是由于在搜索过程产生了重复,具体的可以仿照上一题画出递归树来比对。关于如何去重有两种方法:
1,采用 Set 去重
参考代码如下:(缺点是效率太慢,勉强能用)
Set<List<Integer>> ans=new HashSet<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
solve(candidates,target);
List<List<Integer>> re=new ArrayList<>();
for(List l:ans)
re.add(l);
return re;
}
void solve(int[] candi,int target){
if(target==0){
List<Integer> t=new LinkedList<>(tmp);
Collections.sort(t);//这里需要先排序,从而在添加进集合的时候去重。
ans.add(t);
return;
}
for(int i=0;i<candi.length;i++){
if(candi[i]>target)
break;
tmp.add(candi[i]);
solve(candi,target-candi[i]);
tmp.remove(tmp.size()-1);
}
}
2,在递归搜索的时候去重(推荐解法)
参考文章:https://leetcode-cn.com/problems/combination-sum/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/,本题的去重方式采用的是设置搜索起点。
初始解法答案为[2, 2, 3] [2, 3, 2] [3, 2, 2] [7]
。约定:第一轮表示选择一个元素的时候,第二轮选择第二个元素,第三轮选择第三个元素,第四轮选择第四个元素。
初始解法的遍历过程:
当第一轮选择 2 时 ,接下来的遍历为:第二轮选2、3、6、7。… 第三轮选2、3、6、7。… 第四轮选2、3、6、7。 …
当第一轮选择 3 时,要么还是按照之前的遍历思路(第二轮选2、3、6、7;… 第三轮选2、3、6、7;… 第四轮选2、3、6、7 …),要么就舍弃第一轮已经选过的 2,新的遍历方式(第二轮选3、6、7;… 第三轮选3、6、7;… 第四轮选3、6、7 …)。
新方式的遍历过程:
okk,感觉这样好像有点合理哈,因为我们的目的是求一些元素的组合,而新的遍历方式并不会漏掉任何组合方式。也刚好解决了初始解法中重复问题(能够轻易的发现[3, 2, 2]
被排除掉了)。
当第一轮选择 2 时,第二轮选择2、3、6、7,第三轮选择 2、3、6、7,第四轮 2、3、6、7时:目标集合[2, 2, 3]
表示第一轮选择 2 、第二轮选择 2 、第三轮选择 3 ;继续执行直到状态为:第一轮选择 2 、第二轮选择 3 时。
如果按照旧的遍历方式,第三轮仍然选 2,得到了重复集合[2, 3, 2]
。但是按照新的遍历方式,在第二轮选择 3 时,第三轮也会从 3 开始选择,所以在此也跳过了重复的集合选取,[2, 3, 2]
就会被排除掉了。
由此可得到的算法参考如下:(采用 begin 变量标记遍历的起点下标)
List<List<Integer>> ans=new ArrayList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
solve(candidates,target,0);
return ans;
}
void solve(int[] candi,int target,int begin){
if(target==0){
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=begin;i<candi.length;i++){
if(candi[i]>target)
break;
tmp.add(candi[i]);
solve(candi,target-candi[i],i);//一旦到了下标i处,打死也不会选择 下标小于i处 的元素了
tmp.remove(tmp.size()-1);
}
}
引申(组合问题)
组合问题的解法-n 叉树
参考上面的新遍历方式的代码可以得到的一种求组合(在candidates中求 k 个数的组合)的解法如下:
List<List<Integer>> ans=new ArrayList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combine(int[] candidates, int k) {
solve(candidates,k,0);
return ans;
}
void solve(int[] candi,int k,int begin){
if(tmp.size()==k){
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=begin;i<candi.length;i++){
if(tmp.size()+candi.length-i)<k)
break; //这里添加了剪枝条件
tmp.add(candi[i]);
solve(candi,k,i+1);//此处同上,选择了 i 处元素之后之后就不会选择下标小于等于i处的元素了
tmp.remove(tmp.size()-1);
}
}
组合问题的解法-二叉树
参考博客:https://blog.csdn.net/Little_ant_/article/details/123676777,代码如下:
List<List<Integer>> ans=new LinkedList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
solve(1,n,k);
return ans;
}
public void solve(int cur,int end,int k){
if(tmp.size()+(end-cur+1)<k)
return;
if(tmp.size()==k){ //碰到每一种成功的情况时应该保存
ans.add(new LinkedList<>(tmp));
return;
}
tmp.add(cur); //选取当前 cur 的情况
solve(cur+1,end,k);
tmp.remove(tmp.size()-1); //不选取的情况
solve(cur+1,end,k);
}
组合问题总结
解法1 如下面的二叉树来示意 (n=4,k=2):(不想画图,凑合一看)
【 】
【1】 【】
【1,2】 【1】 【2】 【】
【1,3】 【1】 【2,3】 【2】 【3】 【】
【1,4】 【1】 【2,4] 【2】 【3,4】 【3】 【4】 【】
解法2 如下面的 n 叉树所示意:
【 】
【1】 【2】 【3】 【4】
【1,2】 【1,3】 【1,4】 【2,3】 【2,4】 【3,4】
以上两种示意图对应两种不同的解法思路。务必领会!
二、另一种解法(二叉树):
组合问题解法1的思路参考上图,本题也可以采用类似的方式,直接上代码:
List<List<Integer>> ans=new ArrayList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
solve(candidates,target,0);
return ans;
}
void solve(int[] candi,int target,int cur){//cur 为当前元素的下标
if(target==0){
ans.add(new LinkedList<>(tmp));
return;
}
if(cur==candi.length)//退出条件1
return;
if(candi[cur]>target)//退出条件2
return;
//选择当前元素,target减小,cur由题意可知,应不变。
tmp.add(candi[cur]);
solve(candi,target-candi[cur],cur);
//不选择当前元素,target不变,cur应加一。
tmp.remove(tmp.size()-1);
solve(candi,target,cur+1);
}
这种解法不会造成有重复结果。因为,一个 solve 中的下标为 start,另一个 solve 中的下标为 start+1,这也表明了,一旦访问到下标 start 处,就再也不会遇到下标小于 start 的元素了。这和初始解法中的推荐解法含义是一样的(用 begin 变量标记下一轮起始元素)
对应的代码为(稍加修改后):
LinkedList<Integer> tmp=new LinkedList<>();
List<List<Integer>> ans=new LinkedList<>();
boolean[] flag;
public List<List<Integer>> permute(int[] nums) {
int len=nums.length;
flag=new boolean[len];//由于每次选择一个元素之后,该元素就不能再被选取了,所以需要标记状态
solve(nums,len-1);
return ans;
}
public void solve(int[] nums,int end) {
if(tmp.size()==end+1){//得到一次排列时,保存结果
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=0;i<=end;i++){//依次选择每一个元素,由于有标记数组存在,所以此处循环每次都从下标 0 开始。
if(flag[i]==false){
tmp.addLast(nums[i]);
flag[i]=true;
solve(nums,end);
flag[i]=false;
tmp.removeLast();
}
}
}
三、组合总和II
题目链接:https://leetcode-cn.com/problems/combination-sum-ii/
题目描述:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
解法1(n 叉树)
初始代码为:
List<List<Integer>> ans=new ArrayList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
solve(candidates,target,candidates.length,0);
return ans;
}
public void solve(int[] c,int target,int len,int begin){
if(target==0){
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=begin;i<len;i++){
if(c[i]>target)
break; //这里和前面的 sort 对应,如果不排序的话应该为 continue
tmp.add(c[i]);
solve(c,target-c[i],len,i+1);//i+1表示不对当前元素重复选取。
tmp.remove(tmp.size()-1);
}
}
但仍会造成重复,重复的原因在于 candidates 数组中本身存在重复元素,比如:[10,1,2,7,6,1,5] ,这里有两个 1 存在。那么如何剪枝去重呢?这里可以先画一画递归搜索树,对 candidates 排完序之后,两个 1 接连排列,在初始代码中:前面那个 1 的搜索空间包含了后面那个 1 的搜索空间,这就是重复发生的原因。解决方法:如果前面那个 1 搜索结束之后,那么后面那个 1 直接跳过,在同一层中的重复数字永远只取第一个。这一点和 全排列II 剪枝非常类似。得到了最终代码如下:
List<List<Integer>> ans=new ArrayList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
boolean[] visited=new boolean[candidates.length];//新加入 visited 数组
solve(candidates,target,candidates.length,0,visited);
return ans;
}
public void solve(int[] c,int target,int len,int begin,boolean[] visited){
if(target==0){
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=begin;i<len;i++){//如果一个 1 被遍历完之后,其他 1 也不能被访问了。
if(c[i]>target)
break;
if(i>0&&c[i]==c[i-1]&&visited[i-1]==false)//false 表示重复元素的第一个已遍历结束,后面的重复元素都需要跳过
continue;
visited[i]=true;
tmp.add(c[i]);
solve(c,target-c[i],len,i+1,visited);
tmp.remove(tmp.size()-1);
visited[i]=false;
}
}
补充:上述代码将 if(i>0&&c[i]==c[i-1]&&visited[i-1]==false) continue;
修改成为 if (i > begin && candidates[i] == candidates[i - 1]) continue;
,也会具有相同的作用。
解法2(二叉树)
初始代码为:
List<List<Integer>> ans=new ArrayList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
solve(candidates,target,candidates.length,0);
return ans;
}
public void solve(int[] c,int target,int len,int cur){
if(target==0){
ans.add(new LinkedList<>(tmp));
return;
}
if(cur==len)
return;
if(c[cur]>target)
return;
tmp.add(c[cur]);
solve(c,target-c[cur],len,cur+1);
tmp.remove(tmp.size()-1);
solve(c,target,len,cur+1);
}
需要去重,保证在同一层中的相同元素永远只取第一个。最终代码如下:
List<List<Integer>> ans=new ArrayList<>();
List<Integer> tmp=new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
solve(candidates,target,candidates.length,0);
return ans;
}
public void solve(int[] c,int target,int len,int cur){
if(target==0){
ans.add(new LinkedList<>(tmp));
return;
}
if(cur==len)// 退出条件1
return;
if(c[cur]>target)// 退出条件2
return;
tmp.add(c[cur]);
solve(c,target-c[cur],len,cur+1);
int a=tmp.get(tmp.size()-1),i;//若当前层有重复元素,那 a 为第一个,并且此处 a 已经被使用过了
tmp.remove(tmp.size()-1);
for(i=cur+1;i<len;i++)//同一层的下一个元素值不能为 a 了,
if(c[i]!=a)
break;
solve(c,target,len,i);
}
四、子集
题目链接:https://leetcode-cn.com/problems/subsets/
题目描述:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
解法1(二叉树)
求集合的子集,本题需要熟悉二叉递归搜索树,因为该树的所有叶子结点就是本题的答案,将所有叶子结点加入到集合并返回。当 nums = { 1, 2, 3 };时,该树如下图所示(画图有点累…):
代码如下:
List<List<Integer>> ans=new LinkedList<>();
Deque<Integer> tmp=new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
solve(nums,0);
return ans;
}
public void solve(int[] nums,int cur){
if(cur==nums.length){
ans.add(new LinkedList<>(tmp));
return;
}
tmp.add(nums[cur]);
solve(nums,cur+1);
tmp.removeLast();
solve(nums,cur+1);
}
解法2(n 叉树)
同理,n 叉递归搜索树的所有结点是本题的答案,所有结点加入集合直接返回即可。当 nums = { 1, 2, 3 };时,该树如下图所示:
代码如下:
List<List<Integer>> ans=new LinkedList<>();
Deque<Integer> tmp=new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
solve(nums,0);
return ans;
}
public void solve(int[] nums,int begin){
ans.add(new LinkedList<>(tmp));
for(int i=begin;i<nums.length;i++){
tmp.add(nums[i]);
solve(nums,i+1);
tmp.removeLast();
}
}
五、子集II
题目链接:https://leetcode-cn.com/problems/subsets-ii/
题目描述:给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
解法1(二叉树)
在上题的基础上去重即可,代码如下:
List<List<Integer>> ans=new LinkedList<>();
Deque<Integer> tmp=new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);//含有重复元素要记得排序哦。
solve(nums,0);
return ans;
}
public void solve(int[] nums,int cur){
if(cur==nums.length){
ans.add(new LinkedList<>(tmp));
return;
}
tmp.add(nums[cur]);
solve(nums,cur+1);
int a=tmp.peekLast(),i;
tmp.removeLast();
for(i=cur+1;i<nums.length;i++)
if(nums[i]!=a)
break;
solve(nums,i);
}
解法2(n 叉树)
同理,代码如下:
List<List<Integer>> ans=new LinkedList<>();
Deque<Integer> tmp=new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
solve(nums,0);
return ans;
}
public void solve(int[] nums,int begin){
ans.add(new LinkedList<>(tmp));
for(int i=begin;i<nums.length;i++){
if(i>begin&&nums[i]==nums[i-1])//如果有重复元素,只会在其第一次出现时选取
continue;
tmp.add(nums[i]);
solve(nums,i+1);
tmp.removeLast();
}
}
采用标记数组的代码如下:
List<List<Integer>> ans=new LinkedList<>();
Deque<Integer> tmp=new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
boolean[] visited=new boolean[nums.length];
solve(nums,0,visited);
return ans;
}
public void solve(int[] nums,int begin,boolean[] visited){
ans.add(new LinkedList<>(tmp));
for(int i=begin;i<nums.length;i++){
if(i>0&&nums[i]==nums[i-1]&&visited[i-1]==false)
continue;
visited[i]=true;
tmp.add(nums[i]);
solve(nums,i+1,visited);
tmp.removeLast();
visited[i]=false;
}
}
六、排列序列
题目链接:https://leetcode-cn.com/problems/permutation-sequence/
题目描述:给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
“123”
“132”
“213”
“231”
“312”
“321”
给定 n 和 k,返回第 k 个排列。
解法1(回溯)
在全排列的基础上要进行剪枝,假设第一轮为选取第一个元素时,第二轮为选取第二个元素时… 那么基本思想是,在一次第一轮遍历可以得到 n-1! 种结果,在一次第二轮遍历可以得到 n-2! 种结果… 既然这样,那么在每一轮遍历开始之前先判断本轮能得到几种结果假设为 tmp,如果它小于 k 的话,本轮直接跳过,令 k=k-tmp,继续下一轮判断;如果 tmp 大于 k 的话,则证明最终的第 k 种结果在本轮中出现,这样就可以避免很多次没必要的递归遍历了,事实上它只会找到我们想要的第 k 次排列,其余的排列根本不会被遍历到。
初始代码如下(有点粗糙):
boolean[] used;// 用来保障全排列的 used 数组
boolean flag=false;// 递归退出标志
public String getPermutation(int n, int k) {
StringBuilder re=new StringBuilder();
used=new boolean[n+1];
solve(re,(char)('0'+n),k);
return new String(re);
}
public void solve(StringBuilder re,char n,int k){
if(re.length()==n-'0'){
flag=true;
return;
}
for(char i='1';i<=n;i++){
if(used[i-'0']==false){
//通过 used 中的 false 变量的个数来判断本轮可以产生多少种结果数,由 tmp 来表示。
//比如 false 的个数 count 为 0,表明这是第一轮,本轮产生的结果数 tmp = n-1!。依次类推。
int tmp=1,count=0; // tmp 初始化为 1,因为当 count 为 1 时,0!等于 1,
for(int j='1'-'0';j<n-'0'+1;j++){
if(used[j]==false)
count++;
}
for(int j=1;j<count;j++)
tmp=tmp*j;
//当 tmp < k 时,修改 k 的值,跳过本轮进入下一轮。
if(tmp<k){
k-=tmp;
continue;
}
re.append(i);
used[i-'0']=true;
solve(re,n,k);
if(flag) //当 flag 为 true,禁止程序对 re 变量进行改变,此时程序直接一层层退出。
break;
used[i-'0']=false;
re.deleteCharAt(re.length()-1);
}
}
}
修改整理之后的代码如下:
boolean[] used;// 全排列需要的 used 数组
boolean flag=false;// 退出标志
int[] f;// 阶乘值,即 f[i] 表示 i 的阶乘
public String getPermutation(int n, int k) {
StringBuilder re=new StringBuilder();
used=new boolean[n+1];
f=new int[n];
f[0]=1;
for(int i=1;i<n;i++)
f[i]=i*f[i-1];
solve(re,(char)('0'+n),k,n-1);//新加的参数 cc 表示本轮可产生 cc! 种结果,即 f[cc]
return new String(re);
}
public void solve(StringBuilder re,char n,int k,int cc){
if(re.length()==n-'0'){
flag=true;
return;
}
for(char i='1';i<=n;i++){
if(used[i-'0']==false){
if(f[cc]<k){
k-=f[cc];
continue;
}
re.append(i);
used[i-'0']=true;
solve(re,n,k,cc-1);
if(flag)
break;
used[i-'0']=false;
re.deleteCharAt(re.length()-1);
}
}
}
解法2(循环)
如果想通过 k 值直接得到最终的字符串,那就在循环里一轮轮判断,内在规律和上面的回溯一样。循环部分采用减法来做的代码如下:
public String getPermutation(int n, int k) {
int[] f=new int[n];
f[0]=1;
for(int i=1;i<n;i++)
f[i]=i*f[i-1];
StringBuilder re=new StringBuilder();
List<Character> c=new LinkedList<>();
for(char i='1';i<=n+'0';i++)
c.add(i);
int index=n-1,indexOfC=0;//标记变量。
while(re.length()!=n){
if(k<=f[index]){
re.append(c.get(indexOfC));
c.remove(indexOfC);
indexOfC=0;
index--;
}
else{
indexOfC++;
k-=f[index];
}
}
return new String(re);
}
七、复原 IP 地址
题目链接:https://leetcode-cn.com/problems/restore-ip-addresses/
题目描述:有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
解法(三叉树)
一个有效的整数可能有一位、两位或者三位,对每一个整数分别取一、二和三位递归回溯即可。在回溯的过程中剪枝,对于不满足条件的情况直接跳过。自己列出了三种情况,分别为:1,每次选取一个整数之后,剩下数的个数不能大于未选取整数的三倍。2,每次选择一个整数时,该数不能大于 255 。3,每次选择一个整数时,如果该整数大于一位,那么最高位不能为 0 。参考代码如下:
List<String> ans=new LinkedList<>();
StringBuilder tmp=new StringBuilder();
public List<String> restoreIpAddresses(String s) {
solve(s,0,s.length(),0);
return ans;
}
public void solve(String s,int start,int end,int count){
if(count==4){
ans.add(tmp.substring(0,tmp.length()-1));//选取4个整数之后,去掉最后一个字符,保存结果
return;
}
for(int remain=start+1;remain<=end&&remain<=start+3;remain++){//[0,remain):已选取字符 [remain,end):未选取字符
if(end-remain>3*(4-count-1)) // 1
continue;
String ss=s.substring(start,remain);
if(ss.length()>1&&ss.charAt(0)=='0') // 3
continue;
int v=Integer.parseInt(ss);
if(v>255)// 2 这里不能采用String的compareTo方法比较,因为"35"大于"255"
continue;
tmp.append(ss).append('.');
solve(s,remain,end,count+1);
int begin=ss.length()+1;
tmp.delete(tmp.length()-begin,tmp.length());
}
}
总结
完