回溯算法之组合问题

1. 组合

回溯法又叫回溯搜索法,是一种搜索算法.回溯是递归的副产品.回溯算法的本质是穷举,虽然可以有剪枝操作,但本质上还是穷举,效率不高.一些问题由于没有别的解法,所以我们只能使用回溯算法
回溯法解决的问题可以抽象为树形结构,集合的大小决定了数的宽度(分支数),递归的深度决定了树的深度(例如k个元素的子集,k就是递归深度)
77.
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

1. 题解

本题首先要画递归树,整体上了解递归树的遍历顺序(细节可以不用扣太细,可能会把自己搞晕),然后将具体问题代入模板即可.关键:画出递归树
递归树的统一画法: 叶子节点为可选集合,分支为选择集合中的某一个元素.递归的顺序为整体上从左到右(所有子节点间),局部从上到下(单个节点).
在这里插入图片描述

2. 代码

 private List<Integer> ipath=new ArrayList<Integer>();
    private  List<List<Integer>> ires=new ArrayList<List<Integer>>();
    private List<Character> cpath=new ArrayList<Character>();
    private  List<List<Character>> cres=new ArrayList<List<Character>>();
public List<List<Integer>> combine(int n, int k) {
    tracebackingOfcombine(n,k,1);
    return ires;
}
public void tracebackingOfcombine(int n,int k,int startIndex){
    if (ipath.size()==k){
        ires.add(new ArrayList<>(ipath));
        return;
    }
    for (int i = startIndex; i <=n; i++) {
        ipath.add(i);
        tracebackingOfcombine(n,k,i+1);
        ipath.remove(ipath.size()-1);
    }
}

3. 剪枝

当当前数组长度(递归宽度)+当前path数组内的元素<k时,数组长度不可能到达,可以剪枝
在这里插入图片描述

/ 组合+剪枝
    public void tracebackingOfcombine02(int n,int k,int startIndex){
        if (ipath.size()==k){
            ires.add(new ArrayList<>(ipath));
            return;
        }
        for (int i = startIndex; i <=n; i++) {
            if (ipath.size()+n-startIndex+1<k){
                return;
            }
            ipath.add(i);
            tracebackingOfcombine(n,k,i+1);
            ipath.remove(ipath.size()-1);
        }
    }

2.组合求和

力扣题目链接(opens new window)

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

所有数字都是正整数。
解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]

示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]

1.思路

本题为组合问题,初始数组[1,…,9],递归深度 k, 终止条件 (path.size==k&&sum==n),初始值 startIndex=1,剪枝条件(组合传统剪枝+sum>=n)

2. 代码

 private List<Integer> ipath=new ArrayList<Integer>();
    private  List<List<Integer>> ires=new ArrayList<List<Integer>>()
 // 9. 组合总和
public List<List<Integer>> combinationSum3(int k, int n) {
    combinationSum3trancebacking(n,1,k,0);
    return ires;
}
public void combinationSum3trancebacking(int n,int startIndex,int k,int sum){
    if (ipath.size()==k&&sum==n){
        ires.add(new ArrayList<>(ipath));
        return;
    }
    for (int i = startIndex; i <=9; i++) {
        if (ipath.size()+9-startIndex+1<k&&sum>n){
            return;
        }
        ipath.add(i);
        sum+=i;
        combinationSum3trancebacking(n,i+1,k,sum);
        //回溯
        ipath.remove(ipath.size()-1);
        sum-=i;
    }

}

3. 上述代码有什么缺点?

== 深度剪枝==

    if (ipath.size()==k&&sum==n){
        ires.add(new ArrayList<>(ipath));
        return;
    }

修改

在这里插入代码片

这段代码,当path的长度到达k,但是sum没有达到标准,会继续向深处执行,并没有达到在第k层就分会的目的.
修改

    if (ipath.size()==k){
     if(sum==n){
         ires.add(new ArrayList<>(ipath));
     }
     
     return;
 }

广度剪枝

  if (ipath.size()+9-startIndex+1<k&&sum>n){
         return;
     }
    这段代码应该是|| 才能最大化的剪枝

4. 特殊情况:求两个值的和时

在这里插入图片描述
先排序,然后使用双指针

 // 法二 双指针法
       public int[] twoSum(int[] nums, int target) {
           int p=0,q=nums.length-1;
           while (p<q){
               if (nums[p]+nums[q]>target){
                   q--;
               }else if (nums[p]+nums[q]<target){
                   p++;
               }else {
                   break;
               }
           }
           return new int[]{nums[p],nums[q]};

       }

3. 组合求和(数组元素可以被重复选取)

1. 思路

终止条件: path和==总和
剪枝:
本题相较与2.组合总和剪枝策略有改进,
上一题中,
if (ipath.size()+9-startIndex+1<k&&sum>n){
return;
}
是当前节点的父节点的sum>n,直接当前节点直接不用加入和递归其子节点了,直接返回
而本题
if (sum+candidates[i]>target){
continue;// 直接剪掉一个不可能的分支,从其兄弟节点进行排查
}
尝试加入当前节点,如果大于sum,则直接放弃该节点,去兄弟节点尝试.
在这里插入图片描述

2. 代码 (标准形式)

//11. 组合总和+重复选取
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        combinationSumTracebacking(candidates,target,0,0);
        return ires;

    }
    public void combinationSumTracebacking(int[] candidates, int target,int sum,int startIndex) {
        if (sum==target){
            ires.add(new ArrayList<>(ipath));
            return;
        }

        for (int i = startIndex; i <candidates.length ; i++) {
            if (sum+candidates[i]>target){
                continue;// 直接剪掉一个不可能的分支,从其兄弟节点进行排查
            }
            ipath.add(candidates[i]);
            sum+=candidates[i];
            combinationSumTracebacking(candidates,target,sum,i);
            ipath.remove(ipath.size()-1);
            sum-=candidates[i];
        }

    }

4. 数组之间的组合

17.电话号码的字母组合
力扣题目链接(opens new window)

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
在这里插入图片描述

17.电话号码的字母组合

示例: 输入:“23” 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].

说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

1. 题解

在这里插入图片描述

2.代码

// 10. 数组之间的组合
         private List<String> res=new ArrayList<>();
    public List<String> letterCombinations(String digits) {
      if (digits==null||digits.length()==0){
        return res;
    }
        letterCombinationsteacebacking(digits,0,new StringBuilder());
        return res;
    }
    public void letterCombinationsteacebacking(String digits,int startIndex,StringBuilder path) {
    if (path.length()==digits.length()){
        res.add(new String(path));
        return;
    }
        
            ArrayList<Character> chars = getStrings(digits.charAt(startIndex));
            for (int j = 0; j <chars.size() ; j++) {
                path.append(chars.get(j));
                letterCombinationsteacebacking(digits,startIndex+1,path);
                path.deleteCharAt(path.length()-1);
            }

        

    }

    private ArrayList<Character> getStrings(char ch) {
        ArrayList<Character> chars = new ArrayList<>();
        if (ch-'0'==0){
            chars.add('!');
            chars.add('@');
            chars.add('#');
        }
        if (2<=ch-'0'&&ch-'0'<=7){
            chars.add((char) ('a'+(ch-'0'-2)*3));
            chars.add((char) ('a'+(ch-'0'-2)*3+1));
            chars.add((char) ('a'+(ch-'0'-2)*3+2));
            if (ch-'0'==7){
                chars.add('s');
            }

        }
        if (ch-'0'==8){
            chars.add('t');
            chars.add('u');
            chars.add('v');
        }
        if (ch-'0'==9){
            chars.add('w');
            chars.add('x');
            chars.add('y');
            chars.add('z');
        }
        return chars;

    }

5.总结

组合问题的关键是画出递归图,然后代入模板,观察题目特性,生成剪枝条件.在递归出口处的剪枝条件可以横向剪枝(剪掉若干层),在循环里面的剪枝可以纵向剪枝,剪 掉一棵树.当遇到组合问题的时候,可以往本文讲的几道问题上靠拢.
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

弈师亦友

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值