回溯:组合问题

LeetCode77. 组合

https://leetcode-cn.com/problems/combinations/

给定两个整数 nk,返回 1 … n 中所有可能的 k 个数的组合。

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

思路

回溯模板

void backtracking(选择列表,路径,结果) 
{
    if (终止条件) 
    {
        存放结果;
        return;
    }
    
    //控制树的横向遍历
    for (在选择列表中选择) 
    {
        处理节点;
        backtracking(选择列表,路径,结果);  //递归:控制树的纵向遍历
        回溯,撤销处理结果
    }
}

所有用回溯解决的问题都可以抽象为树形结构

回溯:组合问题

回溯三部曲

1.递归函数的返回值和参数

定义两个全局变量,一个用来存放符合条件的路径path,一个用来存放所有符合条件的路径的结果集合result。

函数里一定有两个参数:集合n里面取k个数的组合,那么n和k是函数的两个int型参数。

还有一个int型参数startIndex,这个参数用来记录本层递归中,选择列表从哪里开始遍历(选择列表就是[1,…,n] )。随着递归控制树的纵向遍历,可选择的范围进而收缩,调整可选择的范围,就是要靠startIndex。例如,在上图中,在集合[1,2,3,4]取1之后,下一层递归就要从[2,3,4]中取数了,那么下一层递归如何知道要从[2,3,4]中取数呢,就是要靠startIndex来记录下一层递归搜索的起始位置。

    vector<int> path;  //存放符合条件的路径
    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合

    void backtrack(int n, int k, int startIndex)

2.递归函数的终止条件

path的大小如果达到k,那么就说明我们找到一个大小为k的组合了,在上图中path存的就是根节点到叶子节点的路径。此时用result把path保存起来,并终止本层递归返回。

        if (path.size() == k)
        {
            result.push_back(path);
            return;
        }

3.搜索过程

我们定义的backtrack函数其实就像一个指针,在树中游走同时正确维护每个节点的属性。每当走到叶子节点(即碰到递归函数的终止条件),就保存符合条件的路径,返回并回溯。

        //控制树的横向遍历
        for (int i = startIndex; i <= n; ++i)
        {
            path.push_back(i);

            backtrack(n, k, i + 1);  //递归:控制树的纵向遍历

            path.pop_back();
        }

代码

class Solution 
{
private:
    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合result
    vector<int> path;  //存放符合条件的路径path

    void backtrack(int n, int k, int startIndex)
    {
        if (path.size() == k)
        {
            result.push_back(path);
            return;
        }

        //控制树的横向遍历
        for (int i = startIndex; i <= n; ++i)
        {
            path.push_back(i);

            backtrack(n, k, i + 1);  //递归:控制树的纵向遍历

            path.pop_back();
        }
    }

public:
    vector<vector<int>> combine(int n, int k) 
    {
        backtrack(n, k, 1);

        return result;
    }
}

LeetCode39. 组合总和

https://leetcode-cn.com/problems/combination-sum/

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

  • 所有数字(包括 target)都是正整数。
  • 解集不能包含重复的组合。
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]

思路

回溯模板

void backtracking(选择列表,路径,结果) 
{
    if (终止条件) 
    {
        存放结果;
        return;
    }

    for (在选择列表中选择) 
    {
        处理节点;
        backtracking(选择列表,路径,结果); 
        回溯,撤销处理结果
    }
}

所有用回溯解决的问题都可以抽象为树形结构

回溯:组合问题

回溯三部曲

1.递归函数的返回值和参数

定义两个全局变量,一个用来存放符合条件的路径path,一个用来存放所有符合条件的路径的结果集合result。

函数里一定有两个参数:选择列表(数组candidates)和目标值(int型参数target)。

还有一个int型变量来sum统计path里的元素总和

还有一个int型参数startIndex,这个参数用来记录本层递归的中,选择列表从哪里开始遍历(选择列表就是数组candidates)。随着递归控制树的纵向遍历,可选择的范围进而收缩,调整可选择的范围,就是要靠startIndex。

    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合result
    vector<int> path;  //存放符合条件的路径path
    
    void backtrack(vector<int>& candidates, int target, int sum, int startIndex)

2.递归函数的终止条件

终止条件有两种情况,sum大于target和sum等于target。

sum等于target的时候,需要收集path。

        if (sum > target) return;

        if (sum == target)
        {
            result.push_back(path);
            return;
        }

3.搜索过程
我们定义的backtrack函数其实就像一个指针,在树中游走同时正确维护每个节点的属性。每当走到叶子节点(即碰到递归函数的终止条件),就保存符合条件的路径,返回并回溯。

        //控制树的横向遍历
        for (int i = startIndex; i < size && sum + candidates[i] <= target; ++i)
        {
            sum = sum + candidates[i];
            path.push_back(candidates[i]);

            //用i而不用i + 1, 表示可选重复元素
            backtrack(candidates, target, sum, i);  //递归:控制树的纵向遍历

            sum = sum - candidates[i];
            path.pop_back();
        }

代码

class Solution 
{
private:
    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合result
    vector<int> path;  //存放符合条件的路径path
    
    void backtrack(vector<int>& candidates, int target, int sum, int startIndex)
    {
        int size = candidates.size();

        if (sum > target) return;

        if (sum == target)
        {
            result.push_back(path);
            return;
        }

        //控制树的横向遍历
        for (int i = startIndex; i < size && sum + candidates[i] <= target; ++i)
        {
            sum = sum + candidates[i];
            path.push_back(candidates[i]);

            //用i而不用i + 1, 表示可重复选元素
            backtrack(candidates, target, sum, i);  //递归:控制树的纵向遍历

            sum = sum - candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) 
    {
        sort(candidates.begin(), candidates.end());

        backtrack(candidates, target, 0, 0);

        return result;
    }
};

LeetCode40. 组合总和II

https://leetcode-cn.com/problems/combination-sum-ii/

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

说明:

所有数字(包括目标数)都是正整数。
解集不能包含重复的组合

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

思路

回溯模板

void backtracking(选择列表,路径,结果) 
{
    if (终止条件) 
    {
        存放结果;
        return;
    }

    for (在选择列表中选择) 
    {
        处理节点;
        backtracking(选择列表,路径,结果); 
        回溯,撤销处理结果
    }
}

本题与LeetCode39. 组合总和有两个区别:

  • 本题candidates 中的每个数字在每个组合中只能使用一次。
  • 本题数组candidates的元素是有重复的,但不能有重复的组合。

所以本题的大体思路与LeetCode39. 组合总和是相同的,重点是要去重。所谓去重,其实就是使用过的元素不能重复选取。

用回溯解决的问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。

根据题意:元素在同一个组合内是可以重复的,但两个组合不能相同。同一树层上的是两个组合里的元素,所以我们要去重的是同一树层上的“使用过”;而同一树枝上的都是一个组合里的元素,所以不用去重

要在代码层面实现以上的去重逻辑,就需要先对数组进行排序,然后定义一个bool类型数组used。这个数组的元素初始值都为false表示对应的candidate的元素都没被访问过。如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1],此时for循环里应该做continue的操作。

所有用回溯解决的问题都可以抽象为树形结构

回溯:组合问题

代码

class Solution 
{
private:
    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合result
    vector<int> path;  //存放符合条件的路径path

    void backtrack(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used)
    {
        int sz = candidates.size();

        if (sum > target) return;
        
        if (sum == target)
        {
            result.push_back(path);
            return;
        }


        //控制树的横向遍历
        for (int i = startIndex; i < sz && sum + candidates[i] <= target; ++i)
        {
            if (i >= 1 && candidates[i] == candidates[i - 1] && used[i - 1] == false)
            {
                continue;
            } 

            used[i] = true;
            sum = sum + candidates[i];
            path.push_back(candidates[i]);

            //用i + 1而不用i, 表示不可重复选元素
            backtrack(candidates, target, sum, i + 1, used);  //递归:控制树的纵向遍历

            used[i] = false;
            sum = sum - candidates[i];
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) 
    {
        sort(candidates.begin(), candidates.end());

        //used数组存放bool类型的元素,初始值都为false表示数组nums的元素都没被访问过
        vector<bool> used(candidates.size(), false);
        backtrack(candidates, target, 0, 0, used);

        return result;
    }
};

LeetCode216. 组合总和III

https://leetcode-cn.com/problems/combination-sum-iii/

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

说明:

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

思路

回溯模板

void backtracking(选择列表,路径,结果) 
{
    if (终止条件) 
    {
        存放结果;
        return;
    }

    for (在选择列表中选择) 
    {
        处理节点;
        backtracking(选择列表,路径,结果); 
        回溯,撤销处理结果
    }
}

所有用回溯解决的问题都可以抽象为树形结构

回溯:组合问题

回溯三部曲

1.递归函数的返回值和参数

    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合result
    vector<int> path;  //存放符合条件的路径path

    void backtrack(int k, int n, int sum, int startIndex)

2.递归函数的终止条件

        if (sum > n) return;

        if (path.size() == k)
        {
            if (sum == n) result.push_back(path);
            return;
        }

3.搜索过程
我们定义的backtrack函数其实就像一个指针,在树中游走同时正确维护每个节点的属性。每当走到叶子节点(即碰到递归函数的终止条件),就保存符合条件的路径,返回并回溯。

         //控制树的横向遍历
        for (int i = startIndex; i <= 9 && sum + i <= n; ++i)
        {
            sum = sum + i;
            path.push_back(i);

            backtrack(k, n, sum, i + 1);  //递归:控制树的纵向遍历

            sum = sum - i;
            path.pop_back();
        }
    }

代码

class Solution {
private:
    vector<vector<int>> result;  //存放所有符合条件的路径的结果集合result
    vector<int> path;  //存放符合条件的路径path

    void backtrack(int k, int n, int sum, int startIndex)
    {
        if (sum > n) return;

        if (path.size() == k)
        {
            if (sum == n) result.push_back(path);
            return;
        }

        for (int i = startIndex; i <= 9 && sum + i <= n; ++i)
        {
            sum = sum + i;
            path.push_back(i);

            backtrack(k, n, sum, i + 1);

            sum = sum - i;
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum3(int k, int n) 
    {
        backtrack(k, n, 0, 1);

        return result;
    }
};

LeetCode17. 电话号码的字母组合

https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

回溯:组合问题

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"

思路

回溯模板

void backtracking(选择列表,路径,结果) 
{
    if (终止条件) 
    {
        存放结果;
        return;
    }

    for (在选择列表中选择) 
    {
        处理节点;
        backtracking(选择列表,路径,结果); 
        回溯,撤销处理结果
    }
}

所有用回溯解决的问题都可以抽象为树形结构

回溯:组合问题

回溯三部曲

1.递归函数的返回值和参数

定义两个全局变量,一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来。

函数里一定有一个参数:字符串digits,

还有一个int型参数index。注意这个index可不是前面几题的startIndex了,这个index是记录遍历第几个数字了,就是用来遍历字符串digits的,同时index也表示树的深度。

    vector<string> result;
    string s;

    void backtrack(const string& digits, int index)

2.递归函数的终止条件

如果index 等于字符串digits的长度(本来index就是用来遍历字符串digits的),那么就收集结果,结束本层递归。

        if (index == digits.size())
        {
            result.push_back(s);
            return;
        }

3.搜索过程

首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。

然后for循环来处理这个字符集,注意这里的for循环,不像是在前面几题中是从startIndex开始遍历的,因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而前面几题都是是求同一个集合中的组合。

        int digit = digits[index] - '0';
        string letters = numToString[digit];
        for (int i = 0; i < letters.size(); ++i)
        {
            s.push_back(letters[i]);

            backtrack(digits, index + 1);

            s.pop_back();
        }

代码

class Solution 
{
private:
    const vector<string> numToString = 
    {
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };

    vector<string> result;
    string s;

    void backtrack(const string& digits, int index)
    {
        if (index == digits.size())
        {
            result.push_back(s);
            return;
        }

        int digit = digits[index] - '0';
        string letters = numToString[digit];
        for (int i = 0; i < letters.size(); ++i)
        {
            s.push_back(letters[i]);

            backtrack(digits, index + 1);

            s.pop_back();
        }
    }

public:
    vector<string> letterCombinations(string digits) 
    {
        if (digits.size() == 0) return result;
        
        backtrack(digits, 0);

        return result;
    }  
};
上一篇:HJ94


下一篇:Leetcode 40. 组合总和 II dfs