leetcode之深搜递归回溯类之排列与组合类-----77/39/40/216/317 组合 78/90/368 子排列 22/79/93/131 典型递归回溯 46/47 全排列

这部分主要关于:递归的深度搜索,回溯剪枝减少不必要递归


递归深搜回溯题:
1、递归路线是什么

2、需要获取的结果是什么

3、根据题意中的规定,可以在什么时候就停止继续递归


1、OJ77 combinations

给定正整数n和k,找出在[1-n]中,k个数的全部组合;

如n=2,k=1,则结果为[[1], [2]];

如n=2,k=2,,结果为[[1,2]];

如n=3,k=2,结果为[[1,2], [1,3], [2,3]]


直观:1和后边的数组成k个数的组合;2和后边的数组成k个数的组合,3和后边的数组成k个数的组合......完全是后向,不需要找相同组合顺序不同的,如[1, 2, 3]是正确的,还需要把[2,1,3]找出来

这类题意的题,特点是:
1、递归路线是:从数组头向尾递归,不需要总从头遍历,收集当前数后,遍历接下来的数直到都遍历完

2、获取结果是:收集的数达到k个,就是一个结果

3、停止条件是:当前收集达到k个数了记录结果,记录后返回上一层;


OJ77代码:

class Solution {
public:
    void helper (int cur, const int n, const int k, vector r, vector> &res) {
        //递归停止条件, 当前已收集到K个数, 收集当前结果
        if (r.size() == k) {
            res.push_back(r);
            return;
        }
        
        for (int i = cur; i <= n; i++) {
            //在当前层就判断, 只在当前还未达到K个数时向下递归, 否则直接就停止
            if (r.size() < k) {
                r.push_back(i);
                //直接往下一个数
                helper(i + 1, n, k, r, res);
                r.pop_back();
            }
        }
    }
    
    vector> combine(int n, int k) {
        vector> res;
        if (n <= 0) {
            return res;
        }
        
        vector r;
        helper(1, n, k, r, res);
        return res;
    }
};

2、OJ39 combination sum

给定一个数组,比如[2,3,6,7],给定一个整数target比如7,找到数组中的可重复组合,和等于target,比如这个例子结果为:[[7], [2,2,3]]

1、递归路线是:从数据头到尾递归,注意因为元素可以重复出现,所以当前层向下递归时,不可以后移索引;另外,需要返回的结果不可以重复,即如一个结果是[2,2,3],不需要它的其他排列组合如[2,3,2]、[3,2,2]这样,所以,当前层向下一层递归时,索引不变即可,不需要每次从头再遍历

2、获取结果是:数据集之和为target的

3、停止条件是:题目提出了给定的是正数(positive),所以意味着如果当前数据集的和已经大于等于target了,那么就不用继续向下递归;基于此,原始数组应该提前排序,这样利于最大化的减少多余的递归情况


OJ39代码:

class Solution {
public:
    void helper (int idx, vector cur, const int target, const vector& candidates, vector> &res) {
        //计算当前和
        int sum = 0;
        for (auto i: cur) {
            sum += i;
        }
        //已知candidates里都是正数, 如果当前和已经大于等于target, 那么后边的递归就不需要(肯定比target大)
        //所以candidates提前排好序, 能最多的过滤掉大于等于target的case
        if (sum > target) {
            return;
        } else if (sum == target) {
            res.push_back(cur);
            return;
        }
        
        for (int i = idx; i < candidates.size(); i++) {
            cur.push_back(candidates[i]);
            //数组中元素可以重复出现, 不能往下一个数
            helper(i, cur, target, candidates, res);
            cur.pop_back();
        }
    }
    
    vector> combinationSum(vector& candidates, int target) {
        vector> res;
        if (candidates.empty()) {
            return res;
        }
        
        //提前排好序, 有利于最多的减少多余递归
        sort(candidates.begin(), candidates.end());
        vector cur;
        helper(0, cur, target, candidates, res);
        return res;
    }
};

3、OJ40 combination sum II

给定一个数组如[10, 1, 2, 7, 6, 1, 5],给定一个整数target,求数组中的组合,和为target的;如target = 8,则结果为:[[1, 7], [1 ,1, 6], [1, 2, 5], [2, 6]],要求每个数只能出现一次,比如本例中数组有两个1,都可以和7组合成8,但是结果中只能有一个为[1, 7]的结果;

1、递归路线为:从数据头到尾递归,因为结果集中的数不可重复,且原始数据内有重复数据,所以需要注意两个事情:

      1、向下递归时,索引向后移

      2、对于数组中重复的数,不能做同样的递归处理,需要界定数组中重复的数,解决方法是:

    原数组排好序,向下递归前先看看,是不是当前数,前边已经有了

2、获取结果是:数据集之和为target的;

3、停止条件是:和OJ39一样里边都是正数,所以停止条件也是,当前数据集之和大于等于target时,进而也需要原数据排好序


OJ40代码:

class Solution {
public:
    void helper (int idx, const int target, vector cur, const vector &candidates, vector> &res) {
        int sum = 0;
        for (auto i: cur) {
            sum += i;
        }
        
        //因为原数据也都是正数, 所以也是同样的剪枝条件
        if (sum > target) {
            return;
        } else if (sum == target) {
            res.push_back(cur);
            return;
        }
        
        for (int i = idx; i < candidates.size(); i++) {
            //排序后, 相同数会相邻, 题意要求结果不可以有重复, 所以对于前边已经遍历过的索引对应的数, 不要再遍历
            if (i > idx && candidates[i] == candidates[i - 1]) {
                continue;
            } else {
                cur.push_back(candidates[i]);
                //不可以自重复己加自己, 所以索引后移
                helper(i + 1, target, cur, candidates, res);
                cur.pop_back();
            }
        }
    }
    
    vector> combinationSum2(vector& candidates, int target) {
        vector> res;
        if (candidates.empty()) {
            return res;
        }
        
        sort(candidates.begin(), candidates.end());
        vector cur;
        helper(0, target, cur, candidates, res);
        return res;
    }
};

4、OJ216 combination sum III

在[1-9]范围内,给定整数K和n,求K个数的组合,且和为n的;

此题是OJ77变种,OJ77题是要在1-N范围内,找到K个数的全部组合;本题是在1-9的范围内,找到K个数且和为N的全部组合;

1、递归路线是:从数据头到尾递归,不可能重复,所以每次向下递归时索引后移;

2、获取结果是:数据集个数达到K,且和为N的;

3、停止条件是:两个停止条件,符合任意一个都要停止:1、数据集个数达到K;2、原始数据从1-9都是正数,所以数据集之和大于等于N时,也要停止向下递归;


OJ216代码:

class Solution {
public:
    void helper (int st, int k, int n, vector cur, vector> &res) {
        //计算当前和
        int sum = 0;
        for (auto i: cur) {
            sum += i;
        }
        
        //1-9都是正数, 所以做同样的剪枝
        if (sum > n) {
            return;
        } else if (sum == n && !k) {
            res.push_back(cur);
            return;
        }
        
        for (int i = st; i <= 9; i++) {
            //向下递归时, 根据数据集当前个数就做剪枝, 确保不会出现大于K个数的数据集向下继续计算
            if (k) {
                cur.push_back(i);
                helper(i + 1, k - 1, n, cur, res);
                cur.pop_back();
            }
        }
    }
    
    vector> combinationSum3(int k, int n) {
        vector> res;
        vector cur;
        helper(1, k, n, cur, res);
        return res;
        
    }
};

5、OJ377 combination sum IV

给定一个数组如[1,2,3],给定整数target如target=4,找出数组中全部的和为target组合,元素可以重复(如组成4可以是4个首元素1),全部的组合顺序(如组成4可以是[1,1,2]和[2,1,1])


元素可以重复:意味着每次向下递归时,索引不可以后移;同时要求全部的组合顺序,说明当前层的数,还要和它前面的数组合,而不是完全向后的递归,也就是每次递归过程都需要是从头到尾;

递归路线是:从数据头到尾递归,每次都从头再遍历;

获取结果是:数据集和为target的;

停止条件是:正数,所以数据集和大于等于target的同样被剪枝;如果是负数则无这个剪枝条件


按这种办法的OJ377代码:

class Solution {
public:
    void helper (int cur, const vector &nums, const int target, int &res) {
        if (cur == target) {
            ++res;
            return;
        }
        
        for (auto i: nums) {
            cur += i;
            //剪枝放在递归前
            if (cur <= target) {
                helper(cur, nums, target, res);
            }
            cur -= i;
        }
    }
    
    int combinationSum4(vector& nums, int target) {
        int res = 0;
        if (nums.empty()) {
            return res;
        }
        
        sort(nums.begin(), nums.end());
        helper(0, nums, target, res);
        return res;
    }
};


该方法可以得到正确结果,但无法AC会TLE超时,因为每次都从头遍历数组,所以需要更高级的剪枝;

联想跳台阶的优化是什么?

一次可以跳1或2个台阶,N个台阶的走法是F(N) = F(N - 1) + F(N - 2);

优化方法是:

1、已知走1个方法是1种,走2个是两种,那么就知道了走3个是F(1) + F(2)是3种,此时把F(3) = 3记下来;

2、然后计算走4种的方法时,需要计算F(4) = F(3) + F(2),这时不需要递归计算F(3),因为F(3)已经记下来了,直接得到F(4) = 5;

3、计算第5、6、7、.....、N时,每次都可以使用上次结果;


那么对于本题,求组合的方法数,且组合可以重复,而且要求每个数,与包括它自己在内的全部数的组合情况,则本题相当于:

跳target = 4个台阶,每次可以跳[1,2,3]个台阶,有多少种跳法?

那么本题的优化方法是:如对于[1,2,3],则F(target) = F(target - 1) + F(target - 2) + F(target - 3);递归过程中会依次求出F(4)、F(5)、.....、F(target)

经验:需要从头到尾遍历的题,主动思考是不是跳台阶式问题,能否往跳台阶的优化思路去靠;


OJ377优化后可AC代码:

class Solution {
public:
    int helper (int target, const vector &nums, unordered_map &hmap) {
        if (!target) {
            return 1;
        } else if (target > 0) {
            //直接使用已经跑过的更深层的结果
            if (hmap.find(target) != hmap.end()) {
                return hmap[target];
            }
        } else {
            return 0;
        }
        
        //求F(target) = sum{F(target - nums[i])}, 相当于跳台阶的F(N) = F(N - 1) + F(N - 2)
        int count = 0;
        for (auto i: nums) {
            count += helper(target - i, nums, hmap);
        }
        
        //计算完当前层的target的方法后记录下来, 后面就可以直接用
        hmap[target] = count;
        return count;
    }
    
    int combinationSum4(vector& nums, int target) {
        if (nums.empty()) {
            return 0;
        }
        
        unordered_map hmap;
        return helper(target, nums, hmap);
    }
};


6、OJ78 subsets

子集,给定无重复元素的数组[1,2,3],求全部的子排列,结果为:[[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]

递归路线是:从数据头到尾递归

获取结果是:都要

停止条件是:直到都遍历完


OJ78代码:

class Solution {
public:
    void helper (int idx, vector cur, const vector &nums, vector> &res) {
        if (idx <= nums.size()) {
            res.push_back(cur);
            
            for (int i = idx; i < nums.size(); i++) {
                cur.push_back(nums[i]);
                helper(i + 1, cur, nums, res);
                cur.pop_back();
            }
        }
    }
    
    vector> subsets(vector& nums) {
        vector> res = {};
        if (nums.empty()) {
            return res;
        }
        
        vector cur;
        helper(0, cur, nums, res);
        return res;
    }
};

7、OJ90 subsets II

和OJ78唯一区别是,原数组内可能有重复元素,而结果集中不可以有相同元素组成的结果,比如[1,1,7]的结果里不可以有两个[1,7]

和OJ40的套路一模一样,原数组先排好序,然后查找当前数前边的数是否和当前数一样,一样就说明已经有结果了,不要再计算出同样的结果

OJ90代码:

class Solution {
public:
    void helper (int idx, vector cur, const vector &nums, vector> &res) {
        if (idx <= nums.size()) {
            res.push_back(cur);
            
            for (int i = idx; i < nums.size(); i++) {
                if (i > idx && nums[i] == nums[i - 1]) {
                    continue;
                } else {
                    cur.push_back(nums[i]);
                    helper(i + 1, cur, nums, res);
                    cur.pop_back();
                }
            }
        }
    }
    
    vector> subsetsWithDup(vector& nums) {
        vector> res = {};
        if (nums.empty()) {
            return res;
        }
        
        sort(nums.begin(), nums.end());
        vector cur;
        helper(0, cur, nums, res);
        return res;
    }
};

8、OJ368 largest divisible subset

如给定[3,4,16,8],找出最长的能够连续被整除的组合,如本例的结果是:[4,8,16]

递归路线是:如16可以整除8,8可以整除4,那么16自然可以整除4,所以向后递归时:1、原数组排好序,保证大数在后边免于向前遍历;2、向后递归时索引要后移

获取结果是:可以整除的组合中最长的;

停止条件是:除递归到头外,还有一个重要的剪枝条件,当前已经计算出的最长组合的长度,如果已经大于“当前组合长度 + 后边剩余的全部元素个数”,如[1,2,4,8],遍历1时已经得出最长组合为[1,2,4,8]长度为4,那么遍历2时会发现即便后边都能整除也就是最长长度也不过是3,那么对于2的遍历立即可以停止;


经验:因数据大小导致需要向前遍历的题,这种情况下必须先排好序,规避掉这种需求向前遍历的情况


OJ368代码:

class Solution {
public:
    void helper (int idx, vector cur, const vector &nums, vector &res) {
        if (idx <= nums.size()) {
            //已经算出的最长长度, 已经大于现在正在计算的流程中的最大可能长度, 那么就不用继续计算了(如[1,2,4,8], 已经算出[1,2,4,8]时, 后面的都不会更长)
            if (res.size() >= (nums.size() - idx + cur.size())) {
                return;
            }
            //最长长度超过已经计算的最大值就更新
            if (cur.size() > res.size()) {
                res = cur;
            }
            
            for (int i = idx; i < nums.size(); i++) {
                if (!cur.empty()) {
                    if (nums[i] % cur[cur.size() - 1] == 0) {
                        cur.push_back(nums[i]);
                        helper(i + 1, cur, nums, res);
                        cur.pop_back();
                    }
                } else {
                    cur.push_back(nums[i]);
                    helper(i + 1, cur, nums, res);
                    cur.pop_back();
                }
            }
        }
    }
    
    vector largestDivisibleSubset(vector& nums) {
        vector res;
        if (nums.empty()) {
            return res;
        }
        
        sort(nums.begin(), nums.end());
        vector cur;
        helper(0, cur, nums, res);
        return res;
    }
};

9、OJ22 generate parentheses

生成括号,给定K,有多少种括号组合形式,要求:不可以出现右括号领头,如给定3,结果为:"((()))"、"(()())"、"(())()"、"()(())"、"()()()"

递归路线是:从0-K;

停止条件是:左括号和右括号个数都达到K时;


本题注意递归生成时的顺序,必须是,左括号要领先于右括号,设置两个数分别在递归过程中表示当前左括号和右括号的个数,判断依据是:

1、当都为0即最开始的时候,必须生成左括号;

2、当左括号和右括号个数都达到K时,结束了;

3、如果左括号个数,大于右括号个数,那么可以生成左括号,还可以生成右括号,两个递归流程;

4、如果左括号和右括号个数相等,必须生成左括号;

5、左括号肯定率先达到K,这时肯定必须生成右括号;

OJ22代码:

class Solution {
public:
    void helper (int cur1, int cur2, const int n, string cur, vector &res) {
        if (cur1 == n && cur2 == n) {
            res.push_back(cur);
            return;
        }
        
        if (!cur1 && !cur2) {
            cur += "(";
            helper(cur1 + 1, cur2, n, cur, res);
        } else if (cur1 < n && cur1 > cur2) {
            cur += "(";
            helper(cur1 + 1, cur2, n, cur, res);
            cur[cur.length() - 1] = ')';
            helper(cur1, cur2 + 1, n, cur, res);
        } else if (cur1 < n && cur1 == cur2) {
            cur += "(";
            helper(cur1 + 1, cur2, n, cur, res);
        } else {
            cur += ")";
            helper(cur1, cur2 + 1, n, cur, res);
        }
    }
    
    vector generateParenthesis(int n) {
        vector res;
        if (!n) {
            return res;
        }
        
        string cur = "";
        helper(0, 0, n, cur, res);
        return res;
    }
};

10、OJ79 word search

给定一个二维数组,里边有各种英文字符,然后查找一个英文字符串,判断二维数组里是否存在,可以是向上向下向左向右,不可以重复,如:

[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]
word  =  "ABCCED" , -> returns  true ,
word  =  "SEE" , -> returns  true ,
word  =  "ABCB" , -> returns  false .

本题事实上和二叉树里的"子树问题"是一个问题,每一个字符都可以作为起始点做深度遍历,显然需要先判断出和word的首字符相同的再进入深搜;

另外进入深搜后,因为不许重复,所以需要给每个已经走过且判断复合word了的字符做"已使用"的标记,避免重复使用

OJ79代码:

class Solution {
public:
    bool helper (const vector> &board, int x, int y, const int m, const int n, string word, vector> &visit) {
        if (word.empty()) {
            return true;
        } else {
            if (x >= 0 && y >= 0 && x < m && y < n && word[0] == board[x][y] && visit[x][y] == false) {
                string relate = (word.length() > 1)?word.substr(1):"";
                visit[x][y] = true;
                if (relate.empty()) {
                    return true;
                }
                
                //4个方向都做深搜, 任何一个返回成功什么已成功找到
                bool l1 = helper(board, x - 1, y, m, n, relate, visit);
                if (l1) {
                    return true;
                }
                bool l2 = helper(board, x + 1, y, m, n, relate, visit);
                if (l2) {
                    return true;
                }
                bool l3 = helper(board, x, y - 1, m, n, relate, visit);
                if (l3) {
                    return true;
                }
                bool l4 = helper(board, x, y + 1, m, n, relate, visit);
                if (l4) {
                    return true;
                }
                
                //都没有找到, 复位该字符标志位
                visit[x][y] = false;
                return false;
            } else {
                return false;
            }
        }
    }
    
    bool exist(vector>& board, string word) {
        if (board.empty() && word.empty()) {
            return false;
        } else if (board.empty() || word.empty()) {
            return false;
        }
        
        int m = board.size(), n = board[0].size();
        for (int i = 0; i < board.size(); i++) {
            for (int j = 0; j < board[i].size(); j++) {
                if (board[i][j] == word[0]) {
                    //visit用于标识每一次进入深搜后, 标识已经走过且确实被用到的字符
                    vector> visit(board.size(), vector(board[i].size(), false));
                    //找到word首字符后再去深搜,
                    if (helper(board, i, j, m, n, word, visit)) {
                        return true;
                    }
                }
            }
        }
        
        return false;
    }
};



11、OJ46 permutations

无重复元素的全排列

OJ46代码:

class Solution {
public:
    void helper (int st, vector nums, vector> &res) {
        if (st == nums.size()) {
            res.push_back(nums);
            return;
        }
        
        for (int i = st; i < nums.size(); i++) {
            if (i > st) {
                int t = nums[st];
                nums[st] = nums[i];
                nums[i] = t;
            }
            helper(st + 1, nums, res);
            if (i > st) {
                int t = nums[st];
                nums[st] = nums[i];
                nums[i] = t;
            }
        }
    }
    
    vector> permute(vector& nums) {
        vector> res;
        if (nums.empty()) {
            return res;
        }
        
        helper(0, nums, res);
        return res;
    }
};

12、OJ47 permutation II

带重复元素的全排列。

注意,这道题用OJ40、OJ90的避免去重方法不适用,因为当递归到当前数时,向前发现有相同的数,但怎么界定是第一次执行的还是重复的呢?

带重复的全排列的方法是,原数组排序,保证重复元素相邻,然后为每一个元素标记上"是否已使用",遍历时永远从头遍历到尾,如果当前元素和之前元素相同,但之前元素标记未用过,那么说明本次遍历属于重复的;

如[1,1],当第一次从第一个1遍历到第二个1时,得到结果[1,1]并保存;

当第二次从第二个1入结果集,然后再递归遍历时,会发现第一个1的标志位为"未使用",什么当前是跨过了第1个1


OJ47代码:

class Solution {
public:
    void helper (int idx, vector nums, vector cur, vector> &res, vector &visit) {
        if (idx == nums.size()) {
            res.push_back(cur);
            return;
        }
        
        //查看当前数的前面的数, 如果相等且未使用, 则不能再继续执行(如[1,1], 从第一个1到第二个1执行生成[1,1]结果后, 索引后移到第二个1并加入cur, 此时准备加入第一个1时, 发现其状态为false返回, 即避免了生成重复结果)
        for (int i = 0; i < nums.size(); i++) {
            if (visit[i] || (i > 0 && nums[i] == nums[i - 1] && visit[i - 1] == false)) {
                continue;
            }
            
            cur.push_back(nums[i]);
            visit[i] = true;
            helper(idx + 1, nums, cur, res, visit);
            visit[i] = false;
            cur.pop_back();
        }
    }
       
    vector> permuteUnique(vector& nums) {
        vector> res;
        if (nums.empty()) {
            return res;
        }
        
        //必须要排序
        vector cur;
        vector visit(nums.size(), false);
        sort(nums.begin(), nums.end());
        helper(0, nums, cur, res, visit);
        return res;
    }
};



另外,关于OJ31(当前全排列组合的下一个,next permutation)和OJ60(第K个全排列,permutation sequence),和递归回溯并无关,而是基于全排列知识的运用。

你可能感兴趣的:(递归,深度搜索,回溯剪枝,排列,集合)