这部分主要关于:递归的深度搜索,回溯剪枝减少不必要递归
递归深搜回溯题:
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<int> r, vector<vector<int>> &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<vector<int>> combine(int n, int k) {
vector<vector<int>> res;
if (n <= 0) {
return res;
}
vector<int> 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<int> cur, const int target, const vector<int>& candidates, vector<vector<int>> &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<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
if (candidates.empty()) {
return res;
}
//提前排好序, 有利于最多的减少多余递归
sort(candidates.begin(), candidates.end());
vector<int> 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<int> cur, const vector<int> &candidates, vector<vector<int>> &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<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> res;
if (candidates.empty()) {
return res;
}
sort(candidates.begin(), candidates.end());
vector<int> 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<int> cur, vector<vector<int>> &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<vector<int>> combinationSum3(int k, int n) {
vector<vector<int>> res;
vector<int> 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<int> &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<int>& nums, int target) {
int res = 0;
if (nums.empty()) {
return res;
}
sort(nums.begin(), nums.end());
helper(0, nums, target, res);
return res;
}
};
联想跳台阶的优化是什么?
一次可以跳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<int> &nums, unordered_map<int, int> &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<int>& nums, int target) {
if (nums.empty()) {
return 0;
}
unordered_map<int, int> 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<int> cur, const vector<int> &nums, vector<vector<int>> &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<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res = {};
if (nums.empty()) {
return res;
}
vector<int> 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<int> cur, const vector<int> &nums, vector<vector<int>> &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<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>> res = {};
if (nums.empty()) {
return res;
}
sort(nums.begin(), nums.end());
vector<int> 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<int> cur, const vector<int> &nums, vector<int> &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<int> largestDivisibleSubset(vector<int>& nums) {
vector<int> res;
if (nums.empty()) {
return res;
}
sort(nums.begin(), nums.end());
vector<int> 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<string> &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<string> generateParenthesis(int n) {
vector<string> 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<vector<char>> &board, int x, int y, const int m, const int n, string word, vector<vector<bool>> &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<vector<char>>& 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<vector<bool>> visit(board.size(), vector<bool>(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<int> nums, vector<vector<int>> &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<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> 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<int> nums, vector<int> cur, vector<vector<int>> &res, vector<bool> &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<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
if (nums.empty()) {
return res;
}
//必须要排序
vector<int> cur;
vector<bool> visit(nums.size(), false);
sort(nums.begin(), nums.end());
helper(0, nums, cur, res, visit);
return res;
}
};
另外,关于OJ31(当前全排列组合的下一个,next permutation)和OJ60(第K个全排列,permutation sequence),和递归回溯并无关,而是基于全排列知识的运用。