leetcode 17:17. 电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
接口:
class Solution {
public:
vector letterCombinations(string digits) {
}
};
思路:
① digits[0...N-1] = digits[0] + digits[1...N-1] = digits[0] + digits[1] + ... + digits[N-1]。因此,递归函数可以定义为,结果字符串加上 index 位的数字所对应的一种可能的字母,并继续向后递归,递归边界是 index == N-1,这时结果字符串就是一个可能的组合,将它保存下来。
class Solution {
public:
vector ret;
vector table = {"","", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
void helper(int index, const string& digits, const string& s)
{
if(index == digits.size())
{
ret.push_back(s);
return;
}
int num = digits[index] - '0';
for (int i = 0; i < table[num].size(); ++i)
{
helper(index + 1, digits, s + table[num][i]);
}
}
vector letterCombinations(string digits) {
if(digits.size() == 0) return ret;
helper(0, digits, "");
return ret;
}
};
② 递归函数还可以定义为,返回 [index +1, N-1] 的所有可能的组合,然后加上 index 位的数字所对应的每一种可能的字母,这样就得到了 [index, N-1] 的所有可能的组合,递归边界也是 index == N - 1。
class Solution {
public:
vector ret;
vector table = {"","", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
vector helper(int index, const string& digits)
{
vector ret;
if(index == digits.size() - 1)
{
int num = digits[index] - '0';
for (int i = 0; i < table[num].size(); ++i)
{
ret.push_back({table[num][i]});
}
return ret;
}
vector back = helper(index + 1, digits);
int num = digits[index] - '0';
for (int i = 0; i < table[num].size(); ++i)
{
for (int j = 0; j < back.size(); ++j)
{
string tmp = table[num][i] + back[j];
ret.push_back(tmp);
}
}
return ret;
}
vector letterCombinations(string digits) {
if(digits.size() == 0) return ret;
ret = helper(0, digits);
return ret;
}
};
leetcode 22: 22. 括号生成:
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
接口:
class Solution {
public:
vector generateParenthesis(int n) {
}
};
思路:
① 有效的括号组合,最左边一定是"(",最右边一定是")",因此,这个问题本质上是在 2 * n - 2 个位置里,选 n - 1个位置,填入左括号,再判断是否有效。最直接的做法就是先得到所有的可能取法,再依次判断这种括号组合是否有效。如下:
class Solution {
public:
vector ret;
bool isValid(vector &used)
{
stack st;
for (int i = 0; i < used.size(); ++i)
{
if(used[i])
{
st.push('(');
}
else
{
if(st.empty()) return false;
st.pop();
}
}
if(st.empty()) return true;
return false;
}
void helper(int n, int start, int leftCnt, vector &used)
{
if(leftCnt == n)
{
if(isValid(used))
{
string str;
for (int i = 0; i < used.size(); ++i)
{
if(used[i]) str += "(";
else str += ")";
}
ret.push_back(str);
}
return;
}
// [i, 2n-1)应该至少还有 n - leftCnt 个位置可以填左括号
for (int i = start; i + n - leftCnt <= 2 * n - 1; ++i)
{
used[i] = true;
helper(n, i + 1, leftCnt + 1, used);
used[i] = false;
}
}
vector generateParenthesis(int n) {
vector used(2*n, 0);
used[0] = true;
helper(n, 1, 1, used);
return ret;
}
};
② 也可以对这 2*n 个位置逐位处理,当前位要么是左括号,要么是右括号,再进一步递归,边界是左括号的数量或右括号的数量等于n,无效情况是右括号的数量已经大于左括号的数量,或者某种括号的数量已经大于n。
class Solution {
public:
vector generateParenthesis(int n) {
vector res;
func(res, "", 0, 0, n);
return res;
}
void func(vector &res, string str, int l, int r, int n){
if(l > n || r > n || r > l) return ;
if(l == n && r == n) {res.push_back(str); return;}
func(res, str + '(', l+1, r, n);
func(res, str + ')', l, r+1, n);
return;
}
};
leetcode 93: 93. 复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
接口:
class Solution {
public:
vector restoreIpAddresses(string s) {
}
};
思路:可能的IP格式需要在字符串中增加三个”.",并保证被"."分割开的四段,每一段的值都不超过255。因此,问题的本质是在[0,N-1]这N个位置中,选三个位置进行截断,并判断选择的位置组合是否合理。
class Solution {
public:
vector ret;
bool isValid(const string& s)
{
if(s.length() == 0 || s.length() > 3 || stoi(s) > 255 || (s[0] == '0' && s.size() > 1)) return false;
return true;
}
// tmp存放截断的三个位置的下标,例如,tmp = {0, 1, 2}, str = "25525511135",那么截断后的四个字符串是2.5.5.25511135
void helper(const string &s, int start, vector &tmp)
{
if(tmp.size() == 3)
{
string s1 = s.substr(0, tmp[0] + 1);
if(!isValid(s1)) return;
string s2 = s.substr(tmp[0]+1, tmp[1] - tmp[0]);
if(!isValid(s2)) return;
string s3 = s.substr(tmp[1]+1, tmp[2] - tmp[1]);
if(!isValid(s3)) return;
string s4 = s.substr(tmp[2]+1, s.size() - 1 - tmp[2]);
if(!isValid(s4)) return;
string str = s1 + "." + s2 + "." + s3 + "." + s4;
ret.push_back(str);
return;
}
for (int i = start; i < s.length(); ++i)
{
//剪枝,剩下的长度大于合理长度或小于合理长度,跳过。
if(s.length() - i - 1> 3 * (3 - tmp.size()) || s.length() - i - 1 < 3 - tmp.size()) continue;
tmp.push_back(i);
helper(s, i + 1, tmp);
tmp.pop_back();
}
}
vector restoreIpAddresses(string s) {
if(s.size() > 12 || s.size() < 4) return ret;
vector tmp;
helper(s, 0, tmp);
return ret;
}
};
leetcode131: 131. 分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
接口:
class Solution {
public:
vector> partition(string s) {
}
};
思路:如果 [begin, i]是回文串,那么继续递归寻找[i+1, N-1]的所有回文串,递归边界是 begin == N。
class Solution {
public:
bool isParlindrome(const string &s)
{
for (int i = 0; i < s.size() / 2; ++i)
{
if(s[i] != s[s.size() - 1- i]) return false;
}
return true;
}
vector> ret;
vector tmp;
// 如果[begin, i]是回文串,将它存储到tmp中,并继续递归找到[i+1,s.size-1]的回文串分割。
void helper(const string &s, int begin)
{
if(begin == s.size())
{
ret.push_back(tmp);
return;
}
for (int i = begin; i < s.size(); ++i)
{
if(isParlindrome(s.substr(begin, i + 1 - begin)))
{
tmp.push_back(s.substr(begin, i + 1 - begin));
helper(s, i + 1);
tmp.pop_back();
}
}
}
vector> partition(string s) {
helper(s, 0);
return ret;
}
};
leetcode 46: 46. 全排列
给定一个没有重复数字的序列,返回其所有可能的全排列。
接口:
class Solution {
public:
vector> permute(vector& nums) {
}
};
思路:相当于从N个数里,每次选1个数,选N次的问题。因为选择过的元素不能再用,所以要另外开数组记录元素的使用情况,选完第 index 个元素后,递归选择 index+1 个数,直到 index == N。
class Solution {
public:
vector> ret;
vector tmp;
void helper(vector &nums, int index, vector &used)
{
if(index == nums.size())
{
ret.push_back(tmp);
return;
}
for (int i = 0; i < nums.size(); ++i)
{
if(!used[i])
{
used[i] = true;
tmp.push_back(nums[i]);
helper(nums, index + 1, used);
tmp.pop_back();
used[i] = false;
}
}
}
vector> permute(vector& nums) {
if(nums.size() == 0) return ret;
vector used(nums.size(), 0);
helper(nums, 0, used);
return ret;
}
};
leetcode 47: 47. 全排列 II
给定一个可包含重复数字的序列,返回所有不重复的全排列。
接口:
class Solution {
public:
vector> permuteUnique(vector& nums) {
}
};
思路:因为这里有重复的数字,而要求输出不能重复,所以问题不再是从N个数字中每次选1个,选N次这么简单了。避免重复的办法是,先对数组进行排序,这样能保证相同的数字都相邻。之后,考察当前选取的第 index 个数,如果在原数组中,它与前面的数相同,但是前面的数没有被使用,那么就需要跳过这个数,因为这个数在index位的所有组合的可能都已经在之前保存过了。
class Solution {
public:
vector> ret;
vector tmp;
void helper(vector&nums, int index, vector &used)
{
if(index == nums.size())
{
ret.push_back(tmp);
return;
}
for (int i = 0; i < nums.size(); ++i)
{
if(!used[i])
{
if(i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
used[i] = true;
tmp.push_back(nums[i]);
helper(nums, index + 1, used);
tmp.pop_back();
used[i] = false;
}
}
}
vector> permuteUnique(vector& nums) {
if(nums.size() == 0) return ret;
sort(nums.begin(), nums.end());
vector used(nums.size(), 0);
helper(nums, 0, used);
return ret;
}
};
leetcode77: 77. 组合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
接口:
class Solution {
public:
vector> combine(int n, int k) {
}
};
思路:
① 相当于从n个数里,每次选1个数,选k次的问题,但是与排列问题不同的是,先选1后选2 和 先选2后选1是一样的,所以在选择的策略需要改变,选完第 index 个数后,需要在 [index + 1, N] 之间继续选第index +1 个数,而不能在前面选。而在[index + 1, N] 中选剩下的数这一步,可以进行剪枝。
class Solution {
public:
vector> ret;
vector tmp;
void helper(int n, int k, int start)
{
if(tmp.size() == k)
{
ret.push_back(tmp);
return;
}
//剪枝,[i + 1, n]中至少应该还有 k - tmp.size个数可以选
for (int i = start; i + k - tmp.size() - 1<= n; ++i)
{
tmp.push_back(i);
helper(n, k, i + 1);
tmp.pop_back();
}
}
vector> combine(int n, int k) {
if(n < k) return ret;
helper(n, k, 1);
return ret;
}
};
② 相当于依次处理 1~n 这n个数,每个数都有选和不选两条路,直到选满了k个数,保存选出的序列即可。同样的,在进入选或不选的分支前,根据剩下元素的数量进行剪枝。
class Solution {
public:
vector> ret;
vector tmp;
void helper(int n, int k, int index)
{
if(tmp.size() == k)
{
ret.push_back(tmp);
return;
}
//剪枝
if(n - index + 1< k - tmp.size()) return;
tmp.push_back(index);
helper(n, k, index + 1);
tmp.pop_back();
helper(n, k, index + 1);
}
vector> combine(int n, int k) {
if(n < k) return ret;
helper(n, k, 1);
return ret;
}
};
leetcode39: 39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
接口:
class Solution {
public:
vector> combinationSum(vector& candidates, int target) {
}
};
思路:
① 解集不能重复,所以在选完当前元素后,不能再向前找,而只能在当前元素及以后决定剩下的元素。(题目已经保证了数组元素无重复)。
class Solution {
public:
vector> ret;
vector tmp;
void helper(vector &candidates, int target, int start)
{
if(target < 0) return;
if(target == 0)
{
ret.push_back(tmp);
return;
}
for (int i = start; i < candidates.size(); ++i)
{
tmp.push_back(candidates[i]);
helper(candidates, target - candidates[i], i);
tmp.pop_back();
}
}
vector> combinationSum(vector& candidates, int target) {
helper(candidates, target, 0);
return ret;
}
};
② 依次处理每个元素,每个元素都有选或不选两种分支,注意元素可重复选,处理完第index个元素后,要继续递归处理index号元素,直到和大于target。
class Solution {
public:
vector> ret;
vector tmp;
void helper(vector& candidates, int target, int index)
{
if(target == 0)
{
ret.push_back(tmp);
return;
}
if(index == candidates.size() || target < 0) return;
tmp.push_back(candidates[index]);
helper(candidates, target - candidates[index], index);
tmp.pop_back();
helper(candidates, target, index + 1);
}
vector> combinationSum(vector& candidates, int target) {
helper(candidates, target, 0);
return ret;
}
};
leetcode 40: 40. 组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
接口:
class Solution {
public:
vector> combinationSum2(vector& candidates, int target) {
}
};
思路:
由于元素可能有重复的,但是解集不能包含重复组合,所以要考虑去重。去重的策略和之前一样,先将数组排序,在寻找以下标index开头的解集时,判断 index 和前一个元素是否相同,如果是则跳过,因为以 index 开头的解集已经被保存了。
class Solution {
public:
vector> ret;
vector tmp;
void helper(vector& candidates, int target, int start)
{
if(target < 0) return;
if(target == 0)
{
ret.push_back(tmp);
return;
}
for (int i = start; i < candidates.size(); ++i)
{
if(target - candidates[i] < 0) break;
if(i > start && candidates[i] == candidates[i-1]) continue;
tmp.push_back(candidates[i]);
helper(candidates, target - candidates[i], i + 1);
tmp.pop_back();
}
}
vector> combinationSum2(vector& candidates, int target) {
sort(candidates.begin(), candidates.end())
helper(candidates, target, 0);
return ret;
}
};
leetcode216: 216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
接口:
class Solution {
public:
vector> combinationSum3(int k, int n) {
}
};
思路:
① 相当于1 ~ 9 每个数只能选一次,选k个数和为n,由于可选元素本身没有重复的,那么避免出现重复解只需要每次向后查找即可。
class Solution {
public:
vector> ret;
vector tmp;
void helper(int k, int n, int start)
{
if(n < 0 || k < 0) return;
if(n == 0 && k == 0)
{
ret.push_back(tmp);
return;
}
for (int i = start; i <= 9; ++i)
{
if(n < i) break;
tmp.push_back(i);
helper(k - 1, n - i, i + 1);
tmp.pop_back();
}
}
vector> combinationSum3(int k, int n) {
helper(k, n, 1);
return ret;
}
};
② 依次处理 1 ~ 9 中每个数,都有选和不选两条分支。
class Solution {
public:
vector> ret;
vector tmp;
void helper(int k, int n, int index)
{
if(n == 0 && k == 0)
{
ret.push_back(tmp);
return;
}
if(index > 9 || n < 0 || k < 0) return;
tmp.push_back(index);
helper(k-1, n - index, index + 1);
tmp.pop_back();
helper(k, n, index + 1);
}
vector> combinationSum3(int k, int n) {
helper(k, n, 1);
return ret;
}
};
leetcode 78: 78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
接口:
class Solution {
public:
vector> subsets(vector& nums) {
}
};
思路:
① 仍然是从N个数里选数的问题,不过是要保存所有选数的可能。因为数组元素不重复,避免重复子集的方法还是每次只向后查找。每次增加元素时,将当前集合保存。最后需要额外加上空集。
class Solution {
public:
vector> ret;
vector tmp;
void helper(vector&nums, int start)
{
for (int i = start; i < nums.size(); ++i)
{
tmp.push_back(nums[i]);
ret.push_back(tmp);
helper(nums, i + 1);
tmp.pop_back();
}
}
vector> subsets(vector& nums) {
helper(nums, 0);
ret.push_back(vector());
return ret;
}
};
② 依次处理每个元素,都有选和不选两条分支。每次增加元素时,将当前集合保存。最后额外加上空集。
class Solution {
public:
vector> ret;
vector tmp;
void helper(vector&nums, int index)
{
if(index == nums.size()) return;
tmp.push_back(nums[index]);
ret.push_back(tmp);
helper(nums, index + 1);
tmp.pop_back();
helper(nums, index + 1);
}
vector> subsets(vector& nums) {
helper(nums, 0);
ret.push_back(vector());
return ret;
}
};
③ 从后往前看,定义递归函数返回从 [index, N-1]的所有子集,对 [index + 1, N-1]的所有子集,加和不加 index号元素,就形成了 [index, N -1]的所有子集。递归边界是index == N - 1。
class Solution {
public:
vector> helper(vector &nums, int index)
{
vector> ret;
if(index == nums.size() - 1)
{
ret.push_back(vector());
ret.push_back({nums[index]});
return ret;
}
vector> back = helper(nums, index + 1);
for (int i = 0; i < back.size(); ++i)
{
ret.push_back(back[i]);
back[i].push_back(nums[index]);
ret.push_back(back[i]);
}
return ret;
}
vector> subsets(vector& nums) {
vector> ret = helper(nums, 0);
return ret;
}
};
leetcode 90: 90. 子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
接口:
class Solution {
public:
vector> subsetsWithDup(vector& nums) {
}
};
思路:
仍然是涉及到有重复元素的数组的非重复取法问题,去重的策略还是先排序,然后判断是否需要跳过。
class Solution {
public:
vector> ret;
vector tmp;
void helper(vector&nums, int start)
{
for (int i = start; i < nums.size(); ++i)
{
if(i > start && nums[i] == nums[i-1]) continue;
tmp.push_back(nums[i]);
ret.push_back(tmp);
helper(nums, i + 1);
tmp.pop_back();
}
}
vector> subsetsWithDup(vector& nums) {
sort(nums.begin(), nums.end());
helper(nums, 0);
ret.push_back(vector());
return ret;
}
};
leetcode 491: 491. 递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
这一题与上面组合问题的去重不一样的地方是,不可以排序。示例中的两个重复数字7是相邻的,但是测试用例中会有例如1,2,1,2这样的示例,因此不能用 if (i > start && nums[i] == nums[i-1]) continue; 这样的策略来去重。
思考一下 if (i > start && nums[i] == nums[i-1]) continue; 的含义:在同一层(start),相同的数字只取第一个。那么,对于这一题,由于相同的数字不一定相邻,怎么保证在同一层相同的数字只取第一个呢?答案是用set存储当前层已经被取过的数字,在同一层,后面如果出现重复的数字,就跳过,因为只取第一个。
class Solution {
private:
vector> ret;
vector tmp;
void dfs(int start, vector& nums)
{
if(tmp.size() > 1) ret.emplace_back(tmp);
unordered_set set;
for (int i = start; i < nums.size(); ++i)
{
//同一层相同的数只取第一个
if(set.count(nums[i])) continue;
if((!tmp.empty() && nums[i] < tmp.back())) continue;
set.insert(nums[i]);
tmp.push_back(nums[i]);
dfs(i+1, nums);
tmp.pop_back();
}
}
public:
vector> findSubsequences(vector& nums) {
dfs(0, nums);
return ret;
}
};
leetcode 401: 401. 二进制手表
二进制手表顶部有 4 个 LED 代表小时(0-11),底部的 6 个 LED 代表分钟(0-59)。
每个 LED 代表一个 0 或 1,最低位在右侧。给定一个非负整数 n 代表当前 LED 亮着的数量,返回所有可能的时间。
接口:
class Solution {
public:
vector readBinaryWatch(int num) {
}
};
思路:本质上是从10个位置里选 num 个位置填1的问题,只要这10个位置的取值确定了,对应的时间就可以得到。
class Solution {
public:
vector ret;
bool calTime(vector &used, string &str)
{
int hour = used[0] * 8 + used[1] * 4 + used[2] * 2 + used[3];
int min = used[4] * 32 + used[5] * 16 + used[6] * 8 + used[7] * 4 + used[8] * 2 + used[9];
if(hour <= 11 && min <= 59)
{
str = to_string(hour) + ":";
if(min < 10) str += "0" + to_string(min);
else str += to_string(min);
return true;
}
return false;
}
void helper(int num, vector used, int start)
{
if(num == 0)
{
string str;
if(calTime(used, str)) ret.push_back(str);
return;
}
for (int i = start; i < 10; ++i)
{
if(!used[i])
{
used[i] = 1;
helper(num - 1, used, i + 1);
used[i] = 0;
}
}
}
vector readBinaryWatch(int num) {
vector used(10, 0);
helper(num, used,0);
return ret;
}
};
leetcode 51: 51. N皇后
思路:
每次处理一行,遍历 [0, n-1]中所有列号,找到满足没有重复列、对角线的列号,递归处理下一行,直到n行全部处理完,输出。
class Solution {
public:
vector> ret;
void helper(int n, int index, vector &ans, vector&col, vector&sum, vector&sub)
{
if(index == n)
{
vector s(n, string(n, '.'));
for (int i = 0; i < n; ++i)
{
s[i][ans[i]] = 'Q';
}
ret.push_back(s);
return;
}
for (int column = 0; column < n; ++column)
{
if(!col[column] && !sum[index + column] && !sub[index - column + n - 1])
{
col[column] = true;
sum[index + column] = true;
sub[index - column + n - 1] = true;
ans.push_back(column);
helper(n, index + 1, ans, col, sum, sub);
ans.pop_back();
sub[index - column + n - 1] = false;
sum[index + column] = false;
col[column] = false;
}
}
}
vector> solveNQueens(int n) {
vector ans;
vector col(n, 0), sum(2*n, 0), sub(2*n, 0);
helper(n, 0, ans, col, sum, sub);
return ret;
}
};