暴力搜索回溯-排列、组合[子集、子串、子序列]
Abstract
常见 数组/字符串 找子【序列】、【子串、子数组】类题 是组合类题目,前者元素之间无需连续,后需要连续。在组成res的元组里,元素的相对位置不变,即res中只会出现元组(a, b, c),元组(b,a,c)不会出现在res中。
- 子序列–元素不需连续 – 最长递增子序列LIS、 最长公共子序列LCS
- 子串、子数组数 – 元素需要连续-- - 最大和子数组
排列是和组合共同进退的好朋友,只提其一,必随另一。 排列、组合问题常用解题思路暴力搜索回溯,如果外加求极值,大概率要增加dp
Introduction
排列:英文为 Permutation 或者 Arrangement,因此在数学公式里常用P 或者 A 表示。
排列数:从n个不同的元素中取m个元素,按一定的顺序组成的组合,成为一个排列;不同排列的总数为:
A
n
m
=
n
∗
(
n
−
1
)
∗
.
.
.
.
∗
(
n
−
(
m
−
1
)
)
=
n
!
(
n
−
m
)
!
A_n^m = n * (n-1) * ....*(n-(m-1)) = \frac{n!}{(n-m)!}
Anm=n∗(n−1)∗....∗(n−(m−1))=(n−m)!n!
- 第一个位置有n中选择,第二个位置只剩 n-1个选择,…
- {1, 2} 和 {2, 1} 可以构成不同的排列
组合:英文是 Combination,因此在数学公式里常用 C 表示。
组合数:从n个不同的元素里取m个元素,无序的放在一起,成为一个组合;不同的组合数为:
C
n
m
=
A
n
m
m
!
C_n^m = \frac{A_n^m}{m!}
Cnm=m!Anm
- 第一个位置有n中选择,第二个位置只剩 n-1个选择,…
- {1, 2} 和 {2, 1} 是同一个组合。 m个数的排列方式有m!种,但是这m!种排列方式仅属于一个组合。所以 A n m A_n^m Anm 中 每一个批(m个)数字 都被多排列的m!次,相当于整体扩大了m!倍,所以组合数需要➗m!
组合数的性质:
算总数: 如何把所有的可能性 【不多】 【不少】的统计出来。
参考博文:2小时学会中学排列组合
1. 组合
通过保证元素之间的相对顺序不变来 防止出现重复的组合。即dfs start参数决定了for循环起点(多叉树扩展分支)
78. 子集-(无重复-不复选-不限长)
(无重 不可复选-不限长)–给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
- 各个不同长度的组合 均为一个子集
class Solution {
vector<vector<int>> _res;
vector<int> _nums;
public:
vector<vector<int>> subsets(vector<int>& nums) {
_nums.assign(nums.begin(), nums.end());
vector<int> path;
dfs(path, 0);
return _res;
}
void dfs(vector<int>& path, int start) {
_res.push_back(path);
for (int i = start; i < _nums.size(); i++) {
path.push_back(_nums[i]);
dfs(path, i+1);
path.pop_back();
}
}
};
90. 子集II-(有重复-不复选-不限长)
(无重 不可复选-不限长)–给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
class Solution {
vector<vector<int>> _res;
vector<int> _nums;
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
_nums.assign(nums.begin(), nums.end());
vector<int> path;
dfs(path, 0);
return _res;
}
void dfs(vector<int>& path, int start) {
_res.push_back(path);
// [2, 2, 3]
for(int i = start; i < _nums.size(); i++) { // 横向扩展
if (i > start && _nums[i] == _nums[i-1]) {
continue; // 第一个是需要扩展的
}
path.push_back(_nums[i]);
dfs(path, i+1); // 纵向扩展
path.pop_back();
}
}
};
77. 组合-(无重复-不复选-限长)
(无重 不可复选-限长)-- 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
2. 通过保证元素之间的相对顺序不变来 防止出现重复的组合。
3. 组合总数为
C
n
k
C_n^k
Cnk
class Solution {
vector<vector<int>> res;
int _n;
int _k;
public:
vector<vector<int>> combine(int n, int k) {
_n = n;
_k = k;
vector<int> path;
dfs(path, 0);
return res;
}
void dfs(vector<int>& path, int start) {
if (path.size() == _k) {
res.push_back(path);
}
// start 可以决定 以下代码是否执行
for (int i = start; i < _n; i++) {
path.push_back(i+1);
dfs(path, i+1);
path.pop_back();
}
}
};
40. 组合总和II-(有重复-不复选-限和)
(有重复-不可复选-限和) --给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。candidates[i]>0 【减掉无端递归调用,对于减小耗时处理非常重要】
注意:解集不能包含重复的组合。
class Solution {
vector<vector<int>> _res;
vector<int> _nums;
int _target;
int _path_sum = 0;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
_nums.assign(candidates.begin(), candidates.end());
for (int i = 0; i < candidates.size(); i++) {
cout << candidates[i] << ", ";
}
cout << endl;
_target = target;
vector<int> path;
dfs(path, 0);
return _res;
}
void dfs(vector<int>& path, int start) {
// cout << "start:" << start << endl;
if (_path_sum >= _target) { //
if (_path_sum == _target) {
_res.push_back(path);
}
return;
}
for (int i = start; i < _nums.size(); i++) {
if (i > start && _nums[i] == _nums[i-1]) {
continue;
}
path.push_back(_nums[i]);
_path_sum += _nums[i];
dfs(path, i+1);
_path_sum -= _nums[i];
path.pop_back();
}
}
};
39. 组合总和-(无重复-可复选-限和)
(无重复-可复选-限和)–给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
class Solution {
vector<vector<int>> _res;
vector<int> _nums;
int _target;
int _path_sum;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
_nums.assign(candidates.begin(), candidates.end());
_target = target;
_path_sum = 0;
vector<int> path;
dfs(path, 0);
return _res;
}
void dfs(vector<int>& path, int start) {
if (_path_sum >= _target) {
if (_path_sum == _target) {
_res.push_back(path);
}
return;
}
for (int i = start; i < _nums.size(); i++) {
path.push_back(_nums[i]);
_path_sum += _nums[i];
dfs(path, i); // 元素可以重复选
_path_sum -= _nums[i];
path.pop_back();
}
}
};
2. 排列
46. 全排列-(无重复-不复选-不限长)
(无重复-不可复选-不限长)给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
- bool可选列表-保证不会重复选元素,同时能够选到 某个数字之前的数字。
class Solution {
vector<vector<int>> _res;
vector<bool> _used;
vector<int> _nums;
public:
vector<vector<int>> permute(vector<int>& nums) {
_used.resize(nums.size());
_nums.assign(nums.begin(), nums.end());
vector<int> path;
back_track(path);
return _res;
}
void back_track(vector<int>& path) {
if (path.size() == _nums.size()) {
_res.push_back(path);
return;
}
for (int i = 0; i < _used.size(); i++) {
if (_used.at(i) == false) {
path.push_back(_nums.at(i));
_used.at(i) = true;
back_track(path);
_used.at(i) = false;
path.pop_back();
}
}
}
};
47. 全排列II-(有重复-不复选-不限长)
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
class Solution {
vector<vector<int>> _res;
vector<int> _nums;
vector<bool> _used;
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
_nums.assign(nums.begin(), nums.end());
_used.resize(nums.size());
vector<int> path;
dfs(path);
return _res;
}
void dfs(vector<int>& path) {
if (path.size() == _nums.size()) {
_res.push_back(path);
}
for(int i = 0; i < _nums.size(); i++) {
if (_used[i]) {
continue;
}
if (i > 0 && _nums[i] == _nums[i-1] && !_used[i-1]) {
// 如何保证nums=[2, 2] 0_2-1_2 组成过一次,下一节点1_2-0_2 不能再次构成一个子元,否则出现重复子元
continue;
}
_used[i] = true;
path.push_back(_nums[i]);
dfs(path);
path.pop_back();
_used[i] = false;
}
}
};
3. 回文
// 647.回文子串(数) -- 中心扩展法(左右指针)/aux数组(暴力枚举)
int countSubstrings(string s);
// 5.最长回文子串(串) -- 中心扩展法(左右指针)/aux数组(暴力枚举)
string longestPalindrome(string s)
// 516.最长回文子序列(长) -- dp // dp[i][j]为子串s[i:j]最长回文序列长度,短串的结果能够用于更新长串的结果,子串长度(1 -> 2 -> 3 -> n)慢慢增长,直到全串。
int longestPalindromeSubseq(string s);
// 9.回文数, 234.回文链表 -- 是否
bool isPalindrome(int x);
bool isPalindrome(ListNode* head);
子串须连续,子序列无需连续。
回文子串类 :
- aux数组(暴力枚举) – 二维数组aux 记录s[i][j] 是否为回文子串,需要额外的空间。
- 中心扩展法(左右指针) – for 循环迭代中心,左右指针扩展,记录是否为回文子串,无需额外空间
647. 回文子串(数)
647.回文子串(数) – 给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。子字符串是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
class Solution {
public:
// 647.回文子串(数)-解1: 遍历所有子串,判断是否为回文子串,借助axu数组利用空间换时间
int countSubstrings(string s) {
int n = s.size();
int res = 0;
vector<vector<bool>> aux(n, vector<bool>(n, false));
for (int i = 0; i < n; i++) {
aux[i][i] = true;
res++;
}
for (int i = n -2; i > -1; i--) {
for (int j = i + 1; j < n; j++) {
if (j == i + 1) {
aux[i][j] = (s[i] == s[j]);
} else {
aux[i][j] = (s[i] == s[j] && aux[i+1][j-1]);
}
if (aux[i][j]) {
res++;
}
}
}
return res;
}
// 647.回文子串(数)-解2:真正中心扩展法,以 单/双字符为中心往两端扩展,每次扩展更新计数器,遇到非回文扩展即停止扩展
// 解1 aux数组解法的剪枝版本
int countSubstrings2(string s) {
int n = s.size();
int res = 0;
for (int i = 0; i < n; i++) {
res += palidrome_num(s, i, i);
res += palidrome_num(s, i, i+1);
}
return res;
}
int palidrome_num(string s, int left, int right) {
int n = s.size();
int res = 0;
while(left > -1 && right < n && s[left] == s[right]) {
res++;
left--;
right++;
}
return res;
}
};
5. 最长回文子串(串)
5.最长回文子串(串) – 给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
class Solution {
public:
// 5.最长回文子串-解1: aux解
string longestPalindrome(string s) {
int n = s.size();
vector<vector<bool>> aux(n, vector<bool>(n, false));
string res;
for (int i = 0; i < n; i ++) {
aux[i][i] = true;
res = s[0];
}
for (int i = n - 2; i > -1; i--) {
for (int j = i + 1; j < n; j++) {
if (j == i + 1) {
aux[i][j] = (s[i] == s[j]);
} else {
aux[i][j] = (s[i] == s[j] && aux[i+1][j-1]); // caution 已有条件使用
}
if (aux[i][j] && (res.size() < j - i + 1)) {
res = s.substr(i, j - i + 1);
}
}
}
return res;
}
// 5.最长回文子串-解2: 中心扩展法
string longestPalindrome(string s) {
int n = s.size();
string res;
if (n > 0) {
res = s[0];
}
for (int i = 0; i < n; i++) {
int left = i, right = i;
string s1 = palindrome(s, i, i);
string s2 = palindrome(s, i, i+1);
res = s1.size() > res.size() ? s1 : res;
res = s2.size() > res.size() ? s2 : res;
}
return res;
}
string palindrome(string s, int left, int right) {
int n = s.size();
while (left > -1 && right < n && s[left] == s[right]) {
left--;
right++;
}
return s.substr(left+1, (right - 1) - (left+ + 1) + 1); // 返回字符串,有效的left 和 right 要各自相中心缩小一格
}
};
516. 最长回文子序列(长)dp
516.最长回文子序列 – 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
Note: 子序列无需连续, 从整体序列中抽出几个序列就行。
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0)); // s[i:j] 最长回文序列长度,长度由 1 -> 2 -> 3 -> n
for (int i = 0; i < n; i++) {
dp[i][i] = 1;
}
for (int i = n - 2; i > -1; i--) {
for (int j = i + 1; j < n; j++) {
if (s[i] == s[j]) {
dp[i][j] = dp[i+1][j-1] + 2; // 从对角线更新来的,长度+2
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1]); // 从左边或者下边更新来的,选择大的那个,以备后用
}
}
}
return dp[0][n-1];
}
};
9. 回文数、234. 回文链表(附)
9.回文数 – 给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。
234.回文链表 – 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
// way1: 保存原来的链表,翻转链表,再遍历一次链表,3N,太复杂了,
// way2: 直接存list 遍历一遍list
class Solution {
public:
// 9.回文数, 切割数组, 12321
// left -> right
// 1232 -> 1
// 123 -> 12
// 12 -> 123
bool isPalindrome(int x) {
if (x < 0 || (x > 0 && x % 10 == 0)) {
return false;
}
int right_part = 0;
while(right_part < x) {
right_part = right_part * 10 + (x % 10);
x = x / 10;
}
if (right_part == x || right_part / 10 == x) {
return true;
}
return false;
}
bool isPalindrome(ListNode* head) {
// way1: 保存原来的链表,翻转链表,再遍历一次链表,3N,太复杂了,
// way2: 直接存list 遍历一遍list
vector<int> val_list;
ListNode* cur_node = head;
while(cur_node != nullptr) {
val_list.push_back(cur_node->val);
cur_node = cur_node->next;
}
int left = 0, right = val_list.size() - 1;
while(left < right) {
if (val_list[left] != val_list[right]) {
return false;
} else {
left++;
right--;
}
}
return true;
}
};