组合与排列
参考:
https://www.bilibili.com/video/av34962180?t=1435
https://www.bilibili.com/video/BV1DX4y157r2
一,组合序列
1,dfs
直接用 dfs 遍历所有可能
注意点:
① 当前可选择的数字从上次被选择的数字的下一个开始:避免重复选择相同的数字
② 数字的固定顺序满足字典序:让选择出来的组合符合字典序
2,利用递归的搜索和回溯特性
每个数字只有选择与不选择两种状态。因此,可以利用递归的搜索功能对所有数字进行选择操作,然后,就可以利用递归的回溯功能取消之前的选择,对当前数字进行不选择操作。
当选择完了足够个数的数字后,在保存或者输出后一定要 return,不然对下一个数字进行不选择操作后,就会继续输出相同的序列。
3,参数说明
s:当前搜索的数字
k:还需要选择的个数
4,代码:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<vector> using namespace std; #define N 101 vector<int>way; int n, m; void show() { for (int i = 0; i < way.size(); i++) printf("%d ", way[i]); puts(""); } void dfs(int s, int k) { if (n + 1 - s < k) // 剪枝:还未选择的数 < 还需选择的数(代表这种情况无解) return; if (k == 0) { show(); return; } for (int i = s; i <= n; i++) { way.push_back(i); dfs(i + 1, k - 1); way.pop_back(); } } void trace(int s, int k) { if (n + 1 - s < k) // 剪枝:还未选择的数 < 还需选择的数(代表这种情况无解) return; if (k == 0) { show(); return; } way.push_back(s); trace(s + 1, k - 1); way.pop_back(); trace(s + 1, k); } int main(void) { // 组合:n 个数中选 m 个数 while (scanf("%d%d", &n, &m) != EOF) { printf("dfs:\n"); dfs(1, m); printf("trace:\n"); trace(1, m); } return 0; }
二,排列序列
1,dfs
直接用 dfs 遍历所有可能
注意点:
① 要想选择的数字相同,但顺序不同,需要固定数字的顺序,让可选择的数字下一个数从第一个数字开始
② 用 vis[] 对已经选择的数字:避免重复选择相同的数字
vis[ i ] :为 1 代表第 i 个数字已经被选择了;为 0 代表第 i 个数字没有被选择。
2,利用递归的搜索和回溯特性
因为回溯是只改变数字的选择与被选择状态,无法改变数字被选择的顺序。比方说你的选择的数字是 1 2 3,那么它被选择的顺序只能是按顺序选择的 1 2 3,再怎么回溯也不可能回溯出 1 3 2 或 2 1 3 之类的顺序。所以上述第二种方法无法求解排列问题。
3,参数说明
k:还需要选择的个数
4,代码
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<vector> using namespace std; #define N 101 vector<int>way; int n, m, vis[N]; void show() { for (int i = 0; i < way.size(); i++) printf("%d ", way[i]); puts(""); } void dfs(int k) { if (k == 0) { show(); return; } for (int i = 1; i <= n; i++) { if (vis[i]) continue; vis[i] = 1; way.push_back(i); dfs(k - 1); way.pop_back(); vis[i] = 0; } } int main(void) { // 排列:n 个数中选 m 个数 while (scanf("%d%d", &n, &m) != EOF) { dfs(m); } system("pause"); return 0; }
三,组合数
组合数公式:
c(n, m) = c(n-1, m) + c(n-1, m-1)
可定性理解为:
要从 n 个数中选出 m 个数,对于其中某个数,将其分为选与不选,则有
不选,则变成从 n-1 个数中选 m 个数,即为 c(n-1, m)
选,则变成从 n-1 个数中选出 m-1 个数,即为 c(n-1, m-1)
所以,有 n 个数选 m 个数的情况为选与不选的情况之和。
初始条件(递归出口):
从 n 个数中选 0 个数为 1,即 c(n, 0) 全为 1。
代码见例题 3。
四,例题
1,组合
① 无重复的数字的组合序列输出。
链接:https://leetcode-cn.com/problems/combinations/
class Solution { public: vector<vector<int> >res; vector<int>way; int n; void dfs(int s, int k) { if(n + 1 - s < k) // 剪枝:还未选择的数 < 还需选择的数(代表这种情况无解) return; if (k == 0) { res.push_back(way); return; } for (int i = s; i <= n; i++) { way.push_back(i); dfs(i + 1, k - 1); way.pop_back(); } } void trace(int i, int k) { if(n + 1 - i < k) // 剪枝:还未选择的数 < 还需选择的数(代表这种情况无解) return; if (k == 0) { res.push_back(way); return; } way.push_back(i); trace(i + 1, k - 1); way.pop_back(); trace(i + 1, k); } vector<vector<int>> combine(int n, int k) { this->n = n; //dfs(1, k); trace(1, k); return res; } };
② 组合总和,无重复元素的数组,可以无限制的选择一个元素。
链接:https://leetcode-cn.com/problems/combination-sum/
解题思路:
首先,求和是不针对选择的数字的顺序,所以是组合。
其次,要想实现重复选择同一个元素,就相当于是利用递归实现类 while 循环。
其中,无论是 dfs 还是 trace 都可以分成两步,
Ⅰ判断该元素能否选择
Ⅱ 调用递归函数时不跳到下一个元素,仍然还是选择该元素
这样,就可以实现如果能够选择,就会一直选择了。
class Solution { public: vector<int>a; vector<vector<int>> res; vector<int>way; void dfs(int s, int sum) { if(sum == 0) res.push_back(way); for(int i = s; i < a.size(); i++) { if(sum - a[i] < 0) continue; way.push_back(a[i]); dfs(i, sum - a[i]); way.pop_back(); } } void trace(int i, int sum) { if(sum == 0) { res.push_back(way); return; } if(i == a.size()) return; trace(i + 1, sum); if(sum - a[i] >= 0) { way.push_back(a[i]); trace(i, sum - a[i]); way.pop_back(); } } vector<vector<int>> combinationSum(vector<int>& candidates, int target) { sort(candidates.begin(), candidates.end()); this->a = candidates; //dfs(0, target); trace(0, target); return res; } };
③ 组合总和,有重复元素,但每个元素只能选择一次。
链接:https://leetcode-cn.com/problems/combination-sum-ii/
解题思路:
首先,求和是不针对选择的数字的顺序,所以是组合。
其次,因为有重复元素,所以会出现在同一深度选择不同位置的相同元素的情况,导致出现了相同的序列。
例如,数组 [ 1, 1, 1 ] ,会出现选择第一个元素和第二个元素的 [ 1, 1 ] 和 选择第一个元素和第三个元素的 [ 1, 1 ],这样就出现了重复了。
要想在同一深度中不选择相同的数字的话,只能用 dfs。
因为 dfs 的 for 循环里的是同一深度的元素,而 trace 无法知道当前的元素在哪个深度。
因为,对于同一深度的元素的选择,对于相同值的元素,其选择应该都是一致的,即
如果不选择这个值的话,则所有相同的值的元素都不能选择,
如果选择这个元素的话,那么只能选择相同值中的一个,选择了之后,其它元素不能选择。
所以,在 dfs 中可以将相同的元素通过排序放在一起,在 for 中只对相同值的元素的第一个元素进行选择操作,从而实现有重复元素的不重复选择。
class Solution { public: vector<int>a; vector<vector<int>> res; vector<int>way; void dfs(int s, int sum) { if(sum == 0) res.push_back(way); for(int i = s; i < a.size(); i++) { if(sum - a[i] < 0) // 超出 return; if (i > s && a[i] == a[i - 1]) // 剔除重复的元素 continue; way.push_back(a[i]); dfs(i + 1, sum - a[i]); way.pop_back(); } } vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { sort(candidates.begin(), candidates.end()); this->a = candidates; dfs(0, target); return res; } };
④ 集合的子集遍历
链接:https://leetcode-cn.com/problems/subsets/
解题思路:
首先,子集是不针对选择的数字的顺序,所以是组合。
其次,因为是子集,所以不用限制选择的数量,就可以遍历所有可能了。
class Solution { public: vector<vector<int>>res; vector<int>a; vector<int>way; void dfs(int s) { if (s == a.size()) { res.push_back(way); return; } for (int i = s; i < a.size(); i++) { way.push_back(a[i]); dfs(i + 1); way.pop_back(); } } void trace(int i) { if (i == a.size()) { res.push_back(way); return; } way.push_back(a[i]); trace(i + 1); way.pop_back(); trace(i + 1); } vector<vector<int>> subsets(vector<int>& nums) { this->a = nums; //trace(0); trace(0); return res; } };
2,排列
① 无重复的数字的排列序列输出。
链接:https://leetcode-cn.com/problems/permutations/
class Solution { public: vector<vector<int> >res; vector<int>a; vector<int>way; int vis[10]; void dfs(int k) { if (k == 0) { res.push_back(way); return; } for (int i = 0; i < a.size(); i++) { if(vis[i]) continue; way.push_back(a[i]); vis[i] = 1; dfs(k - 1); vis[i] = 0; way.pop_back(); } } vector<vector<int>> permute(vector<int>& nums) { this->a = nums; dfs(a.size()); return res; } };
② 有重复元素的数字的排列序列输出。
链接:https://leetcode-cn.com/problems/permutations-ii/
class Solution { public: vector<vector<int> >res; vector<int>a; vector<int>way; int vis[15]; void dfs(int k) { if (k == 0) { res.push_back(way); return; } for (int i = 0; i < a.size(); i++) { if(vis[i]) continue; if (i > 0 && a[i] == a[i - 1] && !vis[i - 1]) continue; way.push_back(a[i]); vis[i] = 1; dfs(k - 1); vis[i] = 0; way.pop_back(); } } vector<vector<int>> permuteUnique(vector<int>& nums) { sort(nums.begin(), nums.end()); this->a = nums; dfs(a.size()); return res; } };
3,组合数的计算
http://poj.org/problem?id=1306
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 105 #define ll long long ll c[N][N]; void combination(int n) { for (int i = 0; i <= n; i++) { for (int j = 0; j <= i; j++) if (j == 0) c[i][j] = 1; else c[i][j] = c[i - 1][j] + c[i - 1][j - 1]; } } int main(void) { combination(101); int n, m; while (scanf("%d%d", &n, &m) && (m + n)) { printf("%d things taken %d at a time is %lld exactly.\n", n, m, c[n][m]); } system("pause"); return 0; }
========== ========== ========= ======= ======== ====== ===== ==== == =
一棵开花的树 席慕容