|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "子集生成" |
| 4 | +date: 2019-08-30 |
| 5 | +categories:: 编程 |
| 6 | +tags: |
| 7 | + - leetcode |
| 8 | + - 算法 |
| 9 | + - 暴力求解 |
| 10 | +--- |
| 11 | +很多问题都可以“**暴力解决**”。不需要动太多脑筋,把所有的可能性都列出来,然后一一实验。这样的方法显得很“笨”,却往往是行之有效的。 并且,很多问题拆分后的子问题,也需要用暴力求解的思想,比如 `BFS` 搜索最短路径,就需要列出所有可能,然后加入队列。 |
| 12 | + |
| 13 | +本篇讨论暴力求解的其中一个问题,子集生成问题。其他暴力求解的问题,如简单枚举,枚举排列,回溯法,路径寻找(隐式图的遍历)等问题,本篇暂不讨论。 |
| 14 | + |
| 15 | +## 子集生成问题 |
| 16 | +给定一个集合,枚举出所有可能的子集。[leetcode 第 78 题](https://leetcode.com/problems/subsets/) |
| 17 | + |
| 18 | +所有的自己生成问题都可以用三种方法来解决。**增量构造法**,**位向量法**,和**二进制法**。 |
| 19 | + |
| 20 | +### 增量构造法 |
| 21 | +思路是每次选出一个元素放入集合中。 |
| 22 | + |
| 23 | +比如对于题目中给的例子 `[1,2,3]` 来说,最开始是空集,那么我们现在要处理 `1`,就在空集上加 `1`,为 `[1]`,现在我们有两个自己 `[]` 和 `[1]`,下面我们来处理 `2`,我们在之前的子集基础上,每个都加个 `2`,可以分别得到 `[2]`,`[1, 2]`,那么现在所有的子集合为 `[]`, `[1]`, `[2]`, `[1, 2]`,同理处理 `3` 的情况可得 `[3]`, `[1, 3]`, `[2, 3]`, `[1, 2, 3]`, 再加上之前的子集就是所有的子集合了,代码如下: |
| 24 | +```c++ |
| 25 | +vector<vector<int>> subsets(vector<int> &S) { |
| 26 | + vector<vector<int>> res(1); |
| 27 | + sort(S.begin(), S.end()); |
| 28 | + for (int i = 0; i < S.size(); i++) { |
| 29 | + int size = res.size(); |
| 30 | + for (int j = 0; j < size; j++) { |
| 31 | + res.push_back(res[j]); |
| 32 | + res.back().push_back(S[i]); |
| 33 | + } |
| 34 | + } |
| 35 | + return res; |
| 36 | +} |
| 37 | +``` |
| 38 | +整个添加的顺序为: |
| 39 | +``` |
| 40 | +[] |
| 41 | +[1] |
| 42 | +[2] |
| 43 | +[1 2] |
| 44 | +[3] |
| 45 | +[1 3] |
| 46 | +[2 3] |
| 47 | +[1 2 3] |
| 48 | +``` |
| 49 | +
|
| 50 | +### 位向量法 |
| 51 | +由于原集合每一个数字只有两种状态,要么存在,要么不存在,那么在构造子集时就有选择和不选择两种情况,所以可以构造一棵二叉树,左子树表示选择该层处理的节点,右子树表示不选择,最终的叶节点就是所有子集合,树的结构如下: |
| 52 | +``` |
| 53 | + [] |
| 54 | + / \ |
| 55 | + / \ |
| 56 | + / \ |
| 57 | + [1] [] |
| 58 | + / \ / \ |
| 59 | + / \ / \ |
| 60 | + [1 2] [1] [2] [] |
| 61 | + / \ / \ / \ / \ |
| 62 | + [1 2 3] [1 2] [1 3] [1] [2 3] [2] [3] [] |
| 63 | +``` |
| 64 | +代码如下: |
| 65 | +```c++ |
| 66 | +vector<vector<int>> subsets(vector<int> &S) { |
| 67 | + vector<vector<int>> res; |
| 68 | + vector<int> path; |
| 69 | + sort(S.begin(), S.end()); |
| 70 | + genSubsets(S, 0, path, res); |
| 71 | + return res; |
| 72 | +} |
| 73 | +void genSubsets(vector<int> &S, int pos, vector<int> &path, vector<vector<int>> &res) { |
| 74 | + res.push_back(path); |
| 75 | + for (int i = pos; i < S.size(); i++) { |
| 76 | + path.push_back(S[i]); |
| 77 | + genSubsets(S, i + 1, path, res); |
| 78 | + path.pop_back(); |
| 79 | + } |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +整个添加的顺序为: |
| 84 | +``` |
| 85 | +[] |
| 86 | +[1] |
| 87 | +[1 2] |
| 88 | +[1 2 3] |
| 89 | +[1 3] |
| 90 | +[2] |
| 91 | +[2 3] |
| 92 | +[3] |
| 93 | +``` |
| 94 | + |
| 95 | +### 二进制法 |
| 96 | +由于集合中每个元素只有两种可能,选 与 不选。正好对应二进制的 `1` 和 `0`。于是,很自然的想到用二进制数字来表示集合的选择情况。 |
| 97 | + |
| 98 | +下面是二进制数字和对应的集合。 |
| 99 | + |
| 100 | +| | 1 | 2 | 3 | Subset | |
| 101 | +|-------|-------|---------|---------|-----------| |
| 102 | +| 0 | 0 | 0 | 0 |[] | |
| 103 | +| 1 | 0 | 0 | 1 |[3] | |
| 104 | +| 2 | 0 | 1 | 0 |[2] | |
| 105 | +| 3 | 0 | 1 | 1 |[2,3] | |
| 106 | +| 4 | 1 | 0 | 0 |[1] | |
| 107 | +| 5 | 1 | 0 | 1 |[1,3] | |
| 108 | +| 6 | 1 | 1 | 0 |[1,2] | |
| 109 | +| 7 | 1 | 1 | 1 |[1,2,3] | |
| 110 | + |
| 111 | +对应的代码如下: |
| 112 | +```c++ |
| 113 | +vector<vector<int>> subsets(vector<int> &S) { |
| 114 | + vector<vector<int>> res; |
| 115 | + sort(S.being(), S.end()); |
| 116 | + int max = 1 << S.size(); |
| 117 | + for (int k = 0; k < max; k++) { |
| 118 | + vector<int> out = genSubset(S, k); |
| 119 | + res.push_back(out); |
| 120 | + } |
| 121 | + return res; |
| 122 | +} |
| 123 | + |
| 124 | +vector<int> genSubset(vector<int> &S, int k) { |
| 125 | + vector<int> sub; |
| 126 | + int idx = 0; |
| 127 | + for (int i = k; i > 0; i = i >> 1) { |
| 128 | + if ((i & 1) == 1) { |
| 129 | + sub.push_back(S[idx]); |
| 130 | + } |
| 131 | + idx++; |
| 132 | + } |
| 133 | + return sub; |
| 134 | +} |
| 135 | +``` |
0 commit comments