枚举去重子序列
今天刷力扣遇到了一道题,要枚举所有的子序列并去重,这让我一下没了思路,平时根本没遇到过啊,然后话不多说直接看题解,两种办法:一是二进制枚举,二是递归枚举,直接给我打开了新世界的的大门!这里记录一下~
题目:491. 递增子序列https://leetcode.cn/problems/non-decreasing-subsequences/
参考题解就是官解还有评论区各位大佬们的讨论了
二进制枚举
因为之前没接触过二进制枚举,而且之前遇到二进制的问题直接就复制粘贴,今天下定决心啃一下,发现好像确实没那么难~
假设我们有一个数组[1,2,3,4,5,6]
一共六个数,然后用二进制表示选与不选:1表示选,0表示不选
举个例子:选[1,2,3]
的话那么得出来的二进制序列就是
数据 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
二进制 | 1 | 1 | 1 | 0 | 0 | 0 |
那么我们该怎么用代码写出来呢,力扣官解给出了一个方案:
class Solution {
public:
vector<int> temp;
vector<vector<int>> ans;
unordered_set<int> s;
int n;
void findSubsequences(int mask, vector<int>& nums) {
temp.clear();
for (int i = 0; i < n; ++i) {
if (mask & 1) {
temp.push_back(nums[i]);
}
mask >>= 1;
}
}
int getHash(int base, int mod) {
//(x+101)是因为数据范围是-100~100,保证都是正数
//思路与平时计算进制转化时相反,从左往右进行计算
int hashValue = 0;
for (const auto &x: temp) {
hashValue = 1LL * hashValue * base % mod + (x + 101);
hashValue %= mod;
}
return hashValue;
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
n = nums.size();
for (int i = 0; i < (1 << n); ++i) {
findSubsequences(i, nums);
int hashValue = getHash(263, int(1E9) + 7);
if ( s.find(hashValue) == s.end()) {
ans.push_back(temp);
s.insert(hashValue);
}
}
return ans;
}
};
- 如果我们要构造子序列是不是要将
000000
到111111
的数都表示一遍,那么就有2^n个数所以用代码表示的话就是1 << n
(今天刚学到,之前都是用pow函数),然后调用find函数判断(1加入0不加入)要将数组的哪一个元素加入到数组中进行下一步操作 - 看代码用到了与
&
,意思就是最右边的那一位与1进行与&
运算,结果为真才移入数组,学过逻辑运算的应该都知道只有1&1才是1
其他情况都是0。 - 然后将二进制数右移,即将最右边第二位与1进行与运算重复n次
可是现在只是构造出了所有的子序列,我们的目的是寻找所有的去重子序列,下面就要介绍一个算法:哈希算法(即 Rabin-Karp 编码)见题解https://leetcode.cn/problems/longest-happy-prefix/
Rabin-Karp 字符串编码是一种将字符串映射成整数的编码方式,可以看成是一种哈希算法。具体地,假设字符串包含的字符种类不超过 ∣Σ∣(其中Σ 表示字符集),那么我们选一个大于等于∣Σ∣ 的整数 base ,就可以将字符串看成 base 进制的整数,将其转换成十进制数后,就得到了字符串对应的编码。
例如给定字符串 s =“abca”,其包含的字符种类为 3(即 abc三种)。我们取base=9,将字符串 ss 看成九进制数(0120) ,转换为十进制为 99,也就是说字符串 abca 的编码为 99。
这样做的好处是什么?我们可以发现一个结论:两个字符串 s 和 t 相等,当且仅当它们的长度相等且编码值相等。对于长度为 k 的所有字符串,我们会将它们编码成位数为 k(包含前导零)的base 进制数,这是一个单射,因此结论成立。
这在实现的过程中,我们发现这个哈希值可能非常大,我们可以将它模一个大素数 PP,将这个值映射到intint 的范围内。所以实际上这里的哈希函数是:
玛卡巴卡说了这么多总结一下就是:选择一个大于元素种类的数base,将字符串写成base进制然后转化为十进制,进而判断两个数是否一样,计算过程直接套公式就可以了,因为有数据规模可能太大所以要取模控制一下
代码如下:
int getHash(int base, int mod) {
int hashValue = 0;
for (const auto &x: temp) {
hashValue = 1LL * hashValue * base % mod + (x + 101);
hashValue %= mod;
}
return hashValue;
}
至此就解决完了~是不是感觉挺复杂的?下面介绍一个简单的!
递归枚举
有了前面的基础,直接放代码
vector<vector<int>> ans;
vector<int> temp;
void dfs(int cur, vector<int>& nums) {
if (cur == nums.size()) {
// 判断是否合法,如果合法判断是否重复,将满足条件的加入答案
if (isValid() && notVisited()) {//这里可以改成自己的条件约束
ans.push_back(temp);
}
return;
}
// if 如果选择当前元素
temp.push_back(nums[cur]);
dfs(cur + 1, nums);
temp.pop_back();
// if 如果不选择当前元素
dfs(cur + 1, nums);
}
这是一个递归枚举子序列的通用模板,即用一个临时数组temp 来保存当前选出的子序列,使用cur 来表示当前位置的下标,在 dfs(cur, nums) 开始之前,[0,cur−1] 这个区间内的所有元素都已经被考虑过,而[cur,n] 这个区间内的元素还未被考虑。在执行 dfs(cur, nums) 时,我们考虑cur 这个位置选或者不选,如果选择当前元素,那么把当前元素加入到temp 中,然后递归下一个位置,在递归结束后,应当把 temp 的最后一个元素删除进行回溯;如果不选当前的元素,直接递归下一个位置。
学会就好了~
耗费了一个下午解决了一个困难题和一个中等题,越学越发现自己不会的东西还有很多,还有时间,多做总结变成自己的东西,稳健!🍅