题目链接https://leetcode.com/problems/repeated-dna-sequences/
题意:寻找出现1次以上的长度为10的基因片段,例如:
需要特别注意的是:所有基因均由A, C, G, T 四种核苷酸组成。
看到题目,最先想到的是提取所有的长度为10的片段,放入map中,map的key为长度为10的片段,value为key出现的起始位置。提交之后果断超内存Memory Limit Exceeded了。
后来看到题目的标签为Bit Manipulation ,而且字符串中只有4种字母,1个bit可以表示2个字母的话,2个bit就可以表示4个字母,我们可以用00 (0), 01 (1), 10 (2), 11 (3)分别表示 A, C, G, T,一个片段有10个字母,只需要2*10 = 20个bit即可表示一个片段,即一个int即可。于是可把string key化为int key。代码如下:
class Solution {
private:
int str2i(string s){
int ret = 0;
for(int i=0; i<10; ++i){
int b = 2*i;
switch(s[i]){
case 'A':
ret |= (0<<b);
break;
case 'C':
ret |= (1<<b);
break;
case 'G':
ret |= (2<<b);
break;
case 'T':
ret |= (3<<b);
break;
}
}
return ret;
}
public:
// 00 A, 01 C, 10 G, 11 T
vector<string> findRepeatedDnaSequences(string s) {
vector<string> ret;
int n = s.size();
unordered_map<int, vector<int>> mp;
string sub = s.substr(0,10);
mp[str2i(sub)].push_back(0);
for(int i=10; i<n; ++i){
sub = sub.substr(1);
sub.push_back(s[i]);
int k = str2i(sub);
mp[k].push_back(i-10+1);
}
unordered_map<int, vector<int>>::iterator it;
for(it = mp.begin(); it != mp.end(); ++ it){
if(it->second.size()>1){
ret.push_back(i2str(it->first));
}
}
return ret;
}
};
这里其实无需保存每个片段的起始位置,只保存其出现的次数即可,稍作优化后的代码如下:
class Solution {
private:
int str2i(const string &s, int pos){
int ret = 0;
for(int i=pos; i<pos+10; ++i){
int b = 2*(i-pos);
switch(s[i]){
case 'A':
ret |= (0<<b);
break;
case 'C':
ret |= (1<<b);
break;
case 'G':
ret |= (2<<b);
break;
case 'T':
ret |= (3<<b);
break;
}
}
return ret;
}
public:
// 00 A, 01 C, 10 G, 11 T
vector<string> findRepeatedDnaSequences(string s) {
vector<string> ret;
int n = s.size();
unordered_map<int, int> mp;
for(int i=0; i<=n-10; ++i){
int k = str2i(s,i);
++ mp[k];
if(mp[k] == 2){
ret.push_back(s.substr(i,10));
}
}
return ret;
}
};
最近在二刷,重新做时又发现两处可改进的地方,其中第一处是提速的关键:
(1) 由于我们在扫描长度为10的片段时,总是连续进行的,即:移除开头的字符,并加入后续的一个字符,很明显,在计算这两个连续的10长片段的哈希值时,中间的9个字符都被重复计算了。其实,我们可以利用上一个片段的哈希值,来计算下一个片段的哈希值,例如AAAAACCCCCA,如果我们已知AAAAACCCCC的哈希值,我们只需将开头的A所在的2位归0,并将剩余的数位左移2位,然后加上后面的A对应的哈希码,即为片段AAAACCCCCA对应的哈希值。
(2) 由于片段长度固定为10,其中每一个字符可用两个bit来表示,即总共所需20bit,数值范围为0~2^20(1048576),其实可以开一个大小为1048576的数组进行计数,避免了unordered_map的开销。
代码如下:
class Solution {
public:
int getHashCode(string s, int pos){
int ret = 0;
for(int i=pos; i<pos+10; ++i){
switch(s[i]){
case 'A':
ret = (ret<<2)|0;
break;
case 'C':
ret = (ret<<2)|1;
break;
case 'G':
ret = (ret<<2)|2;
break;
case 'T':
ret = (ret<<2)|3;
break;
}
}
return ret;
}
int getBits(char c){
int ret = 0;
switch(c){
case 'A':
ret = 0;
break;
case 'C':
ret = 1;
break;
case 'G':
ret = 2;
break;
case 'T':
ret = 3;
break;
}
return ret;
}
vector<string> findRepeatedDnaSequences(string s) {
vector<int> st(1048576 ,0);
vector<string> ret;
if(s.size() < 11) return ret;
int tmp = getHashCode(s,0);
st[tmp] = 1;
for(int i=10; i<s.size(); ++i){
tmp = ((tmp & 0xfff3ffff) << 2)|(getBits(s[i]));
if(st[tmp] == 0){
st[tmp] = 1;
}else{
if(st[tmp] == 1){
ret.push_back(s.substr(i-9,10));
}
++ st[tmp];
}
}
return ret;
}
};