1. 程式人生 > >[LeetCode] Repeated DNA Sequences 求重複的DNA序列

[LeetCode] Repeated DNA Sequences 求重複的DNA序列

All DNA is composed of a series of nucleotides abbreviated as A, C, G, and T, for example: "ACGAATTCCG". When studying DNA, it is sometimes useful to identify repeated sequences within the DNA.

Write a function to find all the 10-letter-long sequences (substrings) that occur more than once in a DNA molecule.

For example,

Given s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT",

Return:
["AAAAACCCCC", "CCCCCAAAAA"].

看到這道題想到這應該屬於CS的一個重要分支生物資訊Bioinformatics研究的內容,研究DNA序列特徵的重要意義自然不用多說,但是對於我們廣大碼農來說,還是專注於演算法吧,此題還是用位操作Bit Manipulation來求解,計算機由於其二進位制儲存的特點可以很巧妙的解決一些問題,像之前的Single Number 單獨的數字Single Number II 單獨的數字之二都是很巧妙利用位操作來求解。此題由於構成輸入字串的字元只有四種,分別是A, C, G, T,下面我們來看下它們的ASCII碼用二進位制來表示:

A: 0100 0001  C: 0100 0011  G: 0100 0111  T: 0101 0100

由於我們的目的是利用位來區分字元,當然是越少位越好,通過觀察發現,每個字元的後三位都不相同,故而我們可以用末尾三位來區分這四個字元。而題目要求是10個字元長度的串,每個字元用三位來區分,10個字元需要30位,在32位機上也OK。為了提取出後30位,我們還需要用個mask,取值為0x7ffffff,用此mask可取出後27位,再向左平移三位即可。演算法的思想是,當取出第十個字元時,將其存在雜湊表裡,和該字串出現頻率對映,之後每向左移三位替換一個字元,查詢新字串在雜湊表裡出現次數,如果之前剛好出現過一次,則將當前字串存入返回值的陣列並將其出現次數加一,如果從未出現過,則將其對映到1。為了能更清楚的闡述整個過程,我們用題目中給的例子來分析整個過程:

首先我們取出前九個字元AAAAACCCC,根據上面的分析,我們用三位來表示一個字元,所以這九個字元可以用二進位制表示為001001001001011011011,然後我們繼續遍歷字串,下一個進來的是C,則當前字元為AAAAACCCCC,二進位制表示為001001001001011011011011,然後我們將其存入雜湊表中,用二進位制的好處是可以用一個int變數來表示任意十個字元序列,比起直接存入字串大大的節省了記憶體空間,然後再讀入下一個字元C,則此時字串為AAAACCCCCA,我們還是存入其二進位制的表示形式,以此類推,當某個序列之前已經出現過了,我們將其存入結果res中即可,參見程式碼如下:

解法一:

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        vector<string> res;
        if (s.size() <= 10) return res;
        int mask = 0x7ffffff;
        unordered_map<int, int> m;
        int cur = 0, i = 0;
        while (i < 9) {
            cur = (cur << 3) | (s[i++] & 7);
        }
        while (i < s.size()) {
            cur = ((cur & mask) << 3) | (s[i++] & 7);
            if (m.find(cur) != m.end()) {
                if (m[cur] == 1) res.push_back(s.substr(i - 10, 10));
                ++m[cur]; 
            } else {
                m[cur] = 1;
            }
        }
        return res;
    }
};

上面的方法可以寫的更簡潔一些,這裡我們可以用set來代替雜湊表,只要當前的數已經在雜湊表中存在了,我們就將其加入res中,這裡我們res也定義成set,這樣就可以利用set的不能有重複項的特點,從而得到正確的答案,最後我們將set轉為vector即可,參見程式碼如下:

解法二:

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        unordered_set<string> res;
        unordered_set<int> st;
        int cur = 0, i = 0;
        while (i < 9) cur = cur << 3 | (s[i++] & 7);
        while (i < s.size()) {
            cur = ((cur & 0x7ffffff) << 3) | (s[i++] & 7);
            if (st.count(cur)) res.insert(s.substr(i - 10, 10));
            else st.insert(cur);
        }
        return vector<string>(res.begin(), res.end());
    }
};

上面的方法都是用三位來表示一個字元,這裡我們可以用兩位來表示一個字元,00表示A,01表示C,10表示G,11表示T,那麼我們總共需要20位就可以表示十個字元流,其餘的思路跟上面的方法完全相同,注意這裡的mask只需要表示18位,所以變成了0x3ffff,參見程式碼如下:

解法三:

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        unordered_set<string> res;
        unordered_set<int> st;
        unordered_map<int, int> m{{'A', 0}, {'C', 1}, {'G', 2}, {'T', 3}};
        int cur = 0, i = 0;
        while (i < 9) cur = cur << 2 | m[s[i++]];
        while (i < s.size()) {
            cur = ((cur & 0x3ffff) << 2) | (m[s[i++]]);
            if (st.count(cur)) res.insert(s.substr(i - 10, 10));
            else st.insert(cur);
        }
        return vector<string>(res.begin(), res.end());
    }
};

如果我們不需要考慮節省記憶體空間,那我們可以直接將10個字元組成字串存入set中,那麼也就不需要mask啥的了,但是思路還是跟上面的方法相同:

解法四:

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        set<string> res, st;
        for (int i = 0; i + 9 < s.size(); ++i) {
            string t = s.substr(i, 10);
            if (st.count(t)) res.insert(t);
            else st.insert(t);
        }
        return vector<string>{res.begin(), res.end()};
    }
};

參考資料: