Leetcode 187 Repeated DNA Sequences 重複出現的DNA序列

原題地址

https://leetcode.com/problems/repeated-dna-sequences/

題目描述

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.
所有的DNA都是由簡寫爲A``C``G``T的核苷酸構成的,例如ACGAATTCCG。在研究DNA時,辨別DNA中重複的序列在有些時候是很有用的。

Write a function to find all the 10-letter-long sequences (substrings) that occur more than once in a DNA molecule.
編寫一個程序來找到一個DNA分子中出現次數超過一次的長度爲10的子序列(子串)。

For example,
例如,

Given s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT",
給出 s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT",

Return:
返回:

["AAAAACCCCC", "CCCCCAAAAA"].

Tags Hash Table Bit Manipulation

解題思路

嗯,這個題,如果時間上沒有要求的話,用find和substr搞定是沒問題的,然而在測試數據較大的時候這種方法必然會超時。

這個題的標籤是Hash Table和Bit Manipulation,參考https://leetcode.com/discuss/24478/i-did-it-in-10-lines-of-c中的方案,現做如下整理。

測試數據中只會出現4種字符’A’,’C’,’G’,’T’,其ASC值分別爲65,67,71,84,對應的二進制位如下所示,這裏僅列取32位情況下的低8位,前24位全部爲0。

A --- 65 --- 0100 0001
C --- 67 --- 0100 0011
G --- 71 --- 0100 0111
T --- 84 --- 0101 0100

我們可以發現,僅通過低3位就能把4種字符區分開來,題目又要求了是長度爲10的子串10-letter-long),試想爲什麼不是11或者更大呢,因爲10個字符每個用3位表示一個字符的話剛好是30位,而一個int是32位,剛好能放下,如果是11+就放不下了。因此,我們可以把一個長度爲10的字符串映射成一個int數,用其低30位表示這個串,作爲這個串的key,然後把key存在Hash Table中,當key重複出現時就代表子串重複出現了。

詳細代碼請見下文。

除了上述方法外,受其啓發,我們其實只用2個二進制位就可以唯一區分4中字符,比如A-00,C-01,G-10,T-11,這樣,我們只用int的低20位就可以表示一個字符串的key,這種方法甚至可以最多用於處理長度爲16的子串。比上面一種方法略顯複雜的是,需要手動將ACGT映射成2位二進制數,方法也是多種多樣的。比如:

方法一

/** @return 字符到2位二進制數的映射 */
int function(char c) {
    int ret = 0;
    switch (c) {
        case 'A': break;
        case 'G': ret = 1; break;
        case 'C': ret = 2; break;
        case 'T': ret = 3; break;
    }
    return ret;
}

方法二

/* 數組映射,調用時使用nums[c - 65]即可得到字符c的映射碼 */
int nums[20]; 
nums[0] = 0; 
nums[2] = 1; 
nums[6] = 2; 
nums[19] = 3;

除此外,在討論貼中還看到了另外一種很巧妙的映射方法:

方法三

/** 
 * (s[i] - 64) % 5完成映射
 * A : ('A' - 64) % 5 = 1  (mod 5) = 1 = 01
 * B : ('C' - 64) % 5 = 3  (mod 5) = 3 = 11
 * C : ('G' - 64) % 5 = 7  (mod 5) = 2 = 10
 * D : ('T' - 64) % 5 = 20 (mod 5) = 0 = 00
 */
key = key << 2 & 0xfffff | (s[i] - 64) % 5;

代碼

代碼一

首先是用ACGT的ASC碼的後3位來映射的解法。在代碼編寫中,對於最開始的9個字符是爲了構造第一個key而做功,一般說來從第10個字符開始纔開始判定key的重複出現情況。然而由於AGCT的映射碼均爲三位且沒有任何一個碼是000,因此在前面9個字符也是可以與其他字符一致處理的,不會出現誤判。一致處理的代碼大致如下:

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        vector<string> strs;
        unordered_map<int, int> map;
        int key = 0; 
        for (int i = 0, end = s.size(); i < end; ++i) {
            key = ((key << 3) | (s[i] & 0x7)) & 0x3fffffff;
            if (map[key]++ == 1)
                strs.push_back(s.substr(i - 9, 10));
        }
        return strs; 
    }
};

不一致處理的代碼大概如下,先處理前9個字符,然後處理後面的:

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        vector<string> strs;
        if (s.size() <= 10) return strs;
        unordered_map<int, int> map;
        int key = 0, i = 0; 
        for (; i < 9; ++i)
            key = ((key << 3) | (s[i] & 0x7)) & 0x3fffffff;
        for (int end = s.size(); i < end; ++i) {
            key = ((key << 3) | (s[i] & 0x7)) & 0x3fffffff;
            if (map[key]++ == 1)
                strs.push_back(s.substr(i - 9, 10));
        }
        return strs; 
    }
};

完整代碼https://github.com/Orange1991/leetcode/blob/master/187/cpp/main.cpp

很難說到底哪種代碼更好。如果從代碼精簡的方面來看,前面一個代碼數量更少;但是前面一種代碼對於前9個字符也會把key放入map中且會判定是否key重複出現,這個操作是多餘的,相比於後面的解法來說就多了一些無用的操作,在實際的運行中也證實了這一點,前一種時間爲100ms,而後一種爲96ms。另外,對於前面一種方案,是否可以將前9個字符與其他字符一致處理而不致於誤判,還要看AGCT映射碼中是否有全0的碼,如果有則不能一致處理,就拿接下來馬上要說的解法二來說,我們把A映射爲00,假如我們的串是AA(你沒看錯,就是隻有2位的串),那對於第一個字符,key計算後得到結果爲0(你沒看錯,就是0),我們把0放入map並且其value爲0,然後,在第二個字符,key計算後仍然爲0,這時候根據規則,以第二個A爲結尾的10個字符要放入結果集中,這時候程序一定會崩潰的(s.substr(-8, 10))。因此,我們還是直接用後面一種解法就好,代碼不過多了兩行,而且不會出現誤判,哪怕映射碼中有全0碼也沒問題。

代碼二 自定義映射碼

在前面解題思路中已經說的比較詳細了,因此不再贅述,僅列出代碼

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        vector<string> strs;
        if (s.size() <= 10) return strs;
        unordered_map<int, int> map;
        int key = 0, i = 0; 
        /* 映射邏輯: A-01, C-11, G-10, T-00  */
        for (; i < 9; ++i)
            key = ((key << 2) | ((s[i] - 64) % 5)) & 0xfffff;
        for (int end = s.size(); i < end; ++i) {
            key = ((key << 2) | ((s[i] - 64) % 5)) & 0xfffff;
            if (map[key]++ == 1)
                strs.push_back(s.substr(i - 9, 10));
        }
        return strs; 
    }
};

完整代碼https://github.com/Orange1991/leetcode/blob/master/187/cpp/solution2.cpp

參考

https://leetcode.com/discuss/24478/i-did-it-in-10-lines-of-c
http://blog.csdn.net/ljiabin/article/details/44488619


2015/8/5

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章