2020年6月20日 星期六

Leetcode題解 Python:六月挑戰DAY19 Longest Duplicate Substring

給一字串 S,要找出最大的重複連續字串,重複字串可部分重壘。

參考︰力扣 

在這題出的這天,感覺最近要開始爆忙,也在想要不要先斷一下寫題解?但這題有難度,還是想把它做掉。

這題要用到「Rabin-Karp 's algorithm」,用來字串搜查,使用方法是把子字串做Hash轉換去對比。

但我從頭開始講,為什麼得用 Rabin-Karp 's algorithm ?

要從一字串內找的重複出現子字串時,會使用 suffix Array,中文是後綴表。

suffix Array
例一:
banana,生成後綴表 => [banana, anana, nana, ana, na, a]
然後依字母大小跟長度排序 => [a, ana, anana, na, nana, banana]
拿 [i] [i+1] 去找前綴最長共同長度 => a,ana: 1, ana,anana:3 ,...
共同前綴最長部分就是題目所求,也就是取最大值。
為什麼可行?在排序的時候,就已經把性質相近,組成相似的排在一起,因此只會跟最有可能有最長共同前綴的相比。

這方法的時間複雜度很大,因此需要改良。

提示一:Binary Search
例二:
banana 的長度為 6,因此最大重複字串的長度不會超過五,會落在[0,5]間,從中間先找長度 3 有沒有出現重複子字串,
這會比起例一每次都從長度 1 開始,還快上不少。
比較的參照是用 HashSet,取二分法的長度 m,從頭把長度 m 的子字串輪一遍,先檢查是否在 HashSet,若有就是有重複出現,沒有就把放入HashSet。  
因此 banana 會從長度 3 開始,有,l = 4。接著 m = (6-4)//2+4 = 5,拿長度 5 找,沒有,r = 5。
最後 m = 4,沒有,r = 4,此時 l < r == False,結束。因此可知最大的長度為 4-1 => 3。

找到重複時,就可以回傳其對應到的位置,也不用繼續找 下去。
但是,數量一大,子字串是 string,長度相對也會爆炸式增長,這使得記憶體會巨大消耗,會超使用限制。

提示二:Rabin-Karp 's algorithm
用簡單來講,就是把字串的位置跟值做 hash,然後得到一個能表示字串組合的hash值,該值若有在HashSet,就代表有重複。
把可能出現的值給一個對應值,這題只有英文小寫字母,故會有26個值,從 0 到 25 。
如果長度為2為下,hash值為0,那可對應 aa。hash值26,為ba。
例二的最大問題是「記憶體使用太多」,如果把子字串資訊轉換成hash值,hash值不超過 log2(26*26),
也就是佔用不超過10bit,但長度為2的string,至少也16bit。因此可以有效解決記憶體使用太多的問題。

雖然目前的hash值保證為1對1,但如果這樣算,一旦長度為32以上,hash值可能有超過26**32,這值太大會在某些語言造成溢位出現。
把值對一個大數取餘,就能限制hash值的長度,也會有開始有多值對一個hash的可能,但大數夠大,發生的機率是相當的低。(大部分的hash都會去限制長度,如:MD5、SHA256)

我不是很喜歡這題,因為正確的寫法可能會過不了。

Python
class Solution:
    def longestDupSubstring(self, S: str) -> str:        
        hashMap = dict()
        count = 0
        nums = []
        for c in S:
            if c not in hashMap:
                hashMap[c] = count
                count += 1
            nums.append(hashMap[c])
            
        mod = 2**64
        def search(length):            
            SubStrHash = 0            
            for i in range(length):
                SubStrHash = (SubStrHash*count + nums[i])%mod            
            hashset = {SubStrHash}
            al = (count**length)%mod
            for i in range(length, n):
                SubStrHash = (SubStrHash*count - nums[i-length]*al + nums[i])%mod
                if SubStrHash in hashset:
                    return i        
                hashset.add(SubStrHash)
            return -1                
            
        n = len(S)
        l, r = 0, n
        end = -1
        while r > l:
            m = (r - l)//2 + l
            res = search(m+1)
            if res == -1:
                r = m                 
            else:
                l = m + 1
                end = res
       
        return "" if end == -1 else S[end - r + 1: end+1]