2020年4月6日 星期一

Leetcode題解 Python: Palindrome Pairs

給一串文字串列,問說哪些兩個組合可以變成 Palindrome 回文。

這兩兩組合會有前後順序的差異,如果用最直觀的暴力破解法,跑遍每個組合,時間複雜度會是 O(n*(n-1)),O(n**2)。

列在 trie 的範圍內,這提示我們要使用 trie 去建立搜尋樹。要如何建立能找出回文的 trie ?

想一下回文的特性,如果是回文,頭跟尾會是相等的,頭跟尾往中間移動的字也會兩兩相等。

abcd|dcba, s|lls, lls|sssll => abcd|abcd., s|s.ll, lls|lls.ss

如果用顛倒的字詞建立 trie ,在搜尋過程中就可以前尾兩兩對消,如果過程找到字詞,只要確保「 . 」之後找到底也是回文的就可以。

aa|a => a.a|a.

萬一輸入的字詞找完之前,就先找到比較短的顛倒字詞,也代表後面不會再有字可以添加,先後對消後,只需要針對搜尋詞還沒找部分做回文判斷即可。
class Solution:
    def palindromePairs(self, words):
        result = []
        if words:
            trie = {} #反向     
            # create Trie
            n = len(words)
            for i in range(n):
                cur = trie
                for char in words[i][::-1]:
                    if char not in cur:
                        cur[char] = {}
                    cur = cur[char]
                cur['end'] = i
            # 檢查對稱                
            def isSymmetry(word):                
                n = len(word)
                for i in range(n//2):
                    if word[i] != word[n-1-i]: return False
                return True       
            #
            def search1(idx, temp, cur):
                """搜尋當前字詞"""
                if temp:
                    # 如果過程中找到字詞,檢查當前的剩餘部分是否回文
                    if 'end' in cur and isSymmetry(temp) and idx != cur['end']:                        
                        result.append([idx, cur['end']])
                    if temp[0] in cur:
                        search1(idx, temp[1:], cur[temp[0]])
                else:
                    search2(idx, "", cur)            

            def search2(idx, temp, cur):
                """搜尋剩餘部分是否回文"""
                for key in cur:
                    if key == "end":
                        if isSymmetry(temp) and idx != cur['end']:
                            result.append([idx, cur['end']])                        
                    else:
                        search2(idx, temp+key, cur[key])   
            #
            for i in range(n):
                search1(i, words[i], trie)
            
        return result

在效率上還是太慢,我搜尋是用更廣的對稱,涵蓋更多的可能性。看了一下前段班的寫法,去找更好的思路。

如果在搜尋上,是把字文拆出「可能」組合成回文的部分搜尋,就能提升效率,但我那時有思考到這種方式,但沒有寫想到要怎麼寫,因為我trie總是用成一層層,所以被侷限了。

字詞可以整個直接當成字典的鍵使用,該鍵的值就是位置。

像是 abcd,如果要拆成回文形式,就得找出左右之分。

abcd 前組後:
abcd | "" => ""是回文 => dcba(前面部分反序) => [0,1] #前組後
abc | d => d是回文 => cba => not found
ab | cd => cd不是回文 => X
a | bcd => cbd不是回文 => X
"" | abcd => abcd不是回文 => X

換個字詞

s 前組後:
s | "" => ""是回文 => s => [3,3] => 不合規則
"" | s => s是回文 => "" => not found

lls 前組後:
lls | "" => ""是回文 => sll => not found
ll | s =>> s是回文 => ll => not found
l | ls => ls不是回文 => X
"" | lls => lls不是回文 => X

這樣的搜尋只有「前組後」,沒有考慮到「後組前」,要考慮 後組前 只要把字詞反序做搜尋。
lls 後組前:
sll(字詞反序) | "" => ""是回文 => lls => [2,2] => 不合規則
sl | l => l是回文 => ls => not found
s | ll => ll是回文 => s => [3,2] #後組前
"" | sll => sll不是回文 => X

這樣搜尋速度快不少,不過會有答案會重複,所以要想辦法去掉。
class Solution:
    def palindromePairs(self, words):
        table = dict(map(reversed, enumerate(words)))
        res = set()
        for i, word in enumerate(words):
            for k in range(len(word)+1):                
                a = word[:k]
                b = word[k:]

                if a == a[::-1] and table.get(b[::-1], i) not in [-1, i]:
                    res.add((table.get(b[::-1], -1), i))
                    
                if b == b[::-1] and table.get(a[::-1], i) not in [-1, i]:
                    res.add((i, table.get(a[::-1], -1)))
                    
        return list(res)