1. 程式人生 > >Leetcode演算法——30、尋找所有詞語拼接而成的子串

Leetcode演算法——30、尋找所有詞語拼接而成的子串

給定一個字串s、一個數組words,裡面每個元素都是一個詞語,所有詞的長度相等。

在s中尋找所有子串的索引,子串需要是words中每個詞首尾拼接而成,詞之間沒有其他字元插入,詞的拼接順序沒有要求。

示例:

Example 1:
Input:
  s = "barfoothefoobarman",
  words = ["foo","bar"]
Output: [0,9]
Explanation: Substrings starting at index 0 and 9 are "barfoor" and "foobar" respectively.
The output order does not matter, returning [9,0] is fine too.

Example 2:
Input:
  s = "wordgoodstudentgoodword",
  words = ["word","student"]
Output: []

思路

1、暴力法

由於每個詞長度相等,因此每次取出子串的長度是固定的,而且很容易計算出所有詞語拼接起來的長度。假設所有詞拼接起來的長度為 len。

定義一個指標,從左往右遍歷s,每次遍歷都判斷當前指標開始往後的長度為 len 的子串是否符合要求:

  • 對子串按照詞語長度進行切分,分成候選詞語,候選詞語的個數為陣列 words 的詞語個數。
  • 對於每一個候選詞語,都與陣列 words 中的詞語進行匹配,如果正好可以一一匹配上,則說明符合要求。

小技巧:

可以將words中的每個詞的個數提前統計出來,每次匹配上一個,個數-1,直至所有的詞的個數都變為0,則說明匹配成功。

2、改進版

當某個分詞匹配不上words時,所有包含這個分詞的子串肯定都是不符合要求的,可以迅速忽略掉。

比如一個子串 ‘aaabbbccc’,被切分成了3個候選詞語 ‘aaa’,‘bbb’,‘ccc’,然後依次與words中的詞語進行匹配,結果發現words中並沒有 ‘ccc’ 這個詞語,於是這個子串匹配失敗。

但是我們還可以獲得一個資訊,就是當指標繼續向右移動3次,需要判斷 ‘bbbcccddd’ 這個子串是否符合要求時,
我們可以迅速知道肯定是不符合要求的,因為這個子串也會切分出 ‘ccc’ 這個詞語。

關鍵點在於,是指標向右移動 3 次之後的子串可以迅速判斷不符合要求,這個 3 便是每個詞語的長度。

因此,從當前指標開始,到匹配不上的候選詞語的起始位置結束,這之間的子串,按照詞語長度進行切割,得到每個詞語的起始位置,以這些位置開頭的子串都不符合條件,可以迅速略過。

python實現

import copy
import re

def findSubstring(s, words):
    """
    :type s: str
    :type words: List[str]
    :rtype: List[int]
    暴力法。
    """
    if not s or not words:
        return []
    
    l_word = len(words[0]) # 每個詞的長度
    l_total = l_word * len(words) # 所有詞拼接起來的長度
    
    if len(s) < l_total: # s長度小於所有詞拼接起來的長度
        return []
    
    # 統計words中的詞頻
    word_count_dict = dict()
    for word in words:
        if word in word_count_dict:
            word_count_dict[word] += 1
        else:
            word_count_dict[word] = 1
    
    # 遍歷s
    result = []
    for i in range(0, len(s) - l_total + 1):
        cur_dict = copy.copy(word_count_dict)
        split_list = re.findall(f'.{{{l_word}}}', s[i:i+l_total]) # 按詞長度切割子串
        for split_word in split_list:
            if split_word in cur_dict: # 可以匹配上words中的某個詞
                if cur_dict[split_word] > 1: # 次數-1
                    cur_dict[split_word] -= 1
                else:
                    cur_dict.pop(split_word)
                # 如果詞典為空,則說明全部匹配了一遍
                if not cur_dict:
                    result.append(i)
            else: # 匹配不上,說明當前子串不合格
                break
    return result

def findSubstring2(s, words):
    """
    :type s: str
    :type words: List[str]
    :rtype: List[int]
    改進版。
    當某個分詞匹配不上words時,所有包含這個分詞的子串肯定都是不符合要求的,可以迅速忽略掉。
    """
    if not s or not words:
        return []
    
    l_word = len(words[0]) # 每個詞的長度
    l_total = l_word * len(words) # 所有詞拼接起來的長度
    
    if len(s) < l_total: # s長度小於所有詞拼接起來的長度
        return []
    
    # 統計words中的詞頻
    word_count_dict = dict()
    for word in words:
        if word in word_count_dict:
            word_count_dict[word] += 1
        else:
            word_count_dict[word] = 1
    
    # 遍歷s
    result = []
    ignore_idx_set = set() # 肯定不符合要求的索引
    for i in range(0, len(s) - l_total + 1):
        if i in ignore_idx_set:
            continue
        cur_dict = copy.copy(word_count_dict)
        for j in range(0, len(words)): # 每次遍歷子串的一個分詞
            split_word_start = i + j*l_word # 分詞的起始索引
            split_word = s[split_word_start : split_word_start + l_word] # 子串的第j個分詞
            if split_word in cur_dict: # 可以匹配上words中的某個詞
                if cur_dict[split_word] == 0: # 次數已經用盡,說明此詞是多餘的,子串不符合要求
                    break
                cur_dict[split_word] -= 1
                if j == len(words) - 1: # 已經遍歷完最後一個分詞,說明子串符合要求
                    result.append(i)
            else: # 分詞不存在於words中,則所有包含這個分詞的子串都肯定不符合要求
                k = i + l_word # 包含這個分詞的子串的起始位置
                while(k <= split_word_start):
                    ignore_idx_set.add(k)
                    k += l_word
                break
    return result

if '__main__' == __name__:
    s = "wordgoodgoodgoodbestword"
    words = ["word","good","best","good"]
    print(findSubstring2(s, words))