1. 程式人生 > 實用技巧 >LeetCode 392. 判斷子序列 | Python

LeetCode 392. 判斷子序列 | Python

392. 判斷子序列


題目來源:https://leetcode-cn.com/problems/is-subsequence/

題目


給定字串 s 和 t ,判斷 s 是否為 t 的子序列。

你可以認為 s 和 t 中僅包含英文小寫字母。字串 t 可能會很長(長度 ~= 500,000),而 s 是個短字串(長度 <=100)。

字串的一個子序列是原始字串刪除一些(也可以不刪除)字元而不改變剩餘字元相對位置形成的新字串。(例如,"ace"是"abcde"的一個子序列,而"aec"不是)。

示例 1:

s = "abc", t = "ahbgdc"

返回 true.

示例 2:

s = "axc", t = "ahbgdc"

返回 false.

後續挑戰 :

如果有大量輸入的 S,稱作S1, S2, ... , Sk 其中 k >= 10億,你需要依次檢查它們是否為 T 的子序列。在這種情況下,你會怎樣改變程式碼?

解題思路


思路:雙指標、動態規劃

在這裡,先理清題目所提出的問題,題目要問的是,s 是否是 t 的子序列?而題目中定義這個子序列,是指不改變相對位置在原字串中刪除一些(或者不刪除)字元剩餘的字元。

那麼也就是說,只要能找到 s 在 t 中存在(相對位置順序對應),那麼可以認定 s 是 t 的子序列。例如,題目中所給出的示例,"ace" 是 "abcde" 的一個子序列,而 "aec" 不是。因為 "aec" 改變了相對位置的順序。

在這裡,我們可以從前往後匹配,而且可貪心地靠前匹配出現的字元。

當我們從前往後匹配字元的時候,假設出現的字元 x 在 t 中出現的位置,一個在前面,一個在後面。在這裡,應該考慮匹配 x 在 t 出現前面的字元,這是因為往後匹配,當選定前面位置出現的字元時,能夠更大概率匹配成功。(因為字元 x 出現在後面位置往後能取的字元,前面位置往後也能夠取到,而且前後兩個位置之前的字元也有可選字元。)

那麼具體的演算法如下:

  • 定義雙指標 p、q,分別指向 s 和 t 的初始位置;
  • 這裡匹配前面位置出現的字元(也就是進行貪心匹配),當匹配成功之後,指標同時往後移動;
  • 如果匹配失敗,p 保持不同,移動 q。
  • 如果 p 能夠到達末尾,那麼說明 s 就是 t 的子序列。

具體的程式碼見【程式碼實現 # 雙指標】

還有一個後續挑戰,需要檢驗大量的 s 是否是 t 的子序列。在上面的雙指標的方法當中,從前往後去匹配字元需要大量的時間,那麼這裡再使用雙指標的方法顯然不合適。

這裡參考官方題解,說一下動態規劃如何去快速匹配 s 是否是 t 的子序列。

首先用動態規劃的方法去進行預處理,能夠確定在 t 的每個位置,從該位置往後每個字元第一次出現的位置。

狀態定義

設 dp[i][j] 表示字串 t 中從 i 的位置開始往後匹配,j 第一次出現的位置。

狀態轉移方程

  • 如果 t 中位置 i 的字元就是 j 的話,那麼 dp[i][j] = i;
  • 若不是上面的情況,那麼也就是說 j 出現在 i 位置之後的某個位置(這裡不包含 i),此時 dp[i][j] = dp[i+1][j]

狀態初始化

在這裡,索引從 0 開始,那麼 i 的取值範圍為 [0, t_len),這裡不包含 t_len。那麼,這裡存在邊界問題,當 i = t_len-1 的時候,這裡可能會無法進行轉移。我們讓 i = t_len 的時候,令 dp[t_len][...] 為 t_len,那麼也就說,當 dp[i][j] = t_len 的時候,那麼就表示從 i 開始無法匹配 j。

具體的程式碼見【程式碼實現 # 動態規劃】

程式碼實現


# 雙指標
class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        s_len = len(s)
        t_len = len(t)

        # 定義雙指標,指向 s 和 t 的初始位置
        p = 0
        q = 0

        while p < s_len and q < t_len:
            # 當 s 的字元與 t 的字元匹配時
            # 同時移動 p 和 q 指標
            if s[p] == t[q]:
                p += 1
            # 如果不匹配,只移動 q 指標,與 p 指標所對應的字元繼續匹配判斷
            q += 1
        # 如果 p 指標到達 s 末尾返回 True
        return p == s_len

# 動態規劃
class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        s_len = len(s)
        t_len = len(t)

        dp = [[0] * 26 for _ in range(t_len)]
        # 這裡是為了能夠讓 i = t_len-1 的時候能夠正常轉移
        dp.append([t_len]*26)

        # 在這裡,從後往前列舉,因為 dp[i][j] 可能從 dp[i+1][j] 中轉移而來
        for i in range(t_len-1, -1, -1):
            for j in range(26):
                # 如果位置 i 的字元就是 j 時,那麼 dp[i][j] = i
                if ord(t[i]) == j + ord('a'):
                    dp[i][j] = i
                else:
                    dp[i][j] = dp[i+1][j]
                # dp[i][j] = i if ord(t[i]) == j + ord('a') else dp[i+1][j]
        
        # 開始遍歷匹配 s,檢驗 s 的每個字元在 t 中的某個位置是否存在
        idx = 0
        for i in range(s_len):
            # 如果轉移只有結果為 t_len,表示無法匹配字元,那麼返回 False
            if dp[idx][ord(s[i]) - ord('a')] == t_len:
                return False
            # 當找到匹配當前字元的位置之後,從這個位置的下一個位置開始查詢下一個字元是否出現在 t 中的某個位置
            idx = dp[idx][ord(s[i]) - ord('a')] + 1

        return True

實現結果


雙指標

動態規劃

歡迎關注


公眾號 【書所集錄