算法55----最長子序列【動態規划】


一、題目:最長公共子序列:

給定兩個字符串,求解這兩個字符串的最長公共子序列(Longest Common Sequence)。比如字符串L:BDCABA;字符串S:ABCBDAB

則這兩個字符串的最長公共子序列長度為4,最長公共子序列是:BCBA

思路:動態規划:時間O(n * m),空間O(n * m)

創建 DP數組C[i][j]:表示子字符串L【:i】和子字符串S【:j】的最長公共子序列個數。

狀態方程:

個數代碼:

def LCS(L,S):
    if not L or not S:
        return ""
    dp = [[0] * (len(L)+1) for i in range(len(S)+1)]
    for i in range(len(S)+1):
        for j in range(len(L)+1):
            if i == 0 or j == 0:
                dp[i][j] = 0
            else:
                if L[j-1] == S[i-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1])
    return dp[-1][-1]
L = 'BDCABA'
S = 'ABCBDAB'
LCS(L,S)

最長子序列代碼:設置一個標志

def LCS(L,S):
    if not L or not S:
        return ""
    res = ''
    dp = [[0] * (len(L)+1) for i in range(len(S)+1)]
    flag = [['left'] * (len(L)+1) for i in range(len(S)+1)]
    for i in range(len(S)+1):
        for j in range(len(L)+1):
            if i == 0 or j == 0:
                dp[i][j] = 0
                flag [i][j] = '0'
            else:
                if L[j-1] == S[i-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                    flag[i][j] = 'ok'
                else:
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1])
                    flag[i][j] = 'up' if dp[i][j] == dp[i-1][j] else 'left'
    return dp[-1][-1],flag
def printres(flag,L,S):
    m = len(flag)
    n = len(flag[0])
    res = ''
    i , j = m-1 , n-1
    while i > 0 and j > 0:
        if flag[i][j] == 'ok':
            res += L[j-1]
            i -= 1
            j -= 1
        elif flag[i][j] == 'left':
            j -= 1
        elif flag[i][j] == 'up':
            i -= 1
    return res[::-1]            
L = 'BDCABA'
S = 'ABCBDAB'
num,flag = LCS(L,S)
res = printres(flag,L,S)

 

 


二、題目:最長遞增子序列

給定一個長度為N的數組,找出一個最長的單調自增子序列(不一定連續,但是順序不能亂)。例如:給定一個長度為6的數組A{5, 6, 7, 1, 2, 8},則其最長的單調遞增子序列為{5,6,7,8},長度為4.

解法一:最長公共子序列:O(N^2)

這個問題可以轉換為最長公共子序列問題。如例子中的數組A{5,6, 7, 1, 2, 8},則我們排序該數組得到數組A‘{1, 2, 5, 6, 7, 8},然后找出數組A和A’的最長公共子序列即可。顯然這里最長公共子序列為{5, 6, 7, 8},也就是原數組A最長遞增子序列。

解法二:動態規划法(時間復雜度O(N^2))

 設 dp(j) 表示L中以 L[j] 為末元素的最長遞增子序列的長度。狀態方程:

dp(j) = { max(dp(i)) + 1, i<j且L[i]<L[j] }

這個遞推方程的意思是,在求以L【j】為末元素的最長遞增子序列時,找到所有序號在 j 前面且小於L【j】的元素L【i】,即 i < j 且 L【j】< L【i】。

例如給定的數組為{5,6,7,1,2,8},則 dp(0)=1, dp(1)=2, dp(2)=3, dp(3)=1, dp(4)=2, dp(5)=4。所以該數組最長遞增子序列長度為4,序列為{5,6,7,8}。

代碼:

def LCS1(L):
    if not L:
        return ""
    dp = [1] * len(L)
    for j in range(len(L)):
        for i in range(j):
#當j = 5,i = 0時,dp = [1,2,3,1,2,1]
#當j = 5,i = 0時,dp[5] = 1 < dp[0]+1,故dp(5)更新為dp[0]+1=2,
#當j = 5,i = 1時,dp[5] = 2 < dp[1]+1 =3,故dp(5)更新為dp[1]+1=3
#當j = 5,i = 2時,dp[5] = 4
#當j = 5,i = 3時,dp[5] = 4 > dp[3]+1 = 3,故dp[5]不更新,同理,i = 4時,dp[5]仍等於4
if L[j] > L[i] and dp[j] < dp[i] + 1:
dp[j]
= dp[i]+1 return max(dp) L = [5,6,7,1,2,8] LCS1(L)

得到dp數組之后找出,最長遞增子序列,

  • 先找到dp最大值5,索引為7,然后arr【7】= 9
  • dp【6】 = 5-1 =4,故arr【6】=8
  • dp【4】 = 4-1或者dp【5】 = 4-1,故arr【4】 = 6 / arr【5】=4
  • dp 【2】=3-1或者dp【3】 = 3-1,故arr【2】 = 5 / arr【3】=3
  • 2 / 1

故最長遞增子序列:2→5→6→8→9或者1→3→4→8→9

 

解法三:優化的動態規划,時間O(NlogN),空間效率最壞情況也是O(n),

5 9 4 1 3 7 6 7 2

那么:dp為以下情況,

5 //加入
5 9 //加入
4 9 //用4代替了5
1 9 //用1代替4
1 3 //用3代替9
1 3 7 //加入
1 3 6 //用6代替7
1 3 6 7 //加入

1 2 6 7 //用2代替3

該dp=【1,2,6,7】數組理解為到目前為止長度為1的遞增子序列末尾最小為1,長度為2的遞增子序列末尾最小為2,長度為3的遞增子序列末尾最小為6,長度為4的遞增子序列末尾最小為7.

而2代替3是找到比剛好2大的數3,這個查找過程通過二分查找,故時間復雜度為二分查找的O(NlogN)

最后b中元素的個數就是最長遞增子序列的大小,即4。

要注意的是最后數組里的元素並不就一定是所求的序列,

例如如果輸入 2 5 1

那么最后得到的數組應該是 1 5

而實際上要求的序列是 2 5

 

進階題目:二維數組的最長遞增子序列(生日禮物(京東2016實習生真題))

把卡片套裝在一系列的信封A = {a1,  a2,  ...,  an}中。小東已經從商店中購買了很多的信封,她希望能夠用手頭中盡可能多的信封包裝卡片。為防止卡片或信封被損壞,只有長寬較小的信封能夠裝入大些的信封,同尺寸的信封不能套裝,卡片和信封都不能折疊。

解題思路:

  我們首先定義一個結構體,存放信封的長,寬,及其索引位置,然后把不能裝卡片的信封去除掉(長寬較小的), 然后根據長或寬進行一個排序,這樣就可以轉化成一個最長遞增子序列問題來求解了,2層循環動態規划就很容易求解了。


三、題目:最長遞增子序列個數

 

給定一個未排序的整數數組,找到最長遞增子序列的個數。

示例 1:

輸入: [1,3,5,4,7]
輸出: 2
解釋: 有兩個最長遞增子序列,分別是 [1, 3, 4, 7] 和[1, 3, 5, 7]。

示例 2:

輸入: [2,2,2,2,2]
輸出: 5
解釋: 最長遞增子序列的長度是1,並且存在5個子序列的長度為1,因此輸出5。

注意: 給定的數組長度不超過 2000 並且結果一定是32位有符號整數。

思路:動態規划,時間O(n2),空間O(n2)

定義 dp(n,1) count (n,1) 

用dp[i]表示以nums[i]為結尾的遞推序列的長度,

用cnt[i]表示以nums[i]為結尾的遞推序列的個數,

初始化都賦值為1,只要有數字,那么至少都是1。

狀態方程:

if nums[i] > nums[j] and dp[i] == dp[j] :
       dp[i] = dp[j]+1

  count[i]  = count[j]

elif nums[i] > nums[j] and dp[i] == dp[j]+1:

  count[i] += count[j]

代碼:

def findNumberOfLIS(nums):
    # dp solution, 2 arrays
    # dp[i] stores the longest length ending at nums[i]
    # count[i] counts the number of paths with length dp[i]
    if not nums:
        return 0

    n = len(nums)
    dp = [1] * n
    count  = [1] * n

    for i in range(1, n):
        for j in range(i):
            if nums[i] > nums[j]:
                # dp[i] = max(dp[j]+1, dp[i]) 
                # but we need to compute count also
                if dp[i] == dp[j]:
                    dp[i] = dp[j]+1
                    count[i]  = count[j]
                elif dp[i] == dp[j]+1:
                    count[i] += count[j]

    maxLength = max(dp)
    return sum([count[i] for i in range(n) if dp[i] == maxLength])
nums = [1,3,5,4,6]
findNumberOfLIS(nums)

 


四、題目:最大連續子序列(子串)

最大子序列是要找出由數組成的一維數組中和最大的連續子序列。比如{5,-3,4,2}的最大子序列就是 {5,-3,4,2},它的和是8,達到最大;而 {5,-6,4,2}的最大子序列是{4,2},它的和是6。你已經看出來了,找最大子序列的方法很簡單,只要前i項的和還沒有小於0那么子序列就一直向后擴展,否則丟棄之前的子序列開始新的子序列,同時我們要記下各個子序列的和,最后找到和最大的子序列。

import copy
def maxSubSum(arr):
    if not arr:
        return 0
    maxSum,curSum = 0,0
    SubList,curlist = [],[]
    for i in range(len(arr)):
        if curSum + arr[i] > 0:
            curSum += arr[i]
            maxSum = curSum
            curlist.append(arr[i])
            SubList = copy.deepcopy(curlist)
        else:
            curSum = 0
            curlist = []
    return maxSum,SubList
arr = [5,-6,4,2,-1,3,-9]
maxSubSum(arr)

五、題目:最長公共子串

找出兩個字符串最長連續的公共字符串,如兩個母串cnblogs和belong,最長公共子串為lo

思路:動態規划:時間O(N*M),空間O(N*M)

將二維數組c[i][j]用來記錄具有這樣特點的子串——結尾同時也為子串x1x2⋯xi與y1y2⋯yj的結尾的長度。

代碼:

def lcs(s1,s2):
    if not s1 or not s2:
        return 0
    c = [[0] * len(s2) for i in range(len(s1))]
    result = 0
    for i in range(len(s1)):
        for j in range(len(s2)):
            if i == 0 or j == 0:
                c[i][j] = 0
            else:
                if s1[i-1] == s2[j-1]:
                    c[i][j] = c[i-1][j-1] + 1
                    result = max(c[i][j],result)
                else:
                    c[i][j] = 0
    return result
s1 = 'cnblogs'
s2 ='belong'
lcs(s1,s2)

 


六、題目:最長公共子序列(3個字符串)

設A、B、C是三個長為n的字符串,它們取自同一常數大小的字母表。設計一個找出三個串的最長公共子序列的O(n^3)的時間算法。
       思路:跟上面的求2個字符串的公共子序列是一樣的思路,只不過這里需要動態申請一個三維的數組,三個字符串的尾字符不同的時候,考慮的情況多一些而已。

七、題目:判斷s是否為t的子序列:

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

你可以認為 st 中僅包含英文小寫字母。字符串 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 的子序列。在這種情況下,你會怎樣改變代碼?

 

思路:動態規划:dp[i]表示:s[i] 是否在t中,在則True,不在則False。

初始化:dp = [False] * len(s)

狀態方程:if s[i] == t[j],則dp[i] = True, i+=1,j += 1,否則,j+=1【繼續找,直到找到t的尾部】

代碼:

    def isSubsequence(self, s, t):
        """
        :type s: str
        :type t: str
        :rtype: bool
        """
        if not s:
            return True
        if not t or len(s) > len(t):
            return False
        i , j = 0 , 0
        dp = [False] * len(s)
        while i < len(s) and j < len(t):
            if s[i] == t[j]:
                dp[i] = True
                i += 1
                j += 1
            else:
                j += 1
        return all(dp)

八、題目:平方串

如果一個字符串S是由兩個字符串T連接而成,即S = T + T, 我們就稱S叫做平方串,例如"","aabaab","xxxx"都是平方串.
牛牛現在有一個字符串s,請你幫助牛牛從s中移除盡量少的字符,讓剩下的字符串是一個平方串。換句話說,就是找出s的最長子序列並且這個子序列構成一個平方串。

思路:動態規划:時間O(n^3),空間O(n^2)

首先將字符串s分為s1和s2,求s1和s2最長公共子序列。

拆分s1和s2有n種,如s = ‘ frankfurt',一、s1 = 'f',s2='rankfurt'。二、s1 ='fr',s2 = ’ankfurt‘……

故該方法時間復雜度為n3

 代碼:

def test():
    s = input()
    res = 0
    for i in range(len(s)-1):
        res = max(res,lcs(s,i))
    return 2*res
def lcs(s,i):
    s1 = s[:i+1]
    s2 = s[i+1:]
    dp = [[0] * (len(s2)+1) for i in range(len(s1)+1)]
    for i in range(1,len(s1)+1):
        for j in range(1,len(s2)+1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j],dp[i][j-1])
    return dp[-1][-1]
if __name__ == '__main__':
    print(test())

 九、題目:乘積最大子序列

給定一個整數數組 nums ,找出一個序列中乘積最大的連續子序列(該序列至少包含一個數)。

示例 1:

輸入: [2,3,-2,4]
輸出: 6
解釋: 子數組 [2,3] 有最大乘積 6。

示例 2:

輸入: [-2,0,-1]
輸出: 0
解釋: 結果不能為 2, 因為 [-2,-1] 不是子數組。

思路:設置兩個變量,一個存儲當前最大值,一個存儲最小值。

訪問到每個點的時候,以該點為子序列的末尾的乘積,要么是該點本身,要么是該點乘以以前一點為末尾的序列,注意乘積負負得正,故需要記錄前面的最大最小值。

代碼:

def test(nums):
    if not nums:
        return 0
    minnum = nums[0]
    maxnum = nums[0]
    res = nums[0]
    for i in range(1,len(nums)):
        minnum = min([nums[i],minnum*nums[i],maxnum*nums[i]])
        maxnum = max([nums[i],maxnum*nums[i],minnum*nums[i]])
        res = max(res,maxnum)
    return res
nums = [2,3,-2,4]
test(nums)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM