动态规划问题总结(一)

一、从斐波那契开始

什么叫做斐波那契数列?

1 1 2 3 5 8 13……f(n-2) f(n-1) f(n-1)+f(n-2)

这样的数列就是斐波那契数列,总结起来斐波那契数列的表达式为

n=1,   f(1) = 1

n=2,   f(2) = 1

n=3,4,5……,  f(n) = f(n-1)+f(n-2)

也就是说想要得到斐波那契数列中某个位置的数,还要得到它前两个位置的数据,那么非常常见的一种解决方法就是暴力递归

def fib(n):
    if n == 1 or n == 2:
        return 1
    return fib(n-1) + fib(n-2)

可以很容易的看到,如果使用递归操作来解决斐波那契问题,很容易造成重复运算的问题,因此使用动态规划,即由底向上来解决问题

动态规划的步骤通常为:

1、划分阶段:按照问题的时间或空间特征,将问题划分成若干个阶段。

2、确定状态与状态变量:将问题发展到各种阶段的客观情况,通过状态的形式表达出来

3、状态转移方程

4、确定边界条件

def fib(n):
    if n == 1 or n == 2:
        return 1
    dp = [0 for i in range(n)]
    dp[0] = 1
    dp[1] = 1
    for i in range(2, n):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n-1]

可以看到这样就避免了递归解法的重复运算的问题

二、最长上升子序列、子串

1.最长上升子序列

题目:给定一个无序的整数数组,找到其中最长上升子序列的长度。

输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4

分析:上升子序列无需在乎元素是否连续,因此[2,5,7,101]、[2,3,7,101]、[2,5,7,18]都叫做上升序列,会发现对于包含当前位置元素的上升子序列是与之前的上升子序列相关的,当前元素如果大于前面的某个元素,那么使用前面元素的最长子序列加上当前元素就是当前的最长序列长度。

动态规划就是要确定一个dp数组,那么对于这道题dp的状态变量要如何定义呢?由分析可知,dp[i]可以定义为以i位置元素结尾的最长上升子序列长度。

那么状态转移方程呢,由于dp[i]代表以i位置结尾的最长上升子序列长度,那么我们要搜寻i位置前面比i位置元素小的结尾元素的最长上升子序列长度,找到他们中的最大值再加1.也就是

dp[i] = \max dp[j] + 1, nums[i] > nums[j]

状态转移方程已经写出来了,那么边界条件是什么呢?因为单个元素也是上升子序列,因此如果在当前元素之前没有比它小的元素,则说明以当前元素为结尾的最长上升子序列的长度为1

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if len(nums) == 0:
            return 0
        dp = [1 for i in range(len(nums))]
        # 遍历每一个结尾元素
        for i in range(1, len(nums)):
            # 遍历之前的元素
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

此方法的时间复杂度为O(n^{2}),本题还有一种方法能将复杂度降低至O(n\log n),动态规划+二分查找

2.最长上升子串

子串的意思是必须连续,这是和1问题不同的地方,我们可以直接在1的状态转移方程上做修改,由于子串必须是连续的,我们不能每次都找比当前值小的元素了,而是要判断当前元素前一个元素是不是要小于当前元素,如果小于则加1。再者要使用一个max变量来存储以某个元素为结尾的子串最长的长度。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if len(nums) == 0:
            return 0
        dp = [1 for i in range(len(nums))]
        # 遍历每一个结尾元素
        for i in range(1, len(nums)):
            # 遍历之前的元素
            for j in range(i):
                if nums[i] > nums[j] and i-j == 1:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

 

三.最长公共子序列、子串

1.最长公共子序列

题目描述:给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列。例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

分析:公共子序列不要求连续,因此可以定义状态dp[i][j]代表第一个字符串以第i位置元素为结尾与第二个字符串以第j位置元素为结尾时的最长公共子序列。

边界条件:i,j皆为首位时,text1[i] == text2[j]时dp[i][j]=1,否则为0

状态转移方程:

很容易分析出,当text1[i] == text2[j]时,dp[i][j] = dp[i-1][j-1] + 1

而当text1[i] != text2[j]时呢?既然i与j位置的元素不相等,那么以i,j两个位置为结尾的最长公共子序列一定是前面已知公共子序列的最大值,也就是说dp[i][j] = max(dp[i-1][j], dp[i][j-1])

最终两个字符串的最长公共子序列自然是dp[text1的长度][text2的长度]

拿abcde 与 ace举例,可以画出下表

abcde
a1111
c1122
e1223

 

按照以上分析写出代码

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        if not text1 or not text2:
            return 0
        m = len(text1)
        n = len(text2)
        dp = [[0 for i in range(n+1)]for i in range(m+1)]
        for i in range(1, m+1):
            for j in range(1, n+1):
                if text1[i-1] == text2[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[m][n]

2.最长公共子串

最长公共子串要求必须连续,那么直接更改状态转移方程

因为要求连续,那么当text1[i] != text2[j]时,直接赋0值,当text1[i] == text2[j]时与公共子序列一样

class Solution:
    def longestCommon(self, text1: str, text2: str) -> int:
        if not text1 or not text2:
            return 0
        m = len(text1)
        n = len(text2)
        dp = [[0 for i in range(n+1)]for i in range(m+1)]
        for i in range(1, m+1):
            for j in range(1, n+1):
                if text1[i-1] == text2[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = 0
        return max(dp)

四. 最长回文子串

题目描述:给定一个字符串 s,找到 s 中最长的回文子串。

首先什么是回文串,abba、bb都是回文串,也就是说正着、反着看都是一样的。

这个时候很多人就会提出一种错误的方法,就是先将给定的字符串反过来,然后找到最长公共子串就可以了,但这个方法当遇到S 的其他部分中存在非回文子串的反向副本时,最长公共子串法就会失败,如

S=“abacdfgdcaba”, S′=“abacdgfdcaba” S 以及 S′ 之间的最长公共子串为 “abacd”。显然,这不是回文。那么如何判断回文串呢?

其实我们可以注意到当给定字符串S的头尾元素相同且中间部分的字符串是回文串,那么当前字符串也是回文串,而当子串的长度小于等于2且头尾元素相同那么当前子串的最长回文子串的长度为1.

那么我们不妨使用状态dp[i][j]代表以i为头,j为尾的子字符串是否为回文串

dp[i][j] = True ,if j-i <= 1 and s[i] == s[j]

dp[i][j] = True , if s[i] == s[j] and dp[i+1][j-1] == True

之后我们要使用一个变量记录下最长的回文子串。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ""
        dp = [[False for i in range(len(s))]for i in range(len(s))]
        maxlen = -1
        res = ""
        for j in range(len(s)):
            for i in range(j+1):
                if (j-i <= 1 or dp[i+1][j-1]) and s[i] == s[j]:
                    dp[i][j] = True
                curlen = j-i+1
                if curlen > maxlen and dp[i][j] == True:
                    maxlen = curlen
                    res = s[i:j+1]
                
        return res
                

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值