动态规划Ⅲ:数组区间

动态规划Ⅲ:数组区间

动态规划题目类型 & 做题思路总览动态规划解题套路 & 题型总结 & 思路讲解

文章目录

  • 三、数组区间

    1. 数组区间和

    数组区间和:给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。LeetCode 直达

    如果直接用暴力循环,时间开销是很大的,用动态规划的思路,建立 dp 数组,数组每个位置上是 当前位置的状态,即 从数组开始到当前位置所有元素的和。对于给定区间 [i,j],将求和问题转换为 res = dp[j] - dp[i-1],在具体的代码实现中,为了免去对 i 是否等于 0 的判断,将 dp 的第一个位置初始化为 0。

    class NumArray:
    
        def __init__(self, nums: List[int]):
            if not nums: return None
            self.dp = [0] 
            n = len(nums)
            for i in range(1, n+1):
                # dp[i]为数组0~i-1位置上元素的和
                self.dp.append(self.dp[i-1] + nums[i-1])
    
        def sumRange(self, i: int, j: int) -> int:
            return self.dp[j+1] - self.dp[i]
    
    # Your NumArray object will be instantiated and called as such:
    # obj = NumArray(nums)
    # param_1 = obj.sumRange(i,j)

    2. 等差数列划分

    等差数列划分:如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。请返回数组 A 中所有为等差数组的子数组个数。LeetCode 直达

    • 明确状态:本题唯一的变量就是 “等差数列的个数”,所以 “个数” 就是要找的状态- DP 数组:创建一个和原数组相同大小的 DP 数组,每个位置上的元素值 dp[i] 表示在 A[0] 到 A[i] 区间上,等差数列的个数- 明确选择:从左向右遍历原数列,每添加进来一个新的数字,也许可以和前面构成等差数列,也许不能,具体就要判断 A[i]-A[i-1] == A[i-1]-A[i-2]- 状态间关系:如果当前数字能和前面的两个数构成等差数列,那么当前等差数列的个数 = 前一状态等差数列的个数 + 能够新增的等差数列个数,而能够新增的等差数列个数 = 当前有效等差数列的长度 - 2。所以有状态转移方程:dp[i] = dp[i-1] + (m-2),其中 m 存放了当前 “有效” 等差数列的长度- 确定 base case:元素个数小于等于 2 时,构不成等差数列,dp 数组中对应的状态为 0 为什么我的状态转移方程是 dp[i] = dp[i-1] + (m-2),可以举一个具体的例子:

    如果有数列 [1, 2, 3, 8, 9, 10, 11],那么初始化 DP 数组为 [0, 0, 0, 0, 0, 0, 0]。从 i = 2,即数列元素 3 开始向后遍历。如果能够和前两个数构成等差数列,且当前等差数列长度 m 为 0,则将 m 设为 3,同时 dp[2] = 0 + (3 - 2) = 1。如果 m 不等于 0,说明此时等差数列还是 “连续” 的,新增的这个元素只是扩大了等差数列的规模,m 仅仅自增 1 即可。

    在 i = 3 时,数列元素为 8,不能和前面构成等差数列,也就是等差数列 “断了”,则将 m 重置到 0,且此时 dp[3] = dp[2],即等差数列个数不会变化。

    以此类推,在 i = 6 时,数列元素为 11,此时 dp[5] = 2,添加进来的元素 11 能够和前面的 8,9,10 构成等差数列,不难看出其实新增加的个数就等于当前等差数列长度 - 2。

    class Solution:
        def numberOfArithmeticSlices(self, A: List[int]) -> int:
            n, m = len(A), 0
            if not A or n < 3: return 0
            dp = [0 for _ in range(n)]
            for i in range(2, n):
                if A[i]-A[i-1] == A[i-1]-A[i-2]:
                    if m == 0: m = 3
                    else: m += 1
                    dp[i] = dp[i-1] + (m-2)
                else:
                    m = 0
                    dp[i] = dp[i-1]
            return dp[-1]

    可以发现 dp[i] 仅和 dp[i-1] 有关,可以优化空间,省去 dp 数组:

    class Solution:
        def numberOfArithmeticSlices(self, A: List[int]) -> int:
            n = len(A)
            if not A or n < 3: return 0
            m, cur = 0, 0
            for i in range(2, n):
                if A[i]-A[i-1] == A[i-1]-A[i-2]:
                    if m == 0: m = 3
                    else: m += 1
                    cur = cur + (m-2)
                else: 
                    m = 0
            return cur

    时间复杂度:O(n),遍历长度为 n 的数组
    空间复杂度:O(1)

    还有令一种角度,将 DP 数组中的 dp[i] 看作以 A[i] 做结尾的等差数列的个数,那么当 A[i]-A[i-1] == A[i-1]-A[i-2] 时,dp[i] = dp[i-1] + 1。由于题目条件是等差数列不一定要以最后一个元素做结尾,所以最终的答案应该是 “累加和”。这种做法和第一种做法的区别在于对 dp 数组元素含义的定义。

    举个例子:仍是数列 [1, 2, 3, 8, 9, 10, 11],dp 数组的前两个元素仍初始化为 0。

    -> 3:满足 A[i]-A[i-1] == A[i-1]-A[i-2],对应的 dp[2] = 0 + 1 = 1 -> 9:不满足,对应的 dp[4] = 0 -> 11:满足,dp[6] = dp[5] + 1 = 1 + 1 = 2

    最终的答案是 dp 数组所有元素的累加和,即 1 + 1 + 2 = 4

    class Solution:
        def numberOfArithmeticSlices(self, A: List[int]) -> int:
            n = len(A)
            if not A or n < 3: return 0
            cur, sum = 0, 0
            for i in range(2, n):
                if A[i]-A[i-1] == A[i-1]-A[i-2]:
                    cur += 1
                    sum += cur
                else: cur = 0
            return sum

    时间复杂度:O(n),遍历长度为 n 的数组
    空间复杂度:O(1)

    还可以发现,每次满足条件 A[i]-A[i-1] == A[i-1]-A[i-2] 时,就进行 sum += 1,其实这里可以优化一下,因为 sum 加的是从 1 到 k 的递增序列,可以直接用一个 count 来计数,当不满足等差条件时,再对 sum 自增 1 到 k 的和。

    class Solution:
        def numberOfArithmeticSlices(self, A: List[int]) -> int:
            n = len(A)
            if not A or n < 3: return 0
            cur, sum = 0, 0
            for i in range(2, n):
                if A[i]-A[i-1] == A[i-1]-A[i-2]:
                    cur += 1
                else:
                    sum += int((1+cur)*cur / 2)
                    cur = 0
            sum += int((1+cur)*cur / 2)
            return sum

    ps:1 ~ k 的求和公式:

          (
    
    
          1
    
    
          +
    
    
          k
    
    
          )
    
    
          ×
    
    
          k
    
    
    
         2
    
    
    
    
       \frac{(1 + k) \times k}{2}
    
    
    2(1+k)×k​
    
    

    3. 子数组最大和

    连续子数组的最大和:输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。LeetCode 直达

    注意:子数组一定是连续的。

    【动态规划解题思路】

    • 状态列表 dp:其中 dp[i] 是第 i 个位置的状态(在本题中就是以第 i 个位置的元素 nums[i] 作为结尾的连续子数组的最大和)。确定状态的要点是,每一点的状态都与前面所有的状态有关联(比如本题,要确定第 i+1 个位置的状态 dp[i+1] 就要参考第 i 个位置的状态 dp[i],而第 i 个位置的状态又是从 i-1 确定的,以此类推),且每一点的状态都包含了前面所有点的状态,从而能够仅从 i-1 就确定出 i 的状态。- 状态转移方程:要明确状态列表中各个状态之间如何转换,建立关系式(关键!!!),通常这个关系式是一个分段表达式。在本题中,dp[i+1] 的状态取决于 dp[i],如果 dp[i] &lt; 0,那加了会更小,不符合我们的目标,所以当 dp[i] &lt;= 0dp[i+1] = nums[i+1],当 dp[i] &gt; 0dp[i+1] = nums[i+1] + dp[i]- 初始状态dp[0] = nums[0],要进行初始化- 返回值:返回状态列表 dp 中的最大值,即全局最大值 时间复杂度:O(n),线性遍历长度为 n 的数组
      空间复杂度:O(n),需要维护一个与原数组长度相同的状态列表
    class Solution:
        def maxSubArray(self, nums: List[int]) -> int:
            dp = [nums[0]]  # 状态列表dp
            n, m = len(nums), nums[0]  # 存储最大值
            for i in range(1, n):
                cur = max(nums[i], nums[i]+dp[i-1])  # 计算当前位置状态
                dp.append(cur)
                if cur > m: m = cur
            return m

    再分析发现,位置 i 的状态仅和 i-1 的状态有关,所以实际和前面爬楼梯问题一样,没必要每个状态都存下来,只需要存当前位置的前一个位置状态就可以了。

    时间复杂度:O(n),线性遍历长度为 n 的数组
    空间复杂度:O(1),只维护一个变量 cur 用于指向前一个位置的状态

    class Solution:
        def maxSubArray(self, nums: List[int]) -> int:
            n, m = len(nums), nums[0]  # 存储最大值
            cur = nums[0]  # 存储当前值
            for i in range(1, n):
                cur = max(nums[i], nums[i]+cur)  # 计算当前位置状态
                if cur > m: m = cur
            return m

    4. 单词拆分

    单词拆分:给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。LeetCode 直达

    【动态规划解题思路】

    • 明确状态:该题目的变量是 “能否拆分”,即布尔值 True 或 False- dp 数组dp[i] 表示以第 i 个字符结尾的字符串能否被拆分为字典中出现过的单词- 明确选择:在每个位置 i,都可以选择其前 0~i-1 个字符中任意一个位置作为起始点,与字典单词进行匹配,所以起码有一个二重循环- 状态间关系dp[i] 为 True,当且仅当 [0,j] 位置的字符串属于字典,且 [j,i+1] 位置的字符串也属于字典(这里的区间都是左闭右开的)- 确定 base case:为了方便统一处理,将 dp 数组初始化为 n+1 大小,第 0 位初始化为 True 时间复杂度
        O
    
    
        (
    
    
    
         n
    
    
         2
    
    
    
        )
    
    
    
       O(n^2)
    
    
    O(n2)<br> **空间复杂度**:
    
    
    
    
        O
    
    
        (
    
    
        n
    
    
        )
    
    
    
       O(n)
    
    
    O(n)
    
    
    class Solution:
        def wordBreak(self, s: str, wordDict: List[str]) -> bool:
            if not wordDict: return False
            n = len(s)
            dp = [False for i in range(n+1)]
            dp[0] = True
            for i in range(1, n+1):
                for j in range(i):
                    dp[i] = dp[i] or dp[j] and s[j:i] in wordDict
                    # 或者:
                    # dp[i] = dp[j] and s[j:i] in wordDict
                    # if dp[i] == True: break
            return dp[-1]

    5. 最长重复子数组

    最长重复子数组:给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。LeetCode直达

    【动态规划解题思路】

    • 明确状态:该题目的变量是唯一的,即“最长公共前缀的长度”,由于两个串的起始位置是可以任意匹配的,所以 dp 数组起码需要二维- dp 数组dp[i][j] 表示以 B 数组第 i 个数字做结尾,以 A 数组第 j 个数字做结尾时,最大公共前缀的长度- 状态间关系:在任意位置 dp[i][j],其最长公共前缀长度可以由 dp[i-1][j-1] 推出,若 A[j] == B[i],则 dp[i][j] = dp[i-1][j-1] + 1,否则 dp[i][j] = 0- 确定 base case:初始化 dp 矩阵第一行第一列,若相等则为 1,否则为 0 时间复杂度
        O
    
    
        (
    
    
        n
    
    
        m
    
    
        )
    
    
    
       O(nm)
    
    
    O(nm),n 和 m 分别是两个数组的长度<br> **空间复杂度**:
    
    
    
    
        O
    
    
        (
    
    
        n
    
    
        m
    
    
        )
    
    
    
       O(nm)
    
    
    O(nm)
    
    
    class Solution:
        def findLength(self, A: List[int], B: List[int]) -> int:
            n1, n2 = len(A), len(B)
            dp = [[0]*n1 for _ in range(n2)]
            m = 0
            for i in range(n1):
                if A[i] == B[0]:
                    dp[0][i] = 1
                    m = 1
                else:
                    dp[0][i] = 0
            for j in range(n2):
                if B[j] == A[0]:
                    dp[j][0] = 1
                    m = 1
                else:
                    dp[j][0] = 0
            for i in range(1, n2):
                for j in range(1, n1):
                    if A[j] == B[i]:
                        dp[i][j] = dp[i-1][j-1] + 1
                        m = max(m, dp[i][j])
                    else:
                        dp[i][j] = 0
            return m

    简洁版代码:官方题解

    class Solution:
        def findLength(self, A: List[int], B: List[int]) -> int:
            n, m = len(A), len(B)
            dp = [[0] * (m + 1) for _ in range(n + 1)]
            ans = 0
            for i in range(n - 1, -1, -1):
                for j in range(m - 1, -1, -1):
                    dp[i][j] = dp[i + 1][j + 1] + 1 if A[i] == B[j] else 0
                    ans = max(ans, dp[i][j])
            return ans

    6. 最长有效括号

    最长有效括号:给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。LeetCode直达

    【动态规划解题思路】

    • 明确状态:唯一的变量是有效子串的“长度”- dp 数组dp[i] 表示以第 i 个字符结尾的最长有效括号子串的长度- 状态间关系:需要分 3 种情况来看。如果 s[i] == '(',则 dp[i] == 0,因为不可能有以左括号结尾的有效子串。如果 s[i] == ')',分为两种情况:若 s[i-1] == '(',则直接匹配成功,dp[i] = dp[i-2] + 2,也就是将之前的最大有效子串长度再加上 2;若 s[i-1] == ')',则继续向前寻找是否匹配(比如像 '(())'这种例子,最后一个 ) 应该与第一个 ( 匹配),此时判断 s[i-dp[i-1]-1] == '(' 是否成立,若成立则 dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2]。- 确定 base casedp[0] 初始化为 0,因为第一个字符无论是左右括号都不可能出现有效字符串。 时间复杂度
        O
    
    
        (
    
    
        n
    
    
        )
    
    
    
       O(n)
    
    
    O(n),遍历长度为 n 的字符串一遍<br> **空间复杂度**:
    
    
    
    
        O
    
    
        (
    
    
        n
    
    
        )
    
    
    
       O(n)
    
    
    O(n),需要 dp 数组的额外存储空间
    
    
    class Solution:
        def longestValidParentheses(self, s: str) -> int:
            n = len(s)
            dp = [0 for _ in range(n)]
            m = 0
            for i in range(1, n):
                if s[i] == ')' and i-1 >= 0 and s[i-1] == '(':
                    dp[i] = dp[i-2] + 2
                elif s[i] == ')' and i-dp[i-1]-1 >=0 and s[i-dp[i-1]-1] == '(':
                    dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2]
                m = dp[i] if dp[i] > m else m
            return m

    7. 分割数组的最大值

    分割数组的最大值:给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。LeetCode 直达

    【动态规划解题思路】

    • 明确状态:本题的变量包括 m 和连续子数组,对不同的组合值得到的数组的最大值都是不一样的。考虑用二维 dp 数组来保存状态。- dp 数组dp[i][j] 表示数组前 i 个数划分为 j 个子数组所得到的连续子数组和的最大值的最小值。- 状态间关系:枚举 k,前 k 个数被分割为 j-1 个子数组,第 k+1 到第 i 个数为第 j 个子数组。此时 j 个子数组中和的最大值等于 dp[k][j-1] 与 sub(k+1, i) 中的较大值,其中 sub(i, j) 表示数组 nums 中下标落在区间 [i, j] 内的数之和。 要使得子数组和的最大值最小,有状态转移方程:f[i][j] = min{max(f[k][j-1], sub(k+1, i))}- 确定 base case: dp[0][0] = 0 时间复杂度
        O
    
    
        (
    
    
    
         n
    
    
         2
    
    
    
        m
    
    
        )
    
    
    
       O(n^2m)
    
    
    O(n2m),其中 n 是数组的长度,m 是分成的非空的连续子数组的个数。<br> **空间复杂度**:
    
    
    
    
        O
    
    
        (
    
    
        n
    
    
        m
    
    
        )
    
    
    
       O(nm)
    
    
    O(nm)
    
    
    class Solution:
        def splitArray(self, nums: List[int], m: int) -> int:
            n = len(nums)
            f = [[10**18] * (m + 1) for _ in range(n + 1)]
            sub = [0]
            for elem in nums:
                sub.append(sub[-1] + elem)
    
            f[0][0] = 0
            for i in range(1, n + 1):
                for j in range(1, min(i, m) + 1):
                    for k in range(i):
                        f[i][j] = min(f[i][j], max(f[k][j - 1], sub[i] - sub[k]))
    
            return f[n][m]

    代码来源:官方题解

    8. 回文子串

    回文子串:给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。LeetCode 直达

    【动态规划解题思路】

    • 明确状态:对于字符串的任意起始位置 i 和结束位置 j 都可以组成一个子字符串 s[i][j],可以判断该字符串是否为回文串,是则 dp[i][j] 为 True,否则为 False。- dp 数组:使用二维 dp 数组,dp[i][j] 表示以 i 为起点,j 为重点的字符串 s[i][j] 是否为回文串。- 状态间关系:对于子串 s[i][j],若 s[i+1][j-1] 为回文串,且 s[i] == s[j],则 s[i][j] 也是一个回文串,故有状态转移 dp[i][j] = (dp[i+1][j-1] and s[i] == s[j])。- 确定 base case: 外层循环枚举所有可能的子串长度 length,内层循环枚举所有可能的子串起点,当 length 等于 0 时,表示子字符串只有一个字符,肯定是回文串;当 length 等于 1 时,表示字符串有两个字符,只有当两个字符相同时才为回文串。 时间复杂度
        O
    
    
        (
    
    
    
         n
    
    
         2
    
    
    
        )
    
    
    
       O(n^2)
    
    
    O(n2)<br> **空间复杂度**:
    
    
    
    
        O
    
    
        (
    
    
    
         n
    
    
         2
    
    
    
        )
    
    
    
       O(n^2)
    
    
    O(n2)
    
    
    class Solution:
        def countSubstrings(self, s: str) -> int:
            n = len(s)
            dp = [[False] * n for _ in range(n)]
            for length in range(n):
                for start in range(n):
                    end = start + length
                    if end > n-1:
                        break
                    if length == 0:
                        dp[start][end] = True
                    elif length == 1:
                        dp[start][end] = (s[start] == s[end])
                    else:
                        dp[start][end] = (dp[start+1][end-1] and s[start] == s[end])
    
            # count for number
            count = 0
            for i in range(n):
                for j in range(n):
                    if dp[i][j]:
                        count += 1
            return count

    最长回文子串:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。LeetCode 直达

    和上题做法一模一样…

    class Solution:
        def longestPalindrome(self, s: str) -> str:
            n = len(s)
            res = ""
            dp = [[False] * n for _ in range(n)]
            for length in range(n):
                for start in range(n):
                    end = start + length
                    if end > n-1:
                        break
                    if length == 0:
                        dp[start][end] = True
                    elif length == 1:
                        dp[start][end] = (s[start] == s[end])
                    else:
                        dp[start][end] = (dp[start+1][end-1] and s[start] == s[end])
    
                    if dp[start][end]:
                        res = s[start:end+1]
            return res

    参考:官方题解

    参考:动态规划 LeetCode 题解

posted @ 2021-01-06 14:16  刘桓湚  阅读(155)  评论(0编辑  收藏  举报