经典DP(五)区间类

424 阅读4分钟

概述

DP

  • 在分阶段划分问题时,与 阶段中元素出现的顺序由前一阶段的哪些元素合并而来 有很大关系
  • dp[i][j] = max(dp[i][k] + dp[k+1][j] + cost)
    • 合并:将两个(或多个)部分进行整合
    • 特征:能将问题分解为两两合并的形式
    • 求解:枚举“合并点”,将大问题分解为左右两个subproblem -> 合并两个subprob的最优质 = 原问题的最优质

❤️ 记忆化DFS

  • dfs + memo会比较好处理类似问题,可以从两边互相逼近直到start = end - 1(区间长度=1)
    • 在确定了区间的两个边界之后,暴力尝试不同的区间分割位置,然后recursive call去找切开后的两个部分各自的值,所有切割尝试取最优值

312. 戳气球(Hard)

image.png

Solu:区间类DP

image.png

  • nums的首尾添上两个dummy的'1'
  • dp[i][j] = 吹爆nums[i+1] ~ nums[j-1](闭区间)中所有的气球能得到的最高分
    • dp[i][j] = max{dp[i][k] + dp[k][j] + (nums[k] * nums[i] * nums[j])}

Code 1:记忆化DFS

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        nums = [1] + nums + [1]  # 首位添两个dummy的'1'
        N = len(nums)
        
        @lru_cache(None)
        def dfs(l, r) -> int:
            if l + 1 == r:  # 逼近直到区间长度=1
                return 0
            return max(dfs(l, i) + dfs(i, r) + nums[l] * nums[i] * nums[r] for i in range(l + 1, r))
        
        return dfs(0, N - 1)

Code 2:DP

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        nums = [1] + nums + [1]
        N = len(nums)
        dp = [[0] * N for _ in range(N)]
        for l in range(N - 2, -1, -1):
            for r in range(l + 2, N):
                for i in range(l + 1, r):
                    dp[l][r] = max(dp[l][r], dp[l][i] + dp[i][r] + nums[l] * nums[r] * nums[i])
        return dp[0][N - 1]


1039. 多边形三角剖分的最低得分(Medium)

image.png

Solu:区间类DP

image.png

  • values[0]values[-1]相邻,因此必定首尾两点被划分在同一个三角形中
  • dp[i][j] = values[i]~values[j](闭区间)能剖分出来的最低分
    • dp[i][j] = min{dp[i][k] + dp[k][j] + (values[i] * values[j] * values[k])}

Code 1:记忆化DFS

class Solution:
    def minScoreTriangulation(self, values: List[int]) -> int:
        @lru_cache(None)
        def dfs(start, end):
            if start + 1 == end:  # 逼近直到区间长度=1
                return 0
            return min(
                dfs(start, i) + dfs(i, end) + values[start] * values[i] * values[end] for i in range(start + 1, end))
        
        return dfs(0, len(values) - 1)

Code 2:DP

class Solution:
    def minScoreTriangulation(self, values: List[int]) -> int:
        N = len(values)
        dp = [[0] * len(values) for _ in range(len(values))]
        for i in range(N - 1, -1, -1):
            for j in range(i + 2, N):
                dp[i][j] = float('inf')
                for k in range(i + 1, j):
                    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + values[i] * values[k] * values[j])
        return dp[0][-1]


1547. 切棍子的最小成本(Hard)

image.png

Solu:区间类DP

  • cuts的首尾分别填上[0][n],方便判断当前尺子的长度
  • dp[i][j] = 切完cuts[i+1] ~ cuts[j-1](闭区间)中所有的位置,所需的最少花费为多少
    • dp[i][j] = min{dp[i][k] + dp[k][j] + (cuts[j] - cuts[i])}

Code 1:记忆化DFS

class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        cuts = [0] + cuts + [n]
        cuts.sort()
        
        @lru_cache(None)
        def dfs(start, end):
            if start + 1 == end:  # 逼近直到区间长度=1
                return 0
            return min(dfs(start, i) + dfs(i, end) + (cuts[end] - cuts[start]) for i in range(start + 1, end))
        
        return dfs(0, len(cuts) - 1)

Code 2:DP

class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        cuts = [0] + cuts + [n]
        cuts.sort()
        N = len(cuts)
        dp = [[0] * N for _ in range(N)]
        for i in range(N - 2, -1, -1):
            for j in range(i + 2, N):
                dp[i][j] = float('inf')
                for k in range(i + 1, j):
                    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + (cuts[j] - cuts[i]))
        return dp[0][N - 1]


❤️ 1000. 合并石头的最低成本(Hard)

image.png

Solu:

  • 预检查:如果不满足len(stones) - (k-1)*m = k (m为整数),则必定最终无法合并为一堆
  • dfs(l, r):将[l, r](闭区间)内的石头合并到极致(最少堆),最少需要多少cost
    • base case:if #区间内的石头 = r - l + 1 < k,then 无法继续合并,cost = 0
    • 递归:枚举所有分割点i,将stones分成左右两半([l, i][i+1, r]),其中确保左半段可以最终合并成一堆
      • 因为要确保左半段[l, i]最终可以合并成一堆,所以需要保证(i-l) % (k-1) == 0。因此,i的「步长」为k-1
      • 在左右两半分别合并完成后,如果左右两半的合并结果(左半段:1堆;右半段:m堆(m可能≠k-1))。如果可以再合并在一起(即:满足(r-l) % (k-1) == 0),那么最后的这一下合并所需的cost = sum(stones[l:r+1])
        • 前缀和:在计算sum(stones[l:r+1])可以使用「前缀和」做预处理

Code:

class Solution:
    def mergeStones(self, stones: List[int], k: int) -> int:
        if (len(stones) - k) % (k - 1) != 0:
            return -1
        n = len(stones)
        preSum = [0]
        for i in range(n):
            preSum.append(preSum[-1] + stones[i])
        
        @lru_cache(None)
        def dfs(l, r) -> int:  # (左右闭区间)把[l,r]合并成最少堆需要至少多少cost
            if r - l + 1 < k:  # base case::无法再合并
                return 0
            return min(
                dfs(l, i) + dfs(i + 1, r) + ((preSum[r + 1] - preSum[l]) if (r - l) % (k - 1) == 0 else 0) for i in
                range(l, r, k - 1))
        
        return dfs(0, n - 1)


环形区间DP

593 · 石头游戏 II(Medium)

image.png

Code:前缀和 + 区间DP

  • 原数组(假装)扩充一倍长度,模拟一个cycle。其余部分常规区间DP

Solu:

class Solution:
    def stoneGame2(self, A):
        @lru_cache(None)
        def dfs(l, r) -> int:
            if l == r:  # 已经只剩一堆了,不需要继续操作了
                return 0
            return min(preSum[r + 1] - preSum[l] + dfs(l, i) + dfs(i + 1, r) for i in range(l, r))
        
        if not A:  # edge case
            return 0
        preSum = [0]
        n = len(A)
        for i in range(2 * n):
            preSum.append(preSum[-1] + A[i % n])
        return min(dfs(l, l + n - 1) for l in range(n))


References: