算法学习22—区间 DP:最长回文子序列 最优三角剖分

区间 DP:最长回文子序列 最优三角剖分【基础算法精讲 22】_哔哩哔哩_bilibili

516. 最长回文子序列

516. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

一、记忆化搜索

 递归入口:dfs(0,n-1)

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        @cache
        def dfs(i: int, j: int) -> int:
            if i > j: return 0  # 空串
            if i == j: return 1  # 只有一个字母
            if s[i] == s[j]:  # 都选
                return dfs(i + 1, j - 1) + 2
            return max(dfs(i + 1, j), dfs(i, j - 1))  # 枚举哪个不选
        return dfs(0, len(s) - 1)

二、1:1 翻译成递推 

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        f = [[0] * n for _ in range(n)]
        for i in range(n - 1, -1, -1):
            f[i][i] = 1
            for j in range(i + 1, n):
                if s[i] == s[j]:
                    f[i][j] = f[i + 1][j - 1] + 2
                else:
                    f[i][j] = max(f[i + 1][j], f[i][j - 1])
        return f[0][-1]

三、空间优化

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        f = [0] * n
        for i in range(n - 1, -1, -1):
            f[i] = 1
            pre = 0  # f[i+1][i]
            for j in range(i + 1, n):
                tmp = f[j]
                f[j] = pre + 2 if s[i] == s[j] else max(f[j], f[j - 1])
                pre = tmp
        return f[-1]

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

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

你有一个凸的 n 边形,其每个顶点都有一个整数值。给定一个整数数组 values ,其中 values[i] 是第 i 个顶点的值(即 顺时针顺序 )。

假设将多边形 剖分 为 n - 2 个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 n - 2 个三角形的值之和。

返回 多边形进行三角剖分后可以得到的最低分 。

 一、记忆化搜索

问:区间 DP 有一个「复制一倍,断环成链」的技巧,本题为什么不用这样计算?

答:无论如何旋转多边形,无论从哪条边开始计算,得到的结果都是一样的,那么不妨就从 0-(n−1) 这条边开始计算。

class Solution:
    def minScoreTriangulation(self, v: List[int]) -> int:
        @cache  # 缓存装饰器,避免重复计算 dfs 的结果
        def dfs(i: int, j: int) -> int:
            if i + 1 == j: return 0  # 只有两个点,无法组成三角形
            return min(dfs(i, k) + dfs(k, j) + v[i] * v[j] * v[k]
                       for k in range(i + 1, j))  # 枚举顶点 k
        return dfs(0, len(v) - 1)

 二、1:1 翻译成递推

class Solution:
    def minScoreTriangulation(self, v: List[int]) -> int:
        n = len(v)
        f = [[0] * n for _ in range(n)]
        for i in range(n - 3, -1, -1):
            for j in range(i + 2, n):
                f[i][j] = min(f[i][k] + f[k][j] + v[i] * v[j] * v[k]
                              for k in range(i + 1, j))
        return f[0][-1]

1000. 合并石头的最低成本

1000. 合并石头的最低成本

有 n 堆石头排成一排,第 i 堆中有 stones[i] 块石头。

每次 移动 需要将 连续的 k 堆石头合并为一堆,而这次移动的成本为这 k 堆中石头的总数。

返回把所有石头合并成一堆的最低成本。如果无法合并成一堆,返回 -1 。

 一·一般方法

什么时候输出 −1 呢?

从 n 堆变成 1 堆,需要减少 n−1 堆。而每次合并都会减少 k−1 堆,所以 n−1 必须是 k−1 的倍数。

代码实现时,由于整个递归中有大量重复递归调用(递归入参相同),且递归函数没有副作用(同样的入参无论计算多少次,算出来的结果都是一样的),因此可以用记忆化搜索来优化:

  • 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个memo 数组(或哈希表)中。
  • 如果一个状态不是第一次遇到,那么直接返回 memo 中保存的结果。
class Solution:
    def mergeStones(self, stones: List[int], k: int) -> int:
        n = len(stones)
        if (n - 1) % (k - 1):  # 无法合并成一堆
            return -1
        s = list(accumulate(stones, initial=0))  # 前缀和
        @cache  # 缓存装饰器,避免重复计算 dfs 的结果
        def dfs(i: int, j: int, p: int) -> int:
            if p == 1:  # 合并成一堆
                return 0 if i == j else dfs(i, j, k) + s[j + 1] - s[i]
            return min(dfs(i, m, 1) + dfs(m + 1, j, p - 1) for m in range(i, j, k - 1))
        return dfs(0, n - 1, 1)

class Solution:
    def mergeStones(self, stones: List[int], k: int) -> int:
        n = len(stones)
        if (n - 1) % (k - 1):  # 无法合并成一堆
            return -1
        s = list(accumulate(stones, initial=0))  # 前缀和
        @cache  # 缓存装饰器,避免重复计算 dfs 的结果
        def dfs(i: int, j: int) -> int:
            if i == j:  # 只有一堆石头,无需合并
                return 0
            res = min(dfs(i, m) + dfs(m + 1, j) for m in range(i, j, k - 1))
            if (j - i) % (k - 1) == 0:  # 可以合并成一堆
                res += s[j + 1] - s[i]
            return res
        return dfs(0, n - 1)

二·1:1 翻译成递推

把 dfs 改成 f 数组,把递归改成循环就好了。相当于原来是用递归计算每个状态 (i,j),现在改用循环去计算每个状态 (i,j)。

需要注意循环的顺序:

  • 由于 i<m+1,f[i] 要能从 f[m+1] 转移过来,必须先计算出 f[m+1],所以 i 要倒序枚举;
  • 由于 j>m,f[i][j] 要能从 f[i][m] 转移过来,必须先计算出 f[i][m],所以 j 要正序枚举。
class Solution:
    def mergeStones(self, stones: List[int], k: int) -> int:
        n = len(stones)
        if (n - 1) % (k - 1):  # 无法合并成一堆
            return -1
        s = list(accumulate(stones, initial=0))  # 前缀和
        f = [[0] * n for _ in range(n)]
        for i in range(n - 1, -1, -1):
            for j in range(i + 1, n):
                f[i][j] = min(f[i][m] + f[m + 1][j] for m in range(i, j, k - 1))
                if (j - i) % (k - 1) == 0:  # 可以合并成一堆
                    f[i][j] += s[j + 1] - s[i]
        return f[0][-1]

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值