区间 DP:最长回文子序列 最优三角剖分【基础算法精讲 22】_哔哩哔哩_bilibili
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)
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. 多边形三角剖分的最低得分
你有一个凸的 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)
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. 合并石头的最低成本
有 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)
把 dfs 改成 f 数组,把递归改成循环就好了。相当于原来是用递归计算每个状态 (i,j),现在改用循环去计算每个状态 (i,j)。
需要注意循环的顺序:
- 由于 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]