概述
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)
Solu:区间类DP
- 在
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)
Solu:区间类DP
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)
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)
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])可以使用「前缀和」做预处理
- 前缀和:在计算
- 因为要确保左半段
- base case:if
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)
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: