区间DP之合并石头的最低成本

tech2022-08-08  130

区间DP,顾名思义是在区间上DP,它的主要思想就是先在小区间进行DP得到最优解,然后再利用小区间的最优解合并求大区间的最优解。

区间DP模板

for i in range(n): dp[i][i] = 初始值 for min range(2, n): # 区间长度 for i in range(n-m): # 枚举区间起点 j = i + m # 区间终点 for k in range(i, j): # 枚举分割点,构造状态转移方程 dp[i][j] = max(dp[i][j], dp[i][k] + dp[k+1][j] + w[i][j])

LeetCode 1000. 合并石头的最低成本

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

每次移动(move)需要将连续的 K 堆石头合并为一堆,而这个移动的成本为这 K 堆石头的总数。

找出把所有石头合并成一堆的最低成本。如果不可能,返回 -1 。

示例 1:

输入:stones = [3,2,4,1], K = 2 输出:20 解释: 从 [3, 2, 4, 1] 开始。 合并 [3, 2],成本为 5,剩下 [5, 4, 1]。 合并 [4, 1],成本为 5,剩下 [5, 5]。 合并 [5, 5],成本为 10,剩下 [10]。 总成本 20,这是可能的最小值。

示例 2:

输入:stones = [3,2,4,1], K = 3 输出:-1 解释:任何合并操作后,都会剩下 2 堆,我们无法再进行合并。所以这项任务是不可能完成的。.

示例 3:

输入:stones = [3,5,1,2,6], K = 3 输出:25 解释: 从 [3, 5, 1, 2, 6] 开始。 合并 [5, 1, 2],成本为 8,剩下 [3, 8, 6]。 合并 [3, 8, 6],成本为 17,剩下 [17]。 总成本 25,这是可能的最小值。

提示:

1 <= stones.length <= 30 2 <= K <= 30 1 <= stones[i] <= 100

思路

prefix用来O(1)存取sum(stones[i:j+1]) 状态转移方程: dp[i][j]代表【从i到j合并为最少堆的最小代价】 m:每次合并的区间长度,从K到N。m<K时已经初始化为0,所以不用遍历。 dp[i][i+m-1] = min(dp[i][k] + dp[k+1][i+m-1] for k in range(i, i+m-1, K-1)) + (prefix[i+m] - prefix[i] if (m-1)%(K-1) == 0 else 0) 分析:

min(dp[i][k] + dp[k+1][i+m-1] for k in range(i, i+m-1, K-1)) : 之前已有的合并代价。为什么是k in range(i, i+m-1, K-1)?本质是需要左边的区间能够合并成一堆,或本身就只有一堆,右边的是减去这一堆后剩下的堆。若步长不为K-1,那么左右两堆合并时并不能确定合并的方式,比如 K=5,左边0~3,右边4~7,那还得计算是合并0~4?还是1~5?2~6?3~7? prefix[i+m] - prefix[i] if (m-1)%(K-1) == 0 else 0: 当(m-1)%(K-1) == 0时,可以也必须进行合并成一堆的工作,此时用prefix计算合并代价。 这种方式下dp[0][n-1]本身的值看不出是否能合并成一堆,所以必须预先计算,所以开头有if (N - 1) % (K - 1): return -1

代码

class Solution: def mergeStones(self, stones: List[int], K: int) -> int: n = len(stones) if (n-1) % (K-1): return -1 pre = [0] * (n+1) # 前缀和 for i in range(1, n+1): pre[i] = pre[i-1] + stones[i-1] # 计算前缀和 dp = [[0] * n for _ in range(n)] for length in range(K, n+1): # 遍历区间长度 for i in range(n): # 遍历区间起点 j = i + length - 1 # 区间终点 if j > n-1: # 越界结束 break # 穷举分割点 dp[i][j] = min(dp[i][k] + dp[k+1][j] for k in range(i, j, K-1)) # 以下两行代码是错的,要注意,要用生成式语法 # for k in range(i, j, K-1): # dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j]) if (length-1)%(K-1) == 0: dp[i][j] += pre[j+1]-pre[i] return dp[0][n-1]

LeetCode 312. 戳气球

有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。

求所能获得硬币的最大数量。

说明: 你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100 示例:

输入: [3,1,5,8] 输出: 167 解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167

思路: 以两个数作为左右端点,找出最优解中它们中间那个戳破的气球,中间这个气球把整个队列分为了2部分,要想让中间这个气球和2个端点靠在一起,就需要先把分开的2部分的气球戳破。 dp[i][j]表示i~j最大值,i,j不戳破。 比如k气球在i,j之间时(i,k,j)被戳破,那么要先戳破i,k、k,j之间的气球,所以dp[i][j]=dp[i][k]+dp[k][j]+nums[i]*nums[k]*nums[j] dp[i][j]含义是不包含i,j的i-j之间的数组获得的最大乘积,所以在nums前后添加[1]

class Solution: def maxCoins(self, nums: List[int]) -> int: nums = [1] + nums[:] + [1] n = len(nums) dp = [[0] * n for _ in range(n)] # 逆向遍历,因为最后结果是dp[0][n-1], # 并且dp[i][j]需要通过dp[k][j]得到, # 而k>i,所以先要算出dp[k][j],所以对i要逆向遍历 for i in range(n-1, -1, -1): for j in range(i + 2, n): # j>k。所以用正向遍历 for k in range(i + 1, j): # 不包含两端点 dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]) return dp[0][-1]

括号匹配

给你一个字符串,里面只包含"(",")","[","]"四种符号,请问你需要至少添加多少个括号才能使这些括号匹配起来。 如: []是匹配的 ([])[]是匹配的 ((]是不匹配的 ([)]是不匹配的

动态规划思路 状态定义:使用二维dp,dp[i][j]表示从s[i]到s[j]的最少添加括号个数 初始化:单独一个字符时,dp[i][j]=1,因为一个字符必定需要添加一个括号,其他值先赋值为0 状态转移方程:循环遍历区间长度(区间长度m从1到n),每次考虑[i,j]内的区间(i从0开始,j=i+m),

如果s[i]和s[j]匹配,那么dp[i][j]=dp[i+1][j-1],也即括号里面部分需要添加的最少括号数.分割情况:如果对区间[i,j]内的区间进行遍历分割,如果有更小的值,就采用更小的,也即 dp[i][j]=min(dp[i][j], dp[i][k] + dp[k+1][j])

代码如下

def match(a, b): if a == '[' and b == ']': return True if a == '(' and b == ')': return True return False def kuohao(s): n = len(s) dp = [[0] * (n+1) for _ in range(n+1)] for i in range(n+1): dp[i][i] = 1 for m in range(1, n + 1): for i in range(n - m): j = i + m dp[i][j] = float('inf') if match(s[i], s[j]): dp[i][j] = dp[i + 1][j - 1] for k in range(i, j): dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]) return dp[0][n - 1] s = '((]]()' print(kuohao(s))

整数划分

给出两个整数 n , m ,要求在 n 中加入m - 1 个乘号,将n分成m段,求出这m段的最大乘积

输入 第一行是一个整数T,表示有T组测试数据 接下来T行,每行有两个正整数 n,m ( 1<= n < 10^19, 0 < m <= n的位数); 输出 输出每组测试样例结果为一个整数占一行 样例输入 2 111 2 1111 2 样例输出 11 121

题意: 给出一个数n,要求在n的数位间插入(m-1)个乘号,将n分成了m段,求这m段的最大乘积。

思路: 用dp[i][j]表示从第一位到第i位共插入j个乘号后乘积的最大值。根据区间DP的思想我们可以从插入较少乘号的结果算出插入较多乘号的结果。

当我们要放第j个乘号时枚举放的位置: 状态转移方程: dp[i][j]=max(dp[i][j],dp[k-1][j-1]*num[k][i]) nums[i][j]表示从s[i]到s[j]这段连续区间代表的数值

def int_split(s, m): n = len(s) nums = [[0] * n for _ in range(n)] # dp[i][j]表示在数组第一位到第i位共插入j个乘号后乘积的最大值 dp = [[0] * m for _ in range(n)] # 初始化nums for i in range(n): nums[i][i] = int(s[i]) # 第i位数字 for j in range(i+1, n): # 第i到第j位的数字 nums[i][j] = nums[i][j-1] * 10 + int(s[j]) for i in range(n): # 遍历数组结束位置,从0到第i个位置 dp[i][0] = nums[0][i] # 在数组中[0,i]中插入0个乘号,数组不变 for j in range(1, m): # 在数组中[0,i]中插入j个乘号,也即[1,m-1] if i < j-1: # 乘号数量大于数组长度 dp[i][j] = 0 else: dp[i][j] = -1 # 初始化为最小值 for k in range(1, i): # 遍历乘号的位置 dp[i][j] = max(dp[i][j],dp[k-1][j-1]*nums[k][i]) return dp[n-1][m-1] # 返回数组[0,n-1]中插入m-1个乘号的值 s = '1111' m = 2 print(int_split(s,m))

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

给定 N,想象一个凸 N 边多边形,其顶点按顺时针顺序依次标记为 A[0], A[i], …, A[N-1]。

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

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

示例 1:

输入:[1,2,3] 输出:6 解释:多边形已经三角化,唯一三角形的分数为 6。

示例 2:

输入:[3,7,4,5] 输出:144 解释:有两种三角剖分,可能得分分别为:3*7*5 + 4*5*7 = 245, 或 3*4*5 + 3*4*7 = 144。最低分数为 144。

示例 3:

输入:[1,3,1,4,1,5] 输出:13 解释:最低分数三角剖分的得分情况为 1*1*3 + 1*1*4 + 1*1*5 + 1*1*1 = 13。

提示: 3 <= A.length <= 50 1 <= A[i] <= 100

对于点 0,1,…,n-1构成的多边形三角剖分,考虑点0和n-1,因为总有某个点 j 与点0和n-1构成三角形,

使得原多边形被分成一个三角形和至多两个凸多边形,求解原问题退化成求解两个规模更小的子问题,

即有若 f(0,n-1)表示原问题的解,则存在 j使 f(0,n-1) = f(0,j) + f(j,n-1) + A[0]*A[k]*A[n-1] 遍历 1~n-2的所有点找出这个j 使得得分最低即可

class Solution: def minScoreTriangulation(self, A: List[int]) -> int: n = len(A) dp = [[0] * n for _ in range(n)] for m in range(2, n): # 遍历长度 for i in range(n-m): # 遍历起点 j = i + m # 终点 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] + A[i]*A[k]*A[j]) return dp[0][-1]
最新回复(0)