题目1:合并石头的最低成本(leetcode1000题)
有 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
思路:
对于k=2的情况:
每次合并都是相邻的两堆进行合并。用dp[i][j]
表示从i到j这个区间合并为1个堆时的最小代价。那么有转移方程:
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1])
即从i到j这个区间合并为1个堆时的最小代价等于从i到k这个区间合并为1个堆时的最小代价加从k+1到j这个区间合并为1个堆时的最小代价和两个堆的总数量。
对于k>2的情况:
通过观察上面的例子,我们可以知道K=2时,我们做的其实是将一部分合并为1堆,另一部分合并为1堆,最后形成一堆。典型的子问题划分问题,可以想象归并排序的过程。
现在K!=2了,那么我们就将一部分合并成K-1堆,另一部分合并成1堆,然后合并。
至于为什么不是K-2堆和2堆以及K-3堆和3堆是因为我们的子问题是合并成1堆,当前状态是由前一状态得到的。而我们最初只有dp[i][i][1]=0这个条件。
我们用dp[i][j][m]表示从i到j这个区间形成m堆所需的最小代价。
则转移方程为:
初始化 dp[i][i][1]=0
dp[i][j][K]=min(dp[i][j][k],dp[i][k][K-1]+dp[k+1][j][1])
dp[i][j][1]=min(dp[i][j][K]+sum[j]-sum[k-1])
即
Initial status dp[i][i][1] = 0 and dp[i][i][m] = infinity
dp[i][j][m] = min(dp[i][mid][1] + dp[mid + 1][j][m - 1] + stonesNumber[i][j])
分析:
- 用dp(i,j)代表把第i堆到第j堆合并成一堆的成本;
- 若
(n-1)% (K - 1)==0
即(j - i) % (K - 1)==0
则代表可以合并为一堆; sum[i]
代表前i堆石头的总数;- 在i,j之间移动mid,每次移动K-1个距离,
dp(i,mid)+dp(mid+1,j)
代表以mid为界限合并为两队的成本,取最小值;
,res再加上两堆的石头数(sum[j+1]-sum[i])
dp(i,j)返回res;
若n堆不能合并成一堆((n-1)%(K-1)不等于0),返回-1;
反之返回dp(0,n-1)
代码:
class Solution:
def mergeStones(self, stones, K):
n = len(stones)
if n == 1: return 0
if n < K: return -1
if (n - K) % (K - 1): return -1
inf = float('inf')
memo = {}
def dp(i, j, m):
if (i, j, m) in memo: return memo[(i, j, m)]
if i == j:
res = 0 if m == 1 else inf
memo[(i, j, m)] = res
return res
if m == 1:
res = dp(i, j, K) + sum(stones[i:j + 1])
else:
res = inf
for mid in range(i, j, K - 1): # length must be K+n*(K-1)
res = min(res, dp(i, mid, 1) + dp(mid + 1, j, m - 1))
memo[(i, j, m)] = res
return res
res = dp(0, n - 1, 1)
return res if res < inf else -1
直接用dp数组还得先算m大的,计算方向比较麻烦,直接递归+“备忘录/Memo”优化来的思路清晰一点,如果不加Memo优化,可能会超时。
题目2:击爆气球(leetcode312题)
给定n个气球,索引从0到n-1。每个气球上涂上一些对它表示数组的数组。你被要求爆裂所有的气球。如果你爆气球我你会得到nums[左] *nums[我] *nums[右]硬币。在这里左和右是相邻的I指数后,爆发,左,右然后成为相邻。
提示:
- You may imagine nums[-1] = nums[n] = 1. They are not real therefore you can not burst them.
- 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
示例:
Input: [3,1,5,8]
Output: 167
Explanation: 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
思路:
首先想到的是暴力遍历解空间,但是这道题的暴力搜索将相当的复杂,因为每打爆一个气球,断开的地方又重新挨上,所有剩下的气球又要重新遍历,这使得分治法不能 work,时间复杂度过高,为O(n!)。
对于像这种求极值问题,我们一般都要考虑用动态规划来做。
首先维护一个二维动态数组 dp,其中 dp[i][j]
表示打爆区间[i,j]中的所有气球能得到的最多金币。
题目中说明了边界情况,当气球周围没有气球的时候,旁边的数字按1算,这样我们可以在原数组两边各填充一个1,方便于计算。
这道题的最难点就是找状态转移方程。所谓的状态转移,就是假设已知了其他状态,来推导现在的状态
假如区间只有一个数,比如 dp[i][i],那么计算起来就很简单,直接乘以周围两个数字即可更新。
如果区间里有两个数字,那么就要算两次了,先打破第一个再打破了第二个,或者先打破第二个再打破第一个,比较两种情况,其中较大值就是该区间的dp值。
对于有很多数的区间 [i,j],要遍历这个区间内的每个气球,设指针为k,k在区间 [i, j] 中,假如第k个气球最后被打爆,那么此时区间 [i, j] 被分成了三部分,[i, k-1],[k],和 [k+1, j],只要我们之前更新过了 [i, k-1] 和 [k+1, j] 这两个子区间的 dp 值,可以直接用 dp[i][k-1] 和 dp[k+1][j],那么最后被打爆的第k个气球的得分该怎么算呢,你可能会下意识的说,就乘以周围两个气球被 nums[k-1] * nums[k] * nums[k+1],但其实这样是错误的,为啥呢?dp[i][k-1] 的意义是什么呢,是打爆区间 [i, k-1] 内所有的气球后的最大得分,此时第 k-1 个气球已经不能用了,同理,第 k+1 个气球也不能用了,相当于区间 [i, j] 中除了第k个气球,其他的已经爆了,那么周围的气球只能是第 i-1 个,和第 j+1 个了,所以得分应为 nums[i-1] * nums[k] * nums[j+1]。因此,状态转移方程为:
dp[i][j] = max(dp[i][j], nums[i - 1] * nums[k] * nums[j + 1] + dp[i][k - 1] + dp[k + 1][j]) ( i ≤ k ≤ j )
有了状态转移方程了,接下来要考虑本题的第二大难点——区间的遍历顺序。
一般来说,我们遍历所有子区间的顺序都是i从0到n,然后j从i到n,然后得到的 [i, j] 就是子区间。但是这道题用这种遍历顺序就不对,在前面的分析中已经说了,我们需要先更新完所有的小区间,然后才能去更新大区间,而用这种一般的遍历子区间的顺序,会在更新完所有小区间之前就更新了大区间,从而不一定能算出正确的dp值,比如拿题目中的那个例子 [3, 1, 5, 8] 来说,一般的遍历顺序是:
[3] -> [3, 1] -> [3, 1, 5] -> [3, 1, 5, 8] -> [1] -> [1, 5] -> [1, 5, 8] -> [5] -> [5, 8] -> [8]
显然不是我们需要的遍历顺序,正确的顺序应该是先遍历完所有长度为1的区间,再是长度为2的区间,再依次累加长度,直到最后才遍历整个区间:
[3] -> [1] -> [5] -> [8] -> [3, 1] -> [1, 5] -> [5, 8] -> [3, 1, 5] -> [1, 5, 8] -> [3, 1, 5, 8]
我们其实只是更新了 dp 数组的右上三角区域,我们最终要返回的值存在 dp[1][n] 中,其中n是两端添加1之前数组 nums 的个数。循环和递归写法参见代码如下:
class Solution:
def maxCoins(self, nums) -> int:
# 循环写法
n = len(nums)
if n == 1: return nums[0]
nums.append(1)
nums.insert(0,1)
dp = [[0 for _ in range(n+2)] for _ in range(n+2)]
for l in range(1,n+1):
for i in range(1,n-l+2):
j = i+l-1
for k in range(i,j+1):
dp[i][j] = max(dp[i][j],nums[i-1]*nums[k]*nums[j+1]+dp[i][k-1]+dp[k+1][j])
print(dp)
return dp[1][n]
# # 递归写法
# n = len(nums)
# if n == 1: return nums[0]
# nums.append(1)
# nums.insert(0,1)
# dp = [[0 for _ in range(n + 2)] for _ in range(n + 2)]
#
# def burst(nums,dp,i,j):
# if i>j: return 0
# if dp[i][j] > 0: return dp[i][j]
# res = 0
# for k in range(i,j+1):
# res = max(res, nums[i - 1] * nums[k] * nums[j + 1] + burst(nums, dp, i, k - 1) + burst(nums, dp, k + 1, j))
#
# dp[i][j] = res
# return res
#
# return burst(nums,dp,1,n)
参考网址:
Leetcode1000 合并石头的最低成本 区间DP
LeetCode1000.Minimum Cost to Merge Stones(合并石头的最低成本)
1000. Minimum Cost to Merge Stones
[LeetCode] Burst Balloons 打气球游戏