区间DP
在一个区间上的动态规划,主要思想就是先在小区间上进行动态规划得到最优解,然后再利用小区间的最优解合并求大区间的最优解。
解题模板
for length in range(2,n+1): #区间长度
for i in range(n-length+1): #区间起点,由于j的值不应该大于n,则可以确定i的范围
j = i + length -1 #区间终点
for k in range(i,j): #划分区间
dp[i][j] = max/min() #转移方程
经典例题
1.最长回文子序列
题目:给定一个字符串s,找到其中最长的回文子序列。可以假设s的长度为1000.
示例1:
输入:‘bbbab’
输出:4
示例二:
输入:‘cbbd’
输出:2
解析
回文子序列p两种情况:
- 长度为1的时候,只有一个字母
- 长度大于1的时候,p[0] == p[len(S)-1]
初始化dp
- dp = [[0] * n for _ in range(n)]
- dp[0][0] = dp[1][1] = … = dp[n-1][n-1] = 1
状态转移方程:
- 对于一个子序列,起始点终止点分别为i、j,则dp[i][j]表示字符串中从i到j最大的回文子序列的长度,可知dp[i][j] = max(dp[i+1][j],dp[i][j-1])
- 当s[i] == s[j]时,dp[i][j] = dp[i+1][j-1]+2
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
dp = [[0]*n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for length in range(2,n+1):
for i in range(n-length + 1):
j = i + length - 1
dp[i][j] = max(dp[i+1][j],dp[i][j-1])
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1]+2
return dp[0][n-1]
2.将字符串转换为回文串的最小操作数
题目:给定一个字符串s每一次操作你都可以在字符串的任意位置插入任意字符。请你返回让s成为回文串最少的操作次数。
示例1:
输入:s=‘zzazz’’
输出:0
示例2:
输入:s=‘mbadm’
输出:2
示例3:
输入: s= ‘leetcode’
输出:5
解析
dp[i][j]表示使字符串s从i到j成为回文串的最小操作数。
-
如果s[i] == s[j],则无需进行操作,dp[i][j]=dp[i+1][j-1]
-
若s[i]!=s[j],则有两种操作方法,在i左边插入s[j]或者在s[j]右边插入s[i]
根据上述两种情况即可写出状态转移方程
dp[i][j] = min(dp[i][j],dp[i+1][j-1]) s[i] == s[j]
dp[i][j] = min(dp[i+1][j],dp[i][j-1])+1 s[i] != s[j]
class Solution:
def minInsertions(self, s: str) -> int:
n = len(s)
dp = [[0] * n for _ in range(n)]#初始化
for span in range(2,n+1):#回文串的长度
for i in range(n-span+1):#起始点
j = i + span - 1#终止点
print(i,j)
dp[i][j] = min(dp[i+1][j],dp[i][j-1])+1
if s[i] == s[j]:
dp[i][j] = min(dp[i][j],dp[i+1][j-1])
print(dp)
return dp[0][n-1]
3.石子合并(直线版)
题目:有N堆石子排成一排,每堆石子有一定的数量,现要将N堆石子合并成为一堆。合并的过程每次只能将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值.
示例:
输入:3
1 2 3
输出:7
解析
假设有8堆石子,合并这八堆石子,有7不同种方法,如下图
dp[i][j]表示合并从i堆石子到j堆石子所需要花费的最小代价,合并石子是可以先将从i至k堆石子合并为一堆A,最小花费为dp[i][k],然后合并k+1至j堆石子为B,最小花费为dp[k+1][j],最后合并A,B堆石子,得到dp[i][j].
则状态转移方程为:dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+sum(nums[i:j+1]))
class Solution:
def stonemerge(self,n,nums):
dp = [[0]*(n) for _ in range(n)]
for span in range(2,n+1):
for i in range(0,n-span+1):
j = i + span - 1
dp[i][j] = float('inf')
for k in range(i,j):
dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+sum(nums[i:j+1]))
print(dp[0][n-1])
4.环形石子合并
题目:有N堆石子排成一个圆环,每堆石子有一定的数量,现要将N堆石子合并成为一堆。合并的过程每次只能将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值和最大值
示例:
输入:4
4 5 9 4
输出:43 54
解析
因为是环形的,所以合并i,j的方法可能存在下述情况,所以将石堆扩展为两个相同的石堆并排排序,则可以考虑到环形中的情况:
之后的操作和直线排序情况相同。
class Solution:
def stonemerge(self,n,nums):
nums.extend(nums)
dp = [[0]*(2*n) for _ in range(2*n)]
dp2 = [[0]*(2*n) for _ in range(2*n)]
for span in range(2,2*n+1):
for i in range(0,2*n-span+1):
j = i + span - 1
dp[i][j] = float('inf')
dp2[i][j] = float('-inf')
for k in range(i,j):
dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+sum(nums[i:j+1]))
dp2[i][j] = max(dp2[i][j], dp2[i][k] + dp2[k + 1][j] + sum(nums[i:j + 1]))
##环形石堆,所以要循环找最大最小值
minl = float('inf')
maxl = 0
for i in range(n):
maxl = max(maxl, dp2[i][i + n - 1])
minl = min(minl, dp[i][i + n - 1])
print(minl,maxl)