mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1409 字
4 分钟
动态规划-区间dp
2026-08-09

可能性展开的常见方式

  1. 基于两侧端点讨论的可能性展开: 例题1 2
  2. 基于范围上划分点的可能性展开: 例题3

例题#

1312. 让字符串成为回文串的最少插入次数#

回文串分析#

  • a 最基本的回文串 长度为一
  • aa 最基本的回文串 长度为二
  • aba
  • abba

以上回文串解释了回文串的构成。当回文串的长度为奇数时,长度为1肯定是回文的,长度n大于1时,中心点为n/2n/2(向下取整,下标从0开始)左右分布对应字符串对应相等如aba;当回文串为偶数时,长度为2时,左边起点定义为l,右边起点定义为r,l+1=r,长度大于2时,左边起点定义为l,右边起点定义为r,

s[l+i]=s[ri],0i<rl+12s[l+i]=s[r-i],\qquad 0\le i<\left\lfloor\frac{r-l+1}{2}\right\rfloor

如abba

变成回文串的最小开销#

  • bca ->bcacb / acbca 2
  • bcab -> bcacb / bacab 1

基于以上分析,发现变成回文串的诀窍在于补充对应位置上的字符,并且发现利用中心的特性可以少用一个开销(虽然说用不上)并且存在某种意义上的对称性,从最左边l最右边r开始扫描,如果对应,则l++,r—,如果不对应,有两种尝试方式:

  • 补充左边对应位置的字符,逻辑上说就是r+1上补充l位置的字符,那么l解决了问题l++,r没有对称,不变r
  • 同理补右边,l,r—

那么就存在了递归的可能性,可以通过记忆化搜索解决这个问题

int dfs(string s,int l,int r){ // 基于以上分析写的暴力
if(l==r){ // a
return 0;
}
if(l+1==r){ // aa
return s[l]==s[r]?0:1;
}
if(s[l]==s[r]){ // 相等情况下
return dfs(s,l+1,r-1);
}
else{ // 不相等
return min(dfs(s,l,r-1),dfs(s,l+1,r))+1;
}
}

记忆化搜索解#

int minInsertions(string s) {
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n,-1));
auto mems = [&](auto&& self,int l,int r)->int{
if(dp[l][r]!=-1) return dp[l][r];
int ans;
if(l==r){
ans=0;
}
else if(l+1==r){
ans=(s[l]==s[r]?0:1);
}
else{
if(s[l]==s[r]){
ans=self(self,l+1,r-1);
}
else{
ans=min(self(self,l+1,r),self(self,l,r-1))+1;
}
}
dp[l][r]=ans;
return ans;
};
return mems(mems,0,n-1);
}

递推解#

根据刚刚的分析,我们会先聚焦到某一个单独的字符或者两个字符。边界值就是单个字符和两个字符的情况,单个字符的代价一定为0,两个字符相等为0,不相等为1,然后其他情况开始判断是否s[l]==s[r]s[l]==s[r]是的话,说明对应位置是相等的,代价为0,那么这个位置的答案就等于他的里面字串的情况就是dp[l+1][r1]dp[l+1][r-1];不是的话,那么他需要选择上述两种尝试方式的代价其中一个,代价都是1,选择第一个方式,那么就是选择dp[l+1][r]dp[l+1][r],如果选择第二种方式,那么就是选择dp[l][r1]dp[l][r-1]。(解释见补充)为了使答案最小,那么选择的是两种方式中小的那个情况,整理一下就是min(dp[l+1][r],dp[l][r1])+1min(dp[l+1][r],dp[l][r-1])+1。那么整个dp表纵坐标可以表示l,横坐标可以表示r,那么遍历顺序就是r从0到n-1,l从r到0的双层循环 因此dp状态 dp[l][r]=将子串 s[l..r] 变成回文串所需的最少插入次数dp[l][r]=\text{将子串 }s[l..r]\text{ 变成回文串所需的最少插入次数} 结果从l到r,也就是从0到n-1就是dp[0][n1]dp[0][n-1] 状态转移方程如下:

dp[l][r]={0,l=r0,r=l+1 且 s[l]=s[r]1,r=l+1 且 s[l]s[r]dp[l+1][r1],s[l]=s[r]min(dp[l+1][r],dp[l][r1])+1,s[l]s[r]dp[l][r]= \begin{cases} 0,&l=r\\ 0,&r=l+1\text{ 且 }s[l]=s[r]\\ 1,&r=l+1\text{ 且 }s[l]\ne s[r]\\ dp[l+1][r-1],&s[l]=s[r]\\ \min(dp[l+1][r],dp[l][r-1])+1,&s[l]\ne s[r] \end{cases}

int minInsertions(string s) {
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n,-1));
for(int r=0;r<n;r++){
for(int l=r;l>=0;l--){
if(l==r) dp[l][r]=0;
else if(r-l==1){
dp[l][r]=s[l]==s[r]?0:1;
}
else{
if(s[l]==s[r]) dp[l][r]=dp[l+1][r-1];
else dp[l][r]=min(dp[l+1][r],dp[l][r-1])+1;
}
}
}
return dp[0][n-1];
}

补充#

虽然不理解不影响过题,但是还是补充一下为什么:

这里的 dp[l][r]dp[l][r] 表示将原字符串的子串 s[lr]s[l\dots r] 变成回文串所需的最少插入次数。DP 下标只记录原字符串的区间,后来插入的字符只会产生 11 的代价,不会进入新的 DP 状态。

s[l]s[r]s[l]\ne s[r] 时,两个原端点无法直接配对,因此有两种选择:

  1. 在右侧插入 s[l]s[l]

    结构变为:

    s[l]原左端s[l+1]s[r]剩余的原子串s[l]新插入\underbrace{s[l]}_{\text{原左端}}\quad \underbrace{s[l+1]\dots s[r]}_{\text{剩余的原子串}}\quad \underbrace{s[l]}_{\text{新插入}}

    原来的左端点 s[l]s[l] 和新插入的 s[l]s[l] 已经形成一对回文字符。去掉这对已经解决的字符后,剩下的原字符串区间是 [l+1,r][l+1,r],所以这种选择的代价为:

    dp[l+1][r]+1dp[l+1][r]+1
  2. 在左侧插入 s[r]s[r]

    结构变为:

    s[r]新插入s[l]s[r1]剩余的原子串s[r]原右端\underbrace{s[r]}_{\text{新插入}}\quad \underbrace{s[l]\dots s[r-1]}_{\text{剩余的原子串}}\quad \underbrace{s[r]}_{\text{原右端}}

    新插入的 s[r]s[r] 和原来的右端点 s[r]s[r] 已经形成一对回文字符。去掉这对已经解决的字符后,剩下的原字符串区间是 [l,r1][l,r-1],所以这种选择的代价为:

    dp[l][r1]+1dp[l][r-1]+1

为了使插入次数最少,在两种选择中取最小值:

dp[l][r]=min(dp[l+1][r],dp[l][r1])+1dp[l][r]=\min(dp[l+1][r],dp[l][r-1])+1

可以记成:在右边插入左端字符,就解决原左端点;在左边插入右端字符,就解决原右端点。

486. 预测赢家#

区间dp+博弈dp。因为每次拿取都是在端点上拿取,所以符合区间dp的特性,并且两个人不仅要让自己得到的分数最大,还要不让对方拿到更大的点数,因此不是盲目拿取最大的点数,存在博弈策略:在尽可能不让别人拿到更大的优势下选择更大的点数。暴力递归代码如下:

int solve(int l,int r){
if(l==r){ // 只剩一个,别无选择
return nums[l];
}
if(l+1==r){ // 只剩两个,局部贪心拿最大
return max(nums[l],nums[r]);
}
/*
博弈核心:对方选取最有利的情况 就是自己最差的情况
已知当前的所有抉择中最差的情况中找相对更好的情况
*/
return max(
nums[l]+min(solve(l+2,r),solve(l+1,r-1)),
nums[r]+min(solve(l+1,r-1),solve(l,r-2))
);
}

记忆化搜索解:

bool predictTheWinner(vector<int>& nums) {
int n=nums.size(),sum=0;
for(int i=0;i<n;i++) sum+=nums[i];
vector<vector<int>> dp(n,vecctor<int>(n,-1));
auto mems = [&](auto&& self,int l,int r)->int{
if(dp[l][r]!=-1){
return dp[l][r];
}
if(l==r) return nums[l];
if(l+1==r) return max(nums[l],nums[r]);
return max(
nums[l]+min(self(self,l+2,r),self(self,l+1,r-1)),
nums[r]+min(self(self,l+1,r-1),self(self,l,r-2))
);
};
int ans=mems(mems,0,n-1);
return ans>=sum-ans; // p2=sum-p1得分
}

递推解

bool predictTheWinner(vector<int>& nums) {
int n=nums.size(),sum=0;
for(int i=0;i<n;i++) sum+=nums[i];
vector<vector<int>> dp(n,vector<int>(n,-1));
// 由于要取到l+2,r-2这种数据,所以要预处理
for(int l=0;l<n;l++){
dp[l][l]=nums[l];
}
for(int r=1;r<n;r++){
dp[r-1][r]=max(nums[r-1],nums[r]);
}
for(int r=2;r<n;r++){
for(int l=r-2;l>=0;l--){
dp[l][r]=max(
nums[l]+min(dp[l+2][r],dp[l+1][r-1]),
nums[r]+min(dp[l+1][r-1],dp[l][r-2])
);
}
}
return dp[0][n-1]>=sum-dp[0][n-1];
}

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

一个封闭多边形以任意顶点到其他任意顶点的直线(即分割线)有n-1条,其中能构成三角形的只有n-3条将图形分成n-2个三角形,并且不能相交,那么分割线就是可枚举的,每划出一条分割线将整个图形分成了两个部分,三个状态:分割线左边部分、当前l,i,r组成的三角形的值、分割线右边部分,很明显的重叠子问题,状态转移见代码

记忆化搜索解#

int minScoreTriangulation(vector<int>& values) {
int n=values.size();
vector<vector<int>> dp(n,vector<int>(n,-1));
auto mems = [&](auto&& self,int l,int r)->int{
if(dp[l][r]!=-1) return dp[l][r];
if(l==r||l+1==r) return 0;
int ans=MAXN;
for(int i=l+1;i<r;i++){
ans=min(
ans,
self(self,l,i)+self(self,i,r)+values[l]*values[i]*values[r]
);
}
dp[l][r]=ans;
return ans;
};
return mems(mems,0,n-1);
}

P1775 石子合并(弱化版) - 洛谷#

区间dp+前缀和

```cpp
int n;
void solve(){
cin>>n;
vector<int> a(n+1,0);
for(int i=1;i<=n;i++){
cin>>a[i];
a[i]+=a[i-1];
}
vector<vector<int>> dp(n+1,vector<int>(n+1,MAXN));
for(int r=1;r<=n;r++){
for(int l=r;l>=1;l--){
if(l==r) dp[l][r]=0;
else{
for(int k=l;k<r;k++){
dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]);
}
dp[l][r]+=a[r]-a[l-1];
}
}
}
cout<<dp[1][n];
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

动态规划-区间dp
https://blog.hydrodyio.xyz/posts/动态规划-区间dp/
作者
Hydroiody
发布于
2026-08-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录