区间DP(区间动态规划)是一种用于解决与区间或子区间相关问题的动态规划技巧。其基本思想是将问题拆分为多个子问题,每个子问题对应一个区间,然后通过状态转移方程将这些子区间的最优解组合起来,得到整个问题的最优解。
主要特点
-
区间划分:问题被分解为若干个区间,每个区间的状态表示某个子区间的最优结果。
-
状态转移:通常需要枚举区间内的某个切分点,然后利用子区间(例如左区间和右区间)的最优解来更新当前区间的状态。
-
递归关系:通过对区间不断划分,递归地求解子问题,再合并结果得到最终答案。
典型问题
-
矩阵链乘法:通过确定最佳的矩阵相乘顺序,降低计算复杂度。
-
石子合并问题:如何合并石子使得总的合并代价最小。
-
括号匹配问题:寻找最优的括号匹配或者消除括号带来的不利影响。
总的来说,区间DP特别适用于那些可以通过区间划分并用子区间的最优结果来构造全局最优解的问题。它在解决一些看似复杂的组合优化问题时非常高效,是算法设计中一个重要的技巧。
注意:区间DP时间复杂度为O(n^2),请看好数据范围再选择合适的算法。
典型例题
题目描述
设有 N(N≤300) 堆石子排成一排,其编号为 1,2,3,⋯,N。每堆石子有一定的质量 mi (mi≤1000)。现在要将这 N 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。
输入格式
第一行,一个整数 N。
第二行,N 个整数 mi。
输出格式
输出文件仅一个整数,也就是最小代价。
输入输出样例
输入 #1
4
2 5 3 1
输出 #1
22
具体步骤(实现)
1.初始化
我们先将dp[i][j]定义为从i到j合并的最小值(包括i和j),于是先进行初始化(极大值,因为最后要求最小值)。
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j]=1e9;//初始化
}
}
2.输入并做前缀和
直接输入,然后考虑到后面要求连续一段的和,于是顺便前缀和。
for(int i=1;i<=n;i++){
cin>>a[i];
dp[i][i]=0;//刚才其实有个地方初始化错了,就是这个。因为自己本身不用合并
b[i]=b[i-1]+a[i];//前缀和
}
3.区间DP
我们必须先枚举长度,不然的话你还没有枚举到那么长就直接开始更新了,显然是错误的。
而第二重循环要枚举起点/终点,作者在这里为了方便理解使用了起点。
for(int i=2;i<=n;i++){//相当于len,长度
for(int j=1;j<=n-i+1;j++){//起点
for(int k=j;k<i+j-1;k++){//枚举分割点
dp[j][i+j-1]=min(dp[j][i+j-1],dp[j][k]+dp[k+1][i+j-1]+b[i+j-1]-b[j-1]);//i+j-1就是终点,前缀和求值时要减的是j-1,因为要求起点到终点的值。
}
}
}
最后输出dp[1][n]就可以了。
于是:
模板代码
#include<bits/stdc++.h>
using namespace std;
int n,a[1000],b[1000],dp[301][301];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j]=1e9;//初始化
}
}
for(int i=1;i<=n;i++){
cin>>a[i];
dp[i][i]=0;
b[i]=b[i-1]+a[i];//前缀和
}
for(int i=2;i<=n;i++){//相当于len,长度
for(int j=1;j<=n-i+1;j++){//起点
for(int k=j;k<i+j-1;k++){
dp[j][i+j-1]=min(dp[j][i+j-1],dp[j][k]+dp[k+1][i+j-1]+b[i+j-1]-b[j-1]);
}
}
}
cout<<dp[1][n];//输出最小值
return 0;
}