一、题目
二、题解:
1、区间DP解释(什么是区间DP?):
-
区间dp:就是对于区间的一种动态规划,它将问题划分为若干个子区间,并通过定义状态和状态转移方程来求解每个子区间的最优解,最终得到整个区间的最优解。
-
对于某个区间,它的合并方式可能有很多种,我们需要去枚举所有的方式,通常是去枚举区间的分割点,找到最优的方式(一般是找最少消耗)。
-
例如:对于区间
[i,j]
,它的合并方式有很多种,可以是[i,i+1]
和[i+2,j]
也可以是[i,k]
和[k+1,j]
(其中 i < = k < j i <= k < j i<=k<j)
2、区间DP模板:
- 核心思路:
dp[i][j]
表示[i,j]
的最小消耗 - 1#通常都是先枚举区间长度,区间长度为1就不用合并,所以从2开始枚举,然后枚举左端点,那么右端点就为左端点加区间长度 − 1 -1 −1,再枚举分割点 k k k(即子区间的终点和起点)
- 2#最后计算不同分割点 k 的情况下,合并区间的消耗,
dp[i][j]
选择其中的最小消耗。(需要注意的是要记得根据题意给上初值) - 3#长区间肯定由短区间转移得到,所以先算短区间。
(分割点
k
:
是从
i
到
j
−
1
,
因为
k
+
1
≤
j
)
(分割点k:是从i到j-1,因为k+1≤j)
(分割点k:是从i到j−1,因为k+1≤j)
- 也就是当我们计算一个长度为
5
5
5的区间时,必定是已经把其中
1
、
2
、
3
、
4
1、2、3、4
1、2、3、4的区间给全部求出了(否则就分割成小区间)
3、题目解析:
4
2 5 3 1
1)对于题目,我们可以倒过来想,也就是说我们最后一定是把所有的石头都合并成一堆,且最后一步一定是由两堆合并成.
2)继续下去,两堆也是由四堆合并来的…也就是继续由两个更小的区间合并成的
3)不难发现,最后得到的最大的石子堆是由两个相邻的石子堆合并形成的,而这两个石子堆又分别是由更小的两个相邻石子堆合并而来。
- 因此我们可以得出,一个较大是石子堆一定是由两个较小的相邻石子堆合并形成----->相邻的小区间不断合并成大区间,区间DP。
4)转移方程:一个大的石子堆一定是由两个小的相邻石子堆合并而成。因此我们可以将大的堆拆成两个小的石子堆,枚举中间的点,每次计算合成这两小堆已用代价加上这次合并的代价(这次合并的代价即整个区间的和)
- 找到最小值:
for(int k=i;k<j;k++) //k为分割点
dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+a[i] + a[i+1] +...+ a[j] )
4、核心代码解析
int a[N],prefix[N];
int dp[N][N]; //表示到了区间[i->j]时的最小花费
void solve()
{
memset(dp,0x3f,sizeof(dp));
int n;cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) prefix[i]=prefix[i-1]+a[i]; //前缀和计算区间和
for(int i=1;i<=n;i++) dp[i][i]=0; //单独合并自己的初始化
//区间DP模板
for(int len=2;len<=n;len++) //枚举分割的区间长度
{
for(int i=1;i+len-1<=n;i++) //枚举左端点
{
int j=i+len-1; //此时右端点可计算
for(int k=i;k<j;k++) //再对(i->j)进行小区间分割计算,枚举分割点
{
//遍历计算最优的分割情况,[i][k]<->[k+1][j]+合并(i->j)的所有石头的权重
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+(prefix[j]-prefix[i-1]));
}
}
}
cout<<dp[1][n]<<'\n'; //即输出[1->n]的最小花费
}
这段代码是解决石子合并问题的区间动态规划算法,以下是分块解析:
常量与数组定义
const int N=307;
int a[N],prefix[N];
int dp[N][N]; //表示到了区间[i->j]时的最小花费
N
:定义数组的最大长度为307,足够存储问题中的石子数组。a
:存储石子数组,a[i]
表示第i个石子的重量。prefix
:前缀和数组,prefix[i]
表示前i个石子的总重量,用于快速计算区间和。dp
:动态规划表,dp[i][j]
表示合并区间[i,j]内所有石子的最小花费。
初始化
memset(dp,0x3f,sizeof(dp));
int n;cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) prefix[i]=prefix[i-1]+a[i];
for(int i=1;i<=n;i++) dp[i][i]=0;
memset(dp,0x3f,sizeof(dp))
:将动态规划表初始化为一个很大的值(0x3f
),表示初始状态下的不可达。- 读取石子数量
n
和石子数组a
。 - 计算前缀和数组
prefix
,prefix[i]
表示前i个石子的总重量,用于快速计算任意区间的总重量。 - 初始化
dp[i][i]
为0,因为合并单个石子不需要花费。
动态规划主体
for(int len=2;len<=n;len++) //枚举分割的区间长度
{
for(int i=1;i+len-1<=n;i++) //枚举左端点
{
int j=i+len-1; //此时右端点可计算
for(int k=i;k<j;k++) //再对(i->j)进行小区间分割计算,枚举分割点
{
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+(prefix[j]-prefix[i-1]));
}
}
}
- 外层循环
len
枚举区间长度,从2到n(因为长度为1的区间已经初始化)。 - 中间循环
i
枚举区间的左端点,j
由i
和len
计算得出,表示区间的右端点。 - 内层循环
k
枚举分割点,将区间[i,j]分为[i,k]和[k+1,j]两部分。 - 动态转移方程:
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + (prefix[j] - prefix[i-1]))
,其中prefix[j] - prefix[i-1]
是合并整个区间[i,j]的总重量,即合并后的石子重量。
三、完整代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=307;
int a[N],prefix[N];
int dp[N][N]; //表示到了区间[i->j]时的最小花费
void solve()
{
memset(dp,0x3f,sizeof(dp));
int n;cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) prefix[i]=prefix[i-1]+a[i]; //前缀和计算区间和
for(int i=1;i<=n;i++) dp[i][i]=0; //单独合并自己的初始化
//区间DP模板
for(int len=2;len<=n;len++) //枚举分割的区间长度
{
for(int i=1;i+len-1<=n;i++) //枚举左端点
{
int j=i+len-1; //此时右端点可计算
for(int k=i;k<j;k++) //再对(i->j)进行小区间分割计算,枚举分割点
{
//遍历计算最优的分割情况,[i][k]<->[k+1][j]+合并(i->j)的所有石头的权重
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+(prefix[j]-prefix[i-1]));
}
}
}
cout<<dp[1][n]<<'\n';
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _=1;
while(_--) solve();
return 0;
}