区间DP
定义
区间动态规划(Interval Dynamic Programming),简称区间DP,是动态规划领域的一个重要分支,专门用于解决涉及区间问题的最优化问题。这类问题通常需要在给定的一组区间上找到最优解,比如求解最长上升子序列、最优三角剖分、区间覆盖问题等。
基本概念
- 区间定义:问题中涉及到的区间通常由两个端点(起点和终点)定义,如[i, j]表示一个闭区间。
- 状态表示:区间DP的核心在于状态的定义,状态通常由区间长度和区间位置来描述,例如dp[i][j]可以表示区间[i, j]上的最优解。
- 状态转移:区间DP的状态转移是从较小区间的信息推导出较大区间的信息。通常通过枚举区间长度或枚举区间断点来进行状态转移。
- 决策过程:在区间DP中,一个状态的值可能由多个子区间状态经过某种计算(如最大值、最小值、和等)得到,这是通过决策过程来确定的。
运用情况
通常用于具有区间合并、分割等特征的问题。比如计算一段区间的最优值(如最大和、最小和等),或者判断区间内的某种状态。一些常见的应用场景包括计算字符串的编辑距离、计算区间内的最大连续子段和等。
注意事项
- 正确定义状态,清晰表示出区间的特征和所需的信息。
- 仔细考虑区间的划分和合并方式,确保覆盖所有情况且不重复计算。
- 注意边界条件的处理。
解题思路
- 确定区间的表示方式,通常用左右端点来表示一个区间。
- 设计状态表示,比如用 dp[i][j] 表示区间[i,j]的某种最优值或状态。
- 写出状态转移方程,根据问题的具体要求,确定如何从较小的区间的状态推导出较大区间的状态。
- 按照合适的顺序进行计算,通常是从小到大逐步计算出各个区间的状态。
例如,计算一个数列在某区间内的最大连续子段和问题。可以定义 dp[i][j] 为区间[i,j]内的最大连续子段和,然后通过考虑区间的分割情况来推导出状态转移方程。
解题步骤
- 定义状态:明确dp数组的含义,比如dp[i][j]表示什么。
- 初始化:确定dp数组的起始值,通常是当区间长度为1或2时的初始情况。
- 状态转移方程:根据问题特性,推导出如何从较小的子区间状态计算出较大区间状态的公式。
- 遍历顺序:通常按照区间长度从小到大,然后是区间起始点的顺序进行遍历,确保计算每个状态时,其依赖的所有子状态已经计算完毕。
- 求解目标:根据问题的具体要求,从dp数组中提取出最终答案。
实例应用
- 最长公共子序列(LCS)问题:可以转化为区间DP问题,求解两个序列的最长公共子序列长度。
- 最优三角剖分:在平面上有n个点,每个点的坐标为(xi, yi),找到一个三角剖分,使得所有三角形的面积之和最大。
- 区间覆盖问题:给定一系列区间,找到最少数量的区间,使得它们覆盖整个数轴。
区间DP的难点在于正确定义状态和设计高效的状态转移方程,以及理解区间如何相互作用以达到全局最优解。掌握区间DP的关键在于多练习,理解典型问题的解决方案,并能够抽象出问题的共通模式。
AcWing 320. 能量项链
题目描述
运行代码
cpp
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f;
int n;
int w[N];
int f[N][N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
{
cin >> w[i];
w[i + n] = w[i];
}
for (int len = 2; len <= n + 1; len ++ )
for (int l = 1; l + len - 1 <= n * 2; l ++ )
{
int r = l + len - 1;
for (int k = l + 1; k < r; k ++ )
f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
}
int res = 0;
for (int l = 1; l <= n; l ++ ) res = max(res, f[l][l + n]);
cout << res << endl;
return 0;
}
代码思路
- 首先定义了一些常量和数组,
N
表示最大可能的珠子数量,INF
是一个很大的常数表示无穷大,w
数组用于存储珠子的标记值,f
数组用于存储不同区间聚合的最大能量。 - 输入珠子的数量
n
后,将珠子的标记值读入,并进行了一个循环处理,将原序列重复一遍,这样便于处理环形的情况。 - 然后通过三重循环来计算动态规划数组
f
。最外层循环表示区间长度,从 2 开始递增到n+1
。对于每个确定长度的区间,通过内层的两个循环确定左右端点l
和r
,再通过中间的k
遍历所有可能的分割点,计算当前区间在不同分割情况下的最大能量,并更新f[l][r]
。 - 最后通过一个循环找到所有长度为
n
的区间(对应原环形序列的一圈)中的最大能量值并输出。
总的来说,这段代码通过动态规划的方法逐步计算出所有区间的最优聚合能量,最终得到整个序列的最大能量。
改进思路
- 可以考虑添加一些注释提高代码的可读性。
- 对于一些重复计算的部分,可以进一步优化计算逻辑,避免不必要的重复计算。
改进代码
cpp
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f;
int n;
int w[N];
int f[N][N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
{
cin >> w[i];
w[i + n] = w[i]; // 将序列重复,处理环形情况
}
for (int len = 2; len <= n + 1; len ++ )
for (int l = 1; l + len - 1 <= n * 2; l ++ )
{
int r = l + len - 1;
for (int k = l + 1; k < r; k ++ )
{
// 计算并更新最大能量
f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
}
}
int res = 0;
for (int l = 1; l <= n; l ++ )
res = max(res, f[l][l + n]); // 找到环形一圈的最大能量
cout << res << endl;
return 0;
}