目录
1.区间DP的理解
区间 DP 是动态规划的一种类型,它主要用于解决与区间相关的优化问题。下面从多个方面对区间 DP 进行详细讲解。
1.1 基本概念
区间 DP 的核心思想是将问题分解为不同的区间子问题,通过求解小区间的最优解,逐步合并得到大区间的最优解。通常,问题的状态会用区间的两个端点来表示,状态转移也围绕区间的划分和合并展开。
1.2 一般步骤
1.2.1 定义状态
设 dp[ i ][ j ] 表示区间 [ i , j ] 上的最优解,其中 i 和 j 分别是区间的左右端点。这里的最优解可以是最大 / 小值、方案数等,具体含义取决于问题本身。
1.2.2. 初始化状态
根据问题的边界条件,对一些特殊区间的状态进行初始化。例如,当 i = j 时,区间 [ i , j ] 只包含一个元素,此时 dp[ i ][ j ] 的值可以直接确定。
1.2.3. 状态转移方程
状态转移方程是区间 DP 的关键,它描述了如何从小区间的状态推导出大区间的状态。
- Step 1: 分割点
一般来说,对于区间 [ i , j ],会枚举一个分割点 k (i k < j ),将区间 [ i , j ] 划分为两个子区间 [ i, k ] 和 [ k + 1, j ] ,然后根据问题的规则和子区间的状态计算 dp[ i ][ j ]。
- Step 2:状态转移方程的通常形式

1.2.4. 计算顺序
区间 DP 的计算顺序通常是按照区间长度从小到大进行的。因为大区间的状态依赖于小区间的状态,所以需要先计算出所有小区间的状态,再逐步计算大区间的状态。
✌️总结四个步骤:

2.石子合并
2.1题目描述

2.2解题思路
- 状态定义:dp[ i ][ j ]
状态集合:第 i 堆石子到第 j 堆石子合并成一堆所有可能的付出代价集合。
属性:dp[ i ][ j ]表示这堆集合中的最小值。
- 初始条件
对特殊的区间进行初始化,对区间长度为1的值进行初始化,初始为0,因为只有一堆石子时不需要合并,不需要付出代价。
- 状态转移
设定分割点 k,计算每个区间内合并石子所付出的最小代价。
dp[ i ][ j ] = min ( dp[ i ][ j ] , dp[ i ][ k ] + dp[ k + 1 ][ j ] + s[ j ] -s[ i ] )
- 计算顺序
通过外层循环控制区间长度 len 从小到大,内层循环枚举区间的左端点 l,并计算对应的右端点 r,保证了先计算短区间,再计算长区间。
2.3代码展示
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 305;
int a[N], s[N], n;
int dp[N][N];
void cal() {
// 初始化dp数组
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) {
// 单个石子堆不需要合并,代价为0
dp[i][j] = 0;
}
else {
// 初始化为一个很大的值,方便后续取最小值
dp[i][j] = 1e9;
}
}
}
// 枚举区间长度
for (int len = 2; len <= n; len++) {
// 枚举区间起始位置
for (int i = 1; i + len - 1 <= n; i++) {
int l = i, r = i + len - 1;
// 枚举分割点
for (int k = l; k < r; k++) {
// 状态转移方程,计算合并区间 [l, r] 的最小代价
// s[r] - s[l - 1] 表示区间 [l, r] 内石子的总数!!!
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
}
}
}
// 输出将所有石子合并成一堆的最小代价
cout << dp[1][n];
}
signed main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
// 计算前缀和
s[i] = s[i - 1] + a[i];
}
cal();
return 0;
}
3.矩阵连乘问题
3.1题目描述
给定 n 个矩阵 A1, A2,......, An ,其中 Ai 与 A i + 1 是可乘的,i = 1, 2 ...., n - 1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。
输入格式 :第 1 行一个正整数 n,是矩阵个数;第 2 行是 n + 1 个正整数,表示他们的维数
输出格式 :最少乘法次数
样例:
输入:
6
25 100 25 15 45 20 75
输出:
130375
3.2实例说明

3.3解题思路
- 状态定义: dp[ i ][ j ]
状态集合:所有矩阵 i 到矩阵 j 连乘所有可能的乘法次数。
属性:dp[ i ][ j ]表示集合中乘法次数最小的。
- 初始条件
单个的矩阵不能连乘,所以dp[ i ][ j ] = 0 (当i==j时)。
- 状态转移
设定分割点 k,计算每个区间内矩阵连乘所需要的最小乘法次数。
dp[ i ][ j ] = min ( dp[ i ][ k ] + dp[ k+1 ][ j ] + a[ l ] * a[ k + 1] * a[ r + 1]
- 计算顺序
通过外层循环控制区间长度 len 从小到大,内层循环枚举区间的左端点 l,并计算对应的右端点 r,保证了先计算短区间,再计算长区间。
3.4代码展示
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 505;
int n, a[N];
int dp[N][N];
void cal() {
// 初始化 dp 数组
for (int i = 1; i <= n; i++) { //注意i<=n,j<=n,因为是n个矩阵⚠️
for (int j = 1; j <= n; j++) {
if (i == j) {
dp[i][j] = 0; // 单个矩阵相乘代价为 0
}
else {
dp[i][j] = 1e9; // 初始化为一个很大的值,便于后续取最小值
}
}
}
// 枚举区间长度,从 2 到 n
for (int len = 2; len <= n; len++) { //len<=n,因为最大长度为n个矩阵连乘⚠️
// 枚举区间起始位置
for (int i = 1; i + len - 1 <= n; i++) { //右端点<=n,而不是<=n+1,因为n为最后一个矩阵⚠️⚠️
int l = i, r = i + len - 1;
// 枚举分割点
for (int k = l; k < r; k++) {
// 状态转移方程,计算合并两个子矩阵链的最小代价
// 注意这里数组下标都调整为从 1 开始
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + a[l] * a[k + 1] * a[r + 1]);
}
}
}
// 输出最终结果
cout << dp[1][n] << endl;
}
signed main() {
// 读取矩阵数量
cin >> n;
// 读取矩阵维度信息,从 1 开始存储
for (int i = 1; i <= n + 1; i++) {
cin >> a[i];
}
cal();
return 0;
}
3.5误区
👍len<=n:

👍状态转移方程 a[ l ] * a[ k + 1] * a[ r + 1]

4.凸多边形最优三角剖分
4.1题目描述

4.2解题思路
- 动态规划建模:定义
t[i][j]为顶点i到j构成的子多边形的最小三角剖分权值和。 - 状态转移:枚举分割点
k(i < k < j),将子多边形划分为两部分,状态转移方程为:plaintext
t[i][j] = min(t[i][k] + t[k][j] + weight(i,k,j)) - 区间 DP 遍历:按区间长度从小到大计算,确保子问题先被解决。
- 初始化:相邻顶点的权值为 0(无法形成三角形),其余初始化为无穷大。
- 结果:
t[1][n]即为整个多边形的最小权值和
4.3代码展示
#include<iostream>
#include<vector>
#include<algorithm>
#include<cmath>
#include<map>
#include<queue>
#include<math.h>
#include<string>
#include <set>
#include<stack>
#define int long long
using namespace std;
int n;
int dp[100][100], grid[100][100];
int weight(int a, int b, int c) {
return grid[a][b] + grid[b][c] + grid[c][a]; //注意返回格式⚠️⚠️⚠️
}
void cal() {
// 初始化所有状态
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) {
dp[i][j] = 0; // 单个顶点
}
else if (j == i + 1) {
dp[i][j] = 0; // 相邻顶点无法形成三角形 ⚠️⚠️⚠️
}
else {
dp[i][j] = 1e18; // 其他情况初始化为无穷大
}
}
}
// 区间长度从3开始
for (int len = 3; len <= n; len++) {
for (int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
for (int k = i + 1; k < j; k++) { // k从i + 1开始
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + weight(i, k, j));
}
}
}
cout << dp[1][n];
}
signed main() {
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> grid[i][j];
}
}
cal();
return 0;
}
4748

被折叠的 条评论
为什么被折叠?



