动态规划
01背包问题
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n,v;
cin >> n >> v;
vector<vector<int>> dp(n+1,vector<int> (v+1,0));//嵌套初始化
vector<int> w(n+1);
vector<int> value(n+1);
for (int i = 1; i <= n; i ++ ){
cin >> w[i];
cin >> value[i];
}
for (int i = 1; i <= n; i ++ ){//横坐标代表当前支持的货物,纵坐标代表支持的容量
for (int j = 1; j <= v; j ++ ){
//先把上一个放到当前
dp[i][j] = dp[i-1][j];
//如果当前货物比最大支持的容量小,考虑当前的和之前的加上当前货物的谁大
if(w[i]<=j){
dp[i][j] = max(dp[i][j], dp[i-1][j-w[i]] + value[i]);
}
}
}
cout << dp[n][v];
return 0;
}
完全背包问题
粗略解答。
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n,vn;
int v[1001],w[1001];
int dp[1001][1001];
cin >> n >> vn;
for (int i = 1; i <= n; i ++ ){
cin >> w[i] >> v[i];
}
for (int i = 0; i <= n; i ++ ){
dp[0][i] = 0;
dp[i][0] = 0;
}
for (int i = 1; i <= n; i ++ ){
for (int j = 1; j <= vn; j ++ ){
dp[i][j] = dp[i-1][j];
if(w[i]<=j){
dp[i][j] = max(dp[i][j], dp[i][j-w[i]] + v[i]);//和01背包只有这里不同,因为他允许重复添加,所以不用回退而是直接当前的来加
}
}
}
cout << dp[n][vn];
return 0;
}
优化版本,由于直接继承i-1,可以作为滚动数组直接只用一行,j可以从w[j]直接开始:
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n,vn;
int v[1001],w[1001];
int dp[1001]={0};
cin >> n >> vn;
for (int i = 1; i <= n; i ++ ){
cin >> w[i] >> v[i];
}
for (int i = 1; i <= n; i ++ ){
for (int j = w[i]; j <= vn; j ++ ){//可以从=w[i]开始避免无效便利
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);//和01背包只有这里不同,因为他不用回退而是直接当前的来加
}
}
cout << dp[vn];
return 0;
}
多重背包1
这个是限定了物品的数量,转化成多个01就行。
#include<bits/stdc++.h>
using namespace std;
/*
有 N
种物品和一个容量是 V
的背包。
第 i
种物品最多有 si
件,每件体积是 vi
,价值是 wi
。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
*/
int main()
{
//转化为01背包问题
int n,vn;
cin >> n >> vn;
vector<int> v,w;// v是价值,w是重量
int a,b,c;
int dp[101]={0};
for (int i = 1; i <= n; i ++ ){
cin >> a >> b >> c;
while(c--){
w.push_back(a);
v.push_back(b);
}
}
for (int i = 0; i < v.size(); i ++ ){
for (int j = vn; j >= w[i]; j -- ){//从大到小从而继承上一轮的
dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
}
}
cout << dp[vn];
return 0;
}
分组背包
#include <bits/stdc++.h>
using namespace std;
//循环边界容易出错
int main()
{
int dp[101] = {0};
int n, vn;
cin >> n >> vn;
int si;
int w[101][101];//体积
int v[101][101];//价值
int S[101];
for (int i = 1; i <= n; i ++ ){
cin >> si;
S[i] = si;
for (int j = 1; j <= si; j ++ ){
cin >> w[i][j];
cin >> v[i][j];
}
}
for (int i = 1; i <= n; i ++ ){//遍历物品组
for (int j = vn; j >= 1; j -- ){//从大到小也可以这样(遍历容量
for (int k = 1; k <= S[i]; k ++ ){//遍历物品,每次选一个
if(j>=w[i][k]){
dp[j] = max(dp[j], dp[j-w[i][k]]+v[i][k]);
}
}
}
}
cout << dp[vn];
return 0;
}
数字三角形
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n;
int v;
vector<vector<int>> D(n+1);
for (int i = 1; i <= n; i ++ ){
D[i].resize(i+1);
for (int j = 1; j <= i; j ++ ){
cin >> D[i][j];
}
}
for (int i = n-1; i >= 1; i -- ){//从底下往上选择
for (int j = 1; j <= i; j ++ ){
D[i][j] += max(D[i+1][j], D[i+1][j+1]);//只能选左边或者右边的,找到最大的
//从三角形的倒数第二行开始,左右选,然后一直向上回溯
}
}
cout << D[1][1];
return 0;
}
最长单调子序列
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> dp(n+1,1);
vector<int> P(n+1,1);
for (int i = 1; i <= n; i ++ ){
cin >> P[i];
}
//从最后一个往回看,如果有比他小的就可以构成一个序列,然后看看谁大谁小
for (int i = 1; i <= n; i ++ ){
for (int j = 1; j < i; j ++ ){
if(P[j]<P[i]){
dp[i] = max(dp[i], dp[j]+1);
}
}
}
cout << *max_element(dp.begin(),dp.end());
return 0;
}
最长递增子序列2
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
//贪心➕二分查找
int main()
{
int n;
cin >> n;
vector<int> mintail(n);
vector<int> P(n);
for (int i = 0; i < n; i ++ ){
cin >> P[i];
}
int l = 0;//每次查找比当前数字大一点点的,把他替换。如果不存在其实就是加在最后。如果是空的其实就是加在第一个。
for (int i = 0; i < n; i ++ ){
int now = P[i];
auto pos = lower_bound(mintail.begin(), mintail.begin()+l, now);//找第一个大于now的
int idx = pos - mintail.begin();/
mintail[idx] = now;
if(idx+1>l){
l = idx + 1;
}
}
cout << l;
return 0;
}
在 C++ 标准库的 <algorithm>
头文件中,有几个和 lower_bound
类似的二分查找函数,专门用于处理有序序列(默认要求非递减,即从小到大),它们各有特定用途,非常方便。以下是最常用的几个:
1. upper_bound
—— 找“第一个大于目标值”的位置
功能:
在有序序列中,找到第一个大于目标值 x
的元素位置(返回迭代器/指针)。
(注意和 lower_bound
区分:lower_bound
找的是“第一个大于等于 x
”的位置)
用法:
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 3, 3, 5, 7}; // 必须是有序序列(非递减)
int x = 3;
// 找第一个 >3 的元素位置(这里是5的位置,索引3)
auto it = upper_bound(nums.begin(), nums.end(), x);
// 计算下标
int pos = it - nums.begin(); // pos = 3
return 0;
}
适用场景:
- 统计序列中“小于等于
x
”的元素个数(upper_bound
返回的位置就是计数)。 - 处理“非严格递增”的最长子序列(如允许相等元素时,用
upper_bound
替代lower_bound
)。
2. equal_range
—— 找“等于目标值的所有元素范围”
功能:
返回一个 pair
类型,包含两个迭代器:
- 第一个迭代器是
lower_bound
的结果(第一个 ≥x
的位置); - 第二个迭代器是
upper_bound
的结果(第一个 >x
的位置)。
两者之间的元素就是所有等于x
的元素(因为序列有序)。
用法:
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 3, 3, 5, 7};
int x = 3;
// 找所有等于3的元素范围
auto range = equal_range(nums.begin(), nums.end(), x);
// 第一个 ≥3 的位置(索引1),第一个 >3 的位置(索引3)
int left = range.first - nums.begin(); // left=1
int right = range.second - nums.begin(); // right=3
// 等于3的元素有 right - left = 2 个(索引1和2)
return 0;
}
适用场景:
- 快速获取有序序列中“所有等于
x
的元素”的起始和结束位置(如统计出现次数)。
3. binary_search
—— 检查“目标值是否存在”
功能:
在有序序列中检查目标值 x
是否存在,返回 bool
类型(true
存在,false
不存在)。
用法:
#include <algorithm>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 3, 3, 5, 7};
int x = 3;
bool exists = binary_search(nums.begin(), nums.end(), x); // exists = true
x = 4;
exists = binary_search(nums.begin(), nums.end(), x); // exists = false
return 0;
}
适用场景:
- 只需要判断元素是否存在,不需要知道具体位置(比
lower_bound
更简洁)。
共性与注意事项
-
前提条件:所有这些函数都要求序列是非递减(从小到大) 的,否则结果不可靠。如果序列是递减的,需要传入自定义比较函数(如
greater<int>()
):// 处理递减序列(如 {7,5,3,1}) auto it = lower_bound(nums.begin(), nums.end(), x, greater<int>());
-
返回值类型:
lower_bound
、upper_bound
、equal_range
返回的是迭代器(容器用)或指针(数组用),需要减去起始地址才能得到下标。 -
时间复杂度:均为
O(log n)
,和手动二分查找效率一致,但代码更简洁。
总结
函数名 | 核心功能 | 典型场景 |
---|---|---|
lower_bound |
找第一个 ≥ x 的位置 | 严格递增子序列(LIS) |
upper_bound |
找第一个 > x 的位置 | 非严格递增子序列;统计 ≤x 的元素 |
equal_range |
找所有等于 x 的元素范围(返回 [左, 右)) | 统计 x 的出现次数 |
binary_search |
检查 x 是否存在 | 简单的存在性判断 |
这些函数本质上都是对“二分查找”的封装,根据具体需求选择即可,能大幅简化代码(避免手动写二分的细节)。
最长公共子序列
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
int n,m;
cin >> n >> m;
string A,B;
cin >> A;
cin >> B;
int dp[1001][1001]={0};
for (int i = 1; i <= n; i ++ ){
for (int j = 1; j <= m; j ++ ){
if(A[i-1]==B[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
cout << dp[n][m];
return 0;
}
要理解 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
,需要先回到「最长公共子序列(LCS)」的核心逻辑——当两个字符不匹配时,LCS的长度取决于“更小范围的子问题”的最优解。我们用「子串范围」和「具体例子」拆解这个公式:
第一步:先明确 dp[i][j]
对应的子串范围
在 LCS 的 DP 定义中:
dp[i][j]
代表 字符串 A 的前 i 个字符(A[0..i-1]) 和 字符串 B 的前 j 个字符(B[0..j-1]) 的最长公共子序列长度。
比如i=3, j=2
,对应的是 A 的前 3 个字符(A[0],A[1],A[2])和 B 的前 2 个字符(B[0],B[1])的 LCS 长度。
第二步:为什么会有这个公式?(字符不匹配的场景)
公式成立的前提是 A[i-1] != B[j-1](即 A 的第 i 个字符和 B 的第 j 个字符不相同)。
此时,这两个字符不可能同时出现在 LCS 中,所以 LCS 的长度只能来自于「去掉其中一个字符后的子问题」——而这两个子问题的最优解,就是 dp[i-1][j]
和 dp[i][j-1]
。
第三步:拆解 dp[i-1][j]
和 dp[i][j-1]
的含义
我们用具体例子(样例中的 A=acbd,B=abedc)来解释:
假设当前计算 i=3, j=3
(A 的前 3 个字符是 acb
,B 的前 3 个字符是 abe
),此时 A[2] = b
,B[2] = e
(不匹配),需要计算 max(dp[2][3], dp[3][2])
。
1. dp[i-1][j]
:去掉 A 的第 i 个字符,保留 B 的前 j 个字符
i-1=2
,j=3
→ 对应子问题:A 的前 2 个字符ac
和 B 的前 3 个字符abe
的 LCS 长度。- 含义:既然 A 的第 3 个字符
b
和 B 的第 3 个字符e
不匹配,那 LCS 可能来自「不考虑 A 的b
,只看 A 的前 2 个字符和 B 的前 3 个字符」的最优解。 - 样例中
dp[2][3] = 1
(LCS 是a
)。
2. dp[i][j-1]
:去掉 B 的第 j 个字符,保留 A 的前 i 个字符
i=3
,j-1=2
→ 对应子问题:A 的前 3 个字符acb
和 B 的前 2 个字符ab
的 LCS 长度。- 含义:同理,LCS 也可能来自「不考虑 B 的
e
,只看 A 的前 3 个字符和 B 的前 2 个字符」的最优解。 - 样例中
dp[3][2] = 2
(LCS 是ab
)。
3. 取最大值的原因
LCS 的定义是「最长」的公共子序列,所以我们需要从两个子问题的解中选更大的那个——样例中 max(1, 2) = 2
,因此 dp[3][3] = 2
。
第四步:用“排除法”理解(为什么没有其他可能?)
当 A[i-1] != B[j-1] 时,LCS 只能有两种来源:
- 不包含 A[i-1] 的 LCS(即
dp[i-1][j]
); - 不包含 B[j-1] 的 LCS(即
dp[i][j-1]
)。
不存在“既不包含 A[i-1] 也不包含 B[j-1]”的情况——因为 dp[i-1][j-1]
的值一定 ≤ max(dp[i-1][j], dp[i][j-1])
(比如 dp[i-1][j]
已经包含了 dp[i-1][j-1]
的情况)。
举个例子:dp[i-1][j]
是 A[0..i-2] 与 B[0..j-1] 的 LCS,它本身就包含了“A[0..i-2] 与 B[0..j-2]”的 LCS(即 dp[i-1][j-1]
),所以不需要额外考虑 dp[i-1][j-1]
。
总结:公式的核心逻辑
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
是「字符不匹配时」的最优子结构体现:
- 当两个字符无法同时加入 LCS 时,LCS 的长度由“去掉其中一个字符后的子问题”决定;
- 取两个子问题的最大值,就是当前范围的最长公共子序列长度。
可以类比成:“你和朋友选共同喜欢的电影,当前选的电影你们有一个不喜欢,那你们只能从‘去掉这部电影后,你之前选的列表’或‘去掉这部电影后,朋友之前选的列表’里,选更长的那个共同列表”。
最短编辑距离
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
//dp问题就是,定义数组,初始化,定义转移方程
int main()
{
int n,m;
string A,B;
cin >> n;
cin >> A;
cin >> m;
cin >> B;
int dp[1001][1001]={0};
//注意初始化
for (int i = 0; i <= n; i ++ ){
dp[i][0] = i;
}
for (int j = 0; j <= m; j ++ ){
dp[0][j] = j;
}
for (int i = 1; i <= n; i ++ ){
for (int j = 1; j <= m; j ++ ){
if(A[i-1]==B[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min({dp[i-1][j],dp[i][j-1],dp[i-1][j-1]})+1;
}
}
}
cout << dp[n][m];
return 0;
}
最短编辑距离问题是动态规划的经典应用,核心思路是通过构建二维DP表,记录将字符串 A
的前 i
个字符转换为 B
的前 j
个字符所需的最少操作次数,并基于子问题的最优解推导当前解。以下是详细解法:
一、问题分析
编辑操作包含三种:删除(删 A
的字符)、插入(给 A
加字符)、替换(改 A
的字符)。目标是找到从 A
到 B
的最少操作数。
例如样例中 A=AGTCTGACGC
(10位)和 B=AGTAAGTAGGC
(11位),最少需要4次操作(如替换1个字符、插入2个字符、删除1个字符)。
该问题的关键是最优子结构:将 A[0..i-1]
转为 B[0..j-1]
的最少操作数,可由更小的子问题(如 A[0..i-2]
转 B[0..j-1]
、A[0..i-1]
转 B[0..j-2]
等)推导而来。
二、动态规划设计
1. 状态定义
定义 dp[i][j]
表示:将字符串 A
的前 i
个字符(A[0..i-1]
)转换为字符串 B
的前 j
个字符(B[0..j-1]
)所需的最少操作次数。
2. 状态转移方程
根据 A[i-1]
和 B[j-1]
是否相等,分两种核心情况讨论:
情况1:A[i-1] == B[j-1]
(当前字符匹配)
此时无需对当前字符进行任何操作,最少操作次数等于“A
的前 i-1
个字符转 B
的前 j-1
个字符”的次数,即:
dp[i][j] = dp[i-1][j-1]
情况2:A[i-1] != B[j-1]
(当前字符不匹配)
需要从三种操作中选最少次数的一种,分别对应:
- 删除操作:删除
A
的第i
个字符(A[i-1]
),此时问题转化为“A
的前i-1
个字符转B
的前j
个字符”,操作次数为dp[i-1][j] + 1
(+1表示删除这一步)。 - 插入操作:在
A
的第i
个字符后插入B[j-1]
,此时问题转化为“A
的前i
个字符转B
的前j-1
个字符”,操作次数为dp[i][j-1] + 1
(+1表示插入这一步)。 - 替换操作:将
A[i-1]
替换为B[j-1]
,此时问题转化为“A
的前i-1
个字符转B
的前j-1
个字符”,操作次数为dp[i-1][j-1] + 1
(+1表示替换这一步)。
因此,状态转移方程为:
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
3. 初始状态
需要处理“空字符串”的边界情况:
- 当
i=0
(A
为空):将空字符串转为B
的前j
个字符,只能通过插入j
次实现,因此dp[0][j] = j
。 - 当
j=0
(B
为空):将A
的前i
个字符转为空字符串,只能通过删除i
次实现,因此dp[i][0] = i
。
4. 计算顺序
从 i=1
到 n
(A
的长度),j=1
到 m
(B
的长度)逐行(或逐列)计算,最终 dp[n][m]
即为答案。
三、代码实现
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
int main() {
int n, m;
string a, b;
cin >> n >> a >> m >> b;
// dp[i][j]:a的前i个字符转b的前j个字符的最少操作数
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// 初始状态1:a为空,转b的前j个字符需插入j次
for (int j = 1; j <= m; ++j) {
dp[0][j] = j;
}
// 初始状态2:b为空,转a的前i个字符需删除i次
for (int i = 1; i <= n; ++i) {
dp[i][0] = i;
}
// 填充DP表
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (a[i - 1] == b[j - 1]) {
// 字符匹配,无需额外操作
dp[i][j] = dp[i - 1][j - 1];
} else {
// 字符不匹配,取三种操作的最小值+1
dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
}
}
}
cout << dp[n][m] << endl;
return 0;
}
四、代码执行过程(简化示例)
以简单例子 A="abc"
(n=3)、B="adc"
(m=3)为例,DP表的计算过程如下:
- 初始状态:
dp[0][j] = j
(j=1→3
为1,2,3),dp[i][0] = i
(i=1→3
为1,2,3)。 i=1
(A[0]='a'):j=1
(B[0]='a'):匹配 →dp[1][1] = dp[0][0] = 0
。j=2
(B[1]='d'):不匹配 →min(dp[0][2], dp[1][1], dp[0][1])+1 = min(2,0,1)+1=1
。
i=2
(A[1]='b'):j=2
(B[1]='d'):不匹配 →min(dp[1][2], dp[2][1], dp[1][1])+1 = min(1,2,0)+1=1
。
i=3
(A[2]='c')、j=3
(B[2]='c'):匹配 →dp[3][3] = dp[2][2] = 1
(最终答案为1,只需将 'b' 替换为 'd')。
五、时间和空间复杂度
- 时间复杂度:
O(n*m)
,两层循环分别遍历两个字符串的长度,每次计算仅需常数时间。 - 空间复杂度:
O(n*m)
,存储(n+1)*(m+1)
的DP表。
对于 n,m≤1000
的数据范围,该解法完全可行。若需优化空间,可将二维数组压缩为一维数组(仅保留当前行和上一行),空间复杂度降至 O(min(n,m))
。
这种方法通过枚举所有子问题,利用状态转移方程高效找到最少操作数,是解决最短编辑距离问题的标准解法,也可扩展到带权操作(如不同操作代价不同)的场景。
编辑距离
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int dp[1001][1001] = {0};
vector<string> S;
int sum = 0;
//延续上一个
void check(string qs, string x, int qn){
int a,b;
a = qs.size();
b = x.size();
for (int i = 0; i <= a; i ++ ){
dp[i][0] = i;
}
for (int i = 0; i <= b; i ++ ){
dp[0][i] = i;
}
for (int i = 1; i <= a; i ++ ){
for (int j = 1; j <= b; j ++ ){
if(qs[i-1]==x[j-1]){//这里是i-1 j-1
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min({dp[i-1][j-1],dp[i-1][j],dp[i][j-1]})+1;
}
}
}
if(dp[a][b]<=qn){
sum++;
}
}
int main()
{
int n,m;
cin >> n >> m;
string s;
for (int i = 0; i < n; i ++ ){
cin >> s;
S.push_back(s);
}
string qs;
int qn;
for (int i = 0; i < m; i ++ ){
sum = 0;
cin >> qs >> qn;
for(auto x:S){
check(qs, x, qn);
}
cout << sum << endl;
}
return 0;
}
石头合并
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
int n;
cin >> n;
int A[301];
for (int i = 1; i <= n; i ++ ){
cin >> A[i];
}
int dp[301][301] = {0};
for (int i = 0; i < 301; i ++ ){
for (int j = 0; j < 301; j ++ ){
dp[i][j] = i==j?0:0x3f3f3f3f;
}
}
int prefix[301] = {0};
for (int i = 1; i <= n; i ++ ){
prefix[i] = prefix[i-1] + A[i];
}
// i表示起点,j表示重点,len表示区间长度(核心)
// 区间长度变化
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 ++ ){
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + prefix[j] - prefix[i-1]);
}
}
}
cout << dp[1][n];
}
要解决石子合并问题,我们可以使用动态规划中的区间DP思想。这个问题的核心是找到合并相邻石子的最优顺序,使得总代价最小。
问题分析
- 每次只能合并相邻的两堆石子,合并代价为两堆石子质量之和
- 目标是找到一种合并顺序,使总代价最小
- 这是典型的区间动态规划问题,因为最优解依赖于子区间的最优解
动态规划思路
- 状态定义:设
dp[i][j]
表示合并第i
堆到第j
堆石子的最小代价 - 边界条件:当
i == j
时(只有一堆石子),无需合并,dp[i][j] = 0
- 状态转移:对于区间
[i, j]
,我们可以在i
到j-1
之间找一个分割点k
,将区间分为[i, k]
和[k+1, j]
,则:
其中dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum(i, j)) (i ≤ k < j)
sum(i, j)
表示第i
堆到第j
堆石子的总质量 - 前缀和优化:为快速计算
sum(i, j)
,我们可以预处理前缀和数组prefix
,则sum(i, j) = prefix[j] - prefix[i-1]
C++实现代码
代码解析
-
数据结构:
a
数组存储每堆石子的质量prefix
数组存储前缀和,用于快速计算任意区间的总质量dp
二维数组存储区间合并的最小代价
-
计算顺序:
- 按照区间长度从小到大计算(从2到n)
- 对于每个长度,遍历所有可能的起点
i
,计算对应的终点j
- 对每个区间
[i, j]
,尝试所有可能的分割点k
,找到最小代价
-
时间复杂度:O(n³),其中n为石子堆数(300),计算量在可接受范围内
通过这种区间动态规划的方法,我们能够高效地找到合并石子的最优方案,得到最小总代价。