动态规划

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 更简洁)。

共性与注意事项

  1. 前提条件:所有这些函数都要求序列是非递减(从小到大) 的,否则结果不可靠。如果序列是递减的,需要传入自定义比较函数(如 greater<int>()):

    // 处理递减序列(如 {7,5,3,1})
    auto it = lower_bound(nums.begin(), nums.end(), x, greater<int>());
    
  2. 返回值类型lower_boundupper_boundequal_range 返回的是迭代器(容器用)或指针(数组用),需要减去起始地址才能得到下标。

  3. 时间复杂度:均为 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=2j=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=3j-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 只能有两种来源:

  1. 不包含 A[i-1] 的 LCS(即 dp[i-1][j]);
  2. 不包含 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 的字符)。目标是找到从 AB 的最少操作数。

例如样例中 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=0A 为空):将空字符串转为 B 的前 j 个字符,只能通过插入 j实现,因此 dp[0][j] = j
  • j=0B 为空):将 A 的前 i 个字符转为空字符串,只能通过删除 i实现,因此 dp[i][0] = i

4. 计算顺序

i=1nA 的长度),j=1mB 的长度)逐行(或逐列)计算,最终 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表的计算过程如下:

  1. 初始状态:dp[0][j] = jj=1→3 为1,2,3),dp[i][0] = ii=1→3 为1,2,3)。
  2. 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
  3. 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
  4. 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思想。这个问题的核心是找到合并相邻石子的最优顺序,使得总代价最小。

问题分析

  • 每次只能合并相邻的两堆石子,合并代价为两堆石子质量之和
  • 目标是找到一种合并顺序,使总代价最小
  • 这是典型的区间动态规划问题,因为最优解依赖于子区间的最优解

动态规划思路

  1. 状态定义:设dp[i][j]表示合并第i堆到第j堆石子的最小代价
  2. 边界条件:当i == j时(只有一堆石子),无需合并,dp[i][j] = 0
  3. 状态转移:对于区间[i, j],我们可以在ij-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堆石子的总质量
  4. 前缀和优化:为快速计算sum(i, j),我们可以预处理前缀和数组prefix,则sum(i, j) = prefix[j] - prefix[i-1]

C++实现代码

代码解析

  1. 数据结构

    • a数组存储每堆石子的质量
    • prefix数组存储前缀和,用于快速计算任意区间的总质量
    • dp二维数组存储区间合并的最小代价
  2. 计算顺序

    • 按照区间长度从小到大计算(从2到n)
    • 对于每个长度,遍历所有可能的起点i,计算对应的终点j
    • 对每个区间[i, j],尝试所有可能的分割点k,找到最小代价
  3. 时间复杂度:O(n³),其中n为石子堆数(300),计算量在可接受范围内

通过这种区间动态规划的方法,我们能够高效地找到合并石子的最优方案,得到最小总代价。

posted @ 2025-08-30 22:11  .N1nEmAn  阅读(5)  评论(0)    收藏  举报