2025.4.22动态规划学习(区间DP)

目录

1.区间DP的理解

1.1 基本概念

1.2 一般步骤

1.2.1 定义状态

1.2.2. 初始化状态

1.2.3. 状态转移方程

1.2.4. 计算顺序

2.石子合并

2.1题目描述

2.2解题思路

2.3代码展示

3.矩阵连乘问题

3.1题目描述

3.2实例说明

3.3解题思路

3.4代码展示

3.5误区

4.凸多边形最优三角剖分

4.1题目描述

4.2解题思路

4.3代码展示


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 \leqslant 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解题思路

    建议先听理论理解视频

    1. 动态规划建模:定义t[i][j]为顶点ij构成的子多边形的最小三角剖分权值和。
    2. 状态转移:枚举分割点ki < k < j),将子多边形划分为两部分,状态转移方程为:

      plaintext

      t[i][j] = min(t[i][k] + t[k][j] + weight(i,k,j))
      
    3. 区间 DP 遍历:按区间长度从小到大计算,确保子问题先被解决。
    4. 初始化:相邻顶点的权值为 0(无法形成三角形),其余初始化为无穷大。
    5. 结果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;
    }

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值