【动态规划】区间DP

区间动态规划是动态规划中一类非常经典且重要的问题类型。它主要处理在一段区间上进行操作或决策,最终求解最优解的问题。这类问题的特点是:状态定义与区间有关,通常以区间端点作为状态变量,通过合并子区间信息来求解整个区间。


一、什么是区间DP?

1.1 核心思想

区间DP的核心思想是:

将一个大区间的问题分解为若干个小区间的问题,然后利用小区间的解来推导出大区间的解。

这与分治法有相似之处,但关键区别在于:区间DP会记录所有子问题的解(即状态),避免重复计算,从而实现动态规划的“记忆化”或“填表”过程。

1.2 适用场景

  • 给定一个序列(数组、字符串等),需要在某个区间 [i, j] 上进行操作。
  • 操作通常是合并、删除、分割、配对等。
  • 目标是求某种“最优值”(最小代价、最大得分、最少步数等)。
  • 满足最优子结构重叠子问题性质。

二、区间DP的基本结构

2.1 状态定义

最常见的状态定义是:

dp[i][j] = 在区间 [i, j] 上操作所能得到的最优值

其中 i 是左端点,j 是右端点,i <= j

2.2 状态转移方程

通常,我们会枚举一个分割点 ki <= k < j),将区间 [i, j] 分成 [i, k][k+1, j] 两部分:

dp[i][j] = min/max(dp[i][k] + dp[k+1][j] + cost(i, j, k))

这里的 cost(i, j, k) 是将两个子区间合并时产生的额外代价或收益。

注意:有些问题的 cost 只与 ij 有关,与 k 无关,比如石子合并中的 sum(i, j)

2.3 初始化

  • 单个元素的区间:dp[i][i] = 初始值(通常是0或该元素的值)
  • 对于 i > j 的情况,dp[i][j] 无意义,可设为0或INF。

2.4 遍历顺序

由于 dp[i][j] 依赖于更短的区间 [i, k][k+1, j],我们必须按区间长度从小到大进行遍历。

即:

for (int len = 2; len <= n; len++) {        // 区间长度从2开始(1已初始化)
    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/max(dp[i][j], dp[i][k] + dp[k+1][j] + cost);
        }
    }
}

三、经典例题详解

3.1 石子合并(最小代价版)

题目描述
n 堆石子排成一排,每次可以将相邻的两堆合并成一堆,合并的代价是这两堆石子的总数。求将所有石子合并成一堆的最小总代价。

3.1.1 分析
  • 状态定义dp[i][j] 表示将第 i 到第 j 堆石子合并成一堆的最小代价。
  • 转移方程:枚举最后一步合并的位置 k,即先合并 [i,k][k+1,j],然后合并这两堆:
    dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum[i][j])
    
    其中 sum[i][j] 是从第 i 堆到第 j 堆的石子总数。
  • 初始化dp[i][i] = 0(单堆不需要合并)
  • 目标dp[1][n]
3.1.2 前缀和优化

为了快速计算 sum[i][j],我们可以预处理前缀和数组 prefix

prefix[0] = 0;
for (int i = 1; i <= n; i++) {
    prefix[i] = prefix[i-1] + a[i];
}
sum[i][j] = prefix[j] - prefix[i-1];
3.1.3 C++ 实现
#include <iostream>
#include <vector>
#include <climits>
using namespace std;

const int MAXN = 300;
const int INF = 0x3f3f3f3f;

int main() {
    int n;
    cin >> n;
    vector<int> a(n + 1);
    vector<int> prefix(n + 1, 0);

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        prefix[i] = prefix[i-1] + a[i];
    }

    // dp[i][j] 表示合并第i到第j堆的最小代价
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));

    // 初始化:单个石子堆代价为0
    for (int i = 1; i <= n; i++) {
        dp[i][i] = 0;
    }

    // 枚举区间长度
    for (int len = 2; len <= n; len++) {
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            dp[i][j] = INF; // 初始化为无穷大

            // 枚举分割点k
            for (int k = i; k < j; k++) {
                int cost = prefix[j] - prefix[i - 1]; // sum[i][j]
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + cost);
            }
        }
    }

    cout << "最小合并代价: " << dp[1][n] << endl;
    return 0;
}
3.1.4 复杂度分析
  • 时间复杂度:O(n³)
  • 空间复杂度:O(n²)

3.2 回文串分割(最小分割次数)

题目描述
给定一个字符串 s,将其分割成若干子串,使得每个子串都是回文串。求最小分割次数。

3.2.1 分析

这个问题可以转化为区间DP:

  • 预处理:先用DP判断所有子串是否为回文。
    • isPal[i][j] = true 表示 s[i..j] 是回文。
  • 主DP
    • dp[i] 表示前 i 个字符的最小分割次数。
    • 转移:枚举最后一个回文串的起始位置 j,如果 s[j..i] 是回文,则:
      dp[i] = min(dp[i], dp[j-1] + 1)
      
    • 特殊:如果 s[1..i] 本身就是回文,则 dp[i] = 0

注意:这其实是一个“线性DP + 区间判断”的组合,但回文判断部分是典型的区间DP。

3.2.2 C++ 实现
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

int minCut(string s) {
    int n = s.size();
    if (n == 0) return 0;

    // 步骤1:预处理回文数组 isPal[i][j]
    vector<vector<bool>> isPal(n, vector<bool>(n, false));

    // 长度为1
    for (int i = 0; i < n; i++) {
        isPal[i][i] = true;
    }

    // 长度为2
    for (int i = 0; i < n - 1; i++) {
        if (s[i] == s[i+1]) {
            isPal[i][i+1] = true;
        }
    }

    // 长度 >= 3
    for (int len = 3; len <= n; len++) {
        for (int i = 0; i + len - 1 < n; i++) {
            int j = i + len - 1;
            if (s[i] == s[j] && isPal[i+1][j-1]) {
                isPal[i][j] = true;
            }
        }
    }

    // 步骤2:主DP,dp[i] 表示前i个字符的最小分割次数
    vector<int> dp(n, 0);
    for (int i = 0; i < n; i++) {
        if (isPal[0][i]) {
            dp[i] = 0; // 整个前缀是回文,无需分割
        } else {
            dp[i] = i; // 最坏情况:每个字符都分割
            for (int j = 1; j <= i; j++) {
                if (isPal[j][i]) {
                    dp[i] = min(dp[i], dp[j-1] + 1);
                }
            }
        }
    }

    return dp[n-1];
}

int main() {
    string s;
    cin >> s;
    cout << "最小分割次数: " << minCut(s) << endl;
    return 0;
}

3.3 多边形三角剖分(最大得分)

题目描述
一个 n 边凸多边形,顶点按顺时针编号为 0n-1,每个顶点有一个值。将其三角剖分,每个三角形的得分为三个顶点值的乘积。求最大总得分。

3.3.1 分析
  • 状态定义dp[i][j] 表示从顶点 ij 构成的多边形部分的最大得分。
  • 转移:枚举一个中间点 ki < k < j),形成三角形 (i,k,j),然后递归处理 [i,k][k,j]
    dp[i][j] = max(dp[i][k] + dp[k][j] + val[i]*val[k]*val[j])
    
  • 边界:当 j - i < 2 时,无法构成三角形,dp[i][j] = 0
3.3.2 C++ 实现(简化版)
#include <vector>
#include <algorithm>
using namespace std;

int maxScore(vector<int>& values) {
    int n = values.size();
    if (n < 3) return 0;

    vector<vector<int>> dp(n, vector<int>(n, 0));

    // len: 从3到n(三角形至少3个点)
    for (int len = 3; len <= n; len++) {
        for (int i = 0; i + len - 1 < n; i++) {
            int j = i + len - 1;
            for (int k = i + 1; k < j; k++) {
                int score = dp[i][k] + dp[k][j] + values[i] * values[k] * values[j];
                dp[i][j] = max(dp[i][j], score);
            }
        }
    }

    return dp[0][n-1];
}

四、区间DP的常见优化

4.1 四边形不等式优化

对于形如:

dp[i][j] = min_{i<k<j} { dp[i][k] + dp[k][j] } + w(i,j)

如果权重函数 w(i,j) 满足四边形不等式单调性,则可以使用决策单调性优化,将时间复杂度从 O(n³) 降到 O(n²)。

但该优化较复杂,竞赛中较少要求手写,此处略过。

4.2 前缀和/后缀和优化

如石子合并中,用前缀和快速计算区间和。

4.3 预处理辅助数组

如回文串问题中,先用区间DP预处理 isPal[i][j]


五、区间DP的模板总结

// 通用模板框架
for (int len = 2; len <= n; len++) {           // 区间长度
    for (int i = 1; i <= n - len + 1; i++) {    // 左端点
        int j = i + len - 1;                    // 右端点
        dp[i][j] = 初始化值(如INF或0;
        for (int k = i; k < j; k++) {           // 分割点
            dp[i][j] = merge(dp[i][k], dp[k+1][j], cost);
        }
    }
}

六、注意事项

  1. 初始化要正确:特别是 dp[i][i]dp[i][i+1]
  2. 遍历顺序必须正确:按长度从小到大。
  3. 边界条件处理:如 i > j 或长度不足。
  4. 空间优化:有时可用滚动数组,但因依赖多个子区间,通常难以优化。
  5. 调试技巧:打印 dp 数组中间状态,观察小数据的填表过程。

七、练习题推荐

  1. 石子合并(经典)
  2. 能量项链(NOIP 提高组)
  3. 加分二叉树(NOIP)
  4. 括号匹配(最长合法子串)
  5. 乘积最大(数字串分割)
  6. 多边形游戏(博弈+区间DP)

八、总结

区间DP是一种基于“分治+记忆化”的动态规划方法,关键在于:

  • 正确定义状态 dp[i][j]
  • 找到合理的分割点 k
  • 设计正确的转移方程
  • 控制遍历顺序(按长度)

掌握区间DP,不仅能解决经典问题,还能为更复杂的树形DP、环形DP打下基础。

记住口诀
“区间DP按长枚,左右端点随之走,分割枚举k在中,子区合并得最优。”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值