#c

动态规划

简单区间类型DP

#c
@鞠大龙
魅客科创中心

CONTENTS

目录

1. 动态规划概述

1. 动态规划概述

  • 动态规划(DP) : 是一种通过将复杂问题分解为更简单的子问题来解决问题的算法策略
  • 核心思想:通过存储子问题的解来避免重复计算,从而提高算法效率
  • 动态规划的两个关键特性
    • 最优子结构:问题的最优解包含其子问题的最优解
    • 重叠子问题:同一个子问题在求解过程中会被多次使用
信息学 动态规划之简单区间DP 魅客科创中心

1. 动态规划概述

动态规划的常见分类

  • 线性DP:状态按线性顺序转移
    • 例:最长递增子序列、编辑距离
  • 区间DP:状态定义在区间上
    • 例:石子合并、矩阵链乘法
  • 背包DP:资源分配类问题
    • 例:0-1背包、完全背包
  • 状态压缩DP:使用位运算表示状态
    • 例:旅行商问题、集合覆盖

动态规划的设计步骤

  1. 定义状态(确定DP数组的含义)
  2. 确定状态转移方程
  3. 初始化DP数组
  4. 确定计算顺序
  5. 返回最终结果

本课重点:区间动态规划

信息学 动态规划之简单区间DP 魅客科创中心

2. 区间DP基本概念

2. 区间DP基本概念

  • 区间动态规划:是动态规划的一种特殊形式,用于解决在区间上进行操作的问题
  • 核心思想:将区间问题分解为更小的区间子问题
  • 状态定义:通常使用dp[i][j]表示区间[i,j]上的最优解
  • 计算顺序:通常按照区间长度从小到大进行计算
  • 适用问题
    • 区间合并问题(如石子合并)
    • 区间分割问题(如矩阵链乘法)
    • 区间特性问题(如最长回文子序列)
信息学 动态规划之简单区间DP 魅客科创中心

2. 区间DP基本概念

区间DP的计算过程

  1. 初始化:通常初始化长度为的区间
  2. 按区间长度递增计算:
    • 长度为的区间
    • 长度为的区间
    • ...
    • 长度为的区间
  3. 状态转移:通过已计算的小区间解得到大区间解
  4. 最终结果:通常是dp[1][n]dp[0][n-1]

区间计算顺序示意图

  • 先计算小区间(浅色)
  • 再计算大区间(深色)
  • 箭头表示状态转移方向
信息学 动态规划之简单区间DP 魅客科创中心

2. 区间DP基本概念:区间DP的状态转移

区间DP的状态转移通常有两种形式:

合并相邻区间:将区间[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)是合并这两个子区间的代价

枚举区间中的点:考虑区间[i,j]中的每个点k

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

关键是找到适合问题的状态转移方式和正确的计算顺序。

信息学 动态规划之简单区间DP 魅客科创中心

3. 区间DP的特点

3. 区间DP的特点

  • 区间表示:使用两个下标ij表示区间[i,j]
  • 计算顺序:按照区间长度从小到大计算,确保子问题先于依赖它们的大问题求解
  • 状态转移:通常涉及枚举区间内的分割点,将大区间问题分解为小区间问题
  • 时间复杂度:通常为³,其中是区间长度
    • 两层循环枚举区间起点和长度
    • 内层循环枚举分割点
  • 空间复杂度:通常为²,用于存储所有可能区间的结果
信息学 动态规划之简单区间DP 魅客科创中心

3. 区间DP的特点

区间DP的优势

  • 适合处理具有区间特性的问题
  • 状态定义直观,易于理解
  • 可以解决许多经典问题
  • 思路清晰,实现相对简单

区间DP的局限性

  • 时间复杂度较高,通常为O(n³)
  • 不适合处理大规模数据
  • 有时需要额外的优化技巧
  • 状态转移方程可能不易发现

如何识别区间DP问题

  1. 问题涉及区间操作(合并、分割等)
  2. 最优解可以通过子区间的最优解组合得到
  3. 问题具有重叠子问题特性
  4. 问题可以用dp[i][j]表示区间[i,j]的解

常见优化技巧

  • 利用问题特性减少枚举范围
  • 使用前缀和加速区间和的计算
  • 记忆化搜索代替传统DP
  • 利用单调性优化状态转移
信息学 动态规划之简单区间DP 魅客科创中心

3. 区间DP的特点:区间DP与其他DP的区别

  1. 状态定义
    • 区间DP:dp[i][j]表示区间[i,j]的最优解
    • 线性DP:dp[i]通常表示以位置i结尾的最优解
    • 背包DP:dp[i][j]通常表示前i个物品在容量j下的最优解
  2. 计算顺序
    • 区间DP:按区间长度递增计算
    • 线性DP:通常按索引顺序计算
    • 背包DP:按物品和容量的特定顺序计算
  3. 应用场景
    • 区间DP:适合处理区间合并、分割问题
    • 线性DP:适合处理序列相关问题
    • 背包DP:适合处理资源分配问题

理解这些区别有助于正确识别问题类型并选择合适的解决方案。

信息学 动态规划之简单区间DP 魅客科创中心

4. 区间DP的模板代码

4. 区间DP的模板代码:基本模板

vector<vector<int>> dp(n, vector<int>(n, initial_value));// 初始化DP数组
// 初始化长度为1的区间(基础情况)
for (int i = 0; i < n; i++) {
    dp[i][i] = base_case_value;
}
// 按照区间长度递增计算
for (int len = 2; len <= n; len++) {
    // 枚举区间起点
    for (int i = 0; i <= n - len; i++) {
        int j = i + len - 1;  // 计算区间终点
        dp[i][j] = initial_extreme_value; // 初始化为最大/最小值       
        // 枚举分割点
        for (int k = i; k < j; k++) {
            dp[i][j] = min/max(dp[i][j], dp[i][k] + dp[k+1][j] + cost(i, j, k));// 状态转移
        }
    }
}
// 最终结果通常是dp[0][n-1]或dp[1][n]
return dp[0][n-1];
信息学 动态规划之简单区间DP 魅客科创中心

4. 区间DP的模板代码:记忆化搜索实现区间DP

vector<vector<int>> memo(n, vector<int>(n, -1));// 初始化记忆化数组
// 递归函数
int solve(int i, int j) {
    // 基础情况
    if (i == j) return base_case_value;
    // 如果已经计算过,直接返回结果
    if (memo[i][j] != -1) return memo[i][j];
    int result = initial_extreme_value;// 初始化为最大/最小值
    // 枚举分割点
    for (int k = i; k < j; k++) {
        result = min/max(result, solve(i, k) + solve(k+1, j) + cost(i, j, k)); // 状态转移
    }
    // 存储结果
    memo[i][j] = result;
    return result;
}
return solve(0, n-1);// 调用入口
信息学 动态规划之简单区间DP 魅客科创中心

4. 区间DP的模板代码:区间DP的优化技巧

  1. 使用前缀和优化区间和计算
// 预处理前缀和
vector<int> prefix_sum(n + 1, 0);
for (int i = 1; i <= n; i++) {
    prefix_sum[i] = prefix_sum[i-1] + arr[i-1];
    }
// 计算区间[i,j]的和
int range_sum = prefix_sum[j+1] - prefix_sum[i];
  1. 利用问题特性减少枚举范围
    • 某些问题中,最优分割点可能具有单调性
    • 可以使用决策单调性优化,将时间复杂度从O(n³)降低到O(n²)
// 记录每个区间的最优分割点
vector<vector<int>> opt(n, vector<int>(n));
for (int len = 2; len <= n; len++) {
    for (int i = 0; i <= n - len; i++) {
        int j = i + len - 1;
        // 利用单调性,只在[opt[i][j-1], opt[i+1][j]]范围内枚举k
        // for (int k = opt[i][j-1]; k <= opt[i+1][j]; k++) {
        // // 状态转移
        // // ...
    }
}
  1. 区间DP中的四边形不等式优化
    • 适用于满足特定条件的区间DP问题
    • 可以将时间复杂度从O(n³)降低到O(n²)
信息学 动态规划之简单区间DP 魅客科创中心

5. 实例分析

5.1 实例分析:石子合并

问题描述
有N堆石子排成一排,每堆石子有一定的质量。现在要将这N堆石子合并成一堆,每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和。问题是:将N堆石子合并成一堆,最小的总代价是多少?

分析

  • 状态定义:dp[i][j]表示将区间[i,j]内的石子合并成一堆的最小代价
  • 状态转移:枚举区间内的分割点k,dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum(i,j))
  • 其中sum(i,j)是区间[i,j]内所有石子的质量之和
  • 初始条件:dp[i][i] = 0(单堆石子不需要合并)
信息学 动态规划之简单区间DP 魅客科创中心

5.1 实例分析:石子合并(代码示例)

// 石子合并问题
int mergeStones(vector<int>& stones) {
    int n = stones.size();
    if (n <= 1) return 0;
    
    // 计算前缀和,用于快速获取区间和
    vector<int> prefix_sum(n + 1, 0);
    for (int i = 0; i < n; i++) {
        prefix_sum[i + 1] = prefix_sum[i] + stones[i];
    }
    
    // dp[i][j]表示合并区间[i,j]的最小代价
    vector<vector<int>> dp(n, vector<int>(n, INT_MAX));
    
    // 初始化:单个石子堆不需要合并
    for (int i = 0; i < n; i++) {
        dp[i][i] = 0;
    }
    // 按区间长度递增计算
    for (int len = 2; len <= n; len++) {
        for (int i = 0; i <= n - len; 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_sum[j+1] - prefix_sum[i]);
            }
        }
    }
    
    return dp[0][n-1];
}

时间复杂度:O(n³)
空间复杂度:O(n²)

信息学 动态规划之简单区间DP 魅客科创中心

5.2 实例分析:最长回文子序列

问题描述
给定一个字符串s,找出其中最长的回文子序列,并返回该序列的长度。子序列是指从原序列中可以通过删除某些字符(也可以不删除)而不改变剩余字符相对位置形成的新序列。

分析

  • 状态定义:dp[i][j]表示字符串s在区间[i,j]上的最长回文子序列长度
  • 状态转移:
    • 如果s[i] == s[j],则dp[i][j] = dp[i+1][j-1] + 2
    • 否则,dp[i][j] = max(dp[i+1][j], dp[i][j-1])
  • 初始条件:dp[i][i] = 1(单个字符是长度为1的回文)
信息学 动态规划之简单区间DP 魅客科创中心

5.2 实例分析:最长回文子序列(代码示例)

// 最长回文子序列
int longestPalindromeSubseq(string s) {
    int n = s.length();
    if (n <= 1) return n;
    
    // dp[i][j]表示区间[i,j]上的最长回文子序列长度
    vector<vector<int>> dp(n, vector<int>(n, 0));
    
    // 初始化:单个字符是长度为1的回文
    for (int i = 0; i < n; i++) {
        dp[i][i] = 1;
    }
    // 按区间长度递增计算
    for (int len = 2; len <= n; len++) {
        for (int i = 0; i <= n - len; i++) {
            int j = i + len - 1;
            
            if (s[i] == s[j]) {
                dp[i][j] = dp[i+1][j-1] + 2;
            } else {
                dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
            }
        }
    }
    
    return dp[0][n-1];
}

时间复杂度:O(n²)
空间复杂度:O(n²)

信息学 动态规划之简单区间DP 魅客科创中心

5.3 实例分析:矩阵链乘法

问题描述
给定n个矩阵的链,矩阵i的维度为p[i-1] x p[i]。求完成所有矩阵相乘所需的最小标量乘法次数。

分析

  • 状态定义:dp[i][j]表示计算矩阵链A[i]A[i+1]...A[j]所需的最小乘法次数
  • 状态转移:枚举分割点k,dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])
  • 其中p[i-1]*p[k]*p[j]是合并两个子矩阵链的代价
  • 初始条件:dp[i][i] = 0(单个矩阵不需要乘法)
信息学 动态规划之简单区间DP 魅客科创中心

5.3 实例分析:矩阵链乘法(代码示例)

// 矩阵链乘法
int matrixChainMultiplication(vector<int>& p) {
    int n = p.size() - 1;  // n个矩阵
    if (n <= 1) return 0;
    
    // dp[i][j]表示计算矩阵链A[i]A[i+1]...A[j]所需的最小乘法次数
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, INT_MAX));
    
    // 初始化:单个矩阵不需要乘法
    for (int i = 1; i <= n; i++) {
        dp[i][i] = 0;
    }
    // 按区间长度递增计算
    for (int len = 2; len <= n; len++) {
        for (int i = 1; i <= n - len + 1; 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] + 
                               p[i-1] * p[k] * p[j]);
            }
        }
    }
    
    return dp[1][n];
}

时间复杂度:O(n³)
空间复杂度:O(n²)

信息学 动态规划之简单区间DP 魅客科创中心

5.3 实例分析:矩阵链乘法(优化)

记录最优分割点

// 记录最优分割点
vector<vector<int>> split(n + 1, vector<int>(n + 1, 0));

// 在状态转移时记录
for (int len = 2; len <= n; len++) {
    for (int i = 1; i <= n - len + 1; i++) {
        int j = i + len - 1;
        
        for (int k = i; k < j; k++) {
            int cost = dp[i][k] + dp[k+1][j] + 
                       p[i-1] * p[k] * p[j];
            if (cost < dp[i][j]) {
                dp[i][j] = cost;
                split[i][j] = k;  // 记录最优分割点
            }
        }
    }
}

重构最优解

// 打印最优括号化方案
void printOptimalParens(vector<vector<int>>& split, 
                        int i, int j) {
    if (i == j) {
        cout << "A" << i;
    } else {
        cout << "(";
        printOptimalParens(split, i, split[i][j]);
        printOptimalParens(split, split[i][j] + 1, j);
        cout << ")";
    }
}

// 调用入口
printOptimalParens(split, 1, n);

优化思路

  • 记录最优分割点可以重构最优解
  • 某些情况下可以利用四边形不等式优化
  • 可以使用记忆化搜索简化实现
信息学 动态规划之简单区间DP 魅客科创中心

6. 练习题推荐

6. 练习题推荐

以下是一些适合练习区间动态规划的经典题目:

  • LeetCode 516: 最长回文子序列
    • 求解字符串中的最长回文子序列长度
  • LeetCode 1039: 多边形三角剖分的最低得分
    • 区间DP的经典应用
  • LeetCode 312: 戳气球
    • 复杂的区间DP问题
  • LeetCode 1000: 合并石头的最低成本
    • 石子合并问题的变种
  • LeetCode 546: 移除盒子
    • 高级区间DP问题
  • LeetCode 664: 奇怪的打印机
    • 区间DP的创新应用
  • LeetCode 1130: 叶值的最小代价生成树
    • 区间DP构建最优二叉树
  • POJ 1651: Multiplication Puzzle
    • 经典的区间DP问题
信息学 动态规划之简单区间DP 魅客科创中心

7. 总结

7. 总结

区间DP的核心要点

  • 状态定义:dp[i][j]表示区间[i,j]的最优解
  • 计算顺序:按区间长度从小到大计算
  • 状态转移:通过枚举分割点,将大区间问题分解为小区间问题
  • 时间复杂度:通常为O(n³)
  • 空间复杂度:通常为O(n²)

区间DP的应用场景

  • 区间合并问题
  • 区间分割问题
  • 区间特性问题

常见错误

  • 计算顺序错误,导致依赖的子问题未计算
  • 状态转移方程设计不正确
  • 边界条件处理不当
  • 分割点枚举范围不正确
  • 忽略特殊情况处理

学习建议

  • 从简单问题开始,如石子合并、回文子序列
  • 理解状态定义和转移方程的推导过程
  • 手动模拟小规模实例,理解计算过程
  • 多做练习,熟悉不同类型的区间DP问题
  • 尝试优化算法,如使用记忆化搜索、前缀和等技巧
信息学 动态规划之简单区间DP 魅客科创中心

7. 总结

区间DP的进阶内容

  • 区间DP与分治算法的结合
  • 四边形不等式优化
  • 决策单调性优化
  • 区间DP在树形结构上的应用
  • 多维区间DP问题

常见优化技巧

  • 使用前缀和加速区间和的计算
  • 记忆化搜索代替传统DP
  • 利用问题特性减少枚举范围
  • 使用单调队列或单调栈优化特定问题
  • 空间压缩优化(针对特定问题)

实践建议

  1. 从基础问题开始,逐步提高难度
  2. 理解每个问题的核心思想和状态转移
  3. 手动模拟小规模实例,加深理解
  4. 分析不同问题之间的联系和区别
  5. 尝试不同的实现方式,如迭代和递归
  6. 关注边界条件和特殊情况处理
  7. 练习优化算法,提高效率

记住:区间DP是一种思想,关键在于将大区间问题分解为小区间问题,并找到正确的状态转移方式。

信息学 动态规划之简单区间DP 魅客科创中心
动态规划之简单区间DP

#c

这个讲义使用了Awesome Marp主题的蓝色风格。 演讲者备注不会在演示模式中显示给观众,仅供演讲者参考。 每个关键页面都添加了备注,帮助演讲者更好地讲解内容。

封面页: - 欢迎学生,简要介绍本次课程的主题和目标 - 强调区间DP是动态规划的重要分支,在算法竞赛和实际应用中都很常见 - 可以简单提及本课程将从基础概念到实际应用,全面讲解区间DP的思想和技巧

目录页: - 简要介绍本次讲座的结构和内容安排 - 可以提及讲座的逻辑流程:从基础概念到具体应用,再到实践建议 - 强调我们将通过三个经典例子(石子合并问题、最长回文子序列、矩阵链乘法)来深入理解区间DP - 提醒学生关注每个部分之间的联系,以及如何将理论应用到实践中

转场页: - 这是一个简单的转场页,用于引入动态规划概述部分 - 可以简单提及动态规划是算法设计中的重要范式,有着广泛的应用

动态规划概述: - 介绍动态规划的基本概念和核心思想 - 强调最优子结构和重叠子问题的重要性 - 简要提及动态规划的常见应用场景

动态规划的分类: - 介绍动态规划的几种常见分类 - 强调区间DP是其中一种重要的类型 - 简要说明每种类型的特点和应用场景

转场页: - 引入区间DP的基本概念部分 - 可以简单提及区间DP是处理区间问题的有力工具

区间DP基本概念: - 详细介绍区间DP的定义和基本思想 - 强调区间DP的状态定义和转移特点 - 解释为什么某些问题适合用区间DP解决

区间DP的计算过程: - 详细解释区间DP的计算过程和状态转移 - 使用图示直观展示区间DP的计算顺序 - 强调区间长度的重要性

区间DP的状态转移: - 详细解释区间DP的状态转移方程 - 使用具体例子说明状态转移的过程 - 强调区间划分点k的重要性

转场页: - 引入区间DP的特点部分 - 可以简单提及理解这些特点有助于更好地应用区间DP

区间DP的特点: - 详细介绍区间DP的主要特点 - 强调这些特点如何帮助识别和解决区间DP问题 - 解释区间DP与其他DP类型的区别

区间DP的优缺点: - 分析区间DP的优势和局限性 - 讨论何时应该选择区间DP - 提供一些优化区间DP的思路

区间DP与其他DP的区别: - 比较区间DP与其他类型DP的异同 - 强调区间DP的独特之处 - 提供一些实际应用的例子

转场页: - 引入区间DP的模板代码部分 - 可以简单提及掌握这些模板有助于快速实现区间DP算法

区间DP的基本模板: - 提供区间DP的基本代码模板 - 解释模板中的关键部分 - 强调计算顺序的重要性

区间DP的记忆化搜索模板: - 提供使用记忆化搜索实现区间DP的代码模板 - 解释记忆化搜索的优势 - 比较迭代实现和记忆化搜索实现的异同

区间DP的优化技巧: - 提供一些常见的优化技巧 - 解释这些技巧如何提高算法效率 - 提供一些实际应用的例子

转场页: - 引入实例分析部分 - 可以简单提及我们将通过三个经典例子来深入理解区间DP

石子合并问题: - 详细介绍石子合并问题的描述和分析 - 解释如何使用区间DP解决这个问题 - 强调状态定义和转移方程的推导过程

石子合并问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度

最长回文子序列问题: - 详细介绍最长回文子序列问题的描述和分析 - 解释如何使用区间DP解决这个问题 - 强调状态定义和转移方程的推导过程

最长回文子序列问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度

矩阵链乘法问题: - 详细介绍矩阵链乘法问题的描述和分析 - 解释如何使用区间DP解决这个问题 - 强调状态定义和转移方程的推导过程

矩阵链乘法问题的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度

矩阵链乘法问题的优化: - 介绍矩阵链乘法问题的优化方法 - 解释如何记录最优分割点以重构最优解 - 分析优化后的算法效率

转场页: - 引入练习题推荐部分 - 可以简单提及练习是掌握区间DP的关键

练习题推荐: - 提供一系列适合练习区间DP的题目 - 简要介绍每道题的内容和难度 - 提供题目链接或参考资料

转场页: - 引入总结部分 - 可以简单提及总结将回顾本次讲座的主要内容

总结: - 回顾区间DP的核心概念和特点 - 强调区间DP的应用场景和解题思路 - 提供一些学习和实践的建议

进阶内容和实践建议: - 介绍区间DP的进阶内容和优化技巧 - 提供一些实践建议和学习资源 - 鼓励学生继续深入学习和实践

封底页: - 感谢学生的参与 - 提供联系方式或其他资源 - 鼓励学生继续学习和实践