当前位置: 首页 > backend >正文

动态规划1——线性动态规划

一、线性动态规划的核心概念

1.1 什么是线性动态规划

线性动态规划(Linear DP)是动态规划的一种基本形式,其特点是状态沿着线性序列(如数组或字符串)单向推进。这类问题通常具有以下特征:

  • 单维/双维状态空间:状态通常表示为 dp[i] 或 dp[i][j]

  • 线性依赖关系:当前状态仅依赖于序列中前一个或多个位置的状态

  • 无后效性:当前状态确定后,后续状态发展不受之前状态影响

1.2 基本解题框架

// 1. 初始化状态数组
vector<int> dp(n, 0); // 2. 设置边界条件
dp[0] = base_case; // 3. 状态转移(核心)
for(int i = 1; i < n; i++) {dp[i] = transition(dp[...]); // 状态转移方程
}// 4. 获取最终结果
return dp[n-1] or max(dp);

二、经典例题精讲

2.1 最大子段和(Maximum Subarray)

问题描述:给定整数数组,求连续子数组的最大和。
输入格式:第一行是一个正整数N,表示了序列的长度。(N≤200000)
                第二行是用空格分开的N个整数,表示这个序列
输出格式:一个整数,为最大的子段和是多少,子段的最小长度为1
输入样例:7
2  -4   3   -1   2  -4  3
输出样例:4

状态定义

  • dp[i]:以第 i 个元素结尾的最大子数组和

状态转移方程

dp[i] = max(dp[i-1] + nums[i], nums[i])

推导过程

  • 选项1:将当前元素加入前一个子数组(dp[i-1] + nums[i]

  • 选项2:从当前元素开始新子数组(nums[i]

  • 取两者最大值作为新状态

代码实现

int maxSubarraySum(int nums[], int n) {int dp[100];dp[0] = nums[0];int maxSum = dp[0];for (int i = 1; i < n; i++) {dp[i] = max(dp[i-1] + nums[i], nums[i]); // 状态转移maxSum = max(maxSum, dp[i]); // 更新全局最大值}return maxSum;
}

时间复杂度:O(n)
空间复杂度:O(n)(可优化为O(1))

2.2 最长上升子序列(LIS)

问题描述:求序列中最长的递增子序列长度,

输入格式:第1行是序列的长度N(1 <= N <= 1000)。
                  第2行给出序列中的N个整数,这些整数的取值范围都在0到10000之间
输出格式:一个整数,表示最长上升子序列的长度
输入样例:

7

2  7  3  1  9  4  2
输出样例:

3

状态定义

  • dp[i]:以第 i 个元素结尾的最长上升子序列长度

状态转移方程

dp[i] = max(dp[j]) + 1 (0 ≤ j < i 且 nums[j] < nums[i])

推导过程

  • 遍历所有 j < i

  • 若 nums[j] < nums[i],则 i 可以接在 j 后面

  • 在所有符合条件的 j 中,取 dp[j] 的最大值加1

代码实现

int longestIncreasingSubsequence(int a[], int n) {vector<int> dp(n, 1); // 初始化为1(每个元素自身是长度为1的序列)int maxV = 1;for (int i = 1; i < n; i++) {for (int j = 0; j < i; j++) {if (a[j] < a[i]) {dp[i] = max(dp[i], dp[j] + 1);}}maxV = max(maxV, dp[i]);}return maxV;
}

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

2.3 最长公共子序列(LCS)

问题描述:求两个序列的最长公共子序列长度。

输入格式:
两行。每行为由大写字母构成的长度不超过1000的字符串,表示序列X和Y。
输出格式:
第一行为一个非负整数。表示所求得的最长公共子序列的长度。若不存在公共子
序列,则输出文件一个整数0。

输入样例:
ABCBDAB
BDCABA


输出样例:

4

状态定义

  • dp[i][j]X[0..i-1] 和 Y[0..j-1] 的LCS长度

状态转移方程

dp[i][j] = {dp[i-1][j-1] + 1,             if X[i-1] == Y[j-1]max(dp[i-1][j], dp[i][j-1]),  otherwise
}

推导过程

  • 末尾字符相同:LCS长度+1

  • 末尾字符不同:取两个子问题的最大值

    • 忽略X的末尾(dp[i-1][j]

    • 忽略Y的末尾(dp[i][j-1]

代码实现

int longestCommonSubsequence(string a, string b) {int m = a.size(), n = b.size();vector<vector<int>> dp(m+1, vector<int>(n+1, 0));for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; 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]);}}}return dp[m][n];
}

时间复杂度:O(mn)
空间复杂度:O(mn)

2.4 编辑距离(Edit Distance)

问题描述:设A和B是两个字符串。我们要用最少的字符操作次数,将字符串A转换为字符串B。这里所说的字符操作共有三种:
1、删除一个字符;
2、插入一个字符
3、将一个字符改为另一个字符
对任意的两个字符串A和B,计算出将字符串A变换为字符串B所用的最少字符操作次数。
输入格式:
第一行为字符串A;第二行为字符串B;字符串A和B的长度均小于2000。
输出格式:
只有一个正整数,为最少字符操作次数。

输入样例:
sfdqxbw
gfdgw
输出样例:

4

状态定义

  • dp[i][j]:将 A[0..i-1] 转换为 B[0..j-1] 的最小操作数

状态转移方程

dp[i][j] = {dp[i-1][j-1],                  if A[i-1] == B[j-1]min(dp[i-1][j] + 1,    // 删除A[i]dp[i][j-1] + 1,    // 插入B[j]dp[i-1][j-1] + 1   // 替换A[i]为B[j]), otherwise
}

推导过程

  • 字符相同:无需操作,继承前状态

  • 字符不同:取三种操作的最小值

    • 删除:消耗1次操作,转化为 dp[i-1][j]

    • 插入:消耗1次操作,转化为 dp[i][j-1]

    • 替换:消耗1次操作,转化为 dp[i-1][j-1]

代码实现

int editDistance(string a, string b) {int m = a.size(), n = b.size();vector<vector<int>> dp(m+1, vector<int>(n+1, 0));// 初始化边界条件for (int i = 0; i <= m; i++) dp[i][0] = i;for (int j = 0; j <= n; j++) dp[0][j] = j;for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; 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] + 1,   // 删除dp[i][j-1] + 1,   // 插入dp[i-1][j-1] + 1  // 替换});}}}return dp[m][n];
}

时间复杂度:O(mn)
空间复杂度:O(mn)

三、线性DP的优化技巧

3.1 空间优化:滚动数组

当状态只依赖前有限个状态时,可降维存储:

// 最长上升子序列优化(O(n)空间)
int lisOptimized(int a[], int n) {vector<int> tail; // 存储潜在LIS的末尾元素for (int x : a) {auto it = lower_bound(tail.begin(), tail.end(), x);if (it == tail.end()) {tail.push_back(x); // 可扩展LIS} else {*it = x; // 优化潜在序列}}return tail.size();
}

3.2 时间优化:预处理与剪枝

  • 前缀和/后缀和:用于子数组问题

  • 单调队列/栈:优化滑动窗口类问题

  • 二分查找:将O(n)查找优化为O(logn)

四、练习题库(难度递进)

4.1 基础练习

  1. 爬楼梯问题:每次爬1或2阶,求到n阶的方法数
    状态方程dp[i] = dp[i-1] + dp[i-2]

  2. 打家劫舍:不能偷相邻房屋,求最大收益
    状态方程dp[i] = max(dp[i-1], dp[i-2] + nums[i])

  3. 硬币找零:求组成金额的最少硬币数
    状态方程dp[i] = min(dp[i], dp[i-coin]+1)

4.2 进阶挑战

  1. 最长回文子序列

    dp[i][j] = {2 + dp[i+1][j-1],            if s[i]==s[j]max(dp[i+1][j], dp[i][j-1]), otherwise
    }
  2. 乘积最大子数组

    maxDP[i] = max(nums[i], maxDP[i-1]*nums[i], minDP[i-1]*nums[i])
    minDP[i] = min(nums[i], maxDP[i-1]*nums[i], minDP[i-1]*nums[i])

五、总结与学习建议

5.1 线性DP解题四步法

  1. 定义状态:明确 dp[i] 或 dp[i][j] 的含义

  2. 建立方程:找出状态间的递推关系

  3. 初始化:设置边界条件

  4. 确定顺序:选择正确的计算方向

5.2 核心思维训练

  • 分解思想:将大问题拆解为线性子问题

  • 状态设计:"以...结尾"是最常用状态定义

  • 方程推导:思考最后一步操作的决策点

http://www.xdnf.cn/news/13667.html

相关文章:

  • 创客匠人助力家庭教育IP破局:从0到1打造创始人个人品牌全攻略
  • Android Compose 自定义滑动进度条
  • RAGFlow迁移到GPU服务器(Docker容器元数据修复)
  • Springboot3+的id字符串转化问题
  • LaTeX常用数学公式语法
  • 香橙派3B学习笔记10:snap打包C/C++程序与动态链接库(.so)
  • 数组方法_join()+_concat()+_reverse()+ _indexOf()
  • MS5110模数转换器可pin to pin兼容ADS1110
  • 「AI产业」| 《2025中国低空经济商业洞察报告(商业无人机应用篇)》
  • 【mysql】联合索引和单列索引的区别
  • Ceph分布式存储方案
  • 比亚迪座舱接入通义大模型,未来将联合打造更多AI智能座舱场景
  • 【JUC面试篇】Java并发编程高频八股——线程与多线程
  • 各项目变更频繁时,如何保持整体稳定
  • Linux 内核学习(10) --- Linux sysfs 节点创建
  • Testbed问题记录
  • 【每日likou】704. 二分查找 27. 移除元素 977.有序数组的平方
  • Pandas:你的数据分析瑞士军刀![特殊字符]✨
  • DeepCritic: SFT+RL两阶段训练突破LLM自我监督!显著提升大模型的自我批判能力!!
  • 构建康养人才职业成长加速器 —— 智慧康养实训室虚拟仿真建设方案
  • 【笔记】NVIDIA AI Workbench 中安装 CUDA 12.9
  • 其他UML图示例,用到再学习
  • 心理学行业IP变现新趋势:创客匠人赋能个人品牌崛起
  • 去除百度AI图像中包含的水印内容
  • PocketSCP:蛋白质口袋动态时空拓扑可视化分析新方法
  • 华为云Flexus+DeepSeek征文|华为云一键部署高可用版 Dify LLM 应用开发平台实践详解
  • 训练过程中的 Loss ?
  • DeviceNet转Modbus RTU协议转换网关在石油开采行业的应用
  • 常见系统设计
  • 2024蓝桥杯C/C++ B组国赛