线性动态规划(Linear DP)是动态规划的一种基本形式,其特点是状态沿着线性序列(如数组或字符串)单向推进。这类问题通常具有以下特征:
单维/双维状态空间:状态通常表示为 dp[i]
或 dp[i][j]
线性依赖关系:当前状态仅依赖于序列中前一个或多个位置的状态
无后效性:当前状态确定后,后续状态发展不受之前状态影响
// 1. 初始化状态数组 vectordp(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);
问题描述:给定整数数组,求连续子数组的最大和。
输入格式:第一行是一个正整数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))
问题描述:求序列中最长的递增子序列长度,
输入格式:第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) { vectordp(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)
问题描述:求两个序列的最长公共子序列长度。
输入格式:
两行。每行为由大写字母构成的长度不超过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> dp(m+1, vector (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)
问题描述:设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> dp(m+1, vector (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)
当状态只依赖前有限个状态时,可降维存储:
// 最长上升子序列优化(O(n)空间) int lisOptimized(int a[], int n) { vectortail; // 存储潜在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(); }
前缀和/后缀和:用于子数组问题
单调队列/栈:优化滑动窗口类问题
二分查找:将O(n)查找优化为O(logn)
爬楼梯问题:每次爬1或2阶,求到n阶的方法数
状态方程:dp[i] = dp[i-1] + dp[i-2]
打家劫舍:不能偷相邻房屋,求最大收益
状态方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
硬币找零:求组成金额的最少硬币数
状态方程:dp[i] = min(dp[i], dp[i-coin]+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 }
乘积最大子数组
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])
定义状态:明确 dp[i]
或 dp[i][j]
的含义
建立方程:找出状态间的递推关系
初始化:设置边界条件
确定顺序:选择正确的计算方向
分解思想:将大问题拆解为线性子问题
状态设计:"以...结尾"是最常用状态定义
方程推导:思考最后一步操作的决策点