线性动态规划(Linear DP)是动态规划中最基础、最常用的分支,其核心特征是状态按线性顺序递推,即每个状态仅依赖于前面的若干个前置状态,无环形依赖,求解过程遵循"定义状态→推导转移→初始化边界→线性遍历"的固定流程。本报告基于线性DP的底层逻辑从属关系,将其归纳为三大核心类别(基础最优型、计数型、带约束选优型),明确各类别的核心原理、通用模板及典型应用,摒弃生硬的题型命名,聚焦逻辑本质,助力快速掌握线性DP的解题思路与应用方法。
1. 引言
线性DP是动态规划的入门与核心,广泛应用于求解"最值""方案数""带约束选择"等各类线性序列问题。其核心优势在于将复杂问题拆解为若干个线性推进的子问题,通过记录子问题的最优解(或合法方案数),避免重复计算,高效求解全局最优解。与其他DP分支(如区间DP、树形DP)相比,线性DP的状态递推顺序明确,逻辑清晰,是掌握动态规划思想的基础。
本博客不按具体题型名称分类,而是依据"底层逻辑+从属关系",将线性DP划分为三大类,明确各类别的从属关联(带约束选优型是基础最优型的特殊分支),提炼通用解题套路,帮助使用者快速识别问题类型、套用模板,提升解题效率。
2. 线性DP核心基础
2.1 核心定义
线性DP:状态定义基于线性序列(如数组、字符串、步骤序列),状态递推遵循"从前往后"或"从后往前"的线性顺序,每个状态dp[i](或二维状态dp[i][j])仅依赖于索引小于i的前置状态,无跨区间、环形的依赖关系。
2.2 通用解题步骤
无论何种线性DP题型,均遵循以下4个核心步骤,其中"状态定义"是解题的关键:
- 定义状态 :明确
dp[i](或dp[i][j])的具体含义,确保状态能覆盖子问题的核心特征(如最优值、方案数)。 - 推导转移方程:根据问题约束,确定当前状态如何由前置状态推导而来(如取max/min、累加求和)。
- 初始化边界 :为序列起始位置(如
dp[0]、dp[1])赋值,确保递推能正常启动。 - 线性遍历:按序列顺序遍历,依次计算每个状态的值,最终得到全局解。
2.3 核心从属关系
线性DP的三大类别存在明确的从属逻辑:
- 基础最优型:线性DP的根基,所有求"最值"的线性问题均归于此。
- 计数型:与基础最优型平行,核心目标是"求方案数",而非最值。
- 带约束选优型:基础最优型的特殊分支,在基础最值求解的基础上,增加"相邻禁忌"约束。
3. 线性DP三大核心类别详解
3.1 基础最优型线性DP(前缀递推求最值)
3.1.1 核心本质
核心目标是求解线性序列中的最长、最短、最大收益、最小代价,状态按前缀顺序递推,当前状态的最优解依赖于前面所有或部分前置状态的最优解,通过取max/min完成状态更新。
3.1.2 状态通用定义
一维状态:dp[i] 表示"处理到第i个位置时,当前能获得的最优解"(如最长长度、最小代价)。
二维状态:当问题存在"二选一"约束(如翻转/不翻转、底部/入口、选/不选基础分支)时,扩展为二维状态 dp[i][0] 和 dp[i][1],分别表示"处理到第i个位置时,两种不同分支的最优解"。本质仍是前缀递推,仅将单一最优路径拆分为两条并行的最优路径。
3.1.3 通用转移逻辑
一维状态:dp[i] = max/min( dp[j] + 附加代价/收益 )(其中j < i,且j满足问题约束)。
二维状态:dp[i][0] = max/min( dp[i-1][0] + cost1, dp[i-1][1] + cost2 );dp[i][1] = max/min( dp[i-1][0] + cost3, dp[i-1][1] + cost4 )(cost为不同分支的转移代价)。
3.1.4 典型应用场景
最长递增子序列(LIS):处理到第i个元素时,回头遍历所有j < i且满足nums[j] < nums[i]的位置,取dp[j]+1的最大值作为dp[i]。
每个工件有"翻转/不翻转"两种状态,用二维状态记录两种分支的最小总长度,从上一步的两种状态取min转移。该问题中,工件按顺序线性排列,状态递推严格遵循"第i个工件→第i+1个工件"的线性顺序,每个工件的状态仅依赖于前一个工件的两种状态,无环形或跨区间依赖,完全符合线性DP"状态按线性顺序递推、仅依赖前置状态"的核心定义,属于线性DP的二维扩展形式。
竹竿路径最小时间:每个竹竿有"底部/传送阵"两种到达方式,用二维状态记录两种方式的最小时间,从上一步的两种状态推导当前状态。同理,竹竿按线性顺序排列,状态递推遵循"第i个竹竿→第i+1个竹竿"的线性逻辑,当前竹竿的两种状态仅依赖于前一个竹竿的两种状态,无跨序依赖,是线性DP在多状态场景下的典型应用,属于线性DP范畴。
3.1.5 通用代码模板
一维基础版
java
import java.util.Arrays;
class Solution {
public int solve(int[] nums) {
int n = nums.length;
// dp[i]:以第i个元素结尾的最长最优解(此处为最长递增子序列长度)
int[] dp = new int[n];
Arrays.fill(dp, 1); // 边界初始化:每个元素自身为一个子序列
int maxRes = 1;
// 线性遍历,依次计算每个状态
for (int i = 0; i < n; i++) {
// 遍历所有前置状态j,寻找合法转移
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxRes = Math.max(maxRes, dp[i]);
}
return maxRes;
}
}
二维扩展版
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
String[] s = new String[n];
for (int i = 0; i < n; i++) {
s[i] = sc.next();
}
// dp[i][0]:第i个工件不翻转的最小总长度;dp[i][1]:第i个工件翻转的最小总长度
int[][] dp = new int[n][2];
// 边界初始化:第一个工件无论翻转与否,长度均为2
dp[0][0] = 2;
dp[0][1] = 2;
// 线性递推
for (int i = 1; i < n; i++) {
char a2 = s[i-1].charAt(1); // 上一个工件末尾
char a1 = s[i-1].charAt(0); // 上一个工件翻转后末尾
char b1 = s[i].charAt(0); // 当前工件开头(不翻转)
char b2 = s[i].charAt(1); // 当前工件开头(翻转)
// 当前不翻转,从上一步两种状态转移
dp[i][0] = Math.min(dp[i-1][0] + (a2 == b1 ? 1 : 2), dp[i-1][1] + (a1 == b1 ? 1 : 2));
// 当前翻转,从上一步两种状态转移
dp[i][1] = Math.min(dp[i-1][0] + (a2 == b2 ? 1 : 2), dp[i-1][1] + (a1 == b2 ? 1 : 2));
}
// 全局最优解为两种状态的最小值
System.out.println(Math.min(dp[n-1][0], dp[n-1][1]));
}
}
3.2 计数型线性DP(前缀递推求方案数)
3.2.1 核心本质
与基础最优型线性DP平行,核心目标不是求解最值,而是计算线性序列中合法方案的总数(如走法数、组合数、排布数)。其核心逻辑是:当前位置的总方案数 = 所有能到达当前位置的前置方案数之和,全程仅做加法累加,不涉及max/min运算。
3.2.2 状态通用定义
dp[i] 表示"凑成数值i、走到第i步、排布到第i个位置的总合法方案数"。
3.2.3 通用转移逻辑
若到达当前位置i的前置路径有k种(如可从i-1、i-2、...、i-x到达),则转移方程为:dp[i] = dp[i-1] + dp[i-2] + ... + dp[i-x](其中i-x ≥ 0,满足问题约束)。
关键注意点:边界初始化必须设置dp[0] = 1,表示"空状态(如走0步、凑0值)属于1种合法方案",是递推的基础。
3.2.4 典型应用场景
- 基础爬楼梯:一步可走1格或2格,求走到第n阶的总走法数,转移方程为
dp[i] = dp[i-1] + dp[i-2]。 - 多步组合计数(组合总和Ⅳ):一步可走给定数组中的任意步长,求凑成目标值的总组合数,转移方程为
dp[i] += dp[i-x](x为给定步长,且i ≥ x)。 - 场地排布计数:如地块建房(相邻地块可建或不建),求总排布方案数,本质仍是前缀方案数累加。
3.2.5 通用代码模板
基础版
java
class Solution {
public int climbStairs(int n) {
// dp[i]:走到第i阶楼梯的总走法数
int[] dp = new int[n+1];
// 边界初始化:dp[0]=1(空状态),dp[1]=1(1阶只有1种走法)
dp[0] = 1;
dp[1] = 1;
// 线性递推,累加前置方案数
for (int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
扩展版(组合总和Ⅳ为例)
java
import java.util.Arrays;
class Solution {
public int combinationSum4(int[] nums, int target) {
// dp[i]:凑成目标值i的总组合数
int[] dp = new int[target+1];
// 边界初始化:凑0值有1种方案(不选任何元素)
dp[0] = 1;
// 线性遍历目标值,依次计算每个状态
for (int i = 1; i <= target; i++) {
// 遍历所有可能的步长(前置状态)
for (int x : nums) {
if (i >= x) {
dp[i] += dp[i - x];
}
}
}
return dp[target];
}
}
3.3 带约束选优型线性DP(相邻禁忌类)
3.3.1 核心本质
属于基础最优型线性DP的特殊分支,在基础最值求解的基础上,增加了"相邻禁忌"约束------即"选择当前位置,就不能选择相邻的前一个位置",核心仍是求最优解,只是转移逻辑被约束为固定的二选一。
3.3.2 状态通用定义
一维状态(简化版):dp[i] 表示"处理到第i个位置时,遵守相邻禁忌约束的最优解"(如最大收益)。
二维状态(清晰版):dp[i][0](不选当前位置的最优解)、dp[i][1](选当前位置的最优解),本质与基础最优型的二维扩展一致,但转移逻辑固定。
3.3.3 固定转移逻辑
无论何种场景,转移逻辑均固定为以下两种情况,最终取两种情况的max:
- 不选当前位置:当前最优解继承前一个位置的最优解(无论前一个位置选或不选),即
dp[i][0] = max(dp[i-1][0], dp[i-1][1])(简化版可直接写dp[i] = dp[i-1])。 - 选当前位置:当前最优解 = 前两个位置的最优解 + 当前位置的价值(因为不能选前一个位置),即
dp[i][1] = dp[i-1][0] + val[i](简化版可写dp[i] = dp[i-2] + val[i])。
合并简化版转移方程:dp[i] = max(dp[i-1], dp[i-2] + val[i])。
3.3.4 典型应用场景
- 打家劫舍:不能偷相邻房屋,求最大偷窃收益,直接套用简化版转移方程。
- 删除并获得点数:将数组转化为"数值-总和"映射,等价于"不能选相邻数值",转化为打家劫舍问题求解。
- 相邻不能建房/种花:求最大收益或最多数量,本质仍是相邻禁忌的最优选择。
3.3.5 通用代码模板
简化版(打家劫舍为例)
java
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
if (n == 1) return nums[0];
// dp[i]:前i个房屋的最大偷窃收益
int[] dp = new int[n];
// 边界初始化
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
// 线性递推,套用固定转移方程
for (int i = 2; i < n; i++) {
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
return dp[n-1];
}
}
清晰版(二维状态)
java
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
// dp[i][0]:不偷第i个房屋的最大收益;dp[i][1]:偷第i个房屋的最大收益
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = nums[0];
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
}
return Math.max(dp[n-1][0], dp[n-1][1]);
}
}
4. 核心从属关系与解题技巧
4.1 从属关系总结
线性DP三大类的逻辑从属的关系可简化为:
- 根基:基础最优型线性DP(所有求最值的线性DP均基于此);
- 平行:计数型线性DP(不求最值,仅累加方案数,与基础最优型并列);
- 分支:带约束选优型线性DP(基础最优型+相邻禁忌约束,是其特殊衍生)。
4.2 解题技巧
- 状态定义是关键:遇到线性序列问题,先明确"核心目标"(最值/方案数),再定义dp[i]的含义,避免模糊。
- 转移方程看约束:无约束→基础最优(max/min);无最值→计数(累加);相邻禁忌→固定二选一转移。
- 边界初始化避坑:计数型必须设dp[0]=1;最优型需根据起始位置的实际含义赋值。
- 空间优化:当dp[i]仅依赖dp[i-1]和dp[i-2]时,可使用变量替代数组,降低空间复杂度(如打家劫舍、爬楼梯)。
5. 结论
线性动态规划的核心是"线性递推"与"状态复用",三大核心类别(基础最优型、计数型、带约束选优型)覆盖了绝大多数线性DP题型。掌握各类别的核心逻辑、通用模板及从属关系,可快速识别问题类型,套用模板求解,避免陷入"题海战术"。
解题的关键在于"明确状态定义"和"推导转移方程",只要能清晰说出dp[i]的含义,再结合问题约束推导转移逻辑,就能高效解决各类线性DP问题。