线性动态规划(Linear DP)

线性动态规划(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个核心步骤,其中"状态定义"是解题的关键:

  1. 定义状态 :明确dp[i](或dp[i][j])的具体含义,确保状态能覆盖子问题的核心特征(如最优值、方案数)。
  2. 推导转移方程:根据问题约束,确定当前状态如何由前置状态推导而来(如取max/min、累加求和)。
  3. 初始化边界 :为序列起始位置(如dp[0]dp[1])赋值,确保递推能正常启动。
  4. 线性遍历:按序列顺序遍历,依次计算每个状态的值,最终得到全局解。

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. 基础爬楼梯:一步可走1格或2格,求走到第n阶的总走法数,转移方程为dp[i] = dp[i-1] + dp[i-2]
  2. 多步组合计数(组合总和Ⅳ):一步可走给定数组中的任意步长,求凑成目标值的总组合数,转移方程为dp[i] += dp[i-x](x为给定步长,且i ≥ x)。
  3. 场地排布计数:如地块建房(相邻地块可建或不建),求总排布方案数,本质仍是前缀方案数累加。

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:

  1. 不选当前位置:当前最优解继承前一个位置的最优解(无论前一个位置选或不选),即 dp[i][0] = max(dp[i-1][0], dp[i-1][1])(简化版可直接写dp[i] = dp[i-1])。
  2. 选当前位置:当前最优解 = 前两个位置的最优解 + 当前位置的价值(因为不能选前一个位置),即 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 典型应用场景

  1. 打家劫舍:不能偷相邻房屋,求最大偷窃收益,直接套用简化版转移方程。
  2. 删除并获得点数:将数组转化为"数值-总和"映射,等价于"不能选相邻数值",转化为打家劫舍问题求解。
  3. 相邻不能建房/种花:求最大收益或最多数量,本质仍是相邻禁忌的最优选择。

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 解题技巧

  1. 状态定义是关键:遇到线性序列问题,先明确"核心目标"(最值/方案数),再定义dp[i]的含义,避免模糊。
  2. 转移方程看约束:无约束→基础最优(max/min);无最值→计数(累加);相邻禁忌→固定二选一转移。
  3. 边界初始化避坑:计数型必须设dp[0]=1;最优型需根据起始位置的实际含义赋值。
  4. 空间优化:当dp[i]仅依赖dp[i-1]和dp[i-2]时,可使用变量替代数组,降低空间复杂度(如打家劫舍、爬楼梯)。

5. 结论

线性动态规划的核心是"线性递推"与"状态复用",三大核心类别(基础最优型、计数型、带约束选优型)覆盖了绝大多数线性DP题型。掌握各类别的核心逻辑、通用模板及从属关系,可快速识别问题类型,套用模板求解,避免陷入"题海战术"。

解题的关键在于"明确状态定义"和"推导转移方程",只要能清晰说出dp[i]的含义,再结合问题约束推导转移逻辑,就能高效解决各类线性DP问题。

相关推荐
hetao17338372 小时前
2025-03-24~04-06 hetao1733837 的刷题记录
c++·算法
_深海凉_2 小时前
LeetCode热题100-环形链表
算法·leetcode·链表
原来是猿2 小时前
Linux进程信号详解(三):信号保存
开发语言·c++·算法
2401_892070982 小时前
算法与数据结构精讲:最大子段和(暴力 / 优化 / 分治)+ 线段树从入门到实战
c++·算法·线段树·最大子段和
memcpy03 小时前
LeetCode 904. 水果成篮【不定长滑窗+哈希表】1516
算法·leetcode·散列表
老四啊laosi3 小时前
[双指针] 8. 四数之和
算法·leetcode·四数之和
汀、人工智能3 小时前
[特殊字符] 第24课:反转链表
数据结构·算法·链表·数据库架构··反转链表
田梓燊3 小时前
leetcode 41
数据结构·算法·leetcode
_深海凉_3 小时前
LeetCode热题100-三数之和
算法·leetcode·职场和发展