在动态规划(DP)问题中,有一类高频场景格外“友好”——它们基于线性序列展开,状态仅依赖最近1-2步的结果,不仅逻辑清晰,还能将空间复杂度从O(n)优化到O(1)。本文将通过线性打家劫舍股票交易II爬楼梯三个典型例题,拆解这类“近步依赖DP”的共性,并提炼通用解题思路,帮你快速掌握这类问题的核心解法。

一、典型例题解析:从具体到抽象

1. 线性打家劫舍:不偷相邻的“二选一”决策

题目背景

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素是:相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

完整代码(滚动变量优化版)

def rob(nums):
    # 定义滚动变量:prev记录前2间的最大金额,curr记录前1间的最大金额
    prev, curr = 0, 0
    for num in nums:
        # 状态转移:不偷当前房(沿用curr) vs 偷当前房(prev + 当前金额)
        prev, curr = curr, max(curr, prev + num)
    return curr

# 测试案例
print(rob([1,2,3,1]))  # 输出4(偷1和3)
print(rob([2,7,9,3,1]))# 输出12(偷2、9、1)

核心逻辑拆解

  • 状态定义:没有用传统的dp[i](前i间房的最大金额),而是用两个变量滚动记录关键状态:
    • prev:遍历到当前房屋时,前2间房屋能偷到的最大金额(避免与当前房屋相邻);
    • curr:遍历到当前房屋时,前1间房屋能偷到的最大金额。
  • 状态转移:每个房屋只有两种选择,取最大值即可:
    1. 不偷当前房:最大金额 = 前1间的最大金额(curr);
    2. 偷当前房:最大金额 = 前2间的最大金额 + 当前房屋金额(prev + num),因为偷了当前房就不能偷前1间。
  • 空间优化:无需存储整个DP数组,仅用两个变量,空间复杂度从O(n)降至O(1)。

2. 股票交易II:无限次交易的“持有/不持有”状态

题目背景

给定一个数组prices,其中prices[i]表示一支给定股票第i天的价格。你可以无限次地完成交易(多次买入和卖出一支股票),但你必须在再次购买前出售掉之前的股票。设计一个算法计算你所能获取的最大利润。

完整代码(一维数组优化版)

def maxProfit(prices):
    if not prices:
        return 0
    # 一维数组:dp[0]不持有股票的最大利润,dp[1]持有股票的最大利润
    dp = [0, -float('inf')]
    for price in prices:
        # 先更新不持有状态:前一天不持有 或 前一天持有今天卖出(+当前价格)
        dp[0] = max(dp[0], dp[1] + price)
        # 再更新持有状态:前一天持有 或 前一天不持有今天买入(-当前价格)
        dp[1] = max(dp[1], dp[0] - price)
    # 最后必须不持有股票(持有会浪费利润)
    return dp[0]

# 测试案例
print(maxProfit([7,1,5,3,6,4]))  # 输出7(1买5卖、3买6卖)
print(maxProfit([1,2,3,4,5]))    # 输出4(1买5卖,或多次小交易)

核心逻辑拆解

  • 状态定义:同样放弃传统dp[i][0/1](第i天的持有状态),用长度为2的一维数组记录最近状态:
    • dp[0]:遍历到当前天,不持有股票的最大利润;
    • dp[1]:遍历到当前天,持有股票的最大利润(初始设为负无穷,因为第一天不买就不能持有)。
  • 状态转移:每天的决策围绕“持有”和“不持有”切换,且仅依赖前一天的状态:
    1. 不持有股票:要么前一天就不持有(dp[0]),要么前一天持有、今天卖出(dp[1] + price,卖出赚钱);
    2. 持有股票:要么前一天就持有(dp[1]),要么前一天不持有、今天买入(dp[0] - price,买入花钱)。
  • 关键顺序:必须先更新dp[0]再更新dp[1],避免dp[0]的新值覆盖旧值,导致dp[1]计算错误。

3. 爬楼梯:步数依赖的“累加”计算

题目背景

假设你正在爬楼梯,需要n阶才能到达楼顶。每次你可以爬1或2个台阶,请问有多少种不同的方法可以爬到楼顶?(注意:给定n是一个正整数)

完整代码(滚动变量优化版)

def climbStairs(n):
    # 边界条件:1阶1种方法,2阶2种方法
    if n <= 2:
        return n
    # 滚动变量:a记录n-2阶的方法数,b记录n-1阶的方法数
    a, b = 1, 2
    for _ in range(3, n + 1):
        # 状态转移:n阶方法数 = n-1阶 + n-2阶(最后一步走1阶或2阶)
        a, b = b, a + b
    return b

# 测试案例
print(climbStairs(2))  # 输出2(1+1、2)
print(climbStairs(5))  # 输出8(1+1+1+1+1、1+1+1+2、1+1+2+1等)

核心逻辑拆解

  • 状态定义:用两个变量记录“前两步”的方法数,因为第n阶的方法数仅依赖前两阶:
    • a:第n-2阶的方法数;
    • b:第n-1阶的方法数。
  • 状态转移:每阶的方法数是前两阶之和(最后一步要么走1阶,要么走2阶),即b = a + b,同时更新a为原来的b(滚动向前)。
  • 边界处理:直接处理n<=2的情况,避免循环中的初始值问题,逻辑更简洁。

二、核心共性提炼:这类问题的3个关键特征

通过上述三个例题,我们可以发现“线性序列近步依赖DP”的本质共性,这也是它们能被优化到O(1)空间的根本原因:

共性特征 线性打家劫舍 股票交易II 爬楼梯
1. 线性序列结构 房屋数组是有序线性结构,按顺序决策 价格数组是时间线性序列,按天决策 台阶是有序线性结构,按阶计算
2. 有限决策选择 每个房屋“偷”或“不偷”(2选1) 每天“持有”或“不持有”(2选1) 每阶“走1步”或“走2步”(2选1)
3. 近步状态依赖 依赖前2步(prev、curr)的状态 依赖前1步(dp[0]、dp[1])的状态 依赖前2步(a、b)的状态

三、通用解题思路:4步搞定近步依赖DP

掌握以下4个步骤,遇到这类问题就能快速切入,无需从头推导:

步骤1:识别问题特征,确认是否属于“近步依赖”

先问自己3个问题,满足则大概率是这类问题:

  1. 问题是否基于有序线性结构(数组、时间序列、台阶等)?
  2. 每个节点的决策是否只有2种或固定少数几种互斥选择(偷/不偷、持有/不持有、走1/2步)?
  3. 每个选择的结果是否仅依赖最近1-2步的状态,无需追溯更早的历史?

步骤2:定义“滚动状态”,放弃完整DP数组

无需定义dp[i](前i个节点的状态),直接用2个变量长度为2的一维数组,记录“最近1-2步”的关键状态。核心原则:

  • 依赖前2步 → 用2个独立变量(如打家劫舍的prev/curr、爬楼梯的a/b);
  • 依赖前1步且有2种状态 → 用长度为2的一维数组(如股票II的dp[0]/dp[1])。

步骤3:推导状态转移方程,聚焦“二选一”逻辑

针对每个节点的两种选择,用max(求最大收益/金额)或+(求方法数/累加值)连接历史状态与当前节点值,形成转移方程:

  • 求最大值场景(打家劫舍、股票交易):新状态 = max(选择1结果, 选择2结果)
  • 求累加值场景(爬楼梯):新状态 = 选择1结果 + 选择2结果

步骤4:处理边界条件,初始化滚动变量

根据问题的“初始状态”(序列的前1-2个节点),给滚动变量赋合法初始值,避免遍历开始时状态异常:

  • 打家劫舍:初始prev=0curr=0(无房屋时金额为0);
  • 股票II:初始dp[0]=0(无交易利润为0)、dp[1]=-inf(未买入不能持有);
  • 爬楼梯:初始a=1b=2(对应1阶和2阶的方法数)。

四、拓展思考:如何应对更复杂的近步依赖问题?

如果遇到“3种选择”或“依赖前3步”的问题(如“每次可以走1、2、3阶”的爬楼梯),只需微调思路:

  1. 选择数增加:若有3种选择(如偷/不偷/观望),可增加状态变量(如dp[0]/dp[1]/dp[2]);
  2. 依赖步数增加:若依赖前3步,可增加滚动变量数量(如用a/b/c记录前3步状态)。

核心不变:只保留“关键的最近几步状态”,放弃完整DP数组,始终让空间复杂度保持在O(1)。

总结

线性序列近步依赖DP是DP问题中的“性价比之王”——逻辑简单、代码精简、空间高效。记住“线性结构+有限选择+近步依赖”的核心特征,套用“识别特征→定义滚动状态→推转移方程→处理边界”的四步思路,你就能轻松搞定这类高频题目。