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

动态规划核心模型精讲(上篇):斐波那契模型、路径问题与多状态DP


目录

1.前言

插播一条消息~

2.正文

2.1斐波那契数列模型

2.1.1概念讲解

2.1.2第n个泰波那契数列

2.1.3三步问题

2.1.4使用最小花费爬楼梯

2.1.5解码方法

2.2路径问题

2.2.1概念讲解

2.2.2不同路径

2.2.3不同路径II

2.2.4礼物最大的价值

2.2.5下降路径的最小和

2.2.6最小路径和

2.3简单多状态DP

2.3.1按摩师

2.3.2打家劫舍II

2.3.3删除并获得点数

2.3.4粉刷房子

2.3.5买卖股票的最佳时期含冷冻期

3.小结


1.前言

解决复杂问题的核心往往在于拆解艺术——就像规划跨城旅行时,我们不会一次性考虑所有路口的转向,而是先确定途经的关键节点;又如理财储蓄中,通过每月收益的复利累积实现长期目标,这些场景都暗含着动态规划的核心智慧:将大问题分解为可解决的子问题,用子问题的解逐步构建最终答案。

动态规划的魅力在于其对"无后效性"的精妙运用:当前决策只依赖已有的状态信息,未来的选择不会影响过去的最优解。这种特性使其成为处理多阶段决策问题的利器,但初学者常因复杂模型望而却步。本文将聚焦最基础的动态规划模型,通过斐波那契数列的递归优化、网格路径的状态转移等实例,带你掌握状态定义、转移方程构建等核心方法,为后续学习子序列、背包等复杂问题夯实基础。

学习路径提示:从斐波那契数列的"自顶向下递归"到"自底向上迭代",理解重叠子问题的优化思路;通过路径问题掌握"状态转移方程"的推导逻辑;最终在简单多状态DP模型中实践"状态定义四要素"(问题边界、状态变量、决策选择、转移关系)。

后续章节将按"基础模型→经典问题→实战技巧"的递进结构展开:首先解析斐波那契数列模型中的状态压缩技巧,再通过不同约束条件的路径问题强化多阶段决策思维,最后系统梳理简单多状态DP的通用解题框架,帮助你建立动态规划的完整认知体系。


插播一条消息~

🔍十年经验淬炼 · 系统化AI学习平台推荐

系统化学习AI平台https://www.captainbed.cn/scy/

  • 📚 完整知识体系:从数学基础 → 工业级项目(人脸识别/自动驾驶/GANs),内容由浅入深
  • 💻 实战为王:每小节配套可运行代码案例(提供完整源码)
  • 🎯 零基础友好:用生活案例讲解算法,无需担心数学/编程基础

🚀 特别适合

  • 想系统补强AI知识的开发者
  • 转型人工智能领域的从业者
  • 需要项目经验的学生

2.正文

2.1斐波那契数列模型

2.1.1概念讲解

动态规划的核心思想可通过“存钱罐”类比直观理解:若每日存钱数等于前两日之和(今天的钱 = 昨天的钱 + 前天的钱),这种状态对前序状态的依赖关系,正是斐波那契模型的本质。从数学角度看,其递推关系可表示为 F(n) = F(n-1) + F(n-2)(n ≥ 2,F(0)=0,F(1)=1)。

传统递归解法直接按公式计算,会产生大量重复计算(如计算 F(5) 需重复计算 F(3)、F(2) 等),时间复杂度为 O(2ⁿ)。而动态规划通过存储中间结果(如用数组记录已计算的 F(0) 至 F(n-1)),将时间复杂度优化至 O(n),空间复杂度可进一步优化至 O(1)。这种“用空间换时间”的策略,实现了从数学递推到高效算法的关键跨越。

核心洞察:动态规划通过状态定义(明确子问题)和转移方程(描述状态依赖),将复杂问题分解为重叠子问题,利用存储避免重复计算,是处理多阶段决策优化问题的通用框架。


2.1.2第n个泰波那契数列

泰波那契数列可视为“斐波那契的升级版”,其定义为:第n个泰波那契数Tn等于前三个数之和(T0=0, T1=1, T2=1, Tn=Tn-1+Tn-2+Tn-3)。以下通过动态规划方法拆解并优化求解过程。

动态规划四步骤解析

  1. 状态定义:设dp[i]为第i个泰波那契数;
  2. 转移方程:dp[i] = dp[i-1] + dp[i-2] + dp[i-3](i≥3);
  3. 初始化:dp[0]=0, dp[1]=1, dp[2]=1;
  4. 返回值:dp[n]。

基础实现与空间优化

LeetCode 1137题的基础实现如下,通过数组存储所有状态:

public int tribonacci(int n) { int[] ans = new int[40]; // 创建dp表时大小为40,因题目约束n≤37,确保空间充足ans[0] = 0; // 初始化T0ans[1] = 1; // 初始化T1ans[2] = 1; // 初始化T2if(n > 2){ for(int i = 2; i < n;i++){ // 从i=2开始迭代,计算T3至Tnans[i + 1] = ans[i] + ans[i - 1] + ans[i - 2]; } } return ans[n]; 
}

该实现时间复杂度O(n),空间复杂度O(n)。由于每个状态仅依赖前三个状态,可通过滚动数组优化,用三个变量替代数组:

优化核心:用a、b、c分别代表Tn-3、Tn-2、Tn-1,每次迭代计算Tn = a+b+c,再更新a=b、b=c、c=Tn,空间复杂度降至O(1)。

时空复杂度对比

实现方式时间复杂度空间复杂度
数组存储O(n)O(n)
滚动数组O(n)O(1)

2.1.3三步问题

三步问题可类比“上楼梯的走法计数”:每次可跨 1、2 或 3 阶台阶,求到达第 n 阶的走法总数。其动态规划解法需聚焦状态转移与边界条件:

状态定义:设 dp[i] 为到达第 i 阶的走法数。

转移方程:dp[i] = dp[i-1] + dp[i-2] + dp[i-3],因最后一步可从 i-1 阶跨 1 步、i-2 阶跨 2 步或 i-3 阶跨 3 步到达。

边界条件是关键:n=1 返回 1,n=2 返回 2,n=3 返回 4。生活化案例“3 阶台阶的 4 种走法”可直观理解:

  • 1+1+1(每次跨 1 阶)
  • 1+2(先 1 阶再 2 阶)
  • 2+1(先 2 阶再 1 阶)
  • 3(直接跨 3 阶)
public int waysToStep(int n) {// 1. 创建 dp 表int MOD = (int)1e9 + 7;// 处理一下边界情况if(n == 1 || n == 2)return n;if(n == 3)return 4;// 2. 初始化int[] dp = new int[n + 1];dp[1] = 1;dp[2] = 2;dp[3] = 4;// 3. 填表for(int i = 4; i <= n; i++)dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;// 4. 返回值return dp[n];}

代码实现中,初始化 dp[1]=1、dp[2]=2、dp[3]=4,从 i=4 开始迭代计算,结果需对 MOD=1e9+7 取模防溢出。时间复杂度 O(n),空间复杂度 O(n),可通过滚动数组优化空间至 O(1)(同泰波那契数列,仅保留前三个状态)。


2.1.4使用最小花费爬楼梯

以“花钱爬楼梯的最优策略”类比,问题可描述为:给定楼梯每层花费数组,每次可跳1或2层,求到达顶部的最小总花费。

动态规划四要素

  • 状态定义:dp[i]表示到达第i层的最小花费。
  • 转移方程:dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]),因到达第i层需从i-1层(花费cost[i-1])或i-2层(花费cost[i-2])跳上,取两者最小值。
  • 初始化:dp[0] = 0,dp[1] = 0,因可从第0或1层直接开始,无需额外花费。
  • 返回值:dp[n],其中n为cost数组长度,代表到达顶部(第n层)的最小花费。
public int minCostClimbingStairs(int[] cost) {// 1. 创建 dp 表:dp[i] 表示到达第 i 层台阶时的最小花费int n = cost.length; // 台阶总数int[] dp = new int[n + 1]; // dp 数组大小为 n+1,因为要包含第 n 层(索引 n)// 2. 初始化:dp[0] 和 dp[1] 分别表示跳到第 0 层和第 1 层的花费(可以从任意一层出发)// 注意,这里不需要单独初始化,因为后面会在循环中计算得到// 3. 填表:从第 2 层开始,计算每层的最小花费for (int i = 2; i <= n; i++) {// 花费 = min(从第 i-1 层跳过来的花费 + cost[i-1], 从第 i-2 层跳过来的花费 + cost[i-2])dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}// 4. 返回值:dp[n] 表示跳到第 n 层(即终点)的最小花费return dp[n];}

代码关键解析:dp数组大小为n+1,因需包含第n层(顶部)索引;时间复杂度O(n)(遍历一次),空间复杂度O(n),可优化为O(1)(用a、b变量存储dp[i-2]、dp[i-1],滚动更新)。

问题类型转移方程核心逻辑目标
三步问题dp[i] = dp[i-1]+dp[i-2]+dp[i-3]计算路径总数
最小花费爬楼梯dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])计算最小花费

2.1.5解码方法

解码方法问题可类比密码本翻译规则:将数字字符串按1-26对应A-Z解码,求有效解码方式总数。该问题核心在于多条件状态转移,需通过动态规划处理字符组合的不确定性。

状态定义:设dp[i]为前i个字符的解码方法数(i从0开始,对应空字符串到完整字符串)。

转移方程:需同时考虑两种解码可能:

  1. 若当前字符s[i-1]≠'0'(单独解码),则dp[i] += dp[i-1]
  2. 若前两位字符s[i-2..i-1]组成10-26的数字(组合解码),则dp[i] += dp[i-2]

'0'的处理:若当前字符为'0',单独解码路径失效;若前两位为"00"或"30"等超26的组合,组合解码路径亦失效,此时dp[i] = 0

复杂度分析:时间复杂度O(n)(遍历字符串一次),空间复杂度O(n)dp数组),可优化为O(1)(仅保留前两个状态)。

案例计算过程

字符串dp[0]dp[1]dp[2]dp[3]结果
"0"10--0
"12"111+1=2-2
"226"111+1=22+1=33

核心代码实现(Java):

public int num;public int numDecodings(String s) {int n = s.length();char[] ss = s.toCharArray();int[] dp = new int[n];//初始化第一位if(ss[0] != '0'){dp[0] = 1;}if(n == 1){return dp[0];}//初始化第二位if(ss[1] != '0' && ss[0] != '0'){dp[1]++;}num = 10 * (ss[0] - '0') + (ss[1] - '0');if(num >= 10 && num <= 26){dp[1]++;}for(int i = 2; i < n; i++){if(ss[i] != '0'){dp[i] = dp[i - 1];}num = 10 * (ss[i - 1] - '0') + (ss[i] - '0');if(num >= 10 && num <= 26){dp[i] += dp[i - 2];}}return dp[n - 1];}

2.2路径问题

2.2.1概念讲解

路径问题可类比“城市地图导航”:从 A 地到 B 地仅能向右或向下移动,需计算路线数量或最短路径。动态规划通过二维 dp 表求解,其中 dp[i][j] 定义为到达网格 (i,j) 位置的状态值(如路线数、路径长度)。状态转移方向由移动规则决定,通常来自上方 (i-1,j) 或左方 (i,j-1),特殊场景可包含对角线方向。边界初始化需注意:第一行只能从左向右到达,第一列只能从上到下到达,故这些位置的 dp 值需单独设定初始状态。

路径问题的核心要素包括:网格结构(二维平面的单元格分布)、移动方向(允许的位移向量集合)、目标函数(路线计数或最值优化)、障碍物处理(需标记不可达位置并阻断状态转移)。


2.2.2不同路径

一个机器人从 m 行 n 列网格左上角出发,每次只能向下或向右移动,求到达右下角的不同路径总数。

基础路径计数模型

  • 状态定义dp[i][j] 表示到达网格第 i 行第 j 列的路径总数。
  • 转移方程dp[i][j] = dp[i-1][j] + dp[i][j-1],因机器人只能从上方(i-1,j)或左方(i,j-1)到达当前位置。
  • 边界初始化:第一行 dp[i][0] = 1(仅能从左至右移动),第一列 dp[0][j] = 1(仅能从上至下移动)。
  • 返回值dp[m-1][n-1],即右下角单元格的路径数。
public int uniquePaths(int m, int n) { int[][] dp = new int[m][n]; // 第一行所有单元格只能通过从左向右移动到达,路径数恒为 1for (int i = 0; i < m; ++i) dp[i][0] = 1; // 第一列所有单元格只能通过从上向下移动到达,路径数恒为 1for (int j = 0; j < n; ++j) dp[0][j] = 1; // 填充 dp 表,每个单元格值为上方和左方单元格之和for (int i = 1; i < m; ++i) { for (int j = 1; j < n; ++j) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } return dp[m - 1][n - 1]; 
}

复杂度分析:时间复杂度 O(mn)(需遍历 m×n 个单元格),空间复杂度 O(mn)。可优化为 O(min(m,n)),通过一维数组滚动存储(当前行仅依赖上一行数据,用 dp[j] = dp[j] + dp[j-1] 更新)。

路径计算示例(m=3, n=7):

初始化 3 行 7 列网格,第一行 [1,1,1,1,1,1,1],第一列 [1,1,1]。从 (1,1) 开始计算:

  • (1,1)=1+1=2,(1,2)=1+2=3,(1,3)=1+3=4,(1,4)=1+4=5,(1,5)=1+5=6,(1,6)=1+6=7;
  • (2,1)=1+1=2,(2,2)=2+1=3,(2,3)=3+2=5,(2,4)=4+3=7,(2,5)=5+4=9,(2,6)=6+5=11;
    最终右下角 (2,6) 的路径数为 28。

核心逻辑:路径计数问题本质是组合数学中的「步骤选择」问题,动态规划通过子问题分解将复杂计算转化为网格递推,空间优化利用了状态转移的「单向依赖性」。


2.2.3不同路径II

在“不同路径”问题基础上增加障碍物,需计算从网格起点到终点的不同路径数(障碍物位置不可通行)。核心处理逻辑如下:

障碍物处理机制

  • 状态定义dp[i][j] 表示到达 (i,j) 的路径数,若 (i,j) 有障碍物则 dp[i][j] = 0
  • 转移方程:与“不同路径”一致,但需先判断当前网格是否有障碍物:
    obstacleGrid[i-1][j-1] == 1(因 dp 表扩展),则 dp[i][j] = 0;否则 dp[i][j] = dp[i-1][j] + dp[i][j-1]
  • 边界初始化:通过将 dp 表扩展为 (m+1)×(n+1) 并设置 dp[0][1] = 1,简化第一行/列有障碍物时的初始化逻辑(无需单独处理边界障碍物)。
  • 特殊情况:若起点 (0,0) 或终点 (m-1,n-1) 有障碍物,直接返回 0。

代码实现与优化

以下是 LeetCode 63 题的参考实现:

public int uniquePathsWithObstacles(int[][] obstacleGrid) { int m = obstacleGrid.length; int n = obstacleGrid[0].length; // 起点或终点有障碍物时直接返回 0if (obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1) return 0;int[][] dp = new int[m + 1][n + 1]; dp[0][1] = 1; // 虚拟初始化,简化边界处理for (int i = 1; i <= m; i++){ for (int j = 1; j <= n; j++){ if (obstacleGrid[i-1][j-1] == 1){ dp[i][j] = 0; }else { dp[i][j] = dp[i-1][j] + dp[i][j-1]; } } } return dp[m][n]; 
}

dp 表扩展原因:通过增加一行一列(索引 0 行和 0 列),将第一行/列的边界条件转化为通用转移方程,避免单独处理“首行只能从左到右”“首列只能从上到下”的复杂逻辑。

复杂度分析

  • 时间复杂度:O(mn),需遍历 m×n 网格。
  • 空间复杂度:O(mn),可优化为 O(n)(仅保留当前行数据)。

障碍物影响案例

以 2×2 网格为例,对比有无障碍物的路径数变化:

障碍物位置路径数(无障碍物)路径数(有障碍物)
22
(0,1)21
(1,1)(终点)20

关键结论:障碍物会阻断后续路径传递,首行/列障碍物会导致其右侧/下方所有格子路径数为 0;起点/终点障碍物直接导致无解。


2.2.4礼物最大的价值

"礼物最大的价值"问题是路径规划类动态规划的典型变种,其核心目标从"计算路径数量"转变为"求解最大收益"。以网格捡礼物场景为例,每个单元格对应特定价值的礼物,只能向右或向下移动,需找到从左上角到右下角的最大价值路径。

动态规划解决方案

public int jewelleryValue(int[][] frame) {int m = frame.length;int n = frame[0].length;int[][] dp = new int[m + 1][n + 1];for (int i = 1; i <= m; i++){for (int j = 1; j <= n; j++){dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + frame[i-1][j-1];}}return dp[m][n];}

状态定义:设 dp[i][j] 表示到达网格 (i-1, j-1) 位置时的最大礼物价值(dp 表扩展一行一列以简化边界处理)。

转移方程:到达 (i,j) 的最大价值等于从上方或左方过来的最大价值加上当前礼物价值,即:

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

边界初始化dp[0][j]dp[i][0] 均设为 0,模拟网格外无礼物的边界条件。

返回值:最终结果为 dp[m][n],其中 mn 分别是原网格的行数和列数。

关键实现细节

  • 索引对应关系frame[i-1][j-1] 是由于 dp 表比原网格多一行一列,dp 表的 (i,j) 对应原网格的 (i-1,j-1)
  • 复杂度分析:时间复杂度为 O(mn)(需遍历所有单元格),空间复杂度为 O(mn)(可优化至 O(n),通过一维数组存储上一行状态)。

3×3 网格示例演示

以价值网格 [[1,3,1],[1,5,1],[4,2,1]] 为例,dp 表填充过程如下:

  • dp[1][1] = max(0,0) + 1 = 1
  • dp[1][2] = max(0,1) + 3 = 4
  • dp[2][1] = max(1,0) + 1 = 2
  • dp[2][2] = max(4,2) + 5 = 9(关键决策点,选择左方路径)
  • 最终 dp[3][3] = 12,对应最优路径 1→3→5→2→1

该问题展示了动态规划在多阶段决策中的应用,通过状态转移累积最优解,为复杂场景下的资源分配问题提供了基础模型。


2.2.5下降路径的最小和

以“滑雪下山的最短路径”为类比,该问题要求从矩阵顶部任意位置出发,每次可向正下方、左下方或右下方移动,求解到达底部的最小路径和。动态规划是解决此类问题的高效方法,其核心思路如下:

状态定义:设dp[i][j]表示到达第i行第j列的最小下降路径和。由于矩阵采用 0-based 索引,代码中dp表使用 1-based 索引以便简化边界处理,因此matrix[i-1][j-1]对应原矩阵的元素。

转移方程:每个位置(i,j)可由上一行的(i-1,j-1)(左上)、(i-1,j)(正上)或(i-1,j+1)(右上)到达,故dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1]) + matrix[i-1][j-1]

边界处理关键:为避免判断左右边界越界,dp表设计为(n+1)×(n+2)的大小(n为矩阵边长)。其中第 0 列和第n+1列初始化为Integer.MAX_VALUE,确保边界位置(如j=1j-1=0)的无效路径不影响最小值计算。

结果计算:遍历dp表最后一行(第n行)的所有有效列,取最小值即为答案。

复杂度分析:时间复杂度O(n²)(两层嵌套循环);空间复杂度O(n²)(二维dp表),可优化为O(n)(仅存储上一行状态的一维数组)。

public int minFallingPathSum(int[][] matrix) { int n = matrix.length; int[][] dp = new int[n + 1][n + 2]; // (n+1)行(n+2)列处理边界// 初始化左右边界为最大值for(int i = 1; i <= n; i++){ dp[i][0] = dp[i][n + 1] = Integer.MAX_VALUE; } // 填充dp表for(int i = 1; i <= n; i++){ for(int j = 1; j <= n; j++){ dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i - 1][j + 1])) + matrix[i - 1][j - 1]; } } // 寻找最后一行最小值int ans = Integer.MAX_VALUE; for (int i = 1; i <= n; i++){ ans = Math.min(ans, dp[n][i]); } return ans; 
}

2.2.6最小路径和

最小路径和问题是动态规划中路径规划类问题的典型代表,以带权值的网格为场景,目标是寻找从左上角到右下角的路径中权值之和最小的路径。以下从动态规划四要素展开分析:

状态定义:设 dp[i][j] 表示到达网格中第 i 行第 j 列(均从 1 开始计数)的最小路径和。

转移方程:到达 (i,j) 的路径只能来自上方 (i-1,j) 或左方 (i,j-1),因此取两者中的较小路径和加上当前格子权值:dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1]grid 为原始网格数组,因索引偏移需 i-1j-1)。

边界初始化关键处理:将 dp 数组的首行(dp[i][0])和首列(dp[0][j])初始化为 Integer.MAX_VALUE(无穷大),仅保留 dp[0][1] = 0dp[1][0] = 0。此设计可避免对第一行(只能从左到右)和第一列(只能从上到下)的单独逻辑判断,因无穷大与有效路径和比较时会自动被忽略,确保转移方程普适性。

返回值dp[n][m]nm 分别为网格的行数和列数)。

复杂度分析:时间复杂度为 O(mn)(需遍历整个网格),空间复杂度为 O(mn)(二维 dp 数组),可通过滚动数组优化为 O(m)O(n)(仅保留当前行或列的状态)。

public int minPathSum(int[][] grid) {int n = grid.length;int m = grid[0].length;int[][] dp = new int[n + 1][m + 1];//初始化for(int i = 0; i <= n; i++){dp[i][0] = Integer.MAX_VALUE;}for(int j = 0; j <= m; j++){dp[0][j] = Integer.MAX_VALUE;}dp[0][1] = dp[1][0] = 0;for(int i = 1; i <= n; i++){for(int j = 1; j <= m; j++){dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];}}return dp[n][m];}

与“礼物最大价值”的对比:两者转移方程结构相似,核心差异在于优化目标(最小 vs 最大),如下表所示:

问题转移方程核心操作网格权值性质
最小路径和min(上方, 左方)非负整数
礼物最大价值max(上方, 左方)正整数(价值非负)

上述分析基于 LeetCode 64 题的动态规划解法,通过状态定义、边界处理与转移逻辑的协同设计,实现了问题的高效求解。

2.3简单多状态DP

概念讲解

多状态动态规划(DP)适用于处理存在多种选择或状态的决策问题,可类比“天气预测”场景:明天的天气(晴天、阴天、雨天)概率仅由今天的天气状态决定,每种状态间存在特定转移规律。其核心在于通过多维度DP表定义状态,如用dp[i][0]dp[i][1]dp[i][2]分别表示第i天晴天、阴天、雨天的概率,或用dpYes[i]/dpNo[i]表示第i步“是/否”决策的状态值。状态转移的本质是:当前状态仅依赖前一阶段的特定状态,通过建立状态间的量化关系(转移方程)实现递推。

多状态DP的三大关键步骤

  1. 状态划分:确保状态定义不重不漏,覆盖所有可能情况(如天气问题需包含所有天气类型);
  2. 转移方程:明确各状态间的依赖关系(如明天晴天概率=今天晴天→晴天、今天阴天→晴天、今天雨天→晴天的概率之和);
  3. 结果合并:根据问题目标整合最终状态值(如求第n天的总降水概率需合并阴天和雨天状态)。

2.3.1按摩师

按摩师预约问题是双状态动态规划的典型应用,核心在于通过“接受”或“拒绝”当前预约的二元选择,最大化总服务时长。该问题可通过定义两个状态变量构建动态规划模型:dpYes[i] 表示接受第 i 个预约时的最大时长,dpNo[i] 表示拒绝第 i 个预约时的最大时长。

双状态模型构建

  • 状态定义

    • dpYes[i] = 接受第 i 个预约的最大时长
    • dpNo[i] = 拒绝第 i 个预约的最大时长
  • 转移方程

    • 接受第 i 个预约时,前一个预约必须拒绝(避免连续服务),故 dpYes[i] = dpNo[i-1] + nums[i]
    • 拒绝第 i 个预约时,前一个预约可接受或拒绝,需取两者中的最优值,故 dpNo[i] = max(dpYes[i-1], dpNo[i-1])

关键逻辑:拒绝当前预约时取 max 的原因在于,前一个预约的“接受”或“拒绝”状态已分别存储了对应场景的最优解,取最大值可确保当前拒绝状态下的总时长最优。

  • 初始化:首个预约接受时时长为 nums[0](dpYes[0] = nums[0]),拒绝时为 0(dpNo[0] = 0)。
  • 结果:最终最大时长为最后一个预约的接受与拒绝状态的最大值,即 max(dpYes[n-1], dpNo[n-1])。

代码实现与优化

以下为 Java 实现代码,清晰体现双状态转移逻辑:

public int massage(int[] nums) { int n = nums.length; if(n == 0){ return 0; } int[] dpYes = new int[n]; int[] dpNo = new int[n]; dpYes[0] = nums[0]; dpNo[0] = 0; for (int i = 1; i < n; i++){ dpYes[i] = dpNo[i - 1] + nums[i]; // 接受当前需拒绝前一个dpNo[i] = Math.max(dpYes[i - 1], dpNo[i - 1]); // 拒绝当前取前一个最优} return Math.max(dpYes[n - 1], dpNo[n - 1]); 
}
  • 复杂度分析:时间复杂度 O(n)(遍历一次数组),空间复杂度 O(n)(两个状态数组)。可优化为 O(1),通过两个变量存储前一状态的 dpYes 和 dpNo,替代数组。

2.3.2打家劫舍II

环形街道的抢劫问题因房屋首尾相连形成闭环,导致首屋与尾屋互斥(不可同时抢劫),需通过问题拆分策略解决。核心思路为将环形问题转化为两个单链问题:抢首不抢尾(考虑房屋 0 至 n-2)与抢尾不抢首(考虑房屋 1 至 n-1),分别计算两种情况的最大收益后取最大值。

单链抢劫部分采用双状态动态规划模型(类似按摩师问题):定义 dpYes[i] 为抢劫第 i 间房屋的最大收益,dpNo[i] 为不抢劫第 i 间房屋的最大收益。状态转移方程为 dpYes[i] = dpNo[i-1] + nums[i]dpNo[i] = max(dpYes[i-1], dpNo[i-1])。通过滚动变量优化空间复杂度,用 a1/b1 存储“抢首不抢尾”情况的状态(a1 对应 i-2 收益,b1 对应 i-1 收益),a2/b2 存储“抢尾不抢首”情况的状态,将空间复杂度从 O(n) 降至 O(1)。

环形处理关键:因首尾房屋相邻,同时抢劫会触发警报,故必须拆分场景。两种情况覆盖所有可能的有效抢劫方案,取其最大值即为最优解。

public int rob1(int[] nums) {int n = nums.length;if (n == 0) return 0;if (n == 1) return nums[0];// 情况 1:考虑第一个房屋,但不考虑最后一个房屋int a1 = nums[0];int b1 = Math.max(nums[0], nums[1]);for (int i = 2; i < n - 1; i++) {int temp = b1;b1 = Math.max(b1, a1 + nums[i]);a1 = temp;}int ans1 = Math.max(a1, b1);// 情况 2:考虑第二个房屋,但不考虑最后一个房屋int a2 = nums[1];int b2 = Math.max(nums[1], nums[2]);for (int i = 3; i < n; i++) {int temp = b2;b2 = Math.max(b2, a2 + nums[i]);a2 = temp;}int ans2 = Math.max(a2, b2);// 返回两种情况的最大值return Math.max(ans1, ans2);}

时间复杂度为 O(n)(两次单链遍历,各 O(n)),空间复杂度 O(1)(仅用常数变量存储状态)。

对比维度单链打家劫舍环形打家劫舍
问题结构线性排列,首尾独立环形排列,首尾互斥
处理策略单次动态规划遍历拆分两种单链情况,取最大值
状态转移方程双状态(抢/不抢当前房屋)同单链,需分别应用于两个子数组
空间复杂度优化滚动变量 O(1)滚动变量 O(1)(两组状态变量)

2.3.3删除并获得点数

"删除并获得点数"问题(LeetCode 740)可通过动态规划转化为"打家劫舍"模型求解。核心思路是:选择一个数字 x 则不能选择 x-1 和 x+1,需最大化收集的总点数。

首先进行预处理:创建 arr 数组,其中 arr[x] = x × count(x)(count(x) 为 x 在 nums 中出现次数),将原问题转化为"选择 x 则不能选相邻数字,求最大和"。例如 nums = [3,4,2] 时,arr[2] = 2、arr[3] = 3、arr[4] = 4,问题转化为在 arr 中选择不相邻数字的最大和,此时选 2 和 4 得 6 为最优解。

定义双状态 DP 数组:dpYes[i] 表示选数字 i 的最大点数,dpNo[i] 表示不选 i 的最大点数。转移方程为:dpYes[i] = dpNo[i-1] + arr[i](选 i 则 i-1 必不选);dpNo[i] = max(dpYes[i-1], dpNo[i-1])(不选 i 则 i-1 可选或不选,取较大值)。

代码实现中,arr 数组大小设为 10001,因题目约束 nums 中数字 ≤ 10000。初始化 dpYes[0] = arr[0],从 i=1 遍历至 10000 计算状态值,最终结果为 max(dpYes[10000], dpNo[10000])。

核心代码片段

public int deleteAndEarn(int[] nums) { int n = 10001; // nums 中数字 ≤ 10000,故数组大小为 10001int[] arr = new int[n]; for(int x : nums) { arr[x] += x; } // 预处理:计算每个数字的总贡献int[] dpYes = new int[n]; int[] dpNo = new int[n]; dpYes[0] = arr[0]; for(int i = 1; i < n; i++) { dpYes[i] = dpNo[i - 1] + arr[i]; dpNo[i] = Math.max(dpYes[i - 1], dpNo[i - 1]); } return Math.max(dpYes[n - 1], dpNo[n - 1]); 
}

复杂度分析:时间复杂度 O(n + max(nums)),含预处理遍历 nums(O(n))和 DP 遍历 arr(O(max(nums)));空间复杂度 O(max(nums)),因 arr、dpYes、dpNo 数组大小均为 max(nums)+1。


2.3.4粉刷房子

粉刷房子问题是多状态动态规划的典型案例,其核心在于通过维护多个状态的最优解实现全局优化。以"给房子刷漆的最低成本"问题为例,假设有n个房子,每个房子可刷红、蓝、绿三种颜色之一,且相邻房子颜色不同,求最小总成本。

三状态模型定义

  • 状态定义dp[i][0/1/2]表示第i个房子刷红/蓝/绿的最小成本
  • 转移方程dp[i][0] = min(dp[i-1][1], dp[i-1][2]) + costs[i-1][0](当前刷红需取前一个房子蓝/绿的最小值)
  • 初始化dp[0][0/1/2] = 0(第0个房子为虚拟起点,未实际粉刷)
  • 结果min(dp[n][0], dp[n][1], dp[n][2])(取最后一个房子三种颜色的最小成本)

实现时需注意costs[i-1][0]的索引逻辑:由于dp表从1开始计数(对应第1个房子),而costs数组从0开始,故第i个房子的成本需访问costs[i-1]。以下为Java实现代码:

public int minCost(int[][] costs) { int n = costs.length; int[][] dp = new int[n + 1][3]; for(int i = 1; i <= n; i++){ dp[i][0] = Math.min(dp[i-1][1], dp[i-1][2]) + costs[i-1][0]; dp[i][1] = Math.min(dp[i-1][0], dp[i-1][2]) + costs[i-1][1]; dp[i][2] = Math.min(dp[i-1][0], dp[i-1][1]) + costs[i-1][2]; } return Math.min(dp[n][0], Math.min(dp[n][1], dp[n][2])); 
}

该算法时间复杂度O(n)(遍历n个房子),空间复杂度O(n)(dp表存储n+1个状态)。由于每个状态仅依赖前一个状态,可优化为O(1)空间:用三个变量存储dp[i-1][0/1/2],迭代更新即可。


2.3.5买卖股票的最佳时期含冷冻期

「买卖股票的最佳时机含冷冻期」(LeetCode 309)是典型的多状态动态规划问题,其核心在于通过状态机模型处理「卖出后次日无法交易」的约束条件。该问题需定义三种互斥状态,通过状态转移方程刻画交易过程,最终求解最大利润。

状态定义与转移方程

采用三状态动态规划模型,定义 dp[i][j] 表示第 i 天结束时处于状态 j 的最大利润,其中:

  • 状态 0(持有股票)dp[i][0],可由两种情况转移而来:前一天已持有(继续持有),或前一天处于可交易状态(当日买入),转移方程为 dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i])
  • 状态 1(可交易)dp[i][1],表示未持有股票且非冷冻期,可由前一天可交易状态(继续不操作)或前一天冷冻期结束(冷冻期转为可交易)转移,方程为 dp[i][1] = max(dp[i-1][1], dp[i-1][2])
  • 状态 2(冷冻期)dp[i][2],表示当日刚卖出股票,仅能由前一天持有状态(当日卖出)转移,方程为 dp[i][2] = dp[i-1][0] + prices[i]

初始化与结果计算

  • 初始化:首日(i=0)仅能买入或不操作。dp[0][0] = -prices[0](首日买入),dp[0][1] = 0(首日未买),dp[0][2] = 0(首日无法卖出)。
  • 结果:最后一天持有股票无意义(无法卖出获利),故取 max(dp[n-1][1], dp[n-1][2])

代码实现与优化

以下为 Java 实现代码,包含状态转移逻辑与边界处理:

public int maxProfit(int[] prices) { int n = prices.length; if (n <= 1) return 0; // 不足两天无法交易int[][] dp = new int[n][3]; // 初始化首日状态dp[0][0] = -prices[0]; // 持有(买入)// dp[0][1] 与 dp[0][2] 默认为 0(未操作/无法卖出)for (int i = 1; i < n; i++) { dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]); // 持有状态转移dp[i][1] = Math.max(dp[i-1][1], dp[i-1][2]); // 可交易状态转移dp[i][2] = dp[i-1][0] + prices[i]; // 冷冻期状态转移} return Math.max(dp[n-1][1], dp[n-1][2]); // 最后一天持有无意义
}

关键优化与复杂度分析

  • 空间优化:由于每个状态仅依赖前一天结果,可将二维数组优化为三个变量(prev0prev1prev2),空间复杂度从 O(n) 降至 O(1)。
  • 时间复杂度:O(n),仅需遍历一次价格数组。

状态转换关系

三个状态的转换路径如下:

  • 持有(0)→ 冷冻期(2):当日卖出股票,进入冷冻期。
  • 可交易(1)→ 持有(0):当日买入股票,转为持有状态。
  • 冷冻期(2)→ 可交易(1):冷冻期结束,次日恢复可交易状态。
  • 持有(0)→ 持有(0):继续持有股票,不操作。
  • 可交易(1)→ 可交易(1):继续不操作,保持可交易状态。

通过状态机模型,可清晰描述股票交易过程中的约束条件与利润累积逻辑,为类似带约束的动态规划问题提供通用分析框架。


3.小结

本章围绕动态规划入门阶段的核心模型展开系统梳理,可通过“动态规划入门三板斧”构建知识框架:斐波那契模型路径模型多状态模型,三者分别对应不同复杂度的状态设计与转移逻辑,共同构成动态规划问题的基础认知体系。

三类基础模型核心差异对比

模型类型状态维度转移方向特征目标函数典型形式典型问题示例
斐波那契模型一维单状态依赖前 n 项(线性递推)第 n 项状态值(如方案数、数值)泰波那契数列、爬楼梯
路径模型二维状态方向受限(如右/下移动),需处理边界/障碍物到达终点的路径数/最小代价不同路径、最小路径和
多状态模型多 DP 表共存状态间存在明确转换规则(如持有/未持有)最优状态组合下的目标值按摩师问题、买卖股票系列

动态规划通用解题四步骤(贯穿三类模型的核心方法论):

  1. 定义状态:明确 DP 表/变量的具体含义(如 dp[i] 代表第 i 步的状态值);
  2. 确定转移方程:建立当前状态与前置状态的数学关系(如 dp[i] = dp[i-1] + dp[i-2]);
  3. 初始化:设置边界条件(如斐波那契数列的 dp[0] = 0, dp[1] = 1);
  4. 填表计算:按顺序遍历状态空间,通过递推求解目标值。

上述框架不仅适用于本章模型,更可迁移至复杂问题的分析。通过对状态维度、转移逻辑的拆解,能有效降低动态规划问题的抽象性。后续章节将深入探讨子序列问题(如最长递增子序列)、回文串问题(如最长回文子串)及背包问题(0-1 背包、完全背包等),进一步拓展动态规划的应用边界。建议读者通过针对性练习(如调整模型参数、增设约束条件)巩固基础,逐步建立“状态建模 - 方程推导 - 边界处理”的系统化解题思维。

http://www.dtcms.com/a/479847.html

相关文章:

  • 自己做签名网站jsp网站建设毕业设计
  • Franka Research3 使用问题记录
  • 做电影网站需要用什么空间华为网络工程师认证培训
  • 响应式网站好吗建网站有什么要求
  • 如何利用VLLM方式本地部署DeepSeek大模型
  • 重庆网站建设最大建设银行泰安分行网站
  • 广州网站建设找新际流量卡网站
  • e4a做网站wordpress不能编辑
  • 【微实验】激光测径系列(五)软件上的思考与尝试
  • 记力扣2009:使数组连续的最少操作数 练习理解
  • 怎样健建设一个有利于优化的网站wordpress 萌化主题
  • 网站建设gong广东省建设监理协会网站 - 首页
  • 网页制作与网站建设填空题青岛vps网站
  • 微信公众号对接网站公司网站备案需要什么材料
  • 网站备案核验号网站建设中数据字典
  • 运动网站建设主题兰州学校网站建设
  • 从SEO到GEO:顺应AI搜索优化趋势
  • word超链接网站怎样做家具在线设计平台
  • 【java】mysql
  • 书店手机网站模板个人网站怎么备案可以做哪些
  • 网站大学报名官网入口世界500强企业排名表
  • 查企业信息的国家网站wordpress多城市seo
  • 反转链表及其应用(力扣2130)
  • 沈阳教做网站能挣钱的平台 正规的
  • 合肥企业网站建设公司提供网站建设商家
  • 西安建设学院网站首页wordpress网站设置关键词
  • Java学习之旅第三季-2:异常处理机制之抛出异常
  • 天津市南开区网站开发有限公司小微企业所得税优惠政策
  • 建站之星app公司网站做论坛
  • 做食品那些网站好磁力猫torrentkitty官网