动态规划基础题型
牛客动态规划问题汇总
动态规划基础题型(背包、打家劫舍、股票、子序列)做题技巧归纳
针对你笔记中提到的dp数组及下标含义、递推公式、dp数组初始化、遍历顺序四个核心点,结合四类高频题型(背包、打家劫舍、股票、子序列),为你逐一拆解做题技巧:
一、明确「dp数组及下标的含义」—— 动态规划的“定位锚点”
dp数组的含义是状态的具象化,必须先明确“每个维度、每个下标代表什么现实意义”,否则递推逻辑会完全混乱。
| 题型 | 典型dp数组定义 |
|---|---|
| 01背包 | ● 二维数组:dp[i][j]表示前i个物品、背包容量为j时的最大价值;● 滚动数组: dp[j]表示容量为j的背包能装的最大价值。 |
| 完全背包 | ● 二维数组:dp[i][j]表示前i个物品、背包容量为j时的最大价值;● 滚动数组: dp[j]表示容量为j的背包能装的最大价值。 |
| 打家劫舍 | ● 基本版:dp[i]表示抢劫前i家能获得的最大金额;● 环形版:拆分为“抢前n-1家”和“抢后n-1家”两个子问题, dp[i]含义同上。 |
| 股票问题 | 以“最多交易k次”为框架,dp[i][j]中:- i表示第i天;- j表示交易状态(如0=未持有、1=持有等,不同k对应不同状态数);- 值为当前最大利润。 (例:买卖股票III中, j=0=不操作,j=1=第一次持有,j=2=第一次卖出,j=3=第二次持有,j=4=第二次卖出) |
| 子序列问题 | - 最长递增子序列(LIS):dp[i]表示以第i个元素结尾的最长递增子序列长度;- 最长公共子序列(LCS): dp[i][j]表示text1前i个字符和text2前j个字符的最长公共子序列长度;- 最长回文子序列: dp[i][j]表示字符串i到j区间内的最长回文子序列长度。 |
二、推导「递推公式」—— 动态规划的“逻辑链条”
递推公式是状态转移的核心,需围绕“当前状态由哪些前序状态推导而来”分析。
| 题型 | 核心递推公式 |
|---|---|
| 01背包 | - 二维:dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i])(选或不选第i个物品);- 滚动数组: dp[j] = max(dp[j], dp[j - weight[i]] + value[i])(j从大到小遍历)。 |
| 完全背包 | - 二维:dp[i][j] = max(dp[i-1][j], dp[i][j - weight[i]] + value[i])(物品可重复选,所以依赖当前i的子状态);- 滚动数组: dp[j] = max(dp[j], dp[j - weight[i]] + value[i])(j从小到大遍历)。 |
| 打家劫舍 | - 基本版:dp[i] = max(dp[i-1], dp[i-2] + nums[i])(不抢第i家,或抢第i家则需跳过i-1家);- 环形版:拆分为“抢前n-1家”和“抢后n-1家”,各自按基本版递推,最终取最大值。 |
| 股票问题 | - 买卖I(一次交易):dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])(未持有:保持或卖出);dp[i][1] = max(dp[i-1][1], -prices[i])(持有:保持或买入);- 买卖II(多次交易):仅 dp[i][1]改为max(dp[i-1][1], dp[i-1][0] - prices[i])(因为可重复交易,买入依赖前一天未持有状态);- 买卖III(两次交易):需逐个状态分析(如 dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i])表示“第二次持有,可保持或由第一次卖出后买入”)。 |
| 子序列问题 | - LIS:dp[i] = max(dp[j] + 1, dp[i])(遍历j < i且nums[j] < nums[i],找最长子序列后+1);- LCS: 若 text1[i-1] == text2[j-1],则dp[i][j] = dp[i-1][j-1] + 1;否则 dp[i][j] = max(dp[i-1][j], dp[i][j-1]);- 最长回文子序列: 若 s[i] == s[j],则dp[i][j] = dp[i+1][j-1] + 2;否则 dp[i][j] = max(dp[i+1][j], dp[i][j-1])。 |
三、掌握「dp数组初始化」—— 动态规划的“起点校准”
初始化要匹配dp数组的含义,确保“初始状态无歧义”,为递推提供正确的起点。
| 题型 | 初始化方式 |
|---|---|
| 背包问题 | - 01背包/完全背包: 二维: dp[0][j] = 0(前0个物品,价值为0),dp[i][0] = 0(容量为0,价值为0);滚动数组: dp[0...j] = 0(容量0到V初始价值为0)。 |
| 打家劫舍 | - 基本版:dp[0] = nums[0](只有一家时抢这家),dp[1] = max(nums[0], nums[1])(两家时选最大的);- 环形版:拆分后子问题各自按基本版初始化。 |
| 股票问题 | - 买卖I:dp[0][0] = 0(第0天未持有),dp[0][1] = -prices[0](第0天持有,即买入);- 买卖II/III:初始化逻辑与买卖I一致(多次交易仅改变递推式,初始状态不变)。 |
| 子序列问题 | - LIS:所有dp[i] = 1(每个元素自身是长度为1的子序列);- LCS: dp[0][j] = 0,dp[i][0] = 0(空字符串与任何字符串的LCS长度为0);- 最长回文子序列: dp[i][i] = 1(单个字符是回文,长度1),i > j时dp[i][j] = 0(无意义区间)。 |
四、理清「遍历顺序」—— 动态规划的“执行路径”
遍历顺序决定了“状态转移时,前序状态是否已被正确计算”,尤其在背包和子序列问题中极易出错。
| 题型 | 遍历顺序要求 |
|---|---|
| 背包问题 | 一、01背包(每个物品只能选一次)必须先遍历物品,再遍历容量,遍历顺序的本质是 “保证物品只被处理一次”。物品的遍历顺序不影响结果,容量的先后顺序需要分情况讨论。1、二维数组: 背包容量j的遍历顺序无强制要求,从小到大或从大到小均可。 2、滚动数组: 容量j必须从大到小遍历( j从V到weight[i]),防止物品重复选择;二、 完全背包 物品和容量的遍历顺序是否可调换,需结合问题场景(求最大价值、组合数、排列数)具体分析 1、求「最大价值」场景:顺序可换 (1)二维数组:先遍历物品,再遍历容量(j从小到大); (2)滚动数组:容量j从小到大遍历( j从weight[i]到V),因为物品可重复选;2、 组合问题(如零钱兑换II):先遍历物品,再遍历容量(保证组合不重复); 3、 排列问题(如组合总和IV):先遍历容量,再遍历物品(允许排列顺序不同)。 |
| 打家劫舍 | 从前往后遍历(i从2到n-1),因为dp[i]依赖dp[i-1]和dp[i-2]。 |
| 股票问题 | 从前往后遍历天数(i从1到n-1),因为每天的状态仅依赖前一天的状态。 |
| 子序列问题 | - LIS:从前往后遍历每个元素i,再遍历j从0到i-1(找前面的最长子序列); - LCS:先遍历text1的前i个字符,再遍历text2的前j个字符(i和j从1开始); - 最长回文子序列:从后往前遍历i(i从n-2到0),再遍历j从i+1到n-1(因为状态 dp[i][j]依赖dp[i+1][j-1],需要先计算后面的区间)。 |
