三维DP深度解析
三维DP深度解析
- 一、三维DP基础认知
- 1.1 与低维DP的核心区别
- 1.2 三维DP的设计步骤
- 二、经典案例详解
- 2.1 案例1:三维背包问题(多约束背包)
- 问题描述
- 三维DP设计
- 代码实现
- 空间优化:三维→二维
- 2.2 案例2:买卖股票的最佳时机Ⅲ
- 问题描述
- 三维DP设计
- 代码实现
- 空间优化:三维→二维
- 2.3 案例3:立体网格中的最短路径
- 问题描述
- 三维DP设计
- 代码实现
- 三、三维DP的关键技巧与注意事项
- 3.1 状态维度的提取原则
- 3.2 空间优化的通用思路
- 3.3 常见误区
- 总结
在动态规划的进阶学习中,当问题的复杂度上升——需要同时考虑“多个独立维度的状态变化”(如“时间+选择+持有状态”“体积+重量+数量”)时,一维或二维DP已无法清晰刻画子问题关系。这时,三维DP成为解决问题的关键工具。它通过三个维度的状态定义,将复杂问题拆解为可递推的子问题,适用于处理多约束、多状态的场景。
一、三维DP基础认知
1.1 与低维DP的核心区别
- 状态维度:一维DP用
dp[i]
表示“单一维度的子问题”(如“前i个元素”);二维DP用dp[i][j]
表示“两个维度的关联”(如“前i个元素+容量j”);三维DP则用dp[i][j][k]
表示“三个维度的交叉子问题”,需同时考虑三个独立变量的约束(如“前i个物品+容量j+重量k”)。 - 适用场景:当问题需要满足三个独立约束条件,或子问题的解依赖“三个维度的状态变化”时,三维DP是自然选择。例如:
- 三维背包:需同时考虑物品的“体积、重量、数量”三个约束;
- 股票买卖:需同时考虑“天数、交易次数、持有状态”三个维度;
- 立体网格:需考虑“x坐标、y坐标、z坐标”的移动约束。
- 核心挑战:三维DP的状态定义更复杂(需明确三个维度的含义),且空间占用较高(需优化存储),但本质仍是“分解子问题→定义状态→推导递推关系”的逻辑。
1.2 三维DP的设计步骤
- 提取问题的三个核心维度:从问题中找到三个独立的约束或状态变量(如“物品数、体积、重量”)。
- 定义三维状态:明确
dp[i][j][k]
的具体含义(如“考虑前i个物品,体积不超过j,重量不超过k时的最大价值”)。 - 推导递推关系:分析“是否选择第i个物品”(或其他决策)对
dp[i][j][k]
的影响,建立与dp[i-1][j][k]
(不选)、dp[i-1][j-v][k-w]
(选)等前驱状态的关系。 - 初始化边界条件:处理“i=0”“j=0”“k=0”等最小子问题(如“0个物品时价值为0”)。
- 填充DP表并优化:按顺序计算所有状态,必要时通过“滚动维度”压缩空间(三维→二维)。
二、经典案例详解
2.1 案例1:三维背包问题(多约束背包)
问题描述
有n
个物品,每个物品有体积v[i]
、重量w[i]
、价值val[i]
。现有一个背包,最大容量为V
,最大承重为W
。求能装入背包的最大价值(每个物品只能选一次)。
三维DP设计
- 核心维度:物品数量(i)、背包容量(j)、背包承重(k)。
- 状态定义:
dp[i][j][k]
表示“考虑前i个物品,使用不超过j的体积和不超过k的重量时,能获得的最大价值”。 - 递推关系:
- 不选第i个物品:
dp[i][j][k] = dp[i-1][j][k]
; - 选第i个物品(需满足体积和重量约束):
dp[i][j][k] = dp[i-1][j - v[i-1]][k - w[i-1]] + val[i-1]
; - 取两者最大值:
dp[i][j][k] = max(不选, 选)
。
- 不选第i个物品:
- 边界条件:
i=0
(0个物品):dp[0][j][k] = 0
(价值为0);j=0
或k=0
(容量或承重为0):dp[i][0][k] = 0
,dp[i][j][0] = 0
(无法装物品)。
代码实现
public class ThreeDimensionalKnapsack {public int maxValue(int n, int V, int W, int[] v, int[] w, int[] val) {// 三维DP数组:[物品数][体积][重量]int[][][] dp = new int[n + 1][V + 1][W + 1];// 填充DP表:从1个物品开始遍历for (int i = 1; i <= n; i++) {// 当前物品的体积、重量、价值(i-1对应原数组下标)int currV = v[i - 1];int currW = w[i - 1];int currVal = val[i - 1];for (int j = 0; j <= V; j++) {for (int k = 0; k <= W; k++) {// 不选当前物品:继承前i-1个物品的结果dp[i][j][k] = dp[i - 1][j][k];// 选当前物品:需满足体积和重量约束if (j >= currV && k >= currW) {dp[i][j][k] = Math.max(dp[i][j][k],dp[i - 1][j - currV][k - currW] + currVal);}}}}return dp[n][V][W]; // 考虑所有物品,满约束时的最大价值}public static void main(String[] args) {ThreeDimensionalKnapsack solution = new ThreeDimensionalKnapsack();int n = 3; // 3个物品int V = 5; // 最大容量5int W = 6; // 最大承重6int[] v = {2, 3, 1}; // 体积int[] w = {3, 4, 2}; // 重量int[] val = {5, 6, 2}; // 价值System.out.println(solution.maxValue(n, V, W, v, w, val)); // 输出7(选物品0和2:5+2)}
}
空间优化:三维→二维
观察发现,dp[i][j][k]
仅依赖dp[i-1][j][k]
(上一层),可压缩为二维数组(滚动更新):
public int maxValueOptimized(int n, int V, int W, int[] v, int[] w, int[] val) {// 压缩为二维:[体积][重量](仅保留当前层)int[][] dp = new int[V + 1][W + 1];for (int i = 1; i <= n; i++) {int currV = v[i - 1];int currW = w[i - 1];int currVal = val[i - 1];// 逆序遍历(避免覆盖上一层未使用的状态)for (int j = V; j >= currV; j--) {for (int k = W; k >= currW; k--) {dp[j][k] = Math.max(dp[j][k], dp[j - currV][k - currW] + currVal);}}}return dp[V][W];
}
- 空间复杂度:从O(n×V×W)O(n \times V \times W)O(n×V×W)降至O(V×W)O(V \times W)O(V×W)(删除“物品数”维度)。
2.2 案例2:买卖股票的最佳时机Ⅲ
问题描述
给定一支股票的价格数组prices
,最多完成两笔交易(买入+卖出为一笔),求最大利润(不能同时持有多支股票)。例如:
- 输入:
prices = [3,3,5,0,0,3,1,4]
- 输出:
6
(第一笔0→3,第二笔1→4,利润3+3=6)
三维DP设计
- 核心维度:天数(i)、交易次数(j)、持有状态(k)。
i
:第i天(0~n-1);j
:已完成的交易次数(0~2,最多2笔);k
:持有状态(0:不持有,1:持有)。
- 状态定义:
dp[i][j][k]
表示“第i天,已完成j笔交易,当前持有状态为k时的最大利润”。 - 递推关系:
- 不持有状态(k=0):
- 前一天也不持有:
dp[i-1][j][0]
; - 前一天持有,今天卖出(交易次数+1,需满足j≥1):
dp[i-1][j-1][1] + prices[i]
; - 取最大值:
dp[i][j][0] = max(前一天不持有, 今天卖出)
。
- 前一天也不持有:
- 持有状态(k=1):
- 前一天也持有:
dp[i-1][j][1]
; - 前一天不持有,今天买入:
dp[i-1][j][0] - prices[i]
; - 取最大值:
dp[i][j][1] = max(前一天持有, 今天买入)
。
- 前一天也持有:
- 不持有状态(k=0):
- 边界条件:
- 初始天(i=0):
- 持有:
dp[0][0][1] = -prices[0]
(买入第一天股票); - 不持有:
dp[0][j][0] = 0
;
- 持有:
- 交易次数j=0时,无法卖出(卖出需至少1笔交易)。
- 初始天(i=0):
代码实现
public class BestTimeToBuyAndSellStockIII {public int maxProfit(int[] prices) {int n = prices.length;if (n == 0) return 0;// 三维DP数组:[天数][交易次数][持有状态]// 交易次数0~2,持有状态0(不持有)、1(持有)int[][][] dp = new int[n][3][2];// 初始化第0天dp[0][0][1] = -prices[0]; // 买入第0天股票(0笔交易,持有)// 第0天其他状态(不持有或交易次数>0)均为0或无效(初始化为0)// 填充DP表for (int i = 1; i < n; i++) {for (int j = 0; j <= 2; j++) {// 1. 不持有状态(k=0)// 情况1:前一天不持有int notHoldPrev = dp[i - 1][j][0];// 情况2:今天卖出(需j≥1)int sellToday = (j >= 1) ? (dp[i - 1][j - 1][1] + prices[i]) : 0;dp[i][j][0] = Math.max(notHoldPrev, sellToday);// 2. 持有状态(k=1)// 情况1:前一天持有int holdPrev = dp[i - 1][j][1];// 情况2:今天买入int buyToday = dp[i - 1][j][0] - prices[i];dp[i][j][1] = Math.max(holdPrev, buyToday);}}// 最大利润:最后一天,交易次数0~2,不持有状态的最大值return Math.max(dp[n - 1][2][0], Math.max(dp[n - 1][1][0], dp[n - 1][0][0]));}public static void main(String[] args) {BestTimeToBuyAndSellStockIII solution = new BestTimeToBuyAndSellStockIII();int[] prices = {3, 3, 5, 0, 0, 3, 1, 4};System.out.println(solution.maxProfit(prices)); // 输出6}
}
空间优化:三维→二维
dp[i][j][k]
仅依赖dp[i-1][j][k]
,可压缩为二维数组(保留“交易次数”和“持有状态”):
public int maxProfitOptimized(int[] prices) {int n = prices.length;if (n == 0) return 0;// 压缩为二维:[交易次数][持有状态]int[][] dp = new int[3][2];dp[0][1] = -prices[0]; // 初始化第0天for (int i = 1; i < n; i++) {// 临时数组保存上一天状态(避免覆盖)int[][] prev = new int[3][2];for (int j = 0; j <= 2; j++) {prev[j][0] = dp[j][0];prev[j][1] = dp[j][1];}for (int j = 0; j <= 2; j++) {// 不持有状态dp[j][0] = Math.max(prev[j][0], (j >= 1) ? (prev[j - 1][1] + prices[i]) : 0);// 持有状态dp[j][1] = Math.max(prev[j][1], prev[j][0] - prices[i]);}}return Math.max(dp[2][0], Math.max(dp[1][0], dp[0][0]));
}
2.3 案例3:立体网格中的最短路径
问题描述
在一个x×y×z
的立体网格中,从起点(0,0,0)
到终点(x-1,y-1,z-1)
,每次只能沿x、y、z轴方向移动1步(不能斜向移动),求最短路径的步数(固定为x+y+z-3
,此处扩展为“带权重的网格”,求最小权重和)。
三维DP设计
- 核心维度:x坐标(i)、y坐标(j)、z坐标(k)。
- 状态定义:
dp[i][j][k]
表示“从(0,0,0)
到(i,j,k)
的最小权重和”。 - 递推关系:
- 只能从三个方向到达
(i,j,k)
:- x方向前一步:
(i-1,j,k)
(需i≥1); - y方向前一步:
(i,j-1,k)
(需j≥1); - z方向前一步:
(i,j,k-1)
(需k≥1);
- x方向前一步:
- 因此:
dp[i][j][k] = grid[i][j][k] + min(来自x, 来自y, 来自z)
(加上当前网格的权重)。
- 只能从三个方向到达
- 边界条件:
- 起点
(0,0,0)
:dp[0][0][0] = grid[0][0][0]
; - 仅x轴移动(j=0,z=0):
dp[i][0][0] = dp[i-1][0][0] + grid[i][0][0]
; - 仅y轴或z轴移动类似。
- 起点
代码实现
public class 3DGridShortestPath {public int minPathSum3D(int[][][] grid) {int x = grid.length;int y = grid[0].length;int z = grid[0][0].length;// 三维DP数组:[x][y][z]int[][][] dp = new int[x][y][z];// 初始化起点dp[0][0][0] = grid[0][0][0];// 初始化x轴(y=0,z=0)for (int i = 1; i < x; i++) {dp[i][0][0] = dp[i - 1][0][0] + grid[i][0][0];}// 初始化y轴(x=0,z=0)for (int j = 1; j < y; j++) {dp[0][j][0] = dp[0][j - 1][0] + grid[0][j][0];}// 初始化z轴(x=0,y=0)for (int k = 1; k < z; k++) {dp[0][0][k] = dp[0][0][k - 1] + grid[0][0][k];}// 填充DP表for (int i = 0; i < x; i++) {for (int j = 0; j < y; j++) {for (int k = 0; k < z; k++) {if (i == 0 && j == 0 && k == 0) continue; // 跳过起点int minPrev = Integer.MAX_VALUE;// 检查x方向前一步if (i > 0) minPrev = Math.min(minPrev, dp[i - 1][j][k]);// 检查y方向前一步if (j > 0) minPrev = Math.min(minPrev, dp[i][j - 1][k]);// 检查z方向前一步if (k > 0) minPrev = Math.min(minPrev, dp[i][j][k - 1]);dp[i][j][k] = grid[i][j][k] + minPrev;}}}return dp[x - 1][y - 1][z - 1];}public static void main(String[] args) {3DGridShortestPath solution = new 3DGridShortestPath();int[][][] grid = {{{1, 2, 3},{4, 5, 6}},{{7, 8, 9},{10, 11, 12}}};System.out.println(solution.minPathSum3D(grid)); // 输出33(1→2→3→6→12,或其他最短路径)}
}
三、三维DP的关键技巧与注意事项
3.1 状态维度的提取原则
三维DP的核心是“找到三个独立的状态维度”,提取时需注意:
- 独立性:三个维度应相互独立(如“交易次数”与“持有状态”无关);
- 必要性:每个维度都是问题的核心约束(如三维背包必须同时考虑体积和重量);
- 可递推性:子问题的解能通过三个维度的变化推导(如“前i个物品”可通过“前i-1个物品”推导)。
3.2 空间优化的通用思路
三维DP的空间优化本质是“删除可滚动的维度”:
- 观察状态依赖:若
dp[i][j][k]
仅依赖i-1
层的状态(如三维背包、股票问题),可删除“i”维度,用二维数组滚动更新; - 逆序遍历:压缩维度后,需逆序遍历被删除维度的对应变量(如背包的体积、重量),避免覆盖未使用的前驱状态;
- 临时变量:若依赖同一层的多个状态(如立体网格),需用临时变量保存未更新的状态,避免覆盖。
3.3 常见误区
- 维度冗余:定义了不必要的维度(如可通过二维解决却用三维),增加复杂度;
- 状态定义模糊:未明确
dp[i][j][k]
的具体含义(如“交易次数”是“已完成”还是“已开始”),导致递推错误; - 忽略边界初始化:三维DP的边界包括“i=0”“j=0”“k=0”三种情况,遗漏任何一种都会导致结果错误。
总结
三维DP是处理多约束、多状态问题的强大工具,其核心在于通过三个维度清晰刻画子问题的依赖关系。从三维背包的“体积+重量+物品”,到股票问题的“天数+交易次数+持有状态”,三维DP始终遵循“分解子问题→定义状态→推导递推关系”的逻辑,只是状态维度更复杂。
掌握三维DP的关键是:
- 从问题中提取三个独立的核心维度(约束或状态);
- 明确
dp[i][j][k]
的具体含义(避免模糊); - 基于“决策选择”(如“选/不选物品”“买/卖股票”)推导递推关系;
- 必要时通过“滚动维度”优化空间(三维→二维)。
That’s all, thanks for reading~~
觉得有用就点个赞
、收进收藏
夹吧!关注
我,获取更多干货~