今天面试我只甩出了三道题:摘樱桃(741)、分割数组(410)和区间子数组(795) ,结果候选人直接在第一道题的“贪心陷阱”里全军覆没。
这三道题看似毫不相关,实则精准打击了多维状态压缩、Min-Max 划分思想与单调栈贡献法这三个高阶面试死穴,不懂原理只背代码的,基本这就送走了。
💥 第一关:思维升维打击 —— 摘樱桃 (LeetCode 741)
题目速览: 网格,从 走到 再走回来,路上摘樱桃(1),有刺(-1)不能走。求最大樱桃数。
🐌 [小白的直觉陷阱]
“这还不简单?贪心算法走两遍!第一遍走最大路径,把樱桃吃掉;第二遍再走剩下路径的最大值。”
面试官冷笑:你第一遍贪心吃掉的关键节点,可能直接切断了第二遍的生路,或者堵死了全局最优解。局部最优 全局最优。
⚡ [破局关键:时间折叠]
别想“一来一回”,要把时间折叠看作 “两个人同时从起点出发,去终点”。
只要这两个人走的路线不完全重合,效果等同于一来一回。
✅ [核心逻辑与满分代码]
- 状态定义:
dp[r1][c1][r2][c2]表示两个人分别在(r1,c1)和(r2,c2)时摘到的最大樱桃。 - 空间优化:因为两人步数相同(),其实是 的状态,但面试写 只要逻辑对,通常能过。
Java
// 你的代码复盘:标准的 DFS + 记忆化搜索
// 这种写法比纯迭代式 DP 更容易处理边界条件(刺、越界)
public int cherryPickup(int[][] grid) {
int N = grid.length;
// ☠️ 坑点1:N=50,N^4 约 625万,Integer对象开销大,但在 Java 堆内存允许范围内
// 如果面试官刁难,可以说能优化成 int[N][N][N] 甚至滚动数组
Integer[][][][] memo = new Integer[N][N][N][N];
return Math.max(0, dfs(grid, memo, N, 0, 0, 0, 0));
}
private int dfs(int[][] grid, Integer[][][][] memo, int N, int r1, int c1, int r2, int c2) {
// ☠️ 坑点2:越界或撞墙(-1)返回负无穷,表示此路不通
if (r1 >= N || c1 >= N || r2 >= N || c2 >= N || grid[r1][c1] == -1 || grid[r2][c2] == -1)
return Integer.MIN_VALUE;
// 到达终点
if (r1 == N - 1 && c1 == N - 1) return grid[r1][c1];
if (memo[r1][c1][r2][c2] != null) return memo[r1][c1][r2][c2];
// 四种走法:(下,下), (下,右), (右,下), (右,右)
int maxNext = Math.max(
Math.max(dfs(grid, memo, N, r1 + 1, c1, r2 + 1, c2), dfs(grid, memo, N, r1 + 1, c1, r2, c2 + 1)),
Math.max(dfs(grid, memo, N, r1, c1 + 1, r2 + 1, c2), dfs(grid, memo, N, r1, c1 + 1, r2, c2 + 1))
);
if (maxNext == Integer.MIN_VALUE) {
return memo[r1][c1][r2][c2] = Integer.MIN_VALUE;
}
// ☠️ 坑点3:重合去重。如果两人踩同一格,樱桃只能算一份!
int cherries = grid[r1][c1];
if (r1 != r2 || c1 != c2) {
cherries += grid[r2][c2];
}
return memo[r1][c1][r2][c2] = maxNext + cherries;
}
📊 复杂度:时间 (有效状态受步数限制),空间 。
🔥 第二关:划分型DP与前缀和 —— 分割数组的最大值 (LeetCode 410)
题目速览:将数组
nums分成k个连续子数组,使得这k个子数组各自和的最大值 最小。
🐌 [直觉的误区]
“是不是尽量平均分就行?”
数组元素大小不一,无法直接平均。这题是典型的 Min-Max(极小化极大) 问题。
⚡ [破局关键:划分型 DP]
这题其实有两个流派:
- 二分查找答案(S级解法) :猜一个最大值
mid,看能不能分成k份。复杂度 。 - 动态规划(A级解法,也就是你提供的代码) :更符合传统 DP 面试逻辑。
状态定义:dp[i][j] 表示把 前 i 个数 分成 j 份,所能得到的“子数组和的最大值”的最小值。
✅ [代码复盘]
你的代码使用了 DFS + Memo,本质是自顶向下的 DP。
Java
// 使用前缀和数组 pres 快速计算区间和
int dfs(int[] nums, int[] pres, int i, int j, Integer[][] memo) {
int n = nums.length;
// Base Case: 只剩一份了,剩下的全归这一份
if (j == 1) return pres[n] - pres[i];
if (memo[i][j] != null) return memo[i][j];
int res = Integer.MAX_VALUE;
// 枚举分割点 next。从 i+1 到 n-(j-1) 都是合法的分割位置
// ☠️ 优化点:这里 next 的循环其实可以剪枝,不需要跑到 n
for (int next = i + 1; next <= n - (j - 1); next++) {
// 核心转移方程:Min( Max(子问题结果, 当前这一段的和) )
int currentVal = Math.max(
dfs(nums, pres, next, j - 1, memo),
pres[next] - pres[i]
);
res = Math.min(res, currentVal);
}
return memo[i][j] = res;
}
☠️ 暴毙边缘:
- 边界:
next的枚举范围。如果剩下的元素不够分j-1份,就不用循环了。 - 溢出:虽然题目
int够用,但涉及数组和,通常建议面试时嘴一句“如果数据大要用 long”。
📊 复杂度:状态 ,转移 ,总时间 。比二分法慢,但逻辑更通用。
🏔️ 第三关:贡献法与单调栈 —— 区间子数组个数 (LeetCode 795)
题目速览:求有多少个连续子数组,其 最大值 在
[L, R]之间。
🐌 [暴力解法]
枚举所有子数组,再遍历找最大值?面试官直接让你出门右转。
⚡ [破局关键:谁是老大?]
这是一个经典的 “贡献法” 思路:
与其找子数组,不如问:数组中的每个数字 X,在哪些子数组里能充当“最大值”?
这需要找到 X 左边第一个比它大的位置 L_limit,和右边第一个比它大的位置 R_limit。
在 这个开区间内,X 就是王。
题目要求最大值在 [left, right] 之间。我们可以拆解为:
或者直接用单调栈统计落在 [left, right] 范围内的数字的贡献。
✅ [代码复盘:单调栈的教科书写法]
你的代码用了两次单调栈求 nxMx (Next Max) 和 preNxMx (Previous Max),非常扎实。
Java
// 核心逻辑:求每个元素作为最大值的辐射范围 (L, R)
// 贡献数量 = (i - L) * (R - i)
// 单调栈部分
Stack<Integer> sk = new Stack<>();
for (int i = 0; i < n; i++) {
// 找右边第一个比我大的
// ☠️ 坑点:处理重复元素。
// 如果数组是 [5, 5],谁是最大值?
// 约定:左边找“严格大于”,右边找“大于等于”(或者反过来)。
// 你的代码:nums[sk.peek()] < nums[i] (严格小于则弹出) -> 意味着右边找的是“大于等于”
while (!sk.isEmpty() && nums[sk.peek()] < nums[i]) {
nxMx[sk.pop()] = i;
}
sk.push(i);
}
// ... 同理反向遍历求 preNxMx ...
// 统计部分
for (int i = 0; i < n; i++) {
// 只有当该元素本身在 [left, right] 范围内,它作为最大值的子数组才符合题意
if (left <= nums[i] && right >= nums[i]) {
// 左边辐射长度 * 右边辐射长度
cnt += (i - preNxMx[i]) * (nxMx[i] - i);
}
}
☠️ 暴毙边缘:
- 重复元素处理:这是单调栈最容易挂的地方。必须一侧用
<,一侧用<=,否则会多算或漏算。你的代码中第一遍是<,第二遍是<=,逻辑是闭环的(虽第二遍循环条件需仔细核对,但思路正确)。 - 哨兵节点:数组两端要假想有无穷大的数,否则栈可能不空,导致
nxMx没被正确赋值。你的Arrays.fill(nxMx, n)和Arrays.fill(preNxMx, -1)完美处理了这点。
📊 复杂度:时间 ,空间 。这是处理子数组极值问题的最优解。
🎓 面试官总结
这三道题打通了,你的内功至少提升了一个台阶:
- 741 告诉你:看到“回路”或“两条线”,请大胆升维。
- 410 告诉你:看到“分割数组求最值”,要么DP,要么二分答案。
- 795 告诉你:看到“子数组最大值”,别傻傻遍历,用单调栈算每个元素的统治域。
刷题不是为了背代码,是为了在面试时,当面试官把题目抛出来的那一刻,你能迅速检索到对应的思维模型。