初始化:使用两个指针slow和fast,都指向链表的头节点。 寻找环:让slow每次移动一步,fas...

Created on: January 28, 2026

Answered using GPT-5.2 by Chat01

Question

初始化:使用两个指针slow和fast,都指向链表的头节点。
寻找环:让slow每次移动一步,fast每次移动两步。如果链表中存在环,那么这两个指针最终会在环内相遇。
判断无环:如果fast或fast.next为null,说明链表没有环,返回null。
确定环的起始节点:相遇后,将slow重新指向链表头,然后slow和fast每次都移动一步,再次相遇的节点就是环的起始节点。
返回结果:返回相遇节点即为环的起始节点。 第四点为什么

Answer

好问题,这一步正是 Floyd 判圈算法里最“神奇”的地方 👍
我用直观推导 + 图形化理解给你解释清楚「第四点为什么成立」。


先定义几个量(很关键)

假设:

  • 头节点到环入口 的距离是:a
  • 环入口到第一次相遇点 的距离是:b
  • 环的长度是:c

示意一下(→ 表示走的方向):

head --(a)--> 环入口 --(b)--> 相遇点 --(c-b)--> 环入口

相遇时,slow 和 fast 走了多少步?

slow(一次走 1 步)

走过的距离 = a + b

fast(一次走 2 步)

走过的距离 = 2(a + b)

关键数学关系来了

fast 比 slow 多走了一些整圈,因为它在环里绕圈了:

2(a + b) = a + b + n * c

👉 n 是 fast 在环里多跑的圈数(n ≥ 1)

化简一下:

a + b = n * c

再整理:

a = n * c - b

这行公式意味着什么?(重点)

a = (n-1)*c + (c - b)

解释成一句人话:

从“相遇点”继续往前走 c - b 步,再绕 (n-1) 圈,就正好回到环入口

c - b 是什么?

👉 相遇点到环入口的距离


回到算法的第四步

为什么要这么做?

相遇后:

  • slow 放回头节点
  • fast 留在相遇点
  • 两个指针都每次走 1 步

看它们各自走的路:

slow

  • 从头节点出发
  • a
  • 刚好到环入口

fast

  • 从相遇点出发
  • a
  • 根据上面的推导,也刚好到环入口

🎯 所以它们一定会在环入口再次相遇


用一句话总结第四点

因为在第一次相遇时,slow 到环入口的距离
刚好等于 fast 从相遇点沿环走到入口的距离,
所以两指针同步前进时,必然在环入口相遇。


口诀版(方便记忆)

第一次相遇在环内
第二次相遇在入口


如果你愿意,我也可以:

  • 画 ASCII 图一步步走给你看
  • 或用 Java / C++ / Python 代码配注释解释
  • 或用“跑操场”的生活类比再讲一遍 😄

Question

本地缓存和redis不都是存在内存里吗,他们有啥区别

Answer

对,这俩都在内存里,但差别非常大
一句话先给你个直觉:

本地缓存 = 你兜里的小本子
Redis = 全公司共用的公告板

下面拆开讲,保证不绕。


一、最核心的区别(先看这个)

对比点本地缓存Redis
存在哪里应用进程内存独立服务器内存
是否共享❌ 不共享✅ 多机器共享
进程重启❌ 数据全没⚠️ 可持久化
访问速度🚀 极快⚡ 快,但有网络
分布式支持❌ 不支持✅ 天生支持
数据一致性❌ 很难保证✅ 统一数据源
内存容量大(可集群)

二、本地缓存是啥?(什么时候用)

本地缓存例子

java
Map<Long, User> cache = new ConcurrentHashMap<>();

或者:

  • Guava Cache
  • Caffeine
  • Ehcache(本地模式)

特点一句话

“我自己用,谁也别动”

优点

  • 🚀 最快(没有网络)
  • 👍 简单
  • 👍 不依赖外部组件

致命缺点

多实例数据不一致

举个真实场景:

text
你有 3 台服务器 A、B、C
  • 用户请求 A,缓存里有数据 ✅
  • 用户请求 B,缓存没数据 ❌
  • 数据更新了,只更新了 A ❌❌❌

👉 分布式系统里非常危险


三、Redis 是啥?(为什么大家都用)

Redis 本质

  • 一个独立进程
  • 通过 TCP 网络访问
  • 所有服务共享同一份数据
text
服务 A ─┐ 服务 B ─┼──> Redis 服务 C ─┘

Redis 的强项

1️⃣ 分布式共享

  • 所有机器看到的是同一份缓存

2️⃣ 功能丰富

不仅是 Map:

  • String
  • Hash
  • List
  • Set
  • ZSet
  • Bitmap
  • Stream

👉 能干的事非常多:

  • 缓存
  • 分布式锁
  • 限流
  • 计数器
  • 延迟队列

3️⃣ 可持久化

  • RDB
  • AOF

服务器重启,数据不一定丢


四、为什么还要本地缓存?Redis 不够快吗?

Redis 已经很快了(微秒级),但:

text
本地缓存 ≈ 50ns Redis ≈ 0.2~1ms(网络)

👉 差一个数量级

所以真实项目里经常这样玩 👇


五、真实项目常用架构(重点)

👉 二级缓存(本地 + Redis)

text
请求 本地缓存(L1) ↓ miss Redis(L2) ↓ miss 数据库

优点

  • 🔥 热点数据走本地
  • 🔒 数据一致性靠 Redis
  • 🧠 DB 压力最小

六、典型使用场景对比

用本地缓存 👍

  • 配置数据(不常变)
  • 字典表
  • 本机私有数据
  • 超高频读取 + 允许短暂不一致

用 Redis 👍

  • 用户信息
  • 登录态
  • 秒杀库存
  • 分布式锁
  • 多机器共享数据

七、一个“灵魂总结”

不是“是不是内存”,而是“谁能看到这块内存”

  • 本地缓存:
    👉 只有当前 JVM / 进程看到
  • Redis:
    👉 整个系统都看到

如果你愿意,我可以再给你讲:

  • 为什么本地缓存会引发缓存雪崩
  • Redis 和本地缓存的一致性方案
  • 为什么不用 Redis 直接干掉本地缓存
  • 面试常问:“为什么不用本地缓存替代 Redis?”

你更想听哪一个?

Question

class Solution {
static int count;

text
public int numOfSubarrays(int[] arr, int k, int threshold) { dfs(arr, k, threshold, 0, 1, 0); return count; } public void dfs(int[] arr, int k, int threshold, int sum, int num, int start) { if (num == k) { if (sum >= threshold * k) { count += 1; return; } } for (int i = start; i < arr.length; i++) { sum += arr[i]; num +=1; dfs(arr, k, threshold, sum, num, start + 1); sum -= arr[i]; num -=1; } }

} 这个回溯算法有什么问题

Answer

这段回溯/dfs有好几个致命问题,导致:结果不对、效率极差、甚至多测例会串结果。

1)countstatic 且不重置:多次调用会累加串数据

countstatic,你在 numOfSubarrays 里没把它置 0。
所以同一个 JVM 里多次调用 numOfSubarrays(比如 LeetCode 多个用例、或你本地多次跑)会把上次的结果继续加上去。

✅ 修:在方法开头 count = 0;


2)start 传错了:你一直传 start + 1,而不是 i + 1

这里:

java
dfs(arr, k, threshold, sum, num, start + 1);

应该是沿着你当前选择的下标往后走,所以要用 i + 1

否则会出现:

  • 你选了 i,但下一层却从 start+1 开始,跟 i 没关系
  • 会导致重复组合/漏组合,递归树结构不正确

✅ 应该改成:

java
dfs(arr, k, threshold, sum, num, i + 1);

3)逻辑目标错了:题目要的是“长度为 k 的连续子数组”,你在枚举“组合”

numOfSubarrays(LeetCode 1343)要求的是:

统计 所有长度为 k 的连续子数组,平均值 ≥ threshold 的个数

但你的 DFS 在做的是:从数组里选 k 个数(还不是严格正确的选法),属于组合问题,不是连续子数组。

比如 arr=[1,2,3,4], k=2
连续子数组只有:[1,2],[2,3],[3,4]
而组合会算:[1,3],[1,4],[2,4] 这些都不该出现。

所以即使你把第2点修了,题意也不匹配


4)递归边界不完整:num == k 后不“停止继续搜索”

你这里:

java
if (num == k) { if (sum >= threshold * k) { count += 1; return; } }

如果 num == ksum 不够,你没有 return,会继续进入 for 往下选,变成选 k+1, k+2... 个元素,完全跑偏。

✅ 正确写法至少应是:

java
if (num == k) { if (sum >= threshold * k) count++; return; }

5)初始化也有 off-by-one:dfs(..., 0, 1, 0) 从一开始就“选了1个数”

num 表示已选元素数量的话,你一开始就传 1,但 sum 却是 0,语义对不上。 通常应从 num=0 开始。


6)复杂度爆炸:本题应该 O(n),你这会接近 O(2^n)

连续子数组长度固定为 k,用滑动窗口一次遍历就行;回溯会超时。


这题正确思路:滑动窗口(O(n))

(给你一份直接可用的 Java)

java
class Solution { public int numOfSubarrays(int[] arr, int k, int threshold) { int target = threshold * k; int sum = 0, count = 0; for (int i = 0; i < arr.length; i++) { sum += arr[i]; if (i >= k) sum -= arr[i - k]; if (i >= k - 1 && sum >= target) count++; } return count; } }

如果你想“就回溯代码本身”改到不崩(虽然不适合本题),我也可以按组合枚举的正确写法帮你修一版;但这题面试/刷题标准解就是滑窗。

Question

假如就是组合枚举,把这个回溯改成正确的

Answer

行,那就把它当成“从数组里选 k 个元素的组合,统计满足 sum >= threshold*k 的组合数”。

你这版回溯主要需要改 4 件事:

  1. count 不能用 static(或至少每次调用重置)
  2. num==k 必须直接 return(不然会继续选成 k+1…)
  3. 递归下一层的 start 必须用 i+1(不是 start+1
  4. 初始 num 应该从 0 开始

下面给你一份正确的组合枚举回溯(还加了一个剪枝:剩余元素不够就不走了):

java
class Solution { private int count; public int numOfSubarrays(int[] arr, int k, int threshold) { count = 0; // 每次调用重置 int target = threshold * k; dfs(arr, k, target, 0, 0, 0); return count; } // start: 下一次可选的起始下标 // chosen: 已选元素个数 // sum: 已选元素和 private void dfs(int[] arr, int k, int target, int start, int chosen, int sum) { // 选够 k 个,统计并返回 if (chosen == k) { if (sum >= target) count++; return; } // 剪枝:剩余元素不够选满 k 个,直接返回 if (arr.length - start < k - chosen) return; for (int i = start; i < arr.length; i++) { dfs(arr, k, target, i + 1, chosen + 1, sum + arr[i]); // 注意 i+1 } } }

你原代码的“关键错误点”对应修复

  • dfs(... start + 1)必须是 dfs(... i + 1)
  • num==k 且不满足条件时没 return ➜ 无论满足与否都 return
  • 初始传 num=1改为 0
  • static count改成成员变量,并在入口重置

如果你还想要“组合枚举但要求必须连续”的版本,那就不是组合了,会退化回滑动窗口/固定长度子数组那套。

Question

为什么不需要,“选择,执行dfs,然后撤销”

Answer

这个问题问得非常到位,已经到「真正理解回溯」这一步了 👍
结论先给你:

因为你现在的写法是“参数传值 + 不共享状态”,所以不需要显式撤销。

下面我一步一步拆给你看。


一、你熟悉的“标准回溯模板”为啥要撤销?

你脑子里的应该是这种 👇

java
path.add(arr[i]); // 选择 dfs(...); path.removeLast(); // 撤销

这种必须撤销,是因为:

👉 path 是一个 共享的可变对象(List / 数组)

如果不撤销:

  • 下一轮递归会在同一个 path 上继续叠加
  • 状态就“脏”了

二、而你现在用的是另一种回溯写法(关键区别)

你现在的 dfs 是这样的:

java
dfs(arr, k, target, i + 1, chosen + 1, sum + arr[i]);

注意这三点 👀:

1️⃣ sumint

2️⃣ chosenint

3️⃣ 没有 List / 数组 / 全局状态参与递归

int 是基本类型,Java 是值传递

也就是说:

java
sum + arr[i]

会产生一个新的值,只属于这一层递归。


三、用“栈帧”视角看为什么不用撤销(重点)

假设数组 [1,2,3],k=2

递归过程像这样:

text
dfs(start=0, chosen=0, sum=0) ├─ dfs(start=1, chosen=1, sum=1) │ ├─ dfs(start=2, chosen=2, sum=3) │ └─ dfs(start=3, chosen=2, sum=4) └─ dfs(start=2, chosen=1, sum=2) └─ dfs(start=3, chosen=2, sum=5)

注意:

  • 每一次 dfs(...)
    👉 都是全新的一组参数
  • 父调用的 sumchosen 不会被子调用修改

当子函数返回时:

java
dfs(...); // 结束

父函数里的 sumchosen 自动恢复
👉 因为它们从来没被改过


四、那你原代码为啥“看起来”需要撤销?

你原来写的是:

java
sum += arr[i]; num += 1; dfs(...); sum -= arr[i]; num -= 1;

这是另一种写法,对应的是:

👉 在当前栈帧里修改变量

所以必须手动撤销。

而现在我们把它改成了:

java
dfs(..., chosen + 1, sum + arr[i]);

👉 不在当前层改变量 👉 而是把新状态“传下去”

这就是为什么不需要撤销


五、一句话区分:什么时候要撤销,什么时候不用?

✅ 需要“撤销”的情况

  • 使用 List / 数组 / StringBuilder
  • 使用全局或共享变量
  • 在当前栈帧里修改状态
java
path.add(x); dfs(); path.remove(path.size() - 1);

❌ 不需要“撤销”的情况

  • 只用基本类型
  • 状态通过参数传递
  • 不修改父层变量
java
dfs(sum + x, chosen + 1);

六、回溯的两种“正统写法”(都对)

写法一:修改 + 撤销(你最熟的)

java
sum += arr[i]; dfs(); sum -= arr[i];

写法二:参数传递(更安全、更干净)

java
dfs(sum + arr[i]);

👉 本质一样 👉 第二种更不容易出 bug


七、总结一句你可以直接背的

只要递归状态不共享、不在当前层修改,就不需要“撤销”;
撤销是为了解决“共享可变状态”的副作用。

如果你愿意,我可以下一步帮你总结一张
👉「看到代码就知道要不要撤销」的判断清单
👉 或对比 Java / C++ / Python 的差异

你现在这个问题,已经是中高级水平了 👌

Question

class Solution {
public int maxScore(int[] cardPoints, int k) {
int max = Integer.MIN_VALUE;
if (cardPoints.length < k) {
return 0;
}

text
int leftSum = 0;//左边和 int rightSum = 0;//右边和 for (int i = 0; i < k; i++) { for (int j = 0; j <= i; j++) { leftSum += cardPoints[j]; } for (int j = cardPoints.length - 1; j > cardPoints.length - k + i; j--) { rightSum += cardPoints[j]; } max = Math.max(leftSum + rightSum, max); leftSum = 0; rightSum = 0; } return max; }

} 哪里有问题

Answer

主要问题是:你枚举“左边取 i 张、右边取 k-i 张”的边界写错了,再加上实现是 O(k²),k 大时会超时。


1)最关键的 bug:右边循环少取了 1 张(off-by-one)

你右边这段:

java
for (int j = cardPoints.length - 1; j > cardPoints.length - k + i; j--) { rightSum += cardPoints[j]; }

你想在第 i 次时取 右边 k-i 张

但你用了 j > (n - k + i),这会导致:

  • 当应该取 1 张时(比如 i = k-1),循环条件变成 j > n-1一次都进不去,导致右边取 0 张。

举个最小反例:

  • cardPoints = [1,2,3], k=1
  • 正确答案:3(取右边一张)
  • 你的代码:
    • i=0:左边取 1 张 =1
    • 右边循环:j=2, 条件 j > 3-1+0 =22>2 false ⇒ 右边和=0
    • max=1 ❌

✅ 修正方式:把 > 改成 >=(这样边界才包含那一张)

java
for (int j = cardPoints.length - 1; j >= cardPoints.length - k + i; j--) { rightSum += cardPoints[j]; }

2)枚举范围少了一种情况(没覆盖“左取0张/右取k张”)

你外层是 for (int i = 0; i < k; i++),这里 i 表示“左边取 i+1 张”(因为你左边内层 j<=i)。

因此你永远不会考虑:

  • 左边取 0 张,右边取 k 张

即使你把右边边界修好了,也仍然少一种组合。

✅ 正确枚举应该是:左边取 0..k 张(共 k+1 种)


3)复杂度问题:O(k²) 很容易超时

你每次 i 都重新把左边、右边用内层循环算一遍:

  • 左边求和:1+2+…+k = O(k²)
  • 右边求和:同样 O(k²)

LeetCode 这题(1423)k 可能很大,O(k²) 通常会 TLE。


推荐正确写法:O(k)(滑动/递推)

先算“右边取 k 张”的和,然后每次从右边少取一张、左边多取一张:

java
class Solution { public int maxScore(int[] cardPoints, int k) { int n = cardPoints.length; if (k > n) return 0; int rightSum = 0; for (int i = n - k; i < n; i++) rightSum += cardPoints[i]; int max = rightSum; int leftSum = 0; for (int i = 0; i < k; i++) { leftSum += cardPoints[i]; // 左边多拿一张 rightSum -= cardPoints[n - k + i]; // 右边少拿一张 max = Math.max(max, leftSum + rightSum); } return max; } }

你这段代码“哪里有问题”一句话总结

  • 右边循环边界用 > 少算了一张
  • 外层枚举少了“左取0张”的情况
  • 整体 O(k²) 会超时

如果你想,我也可以在你原结构上“最小改动”修成正确版(但仍然慢),或者帮你把边界推导画成图,保证你以后不再踩这个 off-by-one。

Question

class Solution {
public int maxScore(int[] cardPoints, int k) {
int max = Integer.MIN_VALUE;
if (cardPoints.length < k) {
return 0;
}

text
int leftSum = 0;//左边和 int rightSum = 0;//右边和 for (int i = 0; i < k; i++) { for (int j = 0; j < i; j++) { leftSum += cardPoints[j]; } for (int j = cardPoints.length-1; j>=cardPoints.length-k+i; j--) { rightSum += cardPoints[j]; } max = Math.max(Math.max(leftSum, rightSum), max); leftSum = 0; rightSum = 0; } return max; }

}

Answer

这版依然有几个明显问题(其中有的是逻辑错,不是边界小错)。

1)你算的不是题目要的东西:少算了“总共取 k 张”的得分

题目是“从两端一共取 k 张”,所以每一种选择都应该比较:

  • leftSum + rightSum

但你写成了:

java
max = Math.max(Math.max(leftSum, rightSum), max);

这只是在比较“只取左边”或“只取右边”的最大值,完全不对。

✅ 应该是:

java
max = Math.max(max, leftSum + rightSum);

2)左边取的数量写错了(off-by-one)

你左边循环:

java
for (int j = 0; j < i; j++) leftSum += cardPoints[j];

这表示左边取 i 张(当 i=0 时取0张)。

而右边循环表示右边取 k - i 张(这是对的)。

所以枚举 i=0..k 才能覆盖所有情况,但你外层是:

java
for (int i = 0; i < k; i++)

这只覆盖 i=0..k-1,缺少 i=k(左边取 k 张,右边取 0 张) 这种情况。

✅ 外层应为:

java
for (int i = 0; i <= k; i++)

3)即使修完逻辑,也会超时:O(k²)

你每次 i 都从头把 leftSum / rightSum 重算一遍,复杂度是 O(k²)。k 大会 TLE。


给你“最小改动版”修正确(但仍然 O(k²))

如果你就想按你这个结构改到正确:

java
class Solution { public int maxScore(int[] cardPoints, int k) { int n = cardPoints.length; if (n < k) return 0; int max = Integer.MIN_VALUE; for (int i = 0; i <= k; i++) { // i: 左边取 i 张 int leftSum = 0, rightSum = 0; for (int j = 0; j < i; j++) leftSum += cardPoints[j]; for (int j = n - 1; j >= n - (k - i); j--) rightSum += cardPoints[j]; max = Math.max(max, leftSum + rightSum); // 必须相加 } return max; } }

正确高效版(O(k))

如果你想 AC + 性能好(推荐用这个)我再贴一遍 O(k) 的写法:

java
class Solution { public int maxScore(int[] cardPoints, int k) { int n = cardPoints.length; int rightSum = 0; for (int i = n - k; i < n; i++) rightSum += cardPoints[i]; int max = rightSum, leftSum = 0; for (int i = 0; i < k; i++) { leftSum += cardPoints[i]; rightSum -= cardPoints[n - k + i]; max = Math.max(max, leftSum + rightSum); } return max; } }

如果你把你理解的 “i 的含义”(左取 i 张还是左取 i+1 张)说一下,我也可以按你想的枚举方式帮你把边界公式写成一眼不出错的版本。

Question

class Solution {
public int maxScore(int[] cardPoints, int k) {
int max = Integer.MIN_VALUE;
if (cardPoints.length < k) {
return 0;
}

text
int leftSum = 0;//左边和 int rightSum = 0;//右边和 for (int i = 0; i < k; i++) { for (int j = 0; j <= i; j++) { leftSum += cardPoints[j]; } for (int j = cardPoints.length - 1; j >= cardPoints.length - k + i; j--) { rightSum += cardPoints[j]; } max = Math.max(leftSum + rightSum, max); leftSum = 0; rightSum = 0; } return max; }

}

Answer

这版主要有 两个逻辑问题(边界/枚举含义错),导致会把“取的牌数”算错、并且漏掉一种情况。


1)你把“左取 + 右取”算成了 k + 1 张(从 i=0 开始就错了)

你左边:

java
for (int j = 0; j <= i; j++) leftSum += cardPoints[j];

这表示左边取的是 i+1 张

你右边:

java
for (int j = n - 1; j >= n - k + i; j--) rightSum += cardPoints[j];

这个实际上是在取右边 k - i 张(因为从 n-1 取到 n-k+i 一共 k-i 个)。

所以你每次总共取到的是:

(i+1) + (k-i) = k+1 张 ❌

题目要求总共只能取 k 张

举个最小例子:k=1

  • i=0:左边取 1 张,右边也取 1 张 ⇒ 取了 2 张,明显不对。

2)外层 i < k 会漏掉“全从右边取 k 张”的情况(按你当前左边 i+1 的定义)

你 i 从 0 到 k-1,对应左边取 1..k 张。 但 左边取 0 张、右边取 k 张 这种情况没覆盖到。


怎么改(给你两种等价修法)

✅ 修法 A:保持“左边取 i+1 张”,那右边必须取 k-(i+1) 张,并且 i 只能到 k-1,但要额外覆盖左取0的情况(比较绕,不推荐)

✅ 修法 B(推荐):把左边改成“取 i 张”,i 枚举 0..k(最清晰)

java
class Solution { public int maxScore(int[] cardPoints, int k) { int n = cardPoints.length; int max = Integer.MIN_VALUE; for (int i = 0; i <= k; i++) { // i: 左边取 i 张,右边取 k-i 张 int leftSum = 0, rightSum = 0; for (int j = 0; j < i; j++) leftSum += cardPoints[j]; for (int j = n - 1; j >= n - (k - i); j--) rightSum += cardPoints[j]; max = Math.max(max, leftSum + rightSum); } return max; } }

这版是“组合枚举两端拿牌”的正确逻辑,但复杂度是 O(k²)。想要高效再用滑动 O(k)(你之前那题标准解)。


如果你想继续沿用你当前写法(j<=i 这种),我也能帮你把右边边界推到完全正确,但写起来更容易再踩 off-by-one。

Question

class Solution {
public int maxScore(int[] cardPoints, int k) {
int n = cardPoints.length;
int rightSum = 0;
for (int i = n - k; i < n; i++) rightSum += cardPoints[i];

text
int max = rightSum, leftSum = 0; for (int i = 0; i < k; i++) { leftSum += cardPoints[i]; rightSum -= cardPoints[n - k + i]; max = Math.max(max, leftSum + rightSum); } return max; }

} 这个的算法思路

Answer

这段代码的思路其实很“巧”:把“两端取 k 张”转换成“左边取 i 张 + 右边取 k-i 张”,然后用一个滑动方式把所有 i 从 0 到 k 都快速算一遍,复杂度 O(k)


关键观察

从两端取 k 张,等价于:

  • 左边取 i
  • 右边取 k - i
    其中 i 可以是 0..k

所以我们只要把这 k+1 种情况都比较一遍取最大即可。


代码在做什么

1)先算“全从右边取 k 张”的和(i = 0)

java
int rightSum = 0; for (int i = n - k; i < n; i++) rightSum += cardPoints[i]; int max = rightSum;

此时:

  • leftSum = 0
  • rightSum = 最右边 k 张的和
  • 对应选择:左取0张 + 右取k张

2)然后逐步把牌从“右边集合”挪到“左边集合”

循环 i = 0..k-1

java
leftSum += cardPoints[i]; // 左边多拿一张(第 i 张) rightSum -= cardPoints[n - k + i]; // 右边少拿一张(原右边窗口的最左那张) max = Math.max(max, leftSum + rightSum);

这一步很关键:

  • leftSum 每次增加一张:从左端逐渐拿更多
  • rightSum 每次减少一张:右端拿的张数相应减少
  • 总张数始终是 k 张

对应关系:

  • 第 1 次循环后:左取 1 张 + 右取 k-1 张
  • 第 2 次循环后:左取 2 张 + 右取 k-2 张
  • 最后:左取 k 张 + 右取 0 张

所以它把所有可能都扫了一遍。


为什么下标是 n - k + i

一开始右边取的是区间:

  • [n-k, ..., n-1] 共 k 张

当左边取了 i+1 张时,右边应该只取 k-(i+1)
也就是把原来右边那 k 张里最左侧的那一张丢掉一张、再丢一张……

丢掉的顺序正好是:

  • n-k(第一次)
  • n-k+1(第二次)
  • n-k+2(第三次)

所以用 cardPoints[n - k + i] 来减。


小例子快速对应一下

[1,2,3,4,5,6,1], k=3

  • 初始 rightSum = 6+1+?(右3张:6+1+? 实际是 6+1+?=6+1+?)→右3张是 [6,1]? 不对,右3张是 [6,1]少了。完整是 [6,1]前面是 ?=5,所以 rightSum=5+6+1=12(i=0)
  • i=0:leftSum+=1 =>1,rightSum-=5 =>7,总=8(左1右2)
  • i=1:leftSum+=2 =>3,rightSum-=6 =>1,总=4(左2右1)
  • i=2:leftSum+=3 =>6,rightSum-=1 =>0,总=6(左3右0) max 就是这些里的最大。

总结一句话

先拿右边 k 张当作基准,然后每次“左边加一张、右边减一张”,维护两个和,就能在 O(k) 内枚举所有两端取牌的情况。

如果你想,我可以把这段代码配一张“窗口移动示意图”画出来,你会更直观地看到 rightSum 是怎么“滑”的。

Question

class Solution {
static int minPath = Integer.MAX_VALUE;
public int minPathSum(int[][] grid) {
dfs(grid, grid[0][0], 0, 0);
return minPath;
}

text
void dfs(int[][]grid, int sum, int i, int j){ int r = grid.length; int c = grid[0].length; if(i==r-1 && j == c-1){ if(sum<minPath){ minPath = sum; } return; } if(i>r-1 || j>c-1){ return; } dfs(grid,sum+grid[i][j],i+1,j); dfs(grid,sum+grid[i][j],i,j+1); }

} 有什么问题 力扣64

Answer

这段 DFS 用来做力扣 64(最小路径和)有几个关键问题,会导致结果偏大/重复加值越界风险多用例串值,并且会超时


1)同一个格子被加了两次(核心 bug)

你一开始调用:

java
dfs(grid, grid[0][0], 0, 0);

sum 已经包含了 grid[0][0]

但在 dfs 里你又做了:

java
dfs(grid, sum + grid[i][j], i+1, j); dfs(grid, sum + grid[i][j], i, j+1);

这会把当前格子 grid[i][j] 再加一次

举例:起点 (0,0) 的值会被加两次:一次在入口,另一次在从 (0,0) 出发递归时。

✅ 正确做法二选一:

方式 A:入口传 0,递归时加当前格子

java
dfs(grid, 0, 0, 0); // dfs 内:sum2 = sum + grid[i][j]

方式 B:入口传 grid[0][0],递归时加“下一格”

java
dfs(grid, grid[0][0], 0, 0); // dfs 内往下走时加 grid[i+1][j] / grid[i][j+1]

2)越界判断放错位置(会直接数组越界)

你在判断越界之前就访问了 grid[i][j](加到 sum 里):

java
dfs(grid,sum+grid[i][j],i+1,j);

如果 ij 已经越界,访问 grid[i][j] 会直接崩。

而你现在的越界判断在后面:

java
if(i>r-1 || j>c-1) return;

✅ 必须把越界判断放在任何 grid[i][j] 访问之前,并且用 >= 更直观:

java
if (i >= r || j >= c) return;

3)minPath 用 static 且不重置,会串用例

static int minPath = Integer.MAX_VALUE;

LeetCode 会在同一个 JVM 里跑多个测试,你这个值如果不重置,可能受上一次调用影响。

✅ 在 minPathSum 里重置:

java
minPath = Integer.MAX_VALUE;

或者别用 static,改成成员变量/局部变量。


4)DFS 会超时(复杂度指数级)

从 (0,0) 到 (m-1,n-1) 只能右/下,但路径数是组合数,DFS 暴力枚举是 指数级,网格稍大就 TLE。

力扣 64 标准解:DP / 原地 DP,时间 O(mn)


如果你坚持用 DFS:给你一版“修正且可跑”的 DFS(但大图仍可能超时)

加上剪枝(当前 sum 已经 ≥ minPath 就不用走了),并修复加值/越界/static 问题:

java
class Solution { private int minPath; public int minPathSum(int[][] grid) { minPath = Integer.MAX_VALUE; dfs(grid, 0, 0, 0); return minPath; } void dfs(int[][] grid, int sum, int i, int j) { int r = grid.length, c = grid[0].length; if (i >= r || j >= c) return; sum += grid[i][j]; if (sum >= minPath) return; // 剪枝 if (i == r - 1 && j == c - 1) { minPath = Math.min(minPath, sum); return; } dfs(grid, sum, i + 1, j); dfs(grid, sum, i, j + 1); } }

正解(推荐):DP(O(mn))

java
class Solution { public int minPathSum(int[][] grid) { int r = grid.length, c = grid[0].length; for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { if (i == 0 && j == 0) continue; int up = i > 0 ? grid[i - 1][j] : Integer.MAX_VALUE; int left = j > 0 ? grid[i][j - 1] : Integer.MAX_VALUE; grid[i][j] += Math.min(up, left); } } return grid[r - 1][c - 1]; } }

你现在更想要:
1)把 DFS 彻底剪枝到能 AC(带记忆化),还是 2)把 DP 思路推一遍(为什么这样转移)?

Question

class Solution {
int minPath = Integer.MAX_VALUE;

text
public int minPathSum(int[][] grid) { dfs(grid, grid[0][0], 0, 0); return minPath; } void dfs(int[][] grid, int sum, int i, int j) { int r = grid.length; int c = grid[0].length; if (i == r - 1 && j == c - 1) { minPath = Math.min(minPath, sum); return; } if (i > r - 1 || j > c - 1) { return; } sum += grid[i][j]; dfs(grid, sum, i + 1, j); dfs(grid, sum, i, j + 1); }

} 为什么我结果是错的

Answer

你这版结果错,核心原因就一个:起点格子被加了两次(而且还会影响所有路径)


你现在是怎么“加格子”的?

入口你传的是:

java
dfs(grid, grid[0][0], 0, 0);

也就是 sum 一开始已经包含了 (0,0) 的值。

但在 dfs 里你又做了:

java
sum += grid[i][j];

当第一次进入 dfs(0,0) 时,又把 grid[0][0] 加了一次。

所以每条路径的总和都会 多加一次起点值,导致答案偏大,当然就错了。


还有一个隐藏问题:越界判断位置不对(会导致你“走到越界节点也加值”的逻辑混乱)

你先判断到终点、再判断越界,然后才 sum += grid[i][j]
虽然你越界判断在加值之前(这一点还好),但你的终点判断用的是传入的 sum,而你当前的 sum 是否已经包含当前格子,取决于你怎么传参,很容易造成“终点到底加没加”的错乱(你这版正好也因为起点重复加导致全局错)。


正确修法(2 选 1)

修法 A(推荐):入口 sum=0,在 dfs 里统一加当前格子

java
class Solution { int minPath = Integer.MAX_VALUE; public int minPathSum(int[][] grid) { minPath = Integer.MAX_VALUE; dfs(grid, 0, 0, 0); return minPath; } void dfs(int[][] grid, int sum, int i, int j) { int r = grid.length, c = grid[0].length; if (i >= r || j >= c) return; sum += grid[i][j]; if (i == r - 1 && j == c - 1) { minPath = Math.min(minPath, sum); return; } dfs(grid, sum, i + 1, j); dfs(grid, sum, i, j + 1); } }

修法 B:入口包含起点,那 dfs 里就不要再加当前格子,而是加“下一格”

java
class Solution { int minPath = Integer.MAX_VALUE; public int minPathSum(int[][] grid) { minPath = Integer.MAX_VALUE; dfs(grid, grid[0][0], 0, 0); return minPath; } void dfs(int[][] grid, int sum, int i, int j) { int r = grid.length, c = grid[0].length; if (i == r - 1 && j == c - 1) { minPath = Math.min(minPath, sum); return; } if (i + 1 < r) dfs(grid, sum + grid[i + 1][j], i + 1, j); if (j + 1 < c) dfs(grid, sum + grid[i][j + 1], i, j + 1); } }

顺带一句:就算修对了,纯 DFS 还是很容易 TLE(指数级),力扣 64 标准做法是 DP / 记忆化。

如果你把你跑错的具体用例和输出贴出来,我还能帮你对照看“多加了哪一格”。

Question

class Solution {
int minPath = Integer.MAX_VALUE;

text
public int minPathSum(int[][] grid) { dfs(grid, 0, 0, 0); return minPath; } void dfs(int[][] grid, int sum, int i, int j) { int r = grid.length; int c = grid[0].length; if (i == r - 1 && j == c - 1) { minPath = Math.min(minPath, sum); return; } if (i > r - 1 || j > c - 1) { return; } sum += grid[i][j]; dfs(grid, sum, i + 1, j); dfs(grid, sum, i, j + 1); }

} 为什么还是错的

Answer

还是错的原因:你到达终点时,终点那一格没有被加进 sum(并且越界判断位置也不对)。

你现在的顺序是:

  1. 先判断 (i,j) 是不是终点
  2. 然后才 sum += grid[i][j]

所以当递归走到终点 (r-1,c-1) 时,sum 里面只包含了终点之前的路径和,没有包含 grid[r-1][c-1],自然就偏小/错误。

举个最小例子

grid = [[5]]

调用 dfs(0,0,0,sum=0)
你先判断终点成立 → minPath = min(∞, 0) → 返回
但正确答案应该是 5。


正确写法:先越界判断,再把当前格子加进去,再判断终点

java
class Solution { int minPath = Integer.MAX_VALUE; public int minPathSum(int[][] grid) { minPath = Integer.MAX_VALUE; dfs(grid, 0, 0, 0); return minPath; } void dfs(int[][] grid, int sum, int i, int j) { int r = grid.length, c = grid[0].length; if (i >= r || j >= c) return; // 先判越界 sum += grid[i][j]; // 先把当前格子加进来 if (i == r - 1 && j == c - 1) { // 再判终点 minPath = Math.min(minPath, sum); return; } dfs(grid, sum, i + 1, j); dfs(grid, sum, i, j + 1); } }

注意我把越界条件改成 >=,更标准也更不容易写错。


另外提醒:即使这样改对了,纯 DFS 在大 grid 上会 TLE(路径数指数级)。要想稳 AC,需要 DP 或 DFS+记忆化。

Share this Q&A