📘 四、完整逻辑梳理(结合注释)
python
from collections import deque
def max_score(node_scores, k):
n = len(node_scores)
dp = [0] * n
dp[0] = node_scores[0]
dq = deque([0]) # 队列里存的是"下标",最前面的是当前窗口最大 dp 的下标
for i in range(1, n):
# 1️⃣ 移除超出窗口范围的下标
while dq and dq[0] < i - k:
dq.popleft()
# 2️⃣ 当前窗口最大值在 dq[0]
dp[i] = node_scores[i] + dp[dq[0]]
# 3️⃣ 保持队列单调递减:小于当前 dp[i] 的都不可能成为未来最大值
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
# 4️⃣ 加入当前下标
dq.append(i)
return dp[-1]
为什么循环从 1 开始?
✅ 原因 1:dp[0] 已经初始化
在 i=0 的时候我们已经知道了 dp[0],
所以真正需要"递推"的是从 第 1 个元素开始。
换句话说:
dp[0]是基础;dp[1]开始才需要根据前面计算。
✅ 原因 2:dq[0] 是上一轮的最大值下标
如果你从 i=0 开始跑:
-
dq里还没放任何元素; -
你在这行:
pythondp[i] = node_scores[i] + dp[dq[0]]会出错(因为
dq是空的)。
而从 i=1 开始就没问题:
dq = [0];dq[0] = 0,可以安全取到dp[0]。
✅ 原因 3:循环逻辑对齐窗口大小
比如 k = 3,
那计算 dp[1] 时,窗口 [i-k, i-1] = [-2, 0] 实际上只有 0;
这正好对应初始化状态。
所以 for i in range(1, n) 是完全正确的。
计算
dp[1]时,窗口[i-k, i-1] = [-2, 0],为什么会出现负数?负下标怎么处理?
1️⃣ DP 窗口公式
公式是:

- 每次 dp[i] 只能看前 k 步
- 窗口范围用
[i-k, i-1]表示
2️⃣ 举例:k = 3,i = 1

- 理论上窗口是 [-2, -1, 0]
- 但是数组 dp 只有下标 ≥ 0
- 负数下标在 Python 里有特殊含义 → dp[-1] 会访问最后一个元素,这里我们不希望访问
- 所以在算法里 通过单调队列和判断条件 避免访问负下标
关键点
- 初始化
dq = deque([0])→ 窗口里第一个有效下标就是 0 - 循环
for i in range(1, n) - 在计算 dp[i] 时,真正使用的队列 dq 里只有有效下标 ≥ 0
- 所以即使公式写
[i-k, i-1]出现负数,也不会访问到,因为队列里没有负下标
3️⃣ Python 里负下标的处理方式
-
Python 列表允许负下标 → 从右往左访问
pythona = [10,20,30] print(a[-1]) # 30 print(a[-2]) # 20 -
在单调队列算法里,dq 里只存有效下标 → 负下标不会进入 dq
-
所以不会真的访问 dp[-1] 或 dp[-2],安全无误
4️⃣ 为什么 for i in range(1, n) 是正确的
- i=0 → 已经初始化 dp[0]
- i 从 1 开始 → 公式里的窗口
[i-k, i-1]可能出现负数 - 但是队列 dq 只包含有效下标 ≥ 0 → 自动过滤掉负下标
- 所以循环从 1 开始就完全正确,无需特殊处理
💡 小结
- 公式
[i-k, i-1]是理论上的窗口 - 负下标不会真的被访问 → 通过 单调队列维护的有效下标 自动过滤
- 初始化 dq = [0] 也正好保证了第一步的最大值 dp[0] 可用
为什么需要初始化队列 `dq
在单调队列优化 DP 里:
python
dp[i] = node_scores[i] + max(dp[i-1], dp[i-2], ..., dp[i-k])
- 我们一开始只有 起点 dp[0] 已经知道。
- 队列
dq要维护一个窗口内 dp 的最大值。 - 因此,最开始窗口里只有 下标 0 是有效的。
2️⃣ 初始化写法
python
from collections import deque
dq = deque([0])
deque([0])→ 创建一个 deque,初始里面放下标 0。- 意思是:窗口里最大值对应的下标,起初就是 0。
3️⃣ 接下来循环从 i=1 开始
之后的循环:
python
for i in range(1, n):
# 1️⃣ 移除超出窗口的下标
while dq and dq[0] < i - k:
dq.popleft()
# 2️⃣ 取当前窗口最大值
dp[i] = node_scores[i] + dp[dq[0]]
# 3️⃣ 保持队列单调递减
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
# 4️⃣ 把当前下标放入队尾
dq.append(i)
- 这里
dq会不断更新:左边弹出过期下标,右边保持单调递减。 - 队列里始终存 窗口内可能成为最大值的下标。
deque 常用操作
dq 是 Python 的 deque 对象,常用操作如下:
| 方法 | 含义 |
|---|---|
dq.append(x) |
在队列右端添加元素 x |
dq.appendleft(x) |
在队列左端添加元素 x |
dq.pop() |
弹出队列右端元素 |
dq.popleft() |
弹出队列左端元素 |
dq[0] |
访问队列左端元素(队头) |
dq[-1] |
访问队列右端元素(队尾) |
len(dq) |
队列长度 |
注意:deque 本身没有"自动排序"功能,顺序完全由你自己维护。
单调队列在 DP 中的用法
结合上面循环:
python
for i in range(1, n):
# 1️⃣ 移除超出窗口的下标
while dq and dq[0] < i - k:
dq.popleft()
# 2️⃣ 计算 dp[i]
dp[i] = node_scores[i] + dp[dq[0]]
# 3️⃣ 保持队列单调递减
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
# 4️⃣ 将当前下标加入队尾
dq.append(i)
每行解释:
① 移除窗口外的元素
python
while dq and dq[0] < i - k:
dq.popleft()
dq[0]→ 队头下标;- 如果队头下标 <
i-k,说明它已经不在"最近 k 个元素窗口"内了; popleft()→ 弹出队头(deque 的操作)- 这是 deque 的方法 + 单调队列逻辑结合
② 获取当前窗口最大值
python
dp[i] = node_scores[i] + dp[dq[0]]
- 队头
dq[0]永远是窗口内 dp 的最大值下标; - 所以 dp[i] = 当前节点分数 + 窗口最大 dp 值
- 这里是算法逻辑,不是 deque 操作
③ 保持队列单调递减
python
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
dq[-1]→ 队尾下标- 如果队尾 dp 值 <= 当前 dp[i],说明它 不可能再成为未来窗口最大值
- 所以弹出队尾
pop()(deque 方法) - 这是单调队列维护最大值的核心逻辑
④ 将当前下标加入队尾
python
dq.append(i)
- 把当前下标放入队尾(deque 方法)
- 下一轮循环会用它来计算 dp[i+1]
- 队列顺序:队头最大,队尾最小(单调递减)
1️⃣ while dq and dq[0] < i - k: 分解
Python 里的 and 可以理解成 逻辑"并且":
python
while 条件1 and 条件2:
# 做某事
- 条件1
dq - 条件2
dq[0] < i - k
① 条件1:dq
dq是一个 deque(队列)- 在 Python 里,非空的列表、deque、字符串、字典等都会被当作
True - 空 deque →
False,非空 →True
所以:
python
while dq ...
相当于说:
"只要队列不为空,就继续检查下面的条件"
✅ 这是 安全检查 ,避免下面访问 dq[0] 时出错。
- 如果队列空了,再访问
dq[0]就会报错IndexError。 - 所以必须先判断队列是否为空。
② 条件2:dq[0] < i - k
dq[0]→ 队头下标i - k→ 当前窗口最左边允许的下标- 如果队头下标小于
i-k,说明它过期了,需要弹出
2️⃣ 为什么两个条件要同时写?
python
while dq and dq[0] < i - k:
dq.popleft()
逻辑顺序:
-
先判断队列是否为空 →
dq- 队列空了就停止循环,否则访问 dq[0] 会报错
-
队列不为空时,判断队头下标是否过期 →
dq[0] < i - k- 如果过期 → 弹出
- 如果没过期 → 停止循环
如果不写
dq,队列空了就会直接访问dq[0]→ 出错。
1️⃣ Python 中 [] 与 () 的区别
| 写法 | 用途 |
|---|---|
[] |
索引/下标,取列表、deque、字符串、元组里的元素 |
() |
函数调用,执行函数或方法 |
例子 1:列表或 deque 取元素
python
lst = [10, 20, 30]
print(lst[0]) # 输出 10,取列表第 0 个元素
python
from collections import deque
dq = deque([3,5,7])
print(dq[0]) # 输出 3,取队头元素
这里用
[0]表示索引第 0 个元素。
例子 2:函数调用
python
def f(x):
return x + 1
print(f(0)) # 输出 1,调用函数 f
注意:
f(0)是执行函数 f,把 0 传进去而
f[0]就会报错,因为函数不能用下标访问
2️⃣ 为什么 deque 用 [0] 而不是 (0)?
dq是 deque 对象 不是函数dq[0]→ 取队头元素(索引 0)dq(0)→ Python 会尝试把 dq 当成函数去调用 → 报错
3️⃣ 小结理解
- 方括号
[ ]→ 取元素/下标(数组、列表、队列、字符串) - 圆括号
( )→ 调用函数/方法 - deque 里存的是元素或下标,要访问队头/队尾,用 索引 →
[0]或[-1] - 如果你写成
dq(0)→ Python 会报错,因为它尝试"调用队列对象"
💡 简单记忆:
数组/队列/列表用
[ ],函数/方法用( )