📘 四、完整逻辑梳理(结合注释)
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 会报错,因为它尝试"调用队列对象"
💡 简单记忆:
数组/队列/列表用
[ ]
,函数/方法用( )