【小白笔记】最大化安全评分

📘 四、完整逻辑梳理(结合注释)

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 里还没放任何元素;

  • 你在这行:

    python 复制代码
    dp[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 列表允许负下标 → 从右往左访问

    python 复制代码
    a = [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()

逻辑顺序:

  1. 先判断队列是否为空dq

    • 队列空了就停止循环,否则访问 dq[0] 会报错
  2. 队列不为空时,判断队头下标是否过期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️⃣ 小结理解

  1. 方括号 [ ] → 取元素/下标(数组、列表、队列、字符串)
  2. 圆括号 ( ) → 调用函数/方法
  3. deque 里存的是元素或下标,要访问队头/队尾,用 索引[0][-1]
  4. 如果你写成 dq(0) → Python 会报错,因为它尝试"调用队列对象"

💡 简单记忆:

数组/队列/列表用 [ ],函数/方法用 ( )

相关推荐
新子y6 小时前
【小白笔记】关于 Python 类、初始化以及 PyTorch 数据处理的问题
pytorch·笔记·python
报错小能手7 小时前
linux学习笔记(51)Redis发布订阅 主从复制 缓存 雪崩
linux·笔记·学习
Cathy Bryant7 小时前
大模型微调(四):人类反馈强化学习(RLHF)
笔记·神经网络·机器学习·数学建模·transformer
不会算法的小灰7 小时前
JavaScript 核心知识学习笔记:给Java开发者的实战指南
javascript·笔记·学习
狡猾大先生7 小时前
ESP32S3-Cam实践(LedStrip、RC舵机控制)
笔记
鸽子一号7 小时前
c#笔记之事件
笔记
Lovely Ruby8 小时前
七日 Go 的自学笔记 (一)
开发语言·笔记·golang
wenjie学长8 小时前
[UE学习笔记]—划时代意义的两大功能—lumen和Nanite
笔记·学习·ue·三维数字化
摇滚侠8 小时前
Spring Boot 3零基础教程,WEB 开发 国际化 Spring Boot + Thymeleaf 笔记45
spring boot·笔记·后端