Noita 元胞沙盒笔记

Noita 元胞沙盒笔记

元胞自动机 = 网格 + 每格状态 + 局部规则,反复迭代 → 全局涌现。

本项目的沙盒把"规则"做成了 材质表 + N×N 反应表 + 五阶段主循环,固定 60Hz 推进。

这份笔记分三层:概念(CA 是什么)→ 运作(沙盒怎么动)→ 实现(数据怎么放、循环怎么写),最后附实测现象、勘误和配表。

工程位置:

  • Assets/Scripts/Simulation/ ------ 纯 C#,不引用 UnityEngine(元胞机本体)
  • Assets/Scripts/Sandbox/ ------ Unity 前端(时钟、渲染、操作)
  • Assets/Resources/Data/materials.csv、reactions.csv ------ 配表

项目地址:https://github.com/ExcCoder/NoitaDemo.git

目录

  1. 元胞自动机是什么
  2. Noita 沙盒怎么运作
  3. 数据模型:状态 / 属性 / 契约
  4. 反应系统
  5. 主循环:Step 的五个阶段
  6. 调度:固定时间步长
  7. 易踩的坑(实测)
  8. 勘误:与初稿的差异
  9. 附录

一、元胞自动机是什么

1.1 词面意思

"元胞"就是格子(cell),"自动机"就是状态机:给定当前状态,按规则决定下一步变成什么。

所以:元胞自动机 = 一堆小状态机排成网格,每台只看自己和邻居,然后一起往前走一步。

1.2 三个"离散" + 四个要素

三个离散:

  • 空间离散:整齐的网格,不是连续坐标;
  • 状态离散:每格只存有限个状态,不是浮点物理量;
  • 时间离散:按帧推进,一帧算一步。

四个要素:

要素 含义 本项目对应
网格 空间长什么样 384×256 扁平数组,index = y * Width + x,y = 0 在底部
状态 每格存什么 Cell(4 字节):材质 id + 寿命
邻域 能看哪些邻居 移动看上下左右 4 邻;反应只配对"右 + 上"
规则 怎么变 相态决定移动算法、行为决定特殊规则、N×N 反应表决定相遇结果

同步更新:所有格子一起算下一帧。工程上常用"原地扫描 + 本帧已处理标记"近似同时更新,见 5.2。

1.3 经典例子:生命游戏

每格只有"活 / 死"两种状态,规则只有四条:周围 8 格活邻居为 2~3 个就继续活,否则死;死格恰好有 3 个活邻居就复活。

就这几条规则,能跑出滑翔机、振荡器、繁殖结构------没有一行代码在描述"滑翔机"。

这就是 CA 的精髓:规则在局部,行为在全局。

1.4 和传统物理引擎的区别

物理引擎 元胞自动机
计算对象 连续量(速度、力、碰撞响应) 离散状态(材质 id)
方法 解方程、迭代积分 查规则、局部替换
成本 随物体/接触点增长 随格子数增长,每帧扫一遍
优点 精确、可预测 便宜、天然并行、手感直观、易扩展
缺点 贵、复杂 不精确、没有守恒律

1.5 一句话

元胞自动机 = 规则足够简单,简单到只依赖邻居;迭代足够久,久到宏观行为自己长出来。


二、Noita 沙盒怎么运作

它相当于把"生命游戏"升级成 多状态 + 材质属性 + 概率反应。

2.1 世界 = 材质网格 + 寿命

每格一种材质(空 / 石 / 沙 / 水 / 油 / 木 / 火 / 烟 / 蒸汽 / 酸 / 岩浆 / 灰 / 玻璃 / bedrock)+ 一个寿命。

你看到的沙堆、水池,是大量同材质格子恰好连成一片,不是"一个物体"。所以没有"整体"的物理:沙堆塌了是因为每一格各自判断"下面能不能掉"。

2.2 每格每帧做的三件事

按顺序:

  1. 寿命:火 / 烟 / 蒸汽这类会衰减,到点变成下一种材质(火 → 烟 → 空);
  2. 专属行为:火点燃四邻、酸腐蚀接触到的固体、蒸汽被压住时凝结成水;
  3. 相态移动 :
    • 粉末:下面空 / 更轻就往下掉,不行再试斜下 → 沙堆自然形成 45° 坡;
    • 液体:先下落、再斜下、再向左右铺开 → 水往低处流、找平;
    • 气体:往上飘,能穿过液体、穿不过固体 → 烟和蒸汽上升。

一帧里不同格子的"决策"是这样的:

复制代码
沙:下面空         → 交换位置,下落 1 格
水:下面堵、左下空 → 交换位置,流向低处
火:点燃相邻木头   → 自身寿命 -1 → 再像气体一样上浮
烟:上方空         → 上浮,寿命 -1,到点消失

2.3 密度法则:谁挤得动谁

没有"浮力计算",只有一条规则:更重的能挤进更轻的那格。

本项目实际数值(materials.csv):

材质 密度 直观结果
石 / 玻璃 / bedrock 255 谁都挤不动(而且是 static,根本不参与移动)
沙 200 沉进水里、沉进油里
木 180 static,不会塌
熔岩 / 灰 150 熔岩沉在水下
酸 110 比水重一点,沉底
水 100 ---
油 80 浮在水面
火 10 上浮
蒸汽 8 穿过水往上冒
烟 5 最轻,飘得最高

"能不能进"的完整判定(概念版):

复制代码
目标为空                → 能进(空格是特判,不参与密度比较)
同种材质                → 不能进(水不会穿过水)
目标是 static           → 不能进
向下移动:我比它重       → 能进
向上移动:我比它轻,且目标是液体/气体 → 能进
横向移动:目标只能是气体 → 能进

注意"空气"不是一种密度为 0 的材质参与比较,而是空格直接放行------所以"蒸汽比空气轻所以上浮"这句话在本项目里不成立;气体上浮是因为它先试正上方,而空格无条件通过。

2.4 相遇 = 查反应表

两个不同材质贴在一起时,按规则表结算"你变什么、我变什么、多大几率":

规则 概率 效果
水 + 熔岩 100% 蒸汽 + 石
水 + 火 100% 蒸汽 + 空
沙 + 熔岩 25% 玻璃 + 熔岩
熔岩 + 油 60% 熔岩 + 火
熔岩 + 木 40% 熔岩 + 火
酸 + 石 / 木 / 沙 12% 酸保留,目标变空

规则不是写死的 if,而是一张可配置的表------所以加反应不用改代码,见第四节。

2.5 涌现与局限

以上几条叠加,就有了:木箱烧穿后沙塌进岩浆变玻璃、酸慢慢啃石头、水浇灭火、蒸汽升到顶凝结再落下来。

它的"物理"只有这几条局部规则------没有全局求解器、没有压力场、没有温度场(温度字段是预留的)。所以别期待守恒律:

  • 水量少的时候会摊成稀疏的一层,不会自动找到同一个水平面;
  • 沙堆只会堆成 45° 左右,因为粉末没有"横向滑落";
  • 燃烧是概率的,偶尔剩几格木头烧不着是正常的。

这些不是 bug,是 CA 这类实现的通病/特征。

2.6 一句话

沙盒的全部"物理"就是几条局部规则:往下掉、往低处流、往上飘、比密度、按表反应------宏观的沙、水、火、岩浆都是它们迭代出来的结果。


三、数据模型:状态 / 属性 / 契约

原则:格子(Cell)只存状态,材质(MaterialDef)才存属性,反应(Reaction)只存契约。

三层数据:

csharp 复制代码
Cell[]        cells;      // 每格状态,4 字节;384×256 ≈ 384 KB
MaterialDef[] defs;       // 每种材质的共享属性(flyweight),不随格子复制
Reaction[]    reactions;  // N×N 稠密表,index = a * N + b(当前 14×14 ≈ 0.6 KB)
// 另有 byte[] mMoved(96 KB),只做"本帧是否动过"的防冲撞标记,见 5.2

3.1 每格只存状态:Cell(4 字节)

字段 作用
material 材质 id,对应材质表下标
temperature 温度,预留,当前不参与模拟
lifetime 剩余寿命,0 = 永不变质
reserved 保留,凑满 4 字节

为什么这么小:网格动辄几万几十万格,每格多 1 字节就是几十 KB。而且热循环只做数组索引和字节比较,没有 class、没有虚调用、没有字典。

反过来,"本帧动没动过"这种临时标记故意不放进来------因为它会被交换一起搬走(见 5.2)。

3.2 材质属性表:共用一份(flyweight)

MaterialDef 是每种材质一份,被所有同 id 的格子共享:相态、行为、密度、可燃性、扩散、decayTo、初始寿命、flags、颜色。

好处:改一个材质属性,全图同材质立刻生效;每格不用为"密度是多少"付存储成本。

3.3 枚举用 byte(你的笔记,补本项目落点)

核心语法:

csharp 复制代码
public enum Behavior : byte { None = 0, Fire = 1, Acid = 2, Steam = 3 }
public enum Phase : byte { Empty = 0, Static = 1, Powder = 2, Liquid = 3, Gas = 4 }

为什么可以指定基础类型:byte 能存 0~255,元胞机里"材质种类 / 相态种类"通常远小于 256,够用。

收益:普通 enum 底层是 int(4 字节),指定 byte 后只占 1 字节,省 4 倍。对几万几十万格的数组很划算。

本项目的实际落点 (补一句,免得笔记里对不上):Phase / Behavior 存在共享材质表 里,不是每格一份;每格只有 byte material 和 byte lifetime。所以省内存的主力是 "Cell 只有 4 字节",byte 枚举是配套习惯------它同时决定了 MaterialDef 的结构大小,以及 materials.csv 里这些列的取值范围。

避坑提示:

  • 普通业务逻辑(对象只有几千几万个)不要用。默认 int 处理最快(CPU 原生字长),没有溢出风险,代码也更干净;
  • Unity Inspector 面板序列化 byte 枚举在旧版本可能有 Bug,暴露给编辑器调参的字段慎用;
  • 值域一旦超过 255(例如材质超过 256 种)会直接溢出,本项目用 byte 存材质 id,所以材质上限就是 256。

3.4 结构体对齐与保留字段

现代 CPU 读内存喜欢按 4 / 8 字节的块来读;结构体大小刚好是 4 的倍数时效率最高。

Cell 正好 4 字节(material / temperature / lifetime / reserved 各 1 字节),就是按这个思路凑的:temperature 和 reserved 虽然当前没用,但让结构体对齐到 32 位字长,也顺手给以后留了字段。

缓存命中时提升有限(5%~10%),一旦要频繁回主内存拿数据,正确对齐能差出几倍------元胞机每帧要扫几十万格,正好属于后者。

3.5 反应表的"稠密存放"(Dense Storage)

核心公式:

复制代码
index = a * N + b      // a、b 是材质 id,N 是材质总数

为什么不用 Dictionary:每帧要做几十万次反应检测,哈希计算 + 指针跳转太慢。

直接开一个 N × N 数组,用一次乘法加法算下标,没有指针跳转,CPU 缓存预读友好 。代价是一点内存浪费(大部分格子的 chance = 0),换极致的查询速度,非常划算。

怎么判断"这条反应不存在":数据填满数组,用 chance == 0 表示不存在。

本项目的实际尺寸:N = 14,14 × 14 × 3 字节 ≈ 0.6 KB;如果材质涨到 256,就是 256 × 256 × 3 字节 ≈ 196 KB(仍可接受)。所以"直接开 256×256"是一个理论上限的估算,不是当前实际分配。


四、反应系统

4.1 反应结构体:只负责"一对一"的契约

csharp 复制代码
struct Reaction
{
    byte resultSelf;   // 当前格变成什么
    byte resultOther;  // 邻居变成什么
    byte chance;       // 1~255 概率;0 = 没有这条反应
}

它只描述两个像素碰撞的瞬间:"A 遇到 B,各自变成什么,概率多少"。

它不关心周围是一圈还是几圈,也不存坐标------它只是一张没有空间信息的契约。

4.2 主循环负责"找对象"并执行

"遍历邻居"的逻辑写在主循环里,而不是写在数据结构里。

流程:扫到当前格 a → 找邻居 b → 查表 Reactionab → 判定概率 → 执行结果。

本项目的取舍 :不遍历 8 邻居,而是固定只配对 右 + 上 两个方向。因为每对相邻格只需要被结算一次,这样:

  • 水平对在"左边那个格子"处理时结算,垂直对在"下面那个格子"处理时结算;
  • 每对每帧正好一次,等效于 8 邻居但省一半以上的查表;
  • 不用配置 8 个方向的规则。

代价:同一帧内一个格子理论上可能被多个邻居各配对一次(因为它右/上被查过,左/下又会作为别人的右/上被查)。这种"一帧多次反应"对沙盒来说可以接受。

初稿里"反应后当前格 ID 变了应立刻 break"更适合"一个格子遍历 8 邻居"的写法;本项目每个格子只发起 2 次固定配对,所以没有 break 这一步。

4.3 属性解耦:主动 vs 被动

不要试图用单一属性(如万能 Reactivity)概括所有交互。"燃烧"和"腐蚀"虽然结果都是改变目标像素,但底层逻辑完全不同:

燃烧 腐蚀
方向 被动:木头靠 flammability 标签等火来触发 主动:酸液主动查询邻居,判定能否腐蚀
过程 需要寿命、有燃烧过程、产物复杂(木 → 火 → 烟) 接触瞬间按概率结算,直接消失
结果 有中间态 没有中间态

正确设计:属性拆分 + 查反应表

  • 材质表:只存"我是谁"(密度、可燃性、抗酸性......);
  • 反应表:只存"我遇到你变成啥"(Reaction[火][木头])。

4.4 反应的"正反向回退机制"

如果只配置了 water → lava 的反应,没配 lava → water:

当系统扫到 lava 挨着 water 时,查表 [lava][water] 发现没有,会自动反向查一遍 [water][lava];命中后把 resultSelf 和 resultOther 互换(因为视角反了)。

这样配置表只需要写一半规则,代码里做一次回退 + 结果对调。用极小的运行时代价换掉一半的配置量,是很划算的工程妥协。

4.5 现有关卡:三条典型循环

  • 水火土循环:水 + 熔岩 → 蒸汽 + 石。蒸汽上浮、遇到阻挡凝结回水,水又被下一层熔岩变成蒸汽------水在循环,熔岩在变成石头。实测 200 步后水量不变、熔岩少了一大截、石头多出来;
  • 沙 + 熔岩 → 玻璃:沙丘塌进岩浆,变成玻璃;
  • 酸腐蚀:酸只对相邻的 static / powder 生效,按概率把目标变空,偶尔消耗自己。

五、主循环:Step 的五个阶段

5.1 流程

复制代码
Step() 一帧:
┌─ ① 寿命 ──────── lifetime--,到 0 → 换成 decayTo(火→烟→空)
├─ ② 静态跳过 ──── static 且无 behavior(石/木/玻璃)直接跳过
├─ ③ 行为 ──────── 火点燃四邻 / 酸腐蚀 / 蒸汽受阻凝结
├─ ④ 相态移动 ──── powder 落 / liquid 落+横散 / gas 升(mMoved 限一次)
└─ ⑤ 反应 ──────── 全图再扫一遍,右/上邻居配对,按 chance/255 结算

顺序有讲究:

  • 寿命在最前:到点的材质先变成下一种,不再参与本帧移动;
  • 行为在移动前:火先点着邻居,再像气体一样上浮;酸/蒸汽只有"自毁 / 凝结"时才跳过移动;
  • 反应在最后:移动全做完再统一配对,避免移动打断反应判定。

5.2 防冲撞:mMoved 标记

byte[] mMoved 标记"本帧已经移动过"的格子,保证每个格子每帧最多只行动一次。

  • 每帧清空一次;
  • 交换时给目标格打标记;
  • 已标记的格子本帧直接跳过。

它的作用不只是防重复:扫描方向 + mMoved 一起,保证"一帧最多动一格"------落沙不会一帧穿好几格,气体也不会窜。

5.3 扫描方向与方向偏置

问题:主循环必须是某种固定顺序(本项目从下往上、从左往右),而不同相态的"自然方向"不一样------沙子想往下,气体想往上。

本项目的实际做法:从下往上扫,配合 mMoved:

  • 沙子落到的那一格本帧已经处理过了,所以不会一帧掉多格;
  • 气体升到的那一格本帧还没处理,但被 mMoved 拦住,所以也不会一帧升多格。

结论:落和浮都被归一成"一帧一格",不会出现瞬移/穿透。真正的偏置只剩下"同一帧内的处理优先级"(下面的格子先动,上面的后动),以及"新点燃的火本帧不再处理"这类细节。

三种通用解法(知道即可,本项目没用):

解法 做法 代价
A. 交替扫描(蛇形) 第一帧从下往上,第二帧从上往下 实现简单,但规则不再"同帧同序",调试变难
B. 分相态独立循环 粉末/液体从下往上,气体单独从上往下 遍历次数变多(1 遍变 2~3 遍)
C. 双缓冲 读当前帧,写下一帧,最后统一 Swap 彻底消除顺序影响,但双倍内存

本项目的取舍:单次遍历 + mMoved。用每帧一个 byte 的标记数组(384×256 时约 96 KB),换掉双缓冲的双倍内存和多次遍历。

一句话:一帧只有一次遍历,不可能完美兼顾两个方向;用标记把两个方向都压成"一帧一格",是最低成本的妥协。


六、调度:固定时间步长

6.1 为什么不能直接放 Update 里

Update 跟电脑帧率走。如果每帧直接 World.Step():

  • 144Hz 屏幕上,沙子掉得飞快;
  • 20Hz 卡顿时,沙子掉得极慢。

物理模拟必须与画面帧率解耦。

6.2 解决方案:累加器(Accumulator)

设定固定步长(本项目 1/60 秒):

复制代码
每帧:累加真实时间(可乘倍速)
     while 累加值 >= 1/60:
         World.Step()
         累加值 -= 1/60
  • 高帧率:攒好几帧才触发一次逻辑更新;
  • 低帧率:一帧内连续触发多次逻辑更新来"补课"。

6.3 为什么不用 Unity 的 FixedUpdate

  • 累加器代码更轻量;
  • 完全掌控:容易实现 2 倍速、暂停、单步调试;
  • 不受引擎物理系统的时间配置限制。

6.4 逻辑与渲染的顺序

先算账:while 循环跑 World.Step(),更新网格数据;后作画:View.Render() 把最新数据画到屏幕上。

6.5 本项目参数

  • 固定步长 1/60 s;
  • 一帧最多补 8 步,超过就丢弃累积(防止卡顿后"追帧"太久);
  • 倍速 0.25x / 1x / 2x / 4x;
  • 支持 Pause 与单步(暂停时点 Step 走一帧)。

七、易踩的坑(实测)

  1. Step() 的返回值不等于"真的变了" :火的点燃、酸的腐蚀这类"行为改动"不计入返回值。别用 while (world.Step()) 做沉降判断,会提前退出。
  2. 火是气体,会往上飘:在燃料上表面点一撮火,它常常直接飘走,下面的燃料烧不到。要烧东西得让火"困"在燃料内部或侧面(火球打进木箱、点油面)。
  3. 燃烧是概率性的:同一块木箱换不同随机种子,跑完剩下的木头格数不一样;可复现,但结果依赖种子。
  4. 液体没有压力/体积守恒:水量少时会摊成稀疏的 1 格层,还会出现孔洞;水不会自动找同一个水平面。
  5. 液体一步能横跳多格 :单格液体一帧最多横向移动 dispersion 格(水 4、油/酸 3、熔岩 2),不是平滑流动。
  6. 反应只发生在相邻格:酸不接触石头就永远不腐蚀;把酸放在悬空石块下方,腐蚀几格后就停住,因为酸不会爬上去。
  7. 新增材质有格式约束 :id 必须从 0 连续、不超过 255、名字唯一、id 0 必须是空气;decayTo 必须是已存在的材质名。
  8. 渲染可能比模拟更贵:每帧把整张图重新上传成纹理,384×256 的 CPU→GPU 上传开销通常大于模拟本身。

八、勘误:与初稿的差异

下面是初稿里和这份代码对不上的地方,已按实际实现改写:

  1. 密度数值:初稿写"沙 100 > 水 50",实际是沙 200、水 100;油 80、熔岩 150、蒸汽 8、火 10、烟 5。
  2. "空气"不参与密度比较 :空格是 id == 0 特判放行,不是"密度 0 的材质"。所以"蒸汽密度 < 空气所以上浮"的因果不成立,气体上浮是因为移动算法先试正上方。
  3. 反应邻居不是 8 个:本项目固定配对"右 + 上",每对每帧一次;没有"遍历邻居时 break"这一步。
  4. 方向偏置:本项目用 mMoved 把落/浮都压成"一帧一格",不会瞬移/穿透;偏置只剩同帧处理优先级。"容忍气体轻微偏置"改为"用每帧 1 字节的标记数组把双向归一化"。
  5. 反应表尺寸 :不是固定 256×256,而是 N×N(N = 材质数,当前 14);256 只是 byte 材质 id 的理论上限。
  6. enum 省内存的落点 :本项目的 Phase / Behavior 存在共享材质表里,不是每格一份;省内存的主力是 Cell 只有 4 字节。

九、附录

9.1 材质表(materials.csv)

id 名称 相态 行为 密度 可燃性 扩散 寿命 产物
0 empty(空气) empty none 0 0 0 0 ---
1 stone(石) static none 255 0 0 0 ---
2 bedrock(基岩) static none 255 0 0 0 不可破坏
3 sand(沙) powder none 200 0 0 0 ---
4 water(水) liquid none 100 0 4 0 ---
5 oil(油) liquid none 80 100 3 0 ---
6 wood(木) static none 180 120 0 0 ---
7 fire(火) gas fire 10 0 0 45 烟
8 smoke(烟) gas none 5 0 0 140 空
9 steam(蒸汽) gas steam 8 0 0 110 水
10 acid(酸) liquid acid 110 0 3 0 ---
11 lava(熔岩) liquid none 150 0 2 0 ---
12 ash(灰) powder none 150 0 0 0 ---
13 glass(玻璃) static none 255 0 0 0 ---

9.2 反应表(reactions.csv)

a b a 变成 b 变成 概率
water lava steam stone 100%
water fire steam empty 100%
acid stone acid empty 12%
acid wood acid empty 12%
acid sand acid empty 12%
sand lava glass lava 25%
lava oil lava fire 60%
lava wood lava fire 40%

表中一行同时覆盖正向和反向(4.4 的回退机制);概率按 percent × 255 / 100 量化成 chance。

9.3 实测数字(本机无头 Release 跑出来的参考值)

现象 实测
落沙速度 1 格/步(4 格沙柱整体下移 1 格)
气体上升速度 1 格/步
单格液体横向 一步最多 4 格(由 dispersion 决定)
火 → 烟 第 45 步
烟 → 空 再约 140 步
水 + 熔岩 接触当帧结算(第 1 帧结算了 44 对)
384×256 竞技场 约 0.33 ms/步(.NET Release,Unity Mono 会慢 2~4 倍)

9.4 主要源码地图

文件 作用
Assets/Scripts/Simulation/PixelWorld.cs 世界、主循环、移动、行为、反应、随机
Assets/Scripts/Simulation/MaterialTable.cs CSV → 材质表 + 反应表
Assets/Scripts/Simulation/Cell.cs 每格状态(4 字节)
Assets/Scripts/Simulation/MaterialDef.cs 材质共享属性
Assets/Scripts/Simulation/Reaction.cs 反应契约
Assets/Scripts/Simulation/Phase.cs / Behavior.cs 相态 / 行为枚举
Assets/Scripts/Sandbox/SandboxRoot.cs 时钟、暂停、单步、倍速
Assets/Scripts/Sandbox/PixelWorldView.cs 网格 → 纹理
Assets/Scripts/Sandbox/PixelPainter.cs 鼠标画笔
Assets/Scripts/Sandbox/PlayerController.cs 玩家碰撞(AABB 查网格)
Assets/Scripts/Sandbox/SpellProjectile.cs / DigBeam.cs 水球/火球、挖洞
Assets/Scripts/Sandbox/SandboxArena.cs 演示地图
Assets/Resources/Data/materials.csv / reactions.csv 配表

9.5 一句话总结

网格上每个格子只管自己:寿命到了就变,该落就落、该流就流、该飘就飘,遇到不同材质就查表------重复六十次每秒,沙、水、火、岩浆就都"活"了。

相关推荐
木井巳2 小时前
【动态规划: 路径问题】不同路径
java·算法·leetcode·动态规划
CQU_JIAKE2 小时前
10.25【A】
数据结构·算法
greencherry_3 小时前
大连模屿石膏课模具配比全固定,调色浇注之外还缺什么?
算法·生活
星之擎3 小时前
Task 很优雅的取消方式是什么?C# 里面。
开发语言·c#
晒太羊的猫3 小时前
hot100子串
java·算法·leetcode
小豆庄4 小时前
视频脱敏引擎的追踪难题:转场误判定、ID撞号和淡入窗口
qt·算法·目标检测·音视频·视频·敏捷流程
海清河晏1114 小时前
加油站问题
数据结构·算法·leetcode
GG.WinGel4 小时前
质数数目(蓝桥杯练习题)
java·数据结构·算法·青少年编程·蓝桥杯
艾iYYY4 小时前
进程的状态
算法