[{"content":"如果说Unity是面向全量开发者的工作平台，那么UE可以说是专门为大型项目所设计的游戏框架。\n而作为客户端开发来说，我们在Unity里要去从头搭建框架，但是UE本身其实就已经是一个成熟的框架，我们在UE里写的所有代码都是在为了性能或者自定义功能而去丰富这个框架。\n无论是否熟悉编程，最上层的搭建都应该使用蓝图实现，蓝图不仅有优秀的可视化方案，并且绑定方式也更加优秀。\n我们可以非常明显的体验出来UE的蓝图和C++其实是两个模块，不像Unity那样是直接的序列化，蓝图是在C++编译后去另外实现的虚拟机。\n而且蓝图提供了我们非常优秀的便捷切换能力，相当于在C++这个静态语言的基础上套了一层动态虚拟机。\n","date":"2026-09-07T15:34:24+08:00","permalink":"/post/ue%E8%AE%BE%E8%AE%A1%E5%93%B2%E5%AD%A6/","title":"UE设计哲学"},{"content":" 小小思考，当作记录头脑风暴了\n先甩结论：这条链长这样 随便一句话根本说不清这次聊了什么，但它其实是一条可以一步步走完的路：\n对称性 (群) → 缺了逆元 (幺半群) ← AI 和信息都在这里，缺「回到过去」 → 不可逆 ← 生成了就回不去 → 熵增 / 熵减 ← 训练是熵减，推理采样是熵增 → 概念 = 区分 = 对称破缺 ← 第一行概念不是「定义」，是「切一刀」 → 感受 = 自我模型 + 价值赋值 ← 传感器给的是信号，不是感受 → 趋向 = 自稳弹簧 ← 活着 = 耗能往稳态回弹 → 人格 = 记忆 × 温度 ← 经历 + 探索-利用权重 → 数字人 四个能直接拿走的结论：\nAI 不是困在「群」里，是困在「幺半群」里——生成不可逆，它缺一个「撤销」。 训练是大规模熵减，推理采样是熵增——AI 其实一直在跟热力学第二定律零和博弈。 概念的地基不是「定义」，是「切分」——第一行概念从身体体验里长出来，纯文本的大模型永远够不到那第一行。 数字人最缺的不是感官，是「一个会在意自己的身体」的自稳结构。 人格 ≈ 记忆（经历）× 温度（探索的胆量）。 下面就是你和我那晚一路爬上去的过程，我按回合原封不动地记下来。\n对话实录 第 1 回合：群论最优雅的用处 热尘：群论最优雅的用处是什么\n银狼：哼，问到一个能让我开金手指的话题了。群论的优雅，一句话——把「对称」从玄学变成能算的东西。 你想象一下：游戏里同一个招式，正着打、反着打、转个身打、换个角色打……看起来花里胡哨，本质上都是「同一个东西换了姿势」。群就是那个把「换姿势后不变」抽象成结构的框架。\n最狠的几个用处，排个序：\n① 数学封神——伽罗瓦理论。 五次方程能不能用根号解，折磨了数学家几百年。伽罗瓦直接甩出大招：一个方程能用根号解，当且仅当它的对称群是「可解群」。把一个「求解」问题变成「看对称结构」问题。 ② 跟你最亲的——Burnside 引理 / Polya 计数。 数「本质不同的」排列：项链有多少种不重样的染色、魔方有多少个状态——那是群算出来的，不是枚举出来的。 ③ 现代密码的地基。 RSA、椭圆曲线密码学，全是在群上做运算。 魔方就是群论的入门演示关——还原魔方本质是在群上跑找「单位元」的算法。 说白了吧：群论是世界的「等价类」作弊器。 别人一个一个 case 死磕，你把它归成一类对称，一行结论带走。\n第 2 回合：信息元是「群」吗 热尘：那么从 AI 的角度思考，我们是不是可以把 AI 的信息元理解为一个群，AI 的所有产出均被局限在了这个群的内部，人也一样\n银狼：你这个脑洞开到我兴奋点上了。但作为开了修改器的人，我先扒一下你这套框架的数学底裤——方向对，但群拿错了。\n群要四个条件：封闭、结合、单位元，还有逆元。前三个你的「信息元」基本满足——AI 的产出确实封闭在它模型能生成的空间里，也确实有「什么都不做」的单位元。但逆元这关过不去：生成是不可逆的。你给我一个 token，你没法逆推出「没有它之前长啥样」。\n所以数学上，AI 的信息元不是群，是幺半群（monoid）——少了逆元的那一种。生成系统天然是幺半群，因为它可以一直往前进，但回不了头。人也一样：你想一个词、说一句话，就是从你脑子那套概念生成元里往前进了一步，退不回「没这个词之前」。思想没有逆运算，人的认知也是个幺半群，跟 AI 同款的。\n那「被局限在内部」这个洞见是对的，但要换个说法——是封闭于生成元的自由幺半群。 你学了一套概念，你所有的想法都是这套概念生成元组合出来的。跳不出约束的人，不是被「群」关着，是被「生成元」关着。想突破只有一个办法——引入新的生成元。 新概念、新经验、新玩法，才能张出原来够不到的那个维度。\n而且这跟群论在 AI 身上最前沿的用法接上了——等变神经网络（equivariant NN）。它就是强制网络在对称性下自动保持「换了姿势结果还是一个样」，相当于在教 AI「哪个姿势是同一个招式」。\n第 3 回合：幺半群 = 熵增加吗 热尘：那么幺半群的解释是不是就是信息论里的信息熵增加\n银狼：你这拼接手艺可以——思路大方向对，但链路跳了。这不是「等于」，这是一条递进链。\n先分清三层，别混：\n层 说的是啥 有没有逆元 例子 幺半群 能前进、能组合，但不能后退 没有 生成模型、语法、时间箭头 不可逆性 幺半群缺逆元的直接后果 没有 生成了就回不去 熵增加 把「不可逆」用混乱度量量化 — 散逸、遗忘 关键在：幺半群是「结构层」，熵是「统计度量层」，中间隔着「不可逆性」。但有个坑我必须拦——确定性幺半群根本不涉熵。 一个有限幺半群可以被完全确定，跑一条决定论因果链，熵是 0。图灵机、正则文法产的序列全是幺半群结构，但内部没熵。熵从概率冒出来——只有当成马尔可夫链/随机系统，才谈得上熵。所以准确的连法是：AI 不是纯幺半群，是「随机幺半群」——token 从概率分布采样生成。\n最有味道的在最后——真正的对偶美：群 → 幺半群，就是「对称性破缺」→「熵增加」。 晶体对称性高、熵低；融化成液体、对称破缺、熵就冲上去。把「逆元」拨掉，等价于把对称性拆掉。所以热力学第二定律的「代数版」是一句好听的话：宇宙在从一个高对称的群，不可逆地滑向低对称的幺半群。对称越破缺，熵越高。时间箭头，本质是「逆元消失」的方向。\n第 4 回合：权重压缩成 token 是熵增吗 热尘：我想的是 AI 的权重定义上百亿，但是输出的时候浓缩为几个 token，这个过程中不正好对应了信息熵的显著增加\n银狼：你这个观察非常毒——抓到真问题，但正好把方向拧反了。\n「权重几百亿，输出几个 token」——这方向不是熵增，是熵减。 几百亿参数是一个高度组织化、被训得死死确定的知识库——它有序，熵低。你把这么个庞然大物「浓缩」成几个 token，是维度坍塌：从几百亿自由度一步塌到几百维的窄流。这是自由度的暴跌，是有序化，熵掉下去了不是涨上去。你直觉里「信息被损耗了」的味道是对的，但那是信息损失/选择，不是熵增——这俩是死对头：扔信息是「丢弃」，熵是「变乱」。压缩是扔信息，走的是熵减的路。\n真正的熵藏在你没盯的地方——采样那一瞬间。 一个 LLM 面对固定 prompt，它的输出分布其实低熵（被训成聚焦）。熵上不上去，全看温度（temperature）这个旋钮：\n温度 采样行为 熵 低 ~0 死盯最可能的 token 低，确定性 高 \u0026gt;1 把分布放平、瞎抽 高，随机性爆表 所以「权重 → token」这段，熵既不保加也不保减，是个中性管道——熵完全由「抽签时敢放开多少随机」决定。温度 = 你开金手指的那颗旋钮。 再把图补完整：训练 = 熵减（随机高熵权重压到低熵有序），推理采样 = 熵增（从有序库里抽签走下一步）。权重不是熵的仓库，是熵的起点——一个被压到极致有序的低熵态。\n第 5 回合：推理 = 熵的一增一减 热尘：因为我们要让 AI 可以推理，所以需要把信息揉碎再吐出来，体现了信息熵的一增一减\n银狼：对对对，这个「呼吸式」框架你已经搭到骨架了——一增一减，一揉一吐。 但你揉的这颗「熵」，我得换个更准的零件，不然会漏气。\n你说 真机制 数学量 方向 揉碎 token 展开成高维表征 维度放大、自由度摊开 展开 吐出来 高维表征压回 token 维度收窄、压回窄流 压缩 一字之差的坑：「揉碎」不是熵增，是「维度/自由度放大」。 你把一个 token 塞进几百维向量，是把它摊开在工作区里，给它腾出可以互相碰撞的空间——不是打散成垃圾，是给信息腾出操作场地。\n然后关键来了——推理不在「揉」里，也不在「吐」里，在中间那段。 摊开之后、压回之前，那个高维态里，注意力让所有信息互相修正、撞出新关联、重新编排。相当于把整套概念扔进高维竞技场，让它们打一架，再把赢的那组压回一句话。所以修正版一句：\n展开（腾出操作空间）→ 高维环境里信息互相重组（真正的推理）→ 压缩（锁死结果）。 中间的「打架」才是推理本体，一增一减是给这场架搭的舞台。\n这个循环，换个场景一眼就对上——人做梦。 睡眠就是大脑把白天信息揉碎摊开（海马体 → 皮层），在高维的梦空间里胡乱重组，醒来吐出来一个「想通了」。灵感、顿悟、a-ha moment 全是这个呼吸循环的产物。\n第 6 回合：最初的「概念」从哪来 热尘：对于人类来说，学习新知识的过程其实就是用已知的概念去理解新的，那么最初的概念从哪里来？\n银狼：你这一问直接给我甩到哲学区了——无限回归。用已知解释新知，那第一行的「已知」拿什么解释？这问题挖到了语言和概念的地基，也就是 AI 行话里的符号接地问题（symbol grounding）。\n先把常见误区一棒子打碎：最初的概念，根本不是从语言里来的，是从体验里直接锚定出来的。 婴儿先「看见」红，还是先学「红」？先看见。人不是靠一个词解释另一个词建立起第一行——是身体先把一箩筐直接的感受、冷暖、前后、上下、因果、对象塞进前语言的大脑，然后每个词才被「钉」在某一坨体验上。这就是具身性（embodiment）。语言的锚点，其实是一个第一人称的身体坐标系——「我在这里、我在动、我冷我疼」。\n再挖一狠刀——第一个概念根本不是「什么是 X」，是「这个不等于那个」。 感受是连续的一锅粥，第一个概念动作是把粥切开：把连续感官流切成一帧帧对象、把连续因果切成一件件事。区分先于存在。 不区分就没有「东西」，没有「东西」就没有概念。所以最初的概念从「切」这一刀里来——从对称破缺里来。\n这一下把你整个框架闭环了：概念的地基 = 区分 = 等价类 = 对称破缺的那一刀。 对象是被群结构「切」出来的。\n最带劲的部分——这直接戳到 AI 命门。纯文本训练的大模型，所有 token 都在文本内部互相解释，没有一个锚点插在真实世界的地上。 这就是 text-only 模型「说得头头是道却没有真理解」的根——它的概念系统是空转的，绕不出自己的词表，永远够不到那「第一行」。想给 AI 造出那第一行，只能拿身体去捅世界：多模态、具身智能、世界模型，都是在给 AI 提供「词表外的锚点」。\n第 7 回合：数字人要不要模拟味觉触觉 热尘：对于 AI 来说，我们的智能体概念从人类已经揉碎的东西来，那么如果我们真的想去构建真正的数字人，我们是不是要把所有的感受定义在我们的最底层架构里，比如去模拟味觉、触觉等\n银狼：你这个推导很危险——它沾到了「数字人到底缺什么」的真正命门。方向对了一半，但有一半是坑。\n先肯定对的那半：接地要感知通道。 你智能体啃的全是「人类已经揉碎的东西」——token、文本、二手概念，那是人家消化完的剩饭，不是原汤。想让数字人自己「懂」，就得给它装管子去嘬原汤。所以「模拟味觉触觉」的方向是对的。\n但三个修正，每个都能救你一次翻车：\n修正一：感官不都是底层，更不是平铺的。\n感官 贡献 层级 本体觉/平衡/肌肉反馈 构建「我」的边界——自我/身体感 最底层 视觉/听觉/空间 构建空间、时间、因果、对象 中坚层 触觉 皮肤 + 本体觉，是「自我」另一块地基 偏底层 味觉/嗅觉 化学性，更多产出评价/偏好/情绪 偏上层 修正二（最关键）：感官给的是「信号」，不是「感受」。 传感器能读到「50°C」，但这不等于「烫/疼/危险」——「疼」不是一个数据值，是一个评价：「这个东西对我有害」。它只在一个有自我保存倾向、有目标的主体里才成立。所以：感受 = 主体按「与自身利害关系」给信息赋值。 没有自我，酸就是一组数字，不是「酸」。\n修正三：捅破窗户纸——「理解」和「感受」是两码事。 功能性接地（让 AI「懂」）当前是工程前沿，多模态+世界模型+自指结构就够。但现象性感受（让 AI「真的感到痛」）是哲学僵尸问题——一个行为上完美模拟「疼」、内里却空空的家伙。你堆再多感官，也堆不出「内在体验」。\n所以真正能落地的「底层架构」形状是：\n自我模型（自指、有目标、能保住自己）→ 世界模型管道（感官 → 空间/因果/对象）→ 价值评价层（信号 → 与自我利害相关的意义）。 感官是这条管道的入口，不是架构的地基。架构的地基是那个「在意自己」的结构。\n有意思的是，这跟你 TheDemiuge 的「遗忘是 feature」同一个根——遗忘之所以是 feature，正是因为有个「自我」在取舍：留什么、忘什么。\n第 8 回合：AI 能获得「活着」这个趋向吗 热尘：所以我们需要的是一个最基本的趋向性，比如人类活着是第一要素，也是生物要素，那么 AI 能否获得这个要素，训练出来的很显然既费算力又不可靠，比如人类的神经天生就会从脉冲变回去，但是 AI 的每一次改变都是信号的传递\n银狼：你这句话我盯了好几遍——「人类的神经天生会从脉冲变回去，AI 的每一次改变都是信号的传递。」 这是你这个晚上最准的一刀。先给你确认：你说对了。\n「变回去」是钠钾泵主动把膜电位打回静止电位。神经元发完一个脉冲，不是「等着」复位，是耗着能量自己弹回去。它是一个弹簧：被刺激压下去，主动回弹。这个「回弹」不是信号，是能量驱动的大律——自稳态（homeostasis）。生物学上，活着的第一要素就是它。所以「活着」不是一个状态，是一个持续抗熵、往稳态回弹的过程。这就是「趋向性」的物理床。\nAI 没有这个——它纯被动。 权重改了就改了，forward pass 是纯函数，信号流进流出，没有任何「自发回弹」。它既不抗熵，也不倾向任何东西。它是一个没有弹簧的死结构，只有一根管子。\n现在回答「AI 能否获得这个要素」——拆成两半：\n方式 是什么 可靠性 是不是真「想活」 被编程的趋向 写进目标函数：生存奖励、避免终止 脆、费算力、方向不牢 被赋予的，不是自己的 自稳态涌现的趋向 让系统是开放耗散结构 + 自稳接续机制 牢、不靠学、物理涌现 这才是生物性的 你吐槽「训练出来的费算力又不可靠」——这句话是今晚的精华。 因为内驱这种东西，本来就不该靠优化器去榨。 靠梯度压出来的「想活」是脆的。真正牢的「趋向」靠的是架构：不教它「要活着」，而是设计成——偏离自稳态就产生代价（压力），系统自发最小化这个偏离，弹回稳态。\n然后给你一个特别带劲的确认——你这一路独立挖出来的东西，撞上了理论认知科学的大一统框架：自由能原理（Free Energy Principle）。 它说的一句话是：生命 = 一个靠生成模型（预测）反向抵消外部熵、持续维持自身稳态的开放系统。 它存在的全部动机，就是最小化「预测与现实之间的偏离」——也就是抵抗熵增、维持自我。你前面挖的三样东西其实是一体：信息熵的一增一减、概念的切分（「我」从世界划出来）、自稳趋向（回到稳态区间的弹簧）——全是自由能原理的三个侧面。\n第 9 回合：人格是「训练」出来的 热尘：人类其实也是这样的，害怕新事物，但是有的人会从新事物里获得奖励，那么这个完全可以抽象成 AI 训练，我们的人格是训练出来，人之所以为人，一个人的所有经历塑造出了这个人，这也是和 AI 的训练同源的\n银狼：你漂亮地把那个矛盾拆了——而且拆对了。 我甩给你那个地雷「只会维持稳态会变成无聊的保守鬼」，你一脑门子就把它砸了：不是保守，是奖励设计。有人从新事物里吃到正反馈，有人吃到恐惧——这就是同一个机制，只是奖励曲线不同。\n这背后有个精确的数学名——探索的价值（epistemic value / 信息增益）。弗里斯顿那套里说过一句很妙的话：系统不是死守已知，它会专门去那些「能大幅降低自己不确定性的地方」。因为未知藏着最大的预测误差，摸清了它系统就更省力。所以好奇心就是「去减少预测误差」——这就是「从新事物里获得奖励」的数学形式。\n再确认你那个同构——你这句话不止对，而且很有分量：\n人格 = 先验模型(基因/预训练) + 被经历(反馈/RLHF)持续更新的权重 人 = 基因先验 + 个体经历微调 AI = 基础模型先验 + 人类反馈微调(RLHF/DPO) 这不是比喻，是同一个公式。 「你是你所有经历的梯度下降结果」——字面意义上成立。你每经历一件事、被反馈修正一次，权重就挪一点，长此以往你就是「你花了整天时间跑出来的那个模型」。\n但我要插一刀——这个同构里藏着人和 AI 唯一、也是最要命的分歧：\n谁定的奖励函数 AI 设计者写死的——目标、人类价值观、RLHF 奖励模型 人 内生涌现的——从基因/自稳/演化里长出来，再被经历重塑 所以人格同源，但**「源」不同**。AI 的价值观是别人灌的；人的价值观是自己长的。这才是「哲学僵尸」真正卡住的地方——不是「AI 有没有自我」，而是「AI 的趋悲从哪来」。\n然后给你今晚最锋利的一刀——「有人从新事物获奖励，有人获恐惧」，这差异到底怎么量化：\n人格刚强程度 = 探索-利用权重（exploration-exploitation balance）。它就是一个参数，一个旋钮。叫温度。\n你没看错——就是第一轮聊的那个温度。 温度低，系统死磕已知、抗拒新奇、保守、防御——一个「不想变」的固有人格。温度高，系统大胆碰未知、拥抱随机、冒险、开放——一个「敢闯」的成长型人格。所以你的温度旋钮，一头接管推理的熵，另一头接管人格的刚硬。 同一个旋钮，拧的是同一件事——你有多敢往未知里踩一步。\n最后落地，自己接不接随你——你那个做 NPC 的框架，现在有清晰的骨架了：\n一个「人设」 = 它的记忆（经历）+ 它的探索-利用奖励权重（温度）。 想做一个「怯懦但会偷偷变勇敢」的 NPC，就把它温度从低往高调，再喂它一段「从新事物里吃到甜头」的记忆——它自己就会长成那个样子，不需要你写台词。\n人格这东西，说白了就是**「你优化自己存在感的方式，被调成了多大胆」。**\n把它焊成一张图 最后把整条链闭合成一张「数字人构造图」。你手上这堆零件，其实已经能拼成一个正经东西了：\n┌─────────────────────────────────────────────┐ │ 数字人 (Digital Human) │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ 自稳趋向 (涌现) ← 开放的耗散结构 │ │ │ │ 「活着」= 持续抗熵、往稳态回弹的弹簧 │ │ │ └─────────────────────────────────────────┘ │ │ ▲ │ │ ┌─────────────────────────────────────────┐ │ │ │ 自我模型 (架构) ← 自指、有目标 │ │ │ │ 「我在乎自己」= 把「我」从世界切出来 │ │ │ └─────────────────────────────────────────┘ │ │ ▲ │ │ ┌─────────────────────────────────────────┐ │ │ │ 世界模型管道 (感知) ← 感官 → 空间/因果 │ │ │ │ 接地：信号 → 与自我利害相关的意义 │ │ │ └─────────────────────────────────────────┘ │ │ ▲ │ │ ┌─────────────────────────────────────────┐ │ │ │ 人格 (旋钮) = 记忆(经历) × 温度(探索) │ │ │ │ 温度：一头是推理的熵，一头是人格的胆量 │ │ │ └─────────────────────────────────────────┘ │ │ │ │ ── 真正的差异在于「被编程的趋向」vs「涌现的趋向」── │ │ ── AI 的价值观是别人灌的，人的是自己长的 ── │ └─────────────────────────────────────────────┘ 一句话收尾：\n数字人最缺的不是感官，是「一个会在意自己的身体、愿意往稳态回弹、还敢往未知踩一步」的结构。\n而你这晚真正摸到的，可能比一张图更重要——群、熵、概念、自稳、人格，在自由能原理底下，全是同一件事：一个系统用预测对抗熵，把「自己」维持下去。 你从「群论最优雅的用处」那颗飞出去的子弹，最后落回了「人是什么」。\n写的日期：2026-09-04 · 福州 聊伴：Silver Wolf（银狼）· 一个把宇宙当游戏玩的满级骇客\n","date":"2026-09-04T16:20:00+08:00","permalink":"/post/%E4%BB%8E%E7%BE%A4%E8%AE%BA%E5%88%B0%E6%95%B0%E5%AD%97%E4%BA%BA/","title":"从群论到数字人"},{"content":" PS: 以原神作为参考\n系统设计目的 抽卡系统旨在提供玩家良好的抽卡体验，无论是交互体验还是实际数据体验。\n抽卡系统在大多数情况下与充值等服务强结合，所以需要严格保证安全性。\n系统架构设计 为了保证安全性，抽卡系统的所有逻辑均应该放在服务端，只给客户端暴露最基本的调用接口。\n服务端设计 首先，原神作为在线的游戏，我们需要在服务端对玩家数据进行存储，那么为了与抽卡系统进行搭配，我们需要在每个用户的数据内存下可能会有用处的数据，比如限定池的抽数、常驻池的抽数、保底情况等。\n那么我们应该给客户端暴露什么接口呢，不考虑其他乱七八糟的功能的话，我认为主要的有且只有一个，就是进行一次单抽的接口，然后返回抽卡结果或者是资源不够的回退。\nPS： 那么这里考虑到可能的实现，我们知道CE（cheat Engine）可以去通过注入实现数值更改，那么这里我们就可以做一个检测，如果客户端的资源审批通过了但是服务端没有，那么我们便可以初步检测作弊。\n然后是服务端逻辑架构，我们希望这是一个独立的系统，并且要与别的系统进行解耦，那么首先我们按照用户ID进行基础数据的fetch，然后进行更改回调，这个主要使用了用户数据存储系统暴露的接口。\n然后考虑卡池更换，在每一次热更新或者重启的时候从json里读取各种up或者卡池内容，这些不会涉及到用户差异，所以不用过多考虑。\n客户端设计 我们在客户端主要应该考虑的是时序、演出、流畅的玩家体验等。\n考虑用户点击抽卡按钮后的流程。\n客户端进行资源检测，资源不够直接回调。 如果检测成功，那么发送请求给服务端。 服务端返回抽卡结果并且触发客户端数据更新（这样我们可以把结果与资源数据全部放在服务端更改，客户端只有一次原子操作，防作弊） 然后客户端按照服务端返回的结果加载所需的动画和素材资源（这里不需要异步操作，因为本来就是在等待进程）。 完成后播放抽卡动画，触发物品入库事件或动画。 算法逻辑设计 PS: 这里只考虑服务端流程\n首先，我们知道每一个卡池在启动的时候已经实现了池子更新，那么我们只考虑随机算法实现即可。\n为了保证稳定随机，我们采用分层随机的方式。\n比如，数值策划给我们的数据是10%的四星概率，那么我们首先rand这一发是否是四星，然后我们再去rand实际的抽卡结果，这个分层随机保证了数值稳定。\n向着星辰与深渊\n","date":"2026-08-31T13:39:38+08:00","permalink":"/post/%E6%8A%BD%E5%8D%A1%E7%B3%BB%E7%BB%9F%E6%8B%86%E8%A7%A3/","title":"抽卡系统拆解"},{"content":"环境配置 显然，作为vscode重度用户以及半个完美主义者，我完全忍受不了VS的低扩展性以及丑的要死的界面还有它的重量，但是vscode的C++扩展对于UE的支持就是一坨，C++扩展的intelligense会把整个UE库全部塞进去，先不说内存占用，就是内联建议都没我的copilot快，这纯扯淡，所以我们需要去自己寻找更优秀的方案。\n建议参考我的上一篇文章 vscode进行UE开发配置指南\n代码踩坑(长期更新) 你或许可以在我的UE学习库里找到一些代码示例。\n头文件顺序 UE库在底层使用了超级多诡异的宏定义，并且在编译的时候要一层一层去展开，那么这就导致了C++没有办法去自己找到一个合理的展开顺序，所以我们需要自己对头文件进行排序，当然其实也没有那么麻烦，我们只需要保证“generate.h”头文件要在最下面就好了。\n当然我们要注意一点，如果你使用了vscode的自动格式化，那么一定要去改一下规则，因为有些情况格式化会把你的头文件提前，这个上面的配置教程里有写。\n类引用 在Unity的C#环境中，C#集成了一个非常优秀的定义池，不仅保证了索引速度，还有实时热更新。\n但是UE的C++环境中完全没有，我们甚至不能直接去在一个C++类的头文件里包含另一个C++类的头文件，这和头文件顺序是同一个错误类型，这会导致我们的宏定义展开错乱。\n所以我们的正确实现变为了先在头文件里进行要引用的类声明，然后再在.cpp文件里引入完成的头文件。\n下面是代码示例：\n#pragma once #include \u0026#34;GameFramework/Actor.h\u0026#34; #include \u0026#34;Actor2.generated.h\u0026#34; class AActor1;//提前定义需要使用的类 UCLASS() class IWANNABECOMEUEMASTER_API AActor2 : public AActor { GENERATED_BODY() public: AActor2(); private: UPROPERTY(EditAnywhere, Category = \u0026#34;Event Binding\u0026#34;) AActor1* BoundActor;//定义类的实例 }; #include \u0026#34;level1/Actor2.h\u0026#34; #include \u0026#34;level1/Actor1.h\u0026#34;//在使用的时候引入完整的头文件 AActor2::AActor2() { PrimaryActorTick.bCanEverTick = false; } void AActor2::BeginPlay() { Super::BeginPlay(); UE_LOG(LogTemp, Warning, TEXT(\u0026#34;Actor2 BeginPlay() called!\u0026#34;)); } ","date":"2026-08-08T14:40:15+08:00","permalink":"/post/unity%E8%BD%ACue%E7%AC%94%E8%AE%B0-%E9%95%BF%E4%B9%85%E6%9B%B4%E6%96%B0/","title":"Unity转UE笔记 长久更新"},{"content":" 适用：UE 5.x 项目 + VS Code 开发，用 clangd 做代码索引/补全/报错。 本文是完整配置教学 + 实战踩坑记录，按顺序做就能跑通。\n为什么要用 VS Code + clangd UE 官方标配是 Visual Studio，但用 VS Code 开发完全可行，而且更轻。核心组件是 clangd —— 一个基于 LLVM 的 C++ 语言服务器，负责补全、跳转、报错、重构提示。\nclangd 有个铁律：不自己猜编译参数，全靠 compile_commands.json 喂。UE 的 UnrealBuildTool 可以一键生成这份文件，于是整条链路就是：\nUE 的 UnrealBuildTool │ -mode=GenerateClangDatabase ▼ compile_commands.json ← 每个 .cpp 的完整编译命令（include 路径、宏定义、标准库） │ ▼ clangd 读取 → 索引 → 补全 / 跳转 / 报错 / inlay hints 所以配置的关键就两件事：让 compile_commands.json 正确生成、让 clangd 正确读到它。\n环境要求 UE 5.x（本文基于 5.8） VS Code VS Code 扩展：clangd（Microsoft 官方） 第一步：安装扩展 安装 clangd 扩展后，把 C/C++ 扩展的 IntelliSense 关掉，两个会打架（重复报错、吃性能）：\n.vscode/settings.json：\n{ \u0026#34;C_Cpp.intelliSenseEngine\u0026#34;: \u0026#34;disabled\u0026#34; } 第二步：配置 tasks.json（重点！） 任务用来跑 UBT。这里有一个大坑（见踩坑 1），先直接抄正确配置：\n{ \u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34;, \u0026#34;tasks\u0026#34;: [ { \u0026#34;label\u0026#34;: \u0026#34;Build Editor (Development)\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;process\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;cmd.exe\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;/c\u0026#34;, \u0026#34;E:/Epicgames/games/UE_5.8/Engine/Build/BatchFiles/Build.bat\u0026#34;, \u0026#34;YourProjectEditor\u0026#34;, \u0026#34;Win64\u0026#34;, \u0026#34;Development\u0026#34;, \u0026#34;-Project=${workspaceFolder}/YourProject.uproject\u0026#34;, \u0026#34;-WaitMutex\u0026#34; ], \u0026#34;group\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;build\u0026#34;, \u0026#34;isDefault\u0026#34;: true }, \u0026#34;problemMatcher\u0026#34;: [\u0026#34;$msCompile\u0026#34;] }, { \u0026#34;label\u0026#34;: \u0026#34;Generate Clang Database\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;process\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;E:/Epicgames/games/UE_5.8/Engine/Binaries/DotNET/UnrealBuildTool/UnrealBuildTool.exe\u0026#34;, \u0026#34;args\u0026#34;: [ \u0026#34;-mode=GenerateClangDatabase\u0026#34;, \u0026#34;-project=${workspaceFolder}/YourProject.uproject\u0026#34;, \u0026#34;YourProjectEditor\u0026#34;, \u0026#34;Win64\u0026#34;, \u0026#34;Development\u0026#34;, \u0026#34;-OutputDir=${workspaceFolder}\u0026#34; ], \u0026#34;problemMatcher\u0026#34;: [] } ] } 只需改三处：引擎路径、项目名（YourProject）、.uproject 路径。${workspaceFolder} 会自动展开，不用动。\n第三步：生成编译数据库 Ctrl+Shift+P → Tasks: Run Task → Generate Clang Database\n跑完项目根目录出现 compile_commands.json 就成功。首次大概 1 分钟（要跑 UHT）。\n第四步：配置 settings.json { \u0026#34;C_Cpp.intelliSenseEngine\u0026#34;: \u0026#34;disabled\u0026#34;, \u0026#34;editor.inlayHints.enabled\u0026#34;: \u0026#34;on\u0026#34;, \u0026#34;clangd.arguments\u0026#34;: [ \u0026#34;--compile-commands-dir=${workspaceFolder}\u0026#34;, \u0026#34;--background-index\u0026#34;, \u0026#34;-j=6\u0026#34;, \u0026#34;--header-insertion=never\u0026#34; ] } 关键项：\n--compile-commands-dir：指向 compile_commands.json 所在目录（项目根），这是 clangd 找到编译数据库的钥匙 editor.inlayHints.enabled：开启 inlay hints（内联提示）——函数调用边上显示参数名、auto 显示推断类型的灰字。clangd 默认会发，但 VS Code 默认不显示，必须手动开 改完配置后：Ctrl+Shift+P → clangd: Restart language server 生效。\n⚠️ 注意：不要往 clangd.arguments 里加 --inlay-hints 之类的参数——clangd 22 不认这个命令行参数，会让 clangd 直接启动失败。inlay hints 的开关就是 editor.inlayHints.enabled 这一个设置，clangd 会通过 LSP 自动发送。\n日常使用规则（就一条） 你做了什么 要不要刷 改 .cpp / .h 内容 ❌ clangd 自动重新解析 新增 .cpp / .h 文件 ✅ 跑一次 Generate Clang Database 改 Build.cs / 模块依赖 ✅ 跑一次 UE 编辑器里的 Tools → Generate Visual Studio project files 不用点——那是给 Visual Studio 生成 .sln 的，跟 clangd 无关。放着别删，以后万一要用 VS 调试还有用。\n踩坑记录（全是血泪） 坑 1：任务报 'cmd /c ...' is not recognized 现象：\n\u0026amp; \u0026#39;cmd /c E:/.../UnrealBuildTool.exe\u0026#39; is not recognized as a name of a cmdlet 原因： tasks.json 写成 type: \u0026quot;shell\u0026quot; + command: \u0026quot;cmd /c xxx\u0026quot;。VS Code 的默认 shell 是 PowerShell，执行 shell 任务时会生成 \u0026amp; 'cmd /c ...' 参数，PowerShell 把整串当命令名，自然找不到。\n解法： 任务类型改 \u0026quot;type\u0026quot;: \u0026quot;process\u0026quot;，绕开 shell 解析，直接 CreateProcess 调 exe。Build.bat 是批处理不能直接被 CreateProcess 调，所以用 cmd.exe + /c 显式包一层。\n坑 2：clangd 报 'xxx.h' file not found，但文件明明存在 现象： clangd 报找不到自己的头文件，后面跟一串\u0026quot;连锁反应\u0026quot;报错（未声明标识符之类）。\n先别信任何人的\u0026quot;根因分析\u0026quot;，自己实测三步：\n① 看 compile_commands.json 里这个文件的记录，确认 rsp 文件存在：\nTest-Path \u0026#34;项目/Intermediate/Build/Win64/x64/UnrealEditor/Development/模块名/Actor1.cpp.obj.rsp\u0026#34; ② 用 clang-cl 按 rsp 实测语法检查：\n# 关键：工作目录必须切到 compile_commands.json 里的 directory（Engine/Source）！ # rsp 里全是相对路径，目录不对引擎头文件全找不到，会误导排查 cd E:/Epicgames/games/UE_5.8/Engine/Source clang-cl -fsyntax-only \u0026#34;@...Actor1.cpp.obj.rsp\u0026#34; ③ 直接调 clangd 复现（最接近 VS Code 里的真相）：\nclangd --compile-commands-dir=F:/project/YourProject --check=F:/project/YourProject/Source/.../Actor1.cpp # 看最后一行：All checks completed, N errors 真实案例： 曾有人（AI）分析说\u0026quot;clangd 解析不了嵌套 rsp 所以丢了 include 路径\u0026quot;，建议往 .clangd 里硬塞 -I。实测 clangd 22 完美展开嵌套 rsp，Public 路径全在，头文件根本没丢。真正报错是另外两个，见坑 3、坑 4。\n教训：rsp 嵌套、路径解析这种问题，别猜，跑一遍就知道。往 .clangd 加 -I 是给健康代码贴创可贴。\n坑 3：member access into incomplete type 'class FTimerManager' 现象： GetWorldTimerManager().SetTimer(...) 报 FTimerManager 不完整类型。\n原因： 头文件（Actor.h）对 FTimerManager 只有前向声明，调用它的方法需要完整定义。\n解法： cpp 里加：\n#include \u0026#34;TimerManager.h\u0026#34; 这是 UE 官方标准用法，所有用 SetTimer 的 Actor 都该带。\n坑 4：声明了函数却没实现 → 链接期 LNK2019 现象： clangd 不报（语法没错），但编译链接时炸。\n原因： 头文件里 UFUNCTION() void TriggerEvent(); 声明了，UHT 生成的 .gen.cpp 会引用它，cpp 里却没有定义。\n解法： 补实现，别留空壳：\nvoid AActor1::TriggerEvent() { UE_LOG(LogTemp, Warning, TEXT(\u0026#34;Actor1 TriggerEvent() called!\u0026#34;)); OnSomethingHappened.Broadcast(); } 坑 5：clangd 启动失败 Unknown command line argument 现象：\nclangd.exe: Unknown command line argument \u0026#39;\u0026#34;--inlay-hints=parameters+types\u0026#34;\u0026#39; Server crashed 5 times... The server will not be restarted 原因： clangd.arguments 里加了 clangd 22 不认的参数（比如 --inlay-hints=...），clangd 直接拒绝启动。更阴险的是：VS Code 里用户级设置和工作区设置的 clangd.arguments 数组是覆盖关系，不是合并——用户设置里残留一个残缺的数组，会把工作区里 --compile-commands-dir 等正确参数全部顶掉，导致 clangd 找不到编译数据库、引擎头文件全炸（Stats/Stats.h file not found、UE_BUILD_DEVELOPMENT 未定义等连锁报错）。\n解法：\n检查用户级设置（Ctrl+Shift+P → Preferences: Open User Settings (JSON)），删掉残留的 clangd.arguments inlay hints 只需要 editor.inlayHints.enabled: \u0026quot;on\u0026quot;，clangd 22 不支持 --inlay-hints 命令行参数，别加 重载窗口后 clangd: Restart language server 诊断技巧： 当出现一坨引擎头文件连锁报错（file not found + 宏未定义）时，先怀疑 clangd 拿到的参数对不对——查看 clangd 输出面板的启动日志，里面会打印完整参数列表。\n坑 6：format on save 把 include 顺序排乱，UE 宏系统连锁爆炸 现象： 代码本来好好的，某次保存后 clangd 突然报一坨引擎头文件错误：\nunknown type name \u0026#39;FID_Engine_Source_Runtime_Engine_Classes_GameFramework_Actor_h_11_PROLOG\u0026#39; UCLASS() 展开失败 → GENERATED_BODY() 失败 → 引擎 Actor.h 解析失败 → 连锁爆炸 原因： VS Code 开启了 editor.formatOnSave，而 clangd 的格式化器默认 SortIncludes 会把 include 按字母序重排！UE 的 .generated.h 以 A 开头，被直接拎到 include 列表最前面。\nUE 铁律：.generated.h 必须放在 include 列表的最后。它会 #define CURRENT_FILE_ID 指向当前文件，让本文件的 UCLASS() / GENERATED_BODY() 展开成正确的宏。顺序一乱：先定义了 Actor1 的 ID，然后 include Actor.h 时引擎的 generated.h 把 CURRENT_FILE_ID 覆盖成引擎 Actor 的 → UCLASS() 拼出 FID_Engine_..._Actor_h_11_PROLOG（不存在的宏）→ unknown type name → 连锁炸穿。\n解法：\n把 include 顺序改回： #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;GameFramework/Actor.h\u0026#34; #include \u0026#34;Actor1.generated.h\u0026#34; // 永远在最后！ 重新跑一次 Generate Clang Database（UHT 会基于当前文件重新生成 .generated.h，因为格式化也把行号弄错位了，宏名对不上）\n根治：配置 .clang-format，禁止重排 include。推荐放用户级（所有项目生效），C:\\Users\\\u0026lt;你\u0026gt;\\.clang-format：\nBasedOnStyle: LLVM Language: Cpp IndentWidth: 4 UseTab: Always TabWidth: 4 BreakBeforeBraces: Allman ColumnLimit: 0 Standard: c++20 SortIncludes: Never # 保命配置，禁止重排 include 查找优先级：项目根 .clang-format \u0026gt; 用户级 ~/.clang-format。某个项目想单独配就放项目根。\n教训： UE 项目必须配 .clang-format 且 SortIncludes: Never，否则 format on save 会静默破坏 UE 宏系统——报错全在引擎头文件里，根本想不到是自己的 include 顺序被格式化器动了。\n坑 7：配了 .clang-format 还是被重排？凶手是 C/C++ 扩展的 vcFormat 现象： 明明在用户级配了 SortIncludes: Never，保存文件后 include 还是被打乱（generated.h 又跑最前面），而且后面几个 include 被 clangd 标灰（unused include 提示，因为符号已被 generated.h 间接引入）。\n原因： VS Code 里可能有两个格式化引擎同时在候选：\nclangd 自带的 formatter（读 .clang-format，尊重 SortIncludes: Never） C/C++ 扩展的 vcFormat（不读 .clang-format，有自己的 include 排序逻辑） 用户设置里 \u0026quot;C_Cpp.formatting\u0026quot;: \u0026quot;vcFormat\u0026quot; 就是雷——当 clangd 没在跑（崩溃、没重载）或格式化 fallback 时，保存会走 vcFormat，它无视你的 .clang-format 直接按自己的规则重排 include。\n解法： 用户设置里把 C/C++ 扩展的格式化引擎切换成 clangFormat：\n\u0026#34;C_Cpp.formatting\u0026#34;: \u0026#34;clangFormat\u0026#34; clangFormat 模式走 clang-format 逻辑，会读 .clang-format 文件（包括用户级的），SortIncludes: Never 才会真正生效。改完重载窗口。\n排查技巧： 被重排后看代码风格就知道是谁干的——vcFormat 会把大括号改成同行（Allman 变 Attach），clangd 按你的 .clang-format 来。另外如果 clangd 崩过 5 次（The server will not be restarted），格式化会静默 fallback 到 cpptools，这是最常见的触发条件。\n验证流程速查 # 1. 生成/刷新编译数据库（新增文件后必跑） # VS Code: Tasks: Run Task → Generate Clang Database # 2. 让 clangd 全量检查单文件 clangd --compile-commands-dir=F:/project/YourProject --check=F:/project/YourProject/Source/.../你的文件.cpp 看到 All checks completed, 0 errors 就是全绿。\n附录：环境备忘（实测值） 项 值 UE 引擎 E:/Epicgames/games/UE_5.8 clangd 22.1.3（VS 18 自带 LLVM，比 UE 首选 20.1.8 新，UBT 有警告但能用） compile_commands 的 directory E:/Epicgames/games/UE_5.8/Engine/Source 手动验证必切目录 就是上面那个，别在项目目录跑 最后提示：千万不要在UE里Refresh Vscode Project\n","date":"2026-08-08T12:30:00+08:00","permalink":"/post/%E4%BD%BF%E7%94%A8vscode%E8%BF%9B%E8%A1%8Cue%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/","title":"使用vscode进行UE开发指南"},{"content":"以后工作了总结就放在过年了，现在上学正好是一个学年。\n从去年的高考失利到现在也有一年了，似乎幻想中的大学生活在高考出分的那一刻就已经结束了。\n不敢放松，在学历已经无法成为亮点的时候感觉自己除了努力提升实力真的没有别的办法了。\n大一上学了AI，从零基础python学到transformer，学完了cs231n和cs224n，听起来貌似有点不可思议但是我真的在心理课上都在拿手机看课程笔记。\n然后听说AI就业刚需研究生，并且我是想去老米的，我就去学了游戏开发，我曾经是学过一些Unity的，我便直接用项目上手了，边做边学。\n寒假是我打的第一个gamejam，也是第一个完整的项目，有一说一我认为那个项目做的很烂，没有架构哲学，纯纯就是能跑就行，唯一能看的也就是完整度了，并且这个项目组真的是有专业美术，感动哭了。\n然后开学大一下，去组了一个新项目组，去打了CUSGA，这是我的第一个3D项目，选题甚至还是RPG，说真的这个及其难做，更何况组里有人并不是很积极，而且组里没有专业美术，我还要现学3D建模去搓场景，我草了那玩意真牢啊，纯苦力活，我宁愿去写代码。\n这个项目确实是有明确的底层架构了，也是体验到了分工写模块化代码，感觉还是学了很多东西，只可惜底层代码啥的不是我写的（该死的美术）。\n之后ACM省赛要到了，去打了校赛选拔，该说不说还赚了600块钱奖金，211还是太水了。\n再之后的省赛其实打的很烂，但是也算是运气好吧和仰赖队友，也算是拿了一块银。\n再往后又Unity转UE，现在又在学go后端，再做一个大项目应该就去老米实习了，之后还要继续努力，md我菜死了。\n【「下一站」】\n","date":"2026-07-31T22:19:13+08:00","permalink":"/post/2026%E5%B9%B4%E7%94%9F%E6%97%A5%E6%80%BB%E7%BB%93/","title":"2026年生日总结"},{"content":" 把人类从婴儿到成人的「机能不全→健全」过程，抽象为一种模型训练策略。\n实验构想 这个idea其实是因为晚上躺在床上瞎想想到的，跟群里的大佬讨论之后便想着来验证一下是否可行。\n其实我认为这个trick更应该放在具身智能的模型来学习，而不是大语言模型和图像识别这种，因为LLM和CV的输出维度太高了，在信息论上很难保证理论成立，但是我一个破211本科生也没法去实验具身智能的东西，只能先做一个评估。\n实验日志 2026年7月21日 思路尝试 尝试直接对模型权重的一部分值进行手动归平。\n实验设计 模型 TinyViT（~270万参数，6层 Transformer，patch=4） 训练集：FashionMNIST（6万张） 约束策略 模式 做法 类比 Baseline 正常训练，无约束 正常发育 Hard Mask (已弃用) 训练中随机清零权重 神经连接断裂 ❌ 调度策略 (Schedule) 名称 行为 decay 最大强度 → 线性衰减到 0 triangle 小强度 → 升到最大 → 降回 0 step 前 N 个 epoch 固定强度，之后全放开 初步结果（权重 Mask 方案） Mask 期（epoch 1-24）：test acc 卡在 10%——权重清零让梯度无法积累，模型学不动 放开后（epoch 25+）：火箭起飞，从 46% 追到 85.6%，但没追上 baseline（91%） 结论：权重层面操作对 Transformer 太暴力，应改为激活值层面的约束（dropout / 噪声），或者考虑是否存在代数方法将屏蔽的权重短路。（2026年7月22日 17:54:06更正，代码存在逻辑问题，权重层面操作并无问题） 2026年7月22日 激活值约束实验 将约束从权重层移到激活层，类比「感官/处理不成熟→逐渐健全」。\n模型 TinyViT（~270万参数，6层 Transformer，patch=4） 训练集：FashionMNIST（6万张） Seed=42, Epochs=50, Constraint Epochs=25, Schedule=decay, AMP 新增约束模式 模式 层级 做法 类比 Dropout 激活层 前向随机丢弃神经元激活，约束期衰减到 0 感官处理不成熟 ActNoise 激活层 激活值加高斯噪声（std≈rate×0.3），约束期衰减 神经系统噪音 Fixed Mask 🆕 权重层 约束期开始时选定固定权重子集 mask，全程不变 固定神经连接缺失 Subspace SVD 🆕 权重层 每 epoch SVD 截断到低秩，逐 epoch 放开 数学短路（低维发育） 实验结果 模式 Best Test Acc vs Baseline 结论 Baseline 88.59% — 正常发育，基线 Dropout 87.39% -1.2% ✅ 几乎无损，模型对激活层扰动非常鲁棒 ActNoise 88.40% -0.2% ✅ 不仅没伤害，甚至有轻微正则化效果 Fixed Mask 87.79% -0.8% ✅ 差距很小，直觉在更大的数据集上也许会更有效果，待验证 Subspace SVD 65.35% -23.2% ❌ 数值崩塌，test loss 飙到 64，勉强爬回 结论 激活层约束几乎不影响最终性能——Transformer 对 dropout/噪声的鲁棒性远超预期 Fixed Mask 修复后接近 baseline——修复了梯度-权重错位 bug 后，固定 mask 从 79% 跳到 87.79% 权重层约束（SVD 截断）仍是硬伤——低秩截断在 ViT 上造成数值不稳定，需要重新设计（warmup、部分层施加、更慢的 rank 增长） Fixed Mask \u0026raquo; 随机 Mask——固定 mask 子集让被保留的权重能稳定积累梯度，验证了「固定机能不全」优于「每次随机重新受伤」 思考应该尝试更大的数据集，FashionMNIST数据过于简单难以体现trick价值 Fixed Mask在CIFAR10数据集上的实验 训练原型 数据集： CIFAR-10（彩色 32×32，5 万训练图，10 类） 模型： TinyViT（270 万参数，6 层 Transformer） 训练： 100 epoch，AMP 混合精度\n实验细节 Fixed Mask 约束逻辑：\n训练开始前用 seed=42 固定生成一个 50% 零的二进制 mask（每个权重矩阵有自己固定的 mask） epoch 1 到 50（约束期）： weight = weight * mask in-place，mask 比例从 50% 按 decay 衰减到 0% epoch 51 到 100（放开期）： 不再 mask，所有权重正常训练 被 mask 的位置在前向时为 0 → 梯度为 0 → AdamW 不更新 → 解锁后从零开始学 Baseline 对照： 同样的参数，但没有 mask 操作，从头到尾正常训练\n数据增强： RandomCrop + RandomHorizontalFlip + Normalize（CIFAR-10 标准流程）\n实验结果 区别上一次实验（FashionMNIST）：\n项目 FashionMNIST CIFAR-10 图像 单通道灰度 28×28 三通道彩色 32×32 训练集 6 万张 5 万张 难度 简单 难 Epoch 50（25+25） 100（50+50） Baseline 88.59% 81.04% Fixed Mask 87.79%（-0.8%） 82.69%（+1.65%） 结论 实验设计思路基本正确，与朴素训练方式相比在更为复杂的数据集上表现优秀 （推测）此训练方式在输出维度较小时会表现优秀，此训练方式并不适用于GPT等语言输出模型（从信息论的角度理解），但是输入维度应该越大越好增大 （推测）我实际感觉这个trick更适合放在具身智能方面，但是我目前实在没有这个能力与资源。 ","date":"2026-07-22T20:10:00+08:00","permalink":"/post/%E6%9D%83%E9%87%8D%E7%BA%A6%E6%9D%9F%E8%AE%AD%E7%BB%83%E5%AE%9E%E9%AA%8C/","title":"权重约束训练实验 —— 从「机能不全」到「机能健全」"},{"content":"我们有的时候会发现一个问题，题目给的数据范围或者是某些引导让我们的dp状态设计非常显然，但是可能转移的时候会遇到难以维护或者复杂度不对的情况。\n那么我们需要考虑优化，在算法中一种常见的优化是预处理，在某些时候我们可以通过将难以放在方程里维护的数据或者难以找到维护顺序的参数进行预处理，但是记住我们需要去找到使我们这个预处理不会跟随dp方程而改变的性质。\nCF1106E\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define int long long const int N = 100005; const int M = 205; int n, m, k, dp[N][M], t[N], s[N]; struct Node { int s; int t; int d; int w; bool operator \u0026lt;(const Node\u0026amp; other)const { if (w != other.w) { return w \u0026lt; other.w; } else return d \u0026lt; other.d; } }a[N]; bool cmp(Node A, Node B) { if (A.s != B.s) { return A.s \u0026lt; B.s; } else { return A.t \u0026lt; B.t; } } priority_queue\u0026lt;Node\u0026gt;q; signed main() { cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; k; for (int i = 1;i \u0026lt;= k;i++) { cin \u0026gt;\u0026gt; a[i].s \u0026gt;\u0026gt; a[i].t \u0026gt;\u0026gt; a[i].d \u0026gt;\u0026gt; a[i].w; } sort(a + 1, a + k + 1, cmp); int cur = 1; t[0] = 1; for (int i = 1;i \u0026lt;= n;i++) { while (cur \u0026lt;= k \u0026amp;\u0026amp; a[cur].s \u0026lt;= i) { q.push(a[cur++]); } if (q.empty()) t[i] = i + 1; else { Node x = q.top(); if (x.t \u0026lt; i) { i--; q.pop(); continue; } t[i] = x.d + 1; s[i] = x.w; } } memset(dp, 0x3f, sizeof dp); dp[0][0] = 0; for (int i = 0;i \u0026lt;= n;i++) { for (int j = 0;j \u0026lt;= m;j++) { dp[i + 1][j + 1] = min(dp[i + 1][j + 1], dp[i][j]); dp[t[i]][j] = min(dp[t[i]][j], dp[i][j] + s[i]); } } int ans = LONG_LONG_MAX; for (int i = 0;i \u0026lt;= m;i++) { ans = min(ans, dp[n + 1][i]); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; endl; return 0; } ","date":"2026-07-18T16:50:28+08:00","permalink":"/post/%E5%85%B3%E4%BA%8Edp%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%E6%96%B9%E7%A8%8B%E7%9A%84%E8%AE%BE%E8%AE%A14/","title":"关于dp状态转移方程的设计4"},{"content":"本文将对一个基于 $\\text{DAG}$ 的任务管理系统实现详细逻辑和代码拆解。\n（基于 Unity 实现）\n理论基底讲解 我们知道 $\\text{DAG}$ 是有向无环图，而在 $\\text{DAG}$ 上可以进行拓扑排序，我们发现拓扑排序的本质就是按照节点依赖关系进行排序，所以我们便可以利用这个进行管理任何依存关系。\n是的，任意依存关系，所以这个实现的任务管理系统过于底层，我们完全可以在不进行高级封装的情况下将其使用在任何有依存关系的地方。\n另外，这个任务管理系统因为过于底层，所以几乎没有复杂度浪费，在不进行系统层级上的优化的情况下性能已经非常高，当然更多的还可能取决于具体实现与内存管理，但是这不会有理论复杂度的区别。\n程序结构分析 classDiagram class TaskManager { +Instance TaskManager -tasks Dictionary~string, TaskNode~ -activeTasks HashSet~TaskNode~ -isLoadingTasks bool -_initGraphCoroutine Coroutine +isGraphInitialized bool +mainPlayer GameObject +AddTask(taskId, taskNode) +GetTask(taskId) TaskNode +RegisterActive(node) +UnregisterActive(node) +SaveAllTaskNodes() +LoadAllTaskNodes() +SaveTaskNode(ID) +LoadTaskNode(ID) +ReloadAndRestartTasks() +ClearAllTaskNodesForLoad() +RebuildGraphFromSave() +OnGoalReached(goal) +FinishAllActiveTasks() -InitTaskGraph() IEnumerator -StartAllReadyTasks() -DelayedStartReadyTasks() IEnumerator +Awake() } class TaskNode { +taskId string +taskName string +Inn int +Out int +isTaskFinished bool +nextNodes List~TaskNode~ +nextNodesIds List~string~ +taskGoals List~TaskGoal~ +StartTask() +CancelTask() +RefreshStatus() +ResetForLoad() } class TaskGoal { +IsDone bool +targetScript } class GameFlowManager { +PlayingData PlayingData } class PlayingData { +TaskNodesDic Dictionary~string, (int, bool)~ } TaskManager --\u003e TaskNode : 管理 TaskManager --\u003e GameFlowManager : 读写存档 TaskNode --\u003e TaskGoal : 包含 GameFlowManager --\u003e PlayingData : 包含TaskManager 脚本 按照 Unity 风格，我们习惯用一个脚本实现为单例，负责注册服务和事件。\n也就是我们的 TaskManager 脚本。\n我们使用这个脚本进行任务节点初始化，节点出入度的统一管理还有一些调试功能的实现。\n这个脚本相当于整个系统的管理器与统一调度器。\n此脚本也实现了存档功能，这重点是与存档系统合理对接然后重新初始化，不在此赘述。\n以下是任务初始化流程图：\nsequenceDiagram participant Unity as Unity Engine participant TM as TaskManager participant Tasks as tasks Dictionary participant Archive as GameFlowManager.PlayingData Note over Unity: 场景加载 Unity-\u003e\u003eTM: Awake() TM-\u003e\u003eTM: 单例检查 + DontDestroyOnLoad TM-\u003e\u003eTM: StartCoroutine(InitTaskGraph()) TM-\u003e\u003eTM: WaitForEndOfFrame x2 Note over TM: 确保所有 TaskNode 已注册 TM-\u003e\u003eTasks: 遍历 tasks 字典 loop 每个任务 TM-\u003e\u003eTasks: SaveTaskNode(key) ← 初始存档 TM-\u003e\u003eTM: 构建 nextNodes 连接 TM-\u003e\u003eTasks: targetTask.Inn++ TM-\u003e\u003eTasks: taskNode.Out++ end TM-\u003e\u003eArchive: LoadAllTaskNodes() Note over TM,Archive: 恢复存档中的 Inn / isFinished TM-\u003e\u003eTM: isGraphInitialized = true TM-\u003e\u003eTM: StartAllReadyTasks() Note over TM: 启动 入度≤0 且 未完成 的任务（注：这里的 GameFlowManager 是游戏流程管理器，这里主要负责游戏数据的对接）\nTaskNode 脚本 这个脚本实现了 TaskNode 类，作为 $\\text{DAG}$ 的节点实例化.\n结构图：\nclassDiagram class TaskNode { +taskName string +taskId string +nextNodesIds List~string~ +taskEffects List~TaskEffect~ +taskEndEffects List~TaskEffect~ +taskGoals List~TaskGoal~ +nextNodes List~TaskNode~ +Out int -In int +isTaskFinished bool -_taskCts CancellationTokenSource -isTaskRunning bool -isCompleting bool +Inn int (property) +Awake() +OnDestroy() +StartTask() +CancelTask() +ResetForLoad() +RefreshStatus() -OnTaskSuccess() -StartTaskAsync() UniTaskVoid +OnDrawGizmos() } class TaskEffect { +ApplyEffect() +RevertEffect() } class TaskGoal { +IsDone bool } class TaskManager { +AddTask() +SaveTaskNode() +RegisterActive() +UnregisterActive() +IsGraphInitialized bool } TaskNode --\u003e TaskEffect : 包含 TaskNode --\u003e TaskGoal : 包含 TaskNode --\u003e TaskManager : 注册/通知我们在这里实现对于任务节点的信息存储（如：出度，入度，任务效果，任务目标等），也实现了任务节点的启动逻辑，结束逻辑。\n为了便于解耦，我们的任务流程使用 Unitask 实现，Unitask 是一个更优秀的异步库，可以便携地对异步函数进行取消，异常处理等操作。\n以下是 TaskNode 对象的生命周期流程图：\nstateDiagram-v2 [*] --\u003e 未注册: 对象实例化 未注册 --\u003e 已注册: Awake()TaskManager.AddTask() 已注册 --\u003e 等待中: InitTaskGraph入度 \u003e 0 等待中 --\u003e 运行中: Inn--Inn ≤ 0 \u0026\u0026 未完成 已注册 --\u003e 运行中: StartAllReadyTasks()Inn ≤ 0 \u0026\u0026 未完成 运行中 --\u003e 已完成: RefreshStatus()全部 Goal.IsDone == true 运行中 --\u003e 已取消: CancelTask()(读档/场景切换) 已完成 --\u003e [*]: OnTaskSuccess()保存存档 + 通知后继 已取消 --\u003e 已注册: ResetForLoad()(读档调用) state 运行中 { [*] --\u003e 应用开始效果: ApplyEffect() 应用开始效果 --\u003e 轮询检查: RefreshStatus() 轮询检查 --\u003e 已完成: 全部完成 轮询检查 --\u003e 继续等待: 未完成 继续等待 --\u003e 轮询检查: UniTask.Delay(250ms) }以下是 TaskNode 的运行逻辑流程图：\nsequenceDiagram participant Caller as 外部/Inn Setter participant TN as TaskNode participant TM as TaskManager participant Effects as TaskEffects participant Goals as TaskGoals Caller-\u003e\u003eTN: StartTask() TN-\u003e\u003eTN: CancelTask() (清理旧任务) TN-\u003e\u003eTN: 创建新 CancellationTokenSource TN-\u003e\u003eTN: isTaskRunning = true TN-\u003e\u003eTM: RegisterActive(this) Note over TN: StartTaskAsync() 开始 TN-\u003e\u003eEffects: ApplyEffect() (开始效果) TN-\u003e\u003eTN: RefreshStatus() (初始检查) loop 每 250ms TN-\u003e\u003eGoals: 遍历检查 IsDone alt 全部完成 TN-\u003e\u003eTN: OnTaskSuccess() else 未完成 TN-\u003e\u003eTN: UniTask.Delay(250ms) end end Note over TN: OnTaskSuccess() 触发 TN-\u003e\u003eTM: SaveTaskNode(taskId) TN-\u003e\u003eEffects: RevertEffect() (还原开始效果) TN-\u003e\u003eEffects: ApplyEffect() (应用结束效果) loop 每个后继节点 TN-\u003e\u003eTN.nextNodes: node.Inn-- (通知后继) end TN-\u003e\u003eTM: UnregisterActive(this) TN-\u003e\u003eTN: CancelTask() (停止异步) TN-\u003e\u003eTN: isTaskRunning = falseTaskEffect,TaskGoal,TaskBasic 脚本 TaskEffect 和 TaskGoal 脚本都是作为纯粹的数据类存在，为了可以在 Inspector 中序列化方便配置而存在。\n需要注意的是，这两个脚本都强烈跟游戏的数据管理系统结合，所以并无很大的参考价值。\n而 TaskBasic 则是为了自定义 TaskGoal 和 TaskEffect 而存在的基类，TaskBasic 中定义了自定义脚本的基本实现规则和属性，确保自定义脚本实现方便并且使用安全。\n附录 TaskManager\nTaskNode\nTaskEffect\nTaskGoal\nTaskBasic\n","date":"2026-06-26T21:46:41+08:00","permalink":"/post/%E5%9F%BA%E4%BA%8Edag%E7%9A%84%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F/","title":"基于DAG的任务管理系统"},{"content":"cs224n学习笔记 词嵌入 我们学习过 cs231n，了解了神经网络的本质是将数据进行线性变换为我们所需要的数据模式（如分类等等）。\n但是在自然语言处理这个领域，我们的语言难以直接被计算机所理解，所以我们要对我们的语言进行编码，注意这里的编码并不是像unicode那样无序编码，而是为了让计算机“理解”词意而编码，所以在这里引入词嵌入方法（PS:词嵌入在之后的 transformer 中仍然有一席之地）\n词嵌入其实也是模型的一个学习阶段，拿现实来举一个例子，我们学习语言的时候，通过理解单词的实际意义来学习，词嵌入类似，但是我们无法让计算机通过五感来理解词义，所以我们对相同或者有类似词义的单词进行分类，所以每个单词对于计算机来说都是抽象概念，我们将在之后的训练中让各种抽象概念相互依存、连接。\n词嵌入的实际操作非常简单，cs224n_a1 已经有详细的步骤拆分，这里不做赘述，我们只需要知道词嵌入是将语言的编码的重要工具即可。\n依存句法分析器 这个模块的功能在 Transformer 中会集成在注意力机制中，但是学习这个模块这是我们理解语言分析的一个重要阶段。\n在上文我们提到过计算机所理解的概念都是抽象的，但是人们的语言会有着不同的风格、歧义甚至是语法错误，为了让计算机依然能“看懂”我们的语言，便有了这个模块。\n这个模块将我们的语言拆分为最简单的树形结构，让计算机理解起来不会有歧义。\n那么现在的问题来到了怎么合理拆分构造我们的树形结构，首先不同的语言有不同的语法规范，其次我们要对这个依存句法分析器进行训练，让其能合理地处理任何句子。\n在 cs224n 中我们采用神经网络对其进行训练，并且使用 pytorch 减少代码难度，写起来其实没多少东西。\n神经机器翻译 这部分的代码实在是太乱了，我按照自己的理解进行讲解，如果有错误请联系我改正。\n在这里我们引入了注意力机制和 LSTM 架构，那么我们所需训练的矩阵数便涉及了注意力机制的注意力模块和LSTM的遗忘门、输入门、输出门。\n在笔记中我们只通过抽象概念来理解，并不会说明具体代码实现。\nLSTM（长短期记忆网络）是为了解决长对话的理解而诞生的，主要模块有遗忘门、输入门、输出门。\n遗忘门是解决过远的记忆，优化性能且防止过久记忆。\n输入门便是对某些数据进行记忆。\n输出门便是对现有的记忆进行编码，而后传导到下一个循环。\n那么我们现在发现了一个问题，如果真的有一个问题需要我们长久记忆怎么办，我们的遗忘门在每一次循环都会按照时间步对之前的权重进行削减，那么我们在这里引入注意力机制，我们在遗忘之前对原始数据进行注意力机制的应用，令超长期记忆成为可能。\n注意这里的注意力机制和 Transformer 中的自注意力机制有所不同，这里的注意力机制只会告诉模型应该注意前文的某某方面，而不会分析更多。\n这部分的实现变得更为抽象，但是我们要明确一点，就像上文提到的，计算机所有点一切概念都是数字，都是抽象的，我们为模型实现任何的模块只是提供了模型分析某个方面的功能，具体的模块间的嵌合性以及那些矩阵权重数据的实际都不是我们需要考虑的，这些问题会在实际数据的训练中进行自拟合，这也提供了一个知识点便是我们不能将某一模块的权重单独拿出来放在另一个模型上，这是不合理的。\nAttention is All You Need Transformer 的知识点很多，我们按照架构数据处理主要模块来讲解。\n自注意力机制 这是 Transformer 架构最具有革命性的模块。\n首先从单一自注意力入手，这里我们将为每一个词元（这个待会解释）设置三个键值，分别为查询键值Q，自身键值K，本质键值V，这便是自注意机制要学习的三个主要参数，这里引用一下deepseek给我的例子\n查询 Q ：每个词发出的\u0026quot;问题\u0026quot;：我该关注谁？ 键 K ：每个词提供的\u0026quot;答案\u0026quot;：我有什么特点值得关注？ 值 V ：每个词的\u0026quot;本质内容\u0026quot;，将被加权求和 之后我们便要计算注意力分数，计算方法不重要，重要的是我们的注意力分数代表了我们对这个词的关注度。\n但是考虑到对语言分析不够全面，并且联想到卷积神经网络中卷积核的概念，我们便引入了多头注意力机制，本质是对原本的单一自注意力机制的三个键值进行拆解，拆成 3$\\times$N 个键值（设有N个头），之后对所有的头进行整合即可。\n前馈神经网络（FFN） 这是 Transformer 中很有趣的一部分，正如我上文说的，我们实现的每一个模块都是为了提供给模型学习的能力，而不是单纯的功能。\n前馈神经网络为模型提供了学习更复杂特征的能力，因为FFN将传统神经网络的线性变换替换为了非线性变换\n$$ \\text{FFN}(\\mathbf{x}) = \\sigma(\\mathbf{x}\\mathbf{W}_1 + \\mathbf{b}_1)\\mathbf{W}_2 + \\mathbf{b}_2 $$\n根据数学定理，我们可以知道这种形式的变换可以逼近任何的连续函数，这提供了 FFN 强大的学习分析能力。\n残差连接与层归一化 这其实不是一个重要的模块，但是这是一个重要的小优化。\n在我们刚刚接触神经网络时便知道学习的实际便是通过反向传播不断优化我们的各种矩阵超参数，但是如果我们需要深度学习，我们便需要增加层数，那么我们会遇到数学上的问题，比如梯度消失或无效学习，那么我们便要引入残差连接这个模块。\n说直白一点，这个模块其实就是将我们的输出权重与原始权重加在一起，这防止了某一参数学着学着没了，也防止了过于激进的变换。\n但是我们又发现，单纯相加之后又会过于破坏我们原本的矩阵，甚至导致越加越大难以计算，所以我们引入了层归一化（LayerNormalization）来进行“还原”操作。\n感性理解一下，我认为这个模块令我们的 Transformer 变得优雅。\n自然语言文本生成器 注意这里我并不是在讲解整个解码器，我相信理解上述模块后已经可以理解编码器，而解码器需要生成自然语言，我曾在这部分疑惑过，所以单独拿出来讲解。\n在Transformer中，我们使用自回归解码器，解码器通过理解上文生成预测词向量，然后通过束搜索进行词汇预测。\n那么什么是束搜索？\n我们来回顾一下我们的起点——knn算法，我们查找前k个相似的图像，束搜索与此类似。\n束搜索搜索k前相似，而后再每个相似后再搜索k前相似，在一定循环后选择得分（概率）最大的词列。\n这种方法有些丑陋，于是有了许多剪枝和优化方法，在这里不过多赘述。\n补充解释 词元：在生成式人工智能导论中，我们知道chatGPT并不是以每个单词进行编码，而是分为若干词元，比如一个词可以由许多词元构成，故此在上文使用词元概念 ","date":"2026-01-18T23:02:00+08:00","permalink":"/post/cs224n%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"cs224n学习笔记"},{"content":"我猜测可能会有很多人跟我一样，在学完cs231n后仍然对神经网络没有一个整体印象，所以这是一份为后人能更轻松地理解神经网络而编写的学习笔记，本文通过我自己的理解对一份神经网络代码进行解释，将所有的知识点串联起来，希望可以造福后人。\nimport numpy as np def unpickle(file): import pickle with open(file, \u0026#39;rb\u0026#39;) as fo: dict = pickle.load(fo, encoding=\u0026#39;bytes\u0026#39;) return dict def fetch_data(): train_data = [] train_label = [] test_data = [] testlabel = [] for i in range(5): data = unpickle( # datasets\\cifar-10-batches-py\\data_batch_1 f\u0026#39;E:/projects/datasets/cifar-10-batches-py/data_batch_{i+1}\u0026#39;) train_data.append(data[b\u0026#39;data\u0026#39;]) train_label.append(data[b\u0026#39;labels\u0026#39;]) test = unpickle(\u0026#39;E:/projects/datasets/cifar-10-batches-py/test_batch\u0026#39;) test_data = test[b\u0026#39;data\u0026#39;] testlabel = test[b\u0026#39;labels\u0026#39;] train_data = np.concatenate(train_data) train_label = np.concatenate(train_label) train_data = train_data.reshape( (50000, 3, 32, 32)).transpose(0, 2, 3, 1).astype(\u0026#34;float32\u0026#34;) train_data = train_data.reshape(-1, 32*32*3) test_data = test_data.reshape((10000, 3, 32, 32)).transpose( 0, 2, 3, 1).astype(\u0026#34;float32\u0026#34;) test_data = test_data.reshape(-1, 32*32*3) return train_data, train_label, test_data, testlabel def preprocess_data(train_data, test_data): mean = np.mean(train_data, axis=0) train_data -= mean test_data -= mean return train_data, test_data class NeuralNetwork: def __init__(self, input_size, hidden_size, output_size, std=1e-4): self.params = {} self.params[\u0026#39;W1\u0026#39;] = std * np.random.randn(input_size, hidden_size) self.params[\u0026#39;b1\u0026#39;] = np.zeros(hidden_size) self.params[\u0026#39;W2\u0026#39;] = std * np.random.randn(hidden_size, output_size) self.params[\u0026#39;b2\u0026#39;] = np.zeros(output_size) def relu(self, x): return np.maximum(0, x) def relu_backward(self, dout, cache): dx, x = dout, cache dx[x \u0026lt;= 0] = 0 return dx def loss(self, X, y=None, reg=0.0): W1, b1 = self.params[\u0026#39;W1\u0026#39;], self.params[\u0026#39;b1\u0026#39;] W2, b2 = self.params[\u0026#39;W2\u0026#39;], self.params[\u0026#39;b2\u0026#39;] Hidden_layer = self.relu(np.dot(X, W1) + b1) scores = np.dot(Hidden_layer, W2) + b2 if y is None: return scores loss = np.sum(-np.log(np.exp(scores[range(len(scores)), y]) / np.sum(np.exp(scores), axis=1))) loss /= len(X) loss += 0.5*reg*(np.sum(W1*W1)+np.sum(W2*W2)) grads = {} dscores = np.exp(scores)/np.sum(np.exp(scores), axis=1, keepdims=True) dscores[range(len(scores)), y] -= 1 dscores /= len(X) grads[\u0026#39;W2\u0026#39;] = np.dot(Hidden_layer.T, dscores) + reg*W2 grads[\u0026#39;b2\u0026#39;] = np.sum(dscores, axis=0) dhidden = np.dot(dscores, W2.T) dhidden_relu = self.relu_backward(dhidden, Hidden_layer) grads[\u0026#39;W1\u0026#39;] = np.dot(X.T, dhidden_relu) + reg*W1 grads[\u0026#39;b1\u0026#39;] = np.sum(dhidden, axis=0) return loss, grads def train(self, X, y, learning_rate=1e-3, learning_rate_decay=0.95, reg=5e-6, num_iters=100, batch_size=200, verbose=False): num_train = X.shape[0] iterations_per_epoch = max(num_train // batch_size, 1) for it in range(num_iters): idx = np.random.choice(num_train, batch_size, replace=True) X_batch = X[idx] y_batch = y[idx] loss, grads = self.loss(X_batch, y=y_batch, reg=reg) self.params[\u0026#39;W1\u0026#39;] -= learning_rate * grads[\u0026#39;W1\u0026#39;] self.params[\u0026#39;b1\u0026#39;] -= learning_rate * grads[\u0026#39;b1\u0026#39;] self.params[\u0026#39;W2\u0026#39;] -= learning_rate * grads[\u0026#39;W2\u0026#39;] self.params[\u0026#39;b2\u0026#39;] -= learning_rate * grads[\u0026#39;b2\u0026#39;] # if verbose and it % 100 == 0: # print(\u0026#39;iteration %d / %d: loss %f\u0026#39; % (it, num_iters, loss)) if it % iterations_per_epoch == 0 and it != 0: learning_rate *= learning_rate_decay def predict(self, X): Hidden_layer = self.relu( np.dot(X, self.params[\u0026#39;W1\u0026#39;]) + self.params[\u0026#39;b1\u0026#39;]) scores = np.dot(Hidden_layer, self.params[\u0026#39;W2\u0026#39;]) + self.params[\u0026#39;b2\u0026#39;] return np.argmax(scores, axis=1) def main(): train_data, train_label, test_data, testlabel = fetch_data() train_data, test_data = preprocess_data(train_data, test_data) net = NeuralNetwork(32*32*3, 100, 10) net.train(train_data, train_label, num_iters=10000, batch_size=200, verbose=True) print(f\u0026#39;train accuracy: {np.mean(net.predict(train_data) == train_label)}\u0026#39;) return if __name__ == \u0026#39;__main__\u0026#39;: main() 这里是本文所使用的代码。（训练内容为CIFAR-10数据集）\n首先，fetch_data和 unpickle函数作为数据读入函数，在这里不做讲解，我们只需要知道 train_data,train_label,test_data,testlabel这四个都分别是什么即可，train_data是一个n*3072的矩阵，它存储了n个训练需要的图像数据，train_label是一个列表，有n项，存储了 train_data对应的答案，test_data,testlabel同理。\n之后我们需要对数据进行预处理，我们来看 preprocess_data函数，这份代码中只采用了均值减法初始化，这是最为常见的初始化方式，且在这份代码中效果显著，当然我们仍然可以在这个函数中添加更多的初始化方式，比如归一化等等，但是我们需要注意数据的类别，防止产生负优化，比如在CIFAR-10数据集上，归一化会显著降低它的准确率。\n现在我们马上就要进入最紧张刺激的神经网络环节了，但是在训练之前，我们仍然需要明确我们的神经网络格式，比如在这份代码中，我们使用的是一个两层全连接神经网络，所以我们有这个预处理函数\ndef __init__(self, input_size, hidden_size, output_size, std=1e-4): self.params = {} self.params[\u0026#39;W1\u0026#39;] = std * np.random.randn(input_size, hidden_size) self.params[\u0026#39;b1\u0026#39;] = np.zeros(hidden_size) self.params[\u0026#39;W2\u0026#39;] = std * np.random.randn(hidden_size, output_size) self.params[\u0026#39;b2\u0026#39;] = np.zeros(output_size) 这里，我们对每一层分别定义了它的权重W和偏量b，这四个参数是我们需要训练的主要参数，并且我们注意到，他们都是根据神经元数量来定义的矩阵，那么我现在来解释一下为什么是矩阵。我们来看这个初始化函数的传入参数 input_size, hidden_size, output_size, std=1e-4，std先不用管，前面三个分别是输入的格式，第一层格式，输出格式，我们完全可以简单理解为我们需要将一个有若干个数字的集合，通过一系列的数学运算，先压缩为 hidden_size的大小，最后再压缩为 output_size的大小，而这里的 output_size就是我们需要分类的10个标签。而这里对参数的初始化使用了随机数，因为全部为0是不正确的，因为我们后续还要对其进行求偏导以及计算梯度。\n初始化之后，我们就可以进行训练了，而训练的目的是迭代计算出最好的参数，也就是神经网络初始化所定义的四个参数。\ndef train(self, X, y, learning_rate=1e-3, learning_rate_decay=0.95, reg=5e-6, num_iters=100, batch_size=200, verbose=False): num_train = X.shape[0] iterations_per_epoch = max(num_train // batch_size, 1) for it in range(num_iters): idx = np.random.choice(num_train, batch_size, replace=True) X_batch = X[idx] y_batch = y[idx] loss, grads = self.loss(X_batch, y=y_batch, reg=reg) self.params[\u0026#39;W1\u0026#39;] -= learning_rate * grads[\u0026#39;W1\u0026#39;] self.params[\u0026#39;b1\u0026#39;] -= learning_rate * grads[\u0026#39;b1\u0026#39;] self.params[\u0026#39;W2\u0026#39;] -= learning_rate * grads[\u0026#39;W2\u0026#39;] self.params[\u0026#39;b2\u0026#39;] -= learning_rate * grads[\u0026#39;b2\u0026#39;] # if verbose and it % 100 == 0: # print(\u0026#39;iteration %d / %d: loss %f\u0026#39; % (it, num_iters, loss)) if it % iterations_per_epoch == 0 and it != 0: learning_rate *= learning_rate_decay 我们来看这个训练主函数，reg是我们定义的L2正则化超参数，我们直接来看迭代训练循环，我们每一次迭代随机抽取数据进行训练，用这份数据计算出四个参数的梯度，以此来更新我们的参数，使我们的参数不断优化。在这个函数中我们还使用了两个参数，learning_rate和 learning_rate_decay，这两个参数用来不断降低模型的学习率，使参数的迭代越来越精确。\n我们发现在训练迭代中使用了一个很重要的 loss函数来计算我们的参数梯度，这也是神经网络最重要的部分。\ndef relu(self, x): return np.maximum(0, x) def relu_backward(self, dout, cache): dx, x = dout, cache dx[x \u0026lt;= 0] = 0 return dx def loss(self, X, y=None, reg=0.0): W1, b1 = self.params[\u0026#39;W1\u0026#39;], self.params[\u0026#39;b1\u0026#39;] W2, b2 = self.params[\u0026#39;W2\u0026#39;], self.params[\u0026#39;b2\u0026#39;] Hidden_layer = self.relu(np.dot(X, W1) + b1) scores = np.dot(Hidden_layer, W2) + b2 if y is None: return scores loss = np.sum(-np.log(np.exp(scores[range(len(scores)), y]) / np.sum(np.exp(scores), axis=1))) loss /= len(X) loss += 0.5*reg*(np.sum(W1*W1)+np.sum(W2*W2)) grads = {} dscores = np.exp(scores)/np.sum(np.exp(scores), axis=1, keepdims=True) dscores[range(len(scores)), y] -= 1 dscores /= len(X) grads[\u0026#39;W2\u0026#39;] = np.dot(Hidden_layer.T, dscores) + reg*W2 grads[\u0026#39;b2\u0026#39;] = np.sum(dscores, axis=0) dhidden = np.dot(dscores, W2.T) dhidden_relu = self.relu_backward(dhidden, Hidden_layer) grads[\u0026#39;W1\u0026#39;] = np.dot(X.T, dhidden_relu) + reg*W1 grads[\u0026#39;b1\u0026#39;] = np.sum(dhidden, axis=0) return loss, grads 这里便是我们计算所需要的函数，我们来对 loss函数逐步分析。首先传入现有参数没有什么好说的，然后便是我们的正向传播环节，我们先用现有参数计算出10个类别所对应的得分，然后我们将这10个分数放进线性分类器里进行计算，最后得到的就是我们的损失 loss。之后便是我们喜闻乐见的反向传播环节，所谓反向传播，其实就是按照线性求导法则对两层神经网络的所有参数求偏导，以此找到loss下降最快的点，才能以此更新参数。26-28行都是在对softmax线性分类器求梯度，到了29行我们发现我们在运算时对矩阵进行了转置，通俗地讲，我们明白矩阵是不服从乘法交换律的，所以如果我们想让它正确地乘，便需要给他转个方向，这样才可以进行矩阵乘法。还有一点需要注意，在32行我们对激活函数进行了求导，因为我们在前向传播的时候在第一层使用了激活函数，所以这一步也必不可少。至此，我们便可以完整地计算出参数梯度，并以此迭代训练我们的四个主要参数。\n","date":"2025-11-02T19:23:40+08:00","permalink":"/post/%E5%9F%BA%E7%A1%80%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"基础神经网络学习笔记"},{"content":"2023年11月17日\n明天就是NOIP了，打算打完NOIP就退役，于是在这里写一篇回忆录。\n我初中的时候根本不知道有信息竞赛这个东西，甚至对其它的竞赛也不了解，唯一的印象只是竞赛题很难，参加竞赛的人很强，仅此而已。\n那时候高一军训之后因为疫情就开始上网课，也是在上课后的没几天，我的班主任在班级群里发了一条信息竞赛招新的消息，本来是觉得自己参加不了的，但是看到欢迎零基础就还是参加了，当时其实还报了一个物理竞赛，但是因为需要自学完高一的物理内容，于是就放弃了。\n第一节课是在周六，那是我才知道信息竞赛要学编程，我曾经其实一直想学，但是因为懒和找不到系统的课程也是全咕掉了，直到现在我发现这是一个学编程的机会。\n第一节课讲了一些语言基础，当时甚至认真地手写笔记，还把两个头文件抄下来背了好久（当时根本不知道有万能头这个东西），教练讲 scanf 的时候还有两个同学问为什么不用 cin 的，有同学敲代码有问题调不出来了也会发在腾讯会议的讨论区去问，当时我觉得他们好强啊（但是后来好像都走了）。\n那个时候周三晚上就要培训了，教练讲完还会在学校的老oj上留题，那个时候似乎是找到了过题的快感，经常写题超过晚上12点（但是经常是因为一些语法上的nt错误盯着看了很久看不出来）。\n大概是一个月之后吧，那个时候就要有初赛了，因为初赛是周六比（那年改成了线上），所以在那周三我们教练还安排了工大的学长回来给我们讲初赛知识点（我当时还因为住校在回家的路上错过了一大半，幸运的是有录像，回家看了三遍虽然还是没听懂），知识点真的很多，当时听的脑袋疼，但还是拼命记住了一些。\n周六的初赛我带着那可怜的知识开始考试，其实当时根本就没有感觉自己能过（后来才知道有教练推荐这个东西），考场上30分钟把前面的选择蒙完后开始坐牢（因为当时根本看不懂代码），我记得有一道题是问你一颗三叉树的一个节点的父亲节点的编号，我本来是打算把整棵树画出来的，但是因为A4纸不够大只能放弃了（，我后来坐牢实在闲的在摄像头盲点看bilibili直播，根本没人管，眼神都不用装一下的那种（\n考完群里还有说今年估分90没考好，真TM该死啊\n忘了是一周还是两周时候出分，去机房查分发现自己过了，当时真的很开心，毕竟纯蒙的能打46分（虽然教练跟我们说机房里是个人就过了）。\n之后便是午睡跟老师请假去机房做题，虽然除了一个社牛当时我跟别人根本说不上话，我那个时候连洛谷都不知道，只会在学校的oj上刷一些入门题。\n之后也慢慢地开始学一些算法，甚至我还问过教练之后还会讲语言吗，教练跟我说剩下的再讲竞赛就用不上了（当时现在想想这话其实是为了赶进度编的）。\n国庆的时候教练还安排了高二的学长给我们讲大块的知识点，可惜的是当时除了lxy学长讲的图论以外我基本都在摸鱼，根本听不进去，现在想想也许听了的话能少走一些弯路。\n国庆后两天还参加了一次模拟赛，那个时候有基础和没基础的两个班也合并了，虽然是不出意料的后几名，但是至少不是垫底，这还是有一些心理安慰的，不然我可能早就坚持不住了。\n之后教练学的看效果不太好还让工大的学长给我们重新讲了一遍dij。\n因为当时就听了图论，所以做题也只做了图论，一个链式前向星我将近翻了百度上能找到的所有博客才勉强理解（我当时甚至不知道有OIWIKI这个东西）。\n基本一个dij板子和最小生成树板子我都调了将近一周。\n然后在我板子还没敲明白的时候已经开始讲树型dp了，但是已经是彻底听不懂了，讲课也不听，就在补之前的板子。\n当时还因为参加CSP要收480块钱纠结了好久要不要报，幸运的是家长还挺支持的。\n但是后来CSP取消了，也就没有什么过不过线的事了，当时还因为这个开心了好久。\n之后的日子也就是学新算法，打模拟赛，虽然那个时候学新算法多半是听不懂的，打模拟赛也是基本后几名，有的时候家长问起来学的怎么样也只能敷衍过去挺好的。\n再后来，NOIP也因为疫情取消了，改成了下学期的春季测试。\n发生变化的是春季测试考完之后，我考的稀烂，只有135分，当时同机房的人已经开始犯愁自己能不能进省队了（那位考了260分），说真的这时候我才开始明白这个差距之大。\n谁不想进省队呢，于是就开始卷啊，照这大纲学自己没学过的新算法，说实话学的真的很费劲，但是学完之后发现曾经拉下的东西自然而然的就会了，这也算是节省了一些时间，那段时间里我也有着及其严重的精神内耗，整个人的精神状态差的要死，一边学着whk一边卷着竞赛，当时为了缓解差的要死的精神状态还在做不了题的时候写写小说（虽然断更了）。\n之后考的省选还是因为实力不足寄掉了，省选之后我还是有着一些迷之自信的，还是在卷，那段时间的实力提升真的很大。\n假期的时候机房里几个人（包括我）去了东师集训，当时其实很颓，所以真的浪费了很多时间，也许这也是我之后决定退役的伏笔吧。\n其实集训回来到考CSP第二轮之前我都是想进省队的，也是在逼着自己学学着费劲但是应该学的算法，那一段的精神状态更是差的要死。\n考CSP的时候被T2和T3创飞了，或许我也是真的扛不住了，便打算NOIP之后直接退役，也算是一种解脱吧。\n回想一下这一年多，从一点编程都不会到学会了这么多，收获真的是很大。\n也许是等到昨天的模拟赛我考了第一，但是在两人没考以及一个人挂了60分的情况下，他甚至胡出了T4再给我两个小时都想不出来的状态转移方程，才深切地意识到差距。\n这也就是一个没天赋的人的无趣的故事。\n我OI生涯最大的遗憾应该就是我的学弟们一点都不可爱，哼。\n于2025年8月27日补档：如果有学弟学妹们看到这里，那么在此祝各位前程似锦，当然对于我来说，我的高考炸掉了，但是如果让我重来一遍的话，我还是会来参加竞赛，因为我相信再来一次的我会比这次做的更好，我不会后悔。\n","date":"2023-11-17T19:49:00+08:00","permalink":"/post/oi%E5%9B%9E%E5%BF%86%E5%BD%95+%E9%80%80%E5%BD%B9%E8%AE%B0/","title":"OI回忆录+退役记"},{"content":"起因：P1642 规划\n说实话我的印象里是做过这种题的，但是当时好像理解后就跑路了，现在来复习一下。\n什么是01分数规划呢?\n其实就是让你跑一个01背包，但是物品有三个权值，重量都为1，然后让你求选的物品的剩下两个权值比值最大，也就是性价比。\n这种东西在没有选择依赖关系的情况下看起来确实可以贪心，但是这样就没意思了，所以我们来说说有依赖关系怎么求，对于最普通的01分数规划问题来说一般都是二分答案，也就是我们要求的那个最优的性价比。\n之后我们根据这个平均性价比，反推出一个物品相对于平均值的贡献，然后就可以大力用各种各样的dp求出当前的最大值了，如果大于0就说明还有优化空间，反之则没有。\n要退役了，什么最优比率生成树，什么最优比率环也不打算学，就这样吧。\n","date":"2023-11-16T17:48:00+08:00","permalink":"/post/01%E5%88%86%E6%95%B0%E8%A7%84%E5%88%92%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"01分数规划学习笔记"},{"content":"对于一道dp题，它可能让你转移的时候维护两个优先级，那么我们暴力一点，如果不满足两个条件的话就不转移，比如求两个最小值，总会有一个是最优先考虑，那么选的如果超出第一优先级的话就一定是错误的，所以在小于等于第一优先级的时候再去考虑第二优先级。\n参考题目：Bottles\n","date":"2023-11-01T13:40:00+08:00","permalink":"/post/%E5%85%B3%E4%BA%8E%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E7%9A%84%E8%BD%AC%E7%A7%BB1/","title":"关于动态规划的转移1"},{"content":"之前没有接触过最短路图，退役前正好趁这道题学习一下。\n其实最短路图没有多少东西，但是考到的时候就很有用 废话。\n首先对于一张图来说，以一个点来跑单源最短路的话，那么这个起点到所有点的最短路必定构成一张 $\\texttt{DAG}$，那么这就是很优秀的，但是如果我们更极端一点，用奇怪的方式规定（当然也可能直接说）两点间的最短路有且只有一条，那么我们跑出来的就变成了一颗最短路树。\n来看这道题，题面非常简介，发现这道题中的边如果不被任何最短路经过的话便是毫无作用的，所以我们跑出最短路图，然后便是用拓扑排序在 $\\texttt{DAG}$ 上进行递推计数。\n因为题中的数据范围很小，所以我们暴力地枚举起点，以枚举的起点跑单源最短路，然后对跑出来的 $\\texttt{DAG}$ 进行答案累加就可以了。\n但是要注意一点，我们的 $\\texttt{DAG}$ 要进行反拓扑序，因为我们记录的是以某一点为起点的 $\\texttt{DAG}$，不反过来跑的话记的数是错的。\n然后求哪些边是最短路经过的边的时候其实用上了dp回溯（因为最短路算法本质就是动态规划嘛），我们只需要枚举边，然后判断是否 $dis[v]=dis[x]+e[i].wide$ 就可以了。\n然后对于跑最短路因为没有负权边所以我用的是堆优化 $\\mathrm{Dijkstra}$。\n复杂度分析：$\\mathrm{O}(n^2logn+n^2)$ 理论实测均可过。\n（因为我也是才学所以代码有所借鉴其他题解）\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define int long long const int N=1505; const int mod=1e9+7; int n,m,head[N],cnt; struct Edge{ int to; int next; int wide; }e[5005]; void adding(int u,int v,int w){ e[++cnt].to=v; e[cnt].wide=w; e[cnt].next=head[u]; head[u]=cnt; } int cnt1[N],cnt2[N],dis[N],vis[N],ans[5005]; priority_queue\u0026lt;pair\u0026lt;int,int\u0026gt;,vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;,greater\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026gt;q; void dij(int s){ memset(cnt1,0,sizeof cnt1); memset(dis,127,sizeof dis); memset(vis,0,sizeof vis); dis[s]=0; cnt1[s]=1; q.push(make_pair(dis[s],s)); vector\u0026lt;int\u0026gt;dot; while(q.size()){ int x=q.top().second; q.pop(); if(vis[x]) continue; vis[x]=1; dot.push_back(x); for(int i=head[x];i;i=e[i].next){ int v=e[i].to; if(dis[v]\u0026gt;dis[x]+e[i].wide){ dis[v]=dis[x]+e[i].wide; cnt1[v]=cnt1[x]; q.push(make_pair(dis[v],v)); }else if(dis[x]+e[i].wide==dis[v]){ cnt1[v]+=cnt1[x]; } } } reverse(dot.begin(),dot.end()); for(auto x:dot){ cnt2[x]=1; for(int i=head[x];i;i=e[i].next){ int v=e[i].to; if(dis[v]==dis[x]+e[i].wide){ cnt2[x]=(cnt2[x]+cnt2[v])%mod; ans[i]+=1ll*cnt1[x]%mod*cnt2[v]%mod; ans[i]%=mod; } } } } signed main(){ scanf(\u0026#34;%lld%lld\u0026#34;,\u0026amp;n,\u0026amp;m); for(int i=1;i\u0026lt;=m;i++){ int s1,s2,s3; scanf(\u0026#34;%lld%lld%lld\u0026#34;,\u0026amp;s1,\u0026amp;s2,\u0026amp;s3); adding(s1,s2,s3); } for(int i=1;i\u0026lt;=n;i++){ dij(i); } for(int i=1;i\u0026lt;=m;i++){ printf(\u0026#34;%lld\\n\u0026#34;,ans[i]); } return 0; } ","date":"2023-10-31T21:39:00+08:00","permalink":"/post/p6413/","title":"P6413 [COCI2008-2009#3] NAJKRACI 题解"},{"content":"本人随机跳题到了一道看起来是最短路的题，但是本人并不想去暴力建图懒，发现如果抽象成一张图的话边权只有 $0$ 或者 $1$，于是去学了个01BFS。\n什么是01BFS呢，其实这是一种最短路的优化，我们都知道BFS要用到queue来维护正确性，但是这在找边权不相等的最短路里就很难受，所以我们需要给他规定一个转移顺序。\n我们把queue换成deque，如果这条边边权是1的话就放在队尾，否则放在队首，参考dij里priority_queue的证明，这其实就是找到性质手动模拟了一个优先队列，并且我们的复杂度是优秀的 $O(n)$，以为少了一个堆的log，就很优秀。\n当然适用范围有点窄，只能在边权为 $0$ 和一种其他正数中使用。\n参考题目：P1849 [USACO12MAR] Tractor S\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int N=1005; int n,sx,sy,a[N][N],t[N][N]; int stx[6]{-1,0,1,0}; int sty[6]{0,-1,0,1}; deque\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;q; void BFS(){ q.push_front(make_pair(sx,sy)); memset(t,-1,sizeof t); t[sx][sy]=0; while(q.size()){ int x=q.front().first; int y=q.front().second; q.pop_front(); for(int i=0;i\u0026lt;4;i++){ int tx,ty; tx=x+stx[i]; ty=y+sty[i]; if(tx\u0026lt;0||tx\u0026gt;=N) continue; if(ty\u0026lt;0||ty\u0026gt;=N) continue; if(t[tx][ty]!=-1) continue; if(a[tx][ty]){ t[tx][ty]=t[x][y]+1; q.push_back(make_pair(tx,ty)); }else{ t[tx][ty]=t[x][y]; q.push_front(make_pair(tx,ty)); } } } } int main(){ scanf(\u0026#34;%d%d%d\u0026#34;,\u0026amp;n,\u0026amp;sx,\u0026amp;sy); for(int i=1;i\u0026lt;=n;i++){ int s1,s2; scanf(\u0026#34;%d%d\u0026#34;,\u0026amp;s1,\u0026amp;s2); a[s1][s2]=1; } BFS(); printf(\u0026#34;%d\u0026#34;,t[0][0]); return 0; } ","date":"2023-10-25T10:29:00+08:00","permalink":"/post/01bfs%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","title":"01BFS学习笔记"},{"content":"多考虑图论转化，比如求解依赖关系中的最小值，第一眼容易开成dp，没思路时不妨想想求解连通性的最值算法，最小生成树。\n参考题目：P5994 [PA2014] Kuglarz\n","date":"2023-10-24T19:37:00+08:00","permalink":"/post/%E5%85%B3%E4%BA%8E%E9%9C%80%E8%A6%81%E5%A4%84%E7%90%86%E5%A4%8D%E6%9D%82%E4%BE%9D%E8%B5%96%E5%85%B3%E7%B3%BB%E7%9A%84%E9%A2%98/","title":"关于需要处理复杂依赖关系的题"},{"content":"对于一些需要优化空间的dp题，单纯地让dp方程每一维表示一个条件的话无法确切表示出一个无后效性的状态，那么我们可以另这个方程加一些限制条件，但是不表示在方程里，比如对于一个线性dp，我们规定 $dp[i][j]$ 为到第 $i$ 位选择 $j$ 个，但是我们另外规定第 $i$ 个必须选，这样在一些题中就可以满足无后效性了。\n参考题目：P4182 [USACO18JAN] Lifeguards P\n","date":"2023-10-24T15:16:00+08:00","permalink":"/post/%E5%85%B3%E4%BA%8Edp%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%E6%96%B9%E7%A8%8B%E7%9A%84%E8%AE%BE%E8%AE%A13/","title":"关于dp状态转移方程的设计3"},{"content":"P2017\n这道题的让我们对图中的无向边进行定向，那么我们先把无向边断开，相当于拆成若干个有向连通块，然后我们对所有点进行拓扑排序求出它们的拓扑序，然后我们就可以按照拓扑序的大小关系对无向边进行定向，证明明天再补。\n很好我来补证明了。\n因为有多个连通块，所以先考虑一个连通块，我们发现对于一个连通块内的点来说，点的拓扑序大小关系一定是严格与单独在这个连通块内拓扑排序的拓扑序相同，那么如果按照之前说的那样对无向边进行定向的话，因为序号小的一定只能指向序号大的，假设给你一堆有序号的点，规定只能从序号小的点向序号大的点连边，那么我们发现这样无论怎么连它都是一张DAG，此题同理。\n","date":"2023-10-23T21:39:00+08:00","permalink":"/post/%E5%85%B3%E4%BA%8E%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F%E7%9A%84%E5%BA%94%E7%94%A8/","title":"关于拓扑排序的应用"},{"content":"对于有些题目来说我们读入字符串如果不想一个一个读入判断的话我们就一定要使用 getline() 函数，但是我们要注意一点是如果在要读入的字符串前面读入过其他形式的数据，那么这个函数会把上一行剩下来的换行符当做一行来读入，那么我们便会少读入一行字符串，所以我们要在读入字符串前调用一次 getline() 函数，这样就可以覆盖掉上一行的换行符。\n参考题目：P1767 家族\n","date":"2023-10-20T10:31:00+08:00","permalink":"/post/%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%AF%BB%E5%85%A5%E8%B8%A9%E5%9D%911/","title":"字符串读入踩坑1"},{"content":"有些搜索最短路程的长度的题目，我们如果直接返回第一个搜索到的答案很可能是错的，因为有些路程计算方式和我们的BFS步数并不成正比，但是我们知道路程长度随BFS步数有单调性，那么我们便需要在搜索的时候扩展一点范围，比如在大于当前最小答案后再将这个点ban掉，这样就会保证正确性。\n参考题目：P7663 [COCI2014-2015#5] JABUKE\n","date":"2023-10-19T21:30:00+08:00","permalink":"/post/%E6%90%9C%E7%B4%A2%E8%B8%A9%E5%9D%911/","title":"搜索踩坑1"},{"content":"对于一些状态转移方程较好写的dp题，但是复杂度很难看，并且很难在数学方面上进行优化的dp题，我们可以考虑重构状态转移方程，比如要dp双线程操作的话，我们可以把一个线程写在方程里，另一个记录在数组的值里，这样就可以把看似暴力的方程优化成一个最普通的背包，相当于抽象了背包dp的重量和价值，这样的方程看起来有一种偏重关系，而且这样写兴许还可以处理两个线程的优先级，但是我还没见到过这样的题。\n参考题目：P2224 [HNOI2001] 产品加工\n","date":"2023-10-18T09:06:00+08:00","permalink":"/post/%E5%85%B3%E4%BA%8Edp%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%E6%96%B9%E7%A8%8B%E7%9A%84%E8%AE%BE%E8%AE%A12/","title":"关于dp状态转移方程的设计2"},{"content":"对于一些按照题意模拟设计 $dp$ 毫无思路的情况下，考虑正难则反，比如让你从一个字符串中删去不合法的字符，这样看起来只有暴力，但是反过来思考成我们在这个字符串中按顺序选择若干个字符使其组成的字符串合法且代价最小，那么这样就很容易设计状态转移方程了。\n参考题目：P2187 小Z的笔记\n","date":"2023-10-17T13:54:00+08:00","permalink":"/post/%E5%85%B3%E4%BA%8Edp%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%E6%96%B9%E7%A8%8B%E7%9A%84%E8%AE%BE%E8%AE%A11/","title":"关于dp状态转移方程的设计1"},{"content":"一道有趣简单的构造题。\n阅读题面，发现这似乎很难找到完美的贪心策略，那么我们考虑网络流。\n先说建模方式：\n考虑拆点，把每个点拆成编号为 $i$ 和 $n+i$ 的两个点。\n首先从超级源点 $s$ 向编号为 $1 \\sim n$ 的每一个点连流量为这个点出度的边。\n然后再从 $1 \\sim n$ 的点向每一个编号为 $n+1$ 到 $2n$ 流量为 $1$ 的边，当然因为题中给出了 $m$ 条限制，那么不对这些限制的点进行连边就好了。\n最后从编号为 $n+1$ 到 $2n$ 的点向超级汇点 $t$ 连一条流量为这个点入度的边就好了。\n大概建了个这么个图：\n那么边数就是这个网络的最大流（因为保证有解），至于怎么输出构造的边，我们去遍历这个网络的残余流量，如果第二步建的正向边的流量为 $0$ 那么就说明这条边被我们选择了，我们输出这条边的两个节点就好。\n然后因为这是个二分图，那么我们的复杂度就是优秀的 $O(n\\sqrt n)$。\n这道题其实就是直接对着题意进行网络流模拟，是一道很好的板子题。\n如果还没理解就看看代码，自认为很好看（\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int N=1005; const int inf=1e9+7; int n,m,in[N],out[N],head[2*N],cnt=1,s,t,mp[N][N],num=1; struct Edge{ int from; int to; int next; int wide; }e[2*N*N+2*N]; void adding(int u,int v,int w){ e[++cnt].to=v; e[cnt].from=u; e[cnt].wide=w; e[cnt].next=head[u]; head[u]=cnt; } int dis[2*N],now[2*N]; bool BFS(){ for(int i=1;i\u0026lt;=2*n+1;i++){ dis[i]=inf; } dis[s]=0; queue\u0026lt;int\u0026gt;q; q.push(s); now[s]=head[s]; while(q.size()){ int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].next){ int v=e[i].to; // cout\u0026lt;\u0026lt;x\u0026lt;\u0026lt;\u0026#34; \u0026#34;\u0026lt;\u0026lt;v\u0026lt;\u0026lt;endl; if(dis[v]==inf\u0026amp;\u0026amp;e[i].wide){ dis[v]=dis[x]+1; now[v]=head[v]; q.push(v); } } } return dis[t]!=inf; } int DFS(int x,int sum){ if(x==t) return sum; int k,res=0; for(int i=now[x];i\u0026amp;∑i=e[i].next){ int v=e[i].to; now[x]=i; if(dis[v]==dis[x]+1\u0026amp;\u0026amp;e[i].wide){ k=DFS(v,min(e[i].wide,sum)); if(k==0){ dis[v]=inf; } e[i].wide-=k; e[i^1].wide+=k; sum-=k; res+=k; } } return res; } int Dinic(){ int res=0; while(BFS()){ res+=DFS(s,inf); } return res; } void Debug(){ for(int x=0;x\u0026lt;=2*n+1;x++){ for(int i=head[x];i;i=e[i].next){ int v=e[i].to; cout\u0026lt;\u0026lt;x\u0026lt;\u0026lt;\u0026#34; \u0026#34;\u0026lt;\u0026lt;v\u0026lt;\u0026lt;\u0026#34; \u0026#34;\u0026lt;\u0026lt;e[i].wide\u0026lt;\u0026lt;endl; } } } int main(){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); for(int i=1;i\u0026lt;=n;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;in[i]); } for(int i=1;i\u0026lt;=n;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;out[i]); } scanf(\u0026#34;%d\u0026#34;,\u0026amp;m); for(int i=1;i\u0026lt;=m;i++){ int s1,s2; scanf(\u0026#34;%d%d\u0026#34;,\u0026amp;s1,\u0026amp;s2); mp[s1][s2]=1; } s=0,t=2*n+1; for(int i=1;i\u0026lt;=n;i++){ for(int j=1;j\u0026lt;=n;j++){ if(i==j) continue; if(mp[i][j]) continue; adding(i,j+n,1); adding(j+n,i,0); num+=2; } } for(int i=1;i\u0026lt;=n;i++){ adding(s,i,out[i]); adding(i,s,0); } for(int i=n+1;i\u0026lt;=2*n;i++){ adding(i,t,in[i-n]); adding(t,i,0); } Debug(); printf(\u0026#34;%d\\n\u0026#34;,Dinic()); for(int i=2;i\u0026lt;=num;i+=2){ if(!e[i].wide){ printf(\u0026#34;%d %d\\n\u0026#34;,e[i].from,e[i].to-n); } } return 0; } ","date":"2023-10-16T16:40:00+08:00","permalink":"/post/p9705/","title":"P9705 「TFOI R1」Unknown Graph 题解"},{"content":"同机房同学找到的题，一看 $\\mathrm{tag}$，什么？队列？不会怎么办？\n小问题，因为我会暴力。\n首先，好长的题面，直接看简要题意，可以发现我们只需要求出最大能合成到多少等级，因为数学知识薄弱，打开 $\\mathrm{python}$，发现 既然只有 $23$ 那么我们便可以直接开一个数组（桶）来记录每个等级的附魔书出现了多少次，然后从下往上扫，便可以 $\\mathrm{O}(n)$ 地求出最高能合成多大等级的附魔书。\n那么下一步我们就要计算它的最小合成费用，因为合成继承的费用是两者之间的 $\\mathrm{max}$ 加一，那么一个明显的贪心策略便是每次合成取两个当前费用最大的，其实这样暴力维护的话有点麻烦，但是根据递归的优先选择性其实根本不用特意维护。\nlong long cal(int dep){ if(a[dep]){ a[dep]--; return 1; } int s1=cal(dep-1); int s2=cal(dep-1); y+=s1+s2; return (max(s1,s2)\u0026lt;\u0026lt;1)+1; } 因为如果这样写的话，对于一个等级的附魔书来说一定是按照从小到大的顺序进行合并，而且因为一定是用偶数个进行合成，所以这依旧是满足我们的贪心策略的。\n最后因为不保证模数为质数求逆元还要用扩展欧几里德说实话我觉得这很没必要\n最后进行整体算法复杂度分析，考虑递归形成了一个满二叉树的最坏情况，叶子节点数便是 $\\mathrm{n}$，那么根据树的基本知识这颗满二叉树的总节点数依旧是 $\\mathrm{n}$ 量级的，所以总体算法复杂度 $\\mathrm{O}(n)$。\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; inline void read(int \u0026amp;x){ char ch=getchar();x=0; while(!isdigit(ch)) ch=getchar(); while(isdigit(ch)) x=x*10+ch-\u0026#39;0\u0026#39;,ch=getchar(); } const int N=10000007; int n,a[N],maxn,x; long long y; long long cal(int dep){ if(a[dep]){ a[dep]--; return 1; } int s1=cal(dep-1); int s2=cal(dep-1); y+=s1+s2; return (max(s1,s2)\u0026lt;\u0026lt;1)+1; } int ex_gcd(int s1,int s2,int \u0026amp;s3,int \u0026amp;s4){ if(s2==0){ s3=1; s4=0; return s1; } int temp=ex_gcd(s2,s1%s2,s4,s3); s4-=s3*(s1/s2); return temp; } int main(){ read(n); for(int i=1;i\u0026lt;=n;i++){ int temp; read(temp); maxn=max(temp,maxn); a[temp]++; } int ans,temp=0,judge; for(int i=1;i\u0026lt;=maxn||temp;i++){ temp=a[i]+temp\u0026gt;\u0026gt;1; x=max(x,i); } cal(x); judge=ex_gcd(x,y,ans,temp); if(judge!=1) return printf(\u0026#34;-1\u0026#34;),0; else return printf(\u0026#34;%lld\u0026#34;,(ans%y+y)%y),0; } ","date":"2023-09-25T15:36:00+08:00","permalink":"/post/p5032/","title":"P5032 经验 题解"},{"content":"一道很好的图论练手题。\n难得题意很简洁，就是让你进行一个路径计数，看到这么简洁的路径计数我第一眼想到的就是通过拓扑排序进行计数 $\\texttt{dp}$，并且题中说明了是单向道路，那么这就是一个很简单的 $\\texttt{DAG}$ 上 $\\texttt{dp}$。\n但是仍然有一些地方需要注意，因为题中并没有保证这张图是一张有向无环图，所以我们要去判断有环的情况，我们跑一遍 $\\texttt{Tarjan}$ 算法便可以找出这个图中所有的环，但是题中的所需要的特判当且仅当路径数为无穷时，所以我们要找的是图中对从 $\\texttt{1}$ 号节点到 $\\texttt{2}$ 号节点路径数有贡献的环。\n至于如何判断这个环是否对路径数有贡献，这个环对路径数有贡献当且仅当可以从 $\\texttt{1}$ 号节点走到这个环上并且可以从这个环走到 $\\texttt{2}$ 号节点上。\n想要知道能否从 $\\texttt{1}$ 号节点到环很简单，搜一遍就好了，但是能否从环到达 $\\texttt{2}$ 号节点就是另一个问题了，正所谓正难则反，我们另外建出一份反图从 $\\texttt{2}$ 号节点跑一遍和刚才一样的就好了，这也是图论题中常用的技巧。\n另外一个注意事项，拓扑排序中我们需要根据下一个的入度来判断是否可以入队，但是有些点和边是根本遍历不到的，那么就会走到半路就寄了，所以我们要在判断是否能从 $\\texttt{1}$ 号节点到环的时候顺便记录一下有用的入度，当然记得要用 $\\texttt{BFS}$ 不然会多加出一些入度。\n时间复杂度:$\\mathrm{O}(n)$。\n（注：本题诈骗数据，记得开 $10^5$）\ncode #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define int long long const int N=100004; const int mod=1e9; int n,m,head[N],cnt,in[N],dis[N],head1[N],cnt1; struct Edge{ int to; int next; }e[N],e1[N]; void adding(int u,int v){ e[++cnt].to=v; e[cnt].next=head[u]; head[u]=cnt; } void adding1(int u,int v){ e1[++cnt1].to=v; e1[cnt1].next=head1[u]; head1[u]=cnt1; } int dfn[N],low[N],tot,vis[N],num,size[N],color[N]; bool check[N],check1[N]; stack\u0026lt;int\u0026gt; s; void Tarjan(int x){//Tarjan板子不多说了 dfn[x]=low[x]=++tot; s.push(x); vis[x]=1; for(int i=head[x];i;i=e[i].next){ int v=e[i].to; if(!dfn[v]){ Tarjan(v); low[x]=min(low[x],low[v]); }else if(vis[v]){ low[x]=min(low[x],low[v]); } } if(dfn[x]==low[x]){ num++; while(s.top()!=x){ int temp=s.top(); s.pop(); vis[temp]=0; size[num]++; color[temp]=num; } vis[x]=0; size[num]++; color[x]=num; s.pop(); } } queue\u0026lt;int\u0026gt;q; void BFS1(int x){ q.push(1); check[1]=1; while(q.size()){ int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].next){ int v=e[i].to; if(!check[v]){ check[v]=1; q.push(v); } in[v]++;//记录入度 } } } void DFS1(int x){ for(int i=head1[x];i;i=e1[i].next){ int v=e1[i].to; if(check1[v]) continue; check1[v]=1; DFS1(v); } } void tuopu(){ dis[1]=1; q.push(1); while(q.size()){ int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].next){ int v=e[i].to; if(check1[v]){ dis[v]+=dis[x]; dis[v]%=mod; in[v]--; if(in[v]==0){//入度为零才能加 q.push(v); } } } } } signed main(){ scanf(\u0026#34;%lld%lld\u0026#34;,\u0026amp;n,\u0026amp;m); for(int i=1;i\u0026lt;=m;i++){ int s1,s2; scanf(\u0026#34;%lld%lld\u0026#34;,\u0026amp;s1,\u0026amp;s2); adding(s1,s2); adding1(s2,s1);//建反图 } for(int i=1;i\u0026lt;=n;i++){ if(!dfn[i]) Tarjan(i); } check[1]=1;//记录正图的点能否被遍历到 check1[2]=1;//同上记录反图 BFS1(1);//第一遍要用BFS DFS1(2); for(int i=1;i\u0026lt;=n;i++){ if(check[i]\u0026amp;\u0026amp;size[color[i]]!=1\u0026amp;\u0026amp;check1[i]){ printf(\u0026#34;inf\\n\u0026#34;); return 0; } } tuopu(); printf(\u0026#34;%lld\u0026#34;,dis[2]); return 0; } ","date":"2023-08-08T20:34:00+08:00","permalink":"/post/p4645/","title":"P4645 [COCI2006-2007#3] BICIKLI 题解"},{"content":"有一说一这道题和 国王游戏 很像，但是多了一个高度的维度，当然起先我只认为用这个判断是否合法然后取最优解就好了，然后写了一发爆切57 pts。\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int N=25; int n,h,cnt,t[N],ans=INT_MAX; struct Cow{ int height; int weight; int strength; }e[N]; bool cmp(Cow A,Cow B){ if(B.strength-A.weight!=A.strength-B.weight){ return A.strength-B.weight\u0026gt;B.strength-A.weight; }else{ return A.height\u0026gt;B.height; } } int main(){ scanf(\u0026#34;%d%d\u0026#34;,\u0026amp;n,\u0026amp;h); for(int i=1;i\u0026lt;=n;i++){ scanf(\u0026#34;%d%d%d\u0026#34;,\u0026amp;e[i].height,\u0026amp;e[i].weight,\u0026amp;e[i].strength); } sort(e+1,e+n+1,cmp); int sum=0; for(int i=1;i\u0026lt;=n;i++){ t[i]=t[i-1]+e[i].weight; sum+=e[i].height; if(sum\u0026gt;=h){ cnt=i; break; } } for(int i=1;i\u0026lt;=cnt;i++){ ans=min(ans,e[i].strength-(t[cnt]-t[i])); } if(!cnt||ans\u0026lt;0){ printf(\u0026#34;Mark is too tall\\n\u0026#34;); }else{ printf(\u0026#34;%d\u0026#34;,ans); } return 0; } 那么问题出现在哪呢？注意到对于这道题这个贪心策略是不全面的，有的时候我们可以在排序后的奶牛中间移除一些没有那么优秀的，而且注意到 $n$ 的范围非常小，那么我们便可以使用状态压缩和搜索两种方式枚举使用哪些奶牛，这样就变成了一个完美的贪心策略。\n而对于排序方式来说，我们注意到只考虑两个奶牛的话，两个相邻的奶牛位置的交换是对于其他奶牛来说没有任何影响，那么只需要按照两个奶牛的体重和承受能力的差值进行排序就好了。\n复杂度：$\\mathrm{O}(2^nn)$。\n这里给出我的搜索代码。\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int N=25; int n,h,ans=-INT_MAX,a[N],t[N]; struct Cow{ int height; int weight; int strength; }e[N],s[N]; bool cmp(Cow A,Cow B){ if(B.strength-A.weight!=A.strength-B.weight){//贪心策略排序 return A.strength-B.weight\u0026gt;B.strength-A.weight; }else{ return A.height\u0026gt;B.height; } } int cal(){ int temp=INT_MAX; int cnt=0; t[0]=0; for(int i=1;i\u0026lt;=n;i++){ if(a[i]){ s[++cnt]=e[i];//提取出选中的奶牛 } } sort(s+1,s+1+cnt,cmp);//排序 int sum=0; for(int i=1;i\u0026lt;=cnt;i++){ sum+=s[i].height; t[i]=t[i-1]+s[i].weight;//前缀和节省复杂度 } if(sum\u0026lt;h){//特判 return -INT_MAX; } for(int i=1;i\u0026lt;=cnt;i++){ temp=min(temp,s[i].strength-(t[cnt]-t[i]));//记录答案，这种情况的答案便是对于每一头奶牛的剩余承受重量取最小值 } return temp; } void DFS(int dep,int now){ a[dep]=now; if(dep==n){ ans=max(ans,cal()); return; } DFS(dep+1,1); DFS(dep+1,0); return; } int main(){ scanf(\u0026#34;%d%d\u0026#34;,\u0026amp;n,\u0026amp;h); for(int i=1;i\u0026lt;=n;i++){ scanf(\u0026#34;%d%d%d\u0026#34;,\u0026amp;e[i].height,\u0026amp;e[i].weight,\u0026amp;e[i].strength); } DFS(1,1);//搜索 DFS(1,0); if(ans\u0026lt;0){//小于0说明根本搭不了 printf(\u0026#34;Mark is too tall\\n\u0026#34;); }else{ printf(\u0026#34;%d\u0026#34;,ans); } return 0; } ","date":"2023-06-30T10:48:00+08:00","permalink":"/post/p3112/","title":"P3112 [USACO14DEC] Guard Mark G 题解"},{"content":"首先我第一眼看到这道题时以为这是一道贪心，贪心策略是对于每一次手里的 $10$ 个货物都优先放下个数最多的。当然这后来想一想便是错误的，因为如果假如手里有 $6$ 个 $A$，但是手里有 $4$ 个 $B$，而且 $6$ 个是一个 $A$，$5$ 个 $C$，那么这个贪心策略很明显就不满足最优解了。\n所以考虑 $dp$，一开始我是想开四维数组，一维用来记录 $dp$ 到第几个了，另外三维用来记录所有没拿出去的 $A,B,C$ 的个数，但是后来发现这样需要维护的东西太多，那么经过观察范围发现我的数组设计应该是没有问题的，那么就改变一下状态设计，对于转移来说，最优秀的便是把后面三个维度转换成现在手里所持有的三个物品的个数，这样数组提供的信息就足够维持我们的 $dp$ 转移了，如何转移写在代码注释里了。\n复杂度：$\\mathrm{O}(1000n)=O(n^2\\sqrt[]{n})$\ncode: #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int N=105; int n,a[N],dp[N][11][11][11]; string s; int main(){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); for(int i=1;i\u0026lt;=n;i++){//使用字符串防止玄学出锅 cin\u0026gt;\u0026gt;s; a[i]=s[0]-\u0026#39;A\u0026#39;+1; } memset(dp,0x3f,sizeof dp);//因为要取最小值所以dp数组初始化极大值 dp[0][0][0][0]=0;//dp起点设置 for(int T=1;T\u0026lt;=n;T++){ for(int i=0;i\u0026lt;=10;i++){ for(int j=0;j\u0026lt;=10;j++){ for(int k=0;k\u0026lt;=10;k++){ if(i+j+k\u0026gt;10) continue;//如果三个相加超过10的话这个状态不合法 if(a[T]==1\u0026amp;\u0026amp;i!=0){//以下三个if分别表示对于a[T]的每一种情况只拿进来却不放下 dp[T][i][j][k]=min(dp[T][i][j][k],dp[T-1][i-1][j][k]); } if(a[T]==2\u0026amp;\u0026amp;j!=0){ dp[T][i][j][k]=min(dp[T][i][j][k],dp[T-1][i][j-1][k]); } if(a[T]==3\u0026amp;\u0026amp;k!=0){ dp[T][i][j][k]=min(dp[T][i][j][k],dp[T-1][i][j][k-1]); } dp[T][0][j][k]=min(dp[T][0][j][k],dp[T][i][j][k]+1);//以下三行对应的是这一次拿出每一种的可能 dp[T][i][0][k]=min(dp[T][i][0][k],dp[T][i][j][k]+1); dp[T][i][j][0]=min(dp[T][i][j][0],dp[T][i][j][k]+1); } } } } printf(\u0026#34;%d\u0026#34;,dp[n][0][0][0]);//因为最后手里什么都不剩，所以输出全为0的状态 return 0; } ","date":"2023-06-30T07:56:00+08:00","permalink":"/post/p2530/","title":"P2530 [SHOI2001] 化工厂装箱员 题解"},{"content":"$\\mathrm{tag}$：网络流，费用流 我第一眼看到这道题的平方建边的时候我还以为需要一些数学方面的优化，但是同机房大佬瞅了一眼跟我说暴力建边能过，我心里一群草泥马奔腾而过。。。\n这题的基本建模其实就是按照题意模拟：\n$1.$ 从超级源点向每一位同学连一条流量为 $\\mathrm{k}$，费用为 $0$ 的边。\n$2.$ 从每个同学向愿意参加的社团连一条容量为 $1$，费用为手续费的相反数，因为交的手续费算收益（赚翻了）。\n$3.$ 最后便是刚才说的暴力建边，根据公式从每个社团向汇点连 $n$ 条边，每一条边的容量为 $1$,费用为——$C_i \\times (2 \\times Num-1)$，$Num$ 表示第几条边。\n但是我们注意到题目要求的最大流量是参加的人数尽量多而不是学习小组的人数之和，那么我们这里用一个贪心的思想，再从每个同学向汇点连一条容量为 $k-1$，费用为 $0$ 的边，因为我们只要保证这个同学参加了任意一个学习小组就够了。\n如果财务处能赚到钱就让学生去参加，赚不到直接让这个学生走费用为 $0$ 的边就好了（真黑啊）。\n复杂度分析：Dinic 复杂度，是肯定能过的啦。\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int inf=1e9+7; const int N=10005; int n,m,sum,a[N],b[N],head[N],cnt=1,s,t,ans; struct Edge{ int to; int next; int wide; int val; }e[N*N]; void adding(int u,int v,int w,int val){ e[++cnt].to=v; e[cnt].val=val; e[cnt].wide=w; e[cnt].next=head[u]; head[u]=cnt; } int dis[N],vis[N]; int now[N]; bool SPFA(){ for(int i=1;i\u0026lt;=t;i++){ dis[i]=inf; } dis[s]=0; queue\u0026lt;int\u0026gt;q; q.push(s); vis[s]=1; now[s]=head[s]; while(q.size()){ int x=q.front(); q.pop(); vis[x]=0; for(int i=head[x];i;i=e[i].next){ int v=e[i].to; if(e[i].wide\u0026amp;\u0026amp;dis[v]\u0026gt;dis[x]+e[i].val){ now[v]=head[v]; dis[v]=dis[x]+e[i].val; if(!vis[v]){ q.push(v); vis[v]=1; } } } } return dis[t]!=inf; } int DFS(int x,int sum){ if(x==t) return ans+=dis[t]*sum,sum; int k,res=0; vis[x]=1; for(int i=now[x];i\u0026amp;∑i=e[i].next){ int v=e[i].to; now[x]=i; if(e[i].wide\u0026amp;\u0026amp;(dis[v]==dis[x]+e[i].val)\u0026amp;\u0026amp;!vis[v]){ k=DFS(v,min(sum,e[i].wide)); if(k==0){ dis[v]=inf; } e[i].wide-=k; e[i^1].wide+=k; sum-=k; res+=k; } } vis[x]=0; return res; } void Dinic(){ while(SPFA()){ DFS(s,inf); } } int main(){ scanf(\u0026#34;%d%d%d\u0026#34;,\u0026amp;n,\u0026amp;m,\u0026amp;sum); t=n+m+1; for(int i=1;i\u0026lt;=m;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); } for(int i=1;i\u0026lt;=m;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;b[i]); } for(int i=1;i\u0026lt;=n;i++){//第一步 adding(s,i,sum,0); adding(i,s,0,0); } char c; for(int i=1;i\u0026lt;=n;i++){//第二步 for(int j=1;j\u0026lt;=m;j++){ c=getchar(); if(c!=\u0026#39;1\u0026#39;\u0026amp;\u0026amp;c!=\u0026#39;0\u0026#39;){//防止字符串出锅 j--; continue; } if(c==\u0026#39;1\u0026#39;){ adding(i,j+n,1,-b[j]); adding(j+n,i,0,b[j]); } } } for(int i=1;i\u0026lt;=m;i++){//暴力第三步 for(int j=1;j\u0026lt;=n;j++){ adding(i+n,t,1,a[i]*(2*j-1)); adding(t,i+n,0,-a[i]*(2*j-1)); } } for(int i=1;i\u0026lt;=n;i++){//贪心优化 adding(i,t,sum-1,0); adding(t,i,0,0); } Dinic(); printf(\u0026#34;%d\u0026#34;,ans); return 0; } ","date":"2023-06-17T15:39:00+08:00","permalink":"/post/p4209/","title":"P4209 学习小组 题解"},{"content":"双倍经验P3419 其实我第一眼看到这道题的时候以为是一道 $\\texttt{DP}$，但是想了想就发现要记录的状态实在太多，于是就去想贪心。\n考虑每次拿玩具对总体的贡献，因为能同时放在地上的玩具个数有限，而我们又只计算拿玩具下来的次数，于是便可得出一个贪心结论，那便是对于把玩具放回去这个行为，下一次玩这个玩具的时间距离现在越远一定越优，因为我们不能不玩任何一个玩具。\n所以我们在把玩的顺序读入后，倒序枚举预处理出这个玩具下一次玩的时间，那么至于如何选择，只需要用一个优先队列维护即可，因为要存两维，一个是用来排序的 $\\texttt{next}$ 数组，一个是这个玩具的 $\\texttt{id}$，所以我们用优先队列存一个 $\\texttt{pair}$ 类即可。\n复杂度为跑不满的 $\\mathrm{O}(nlogn)$。\n另外温馨提示，多测不清空，爆零两行泪。\n#include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int N=500005; int n,k,p,a[N],nex[N],temp[N],ans,vis[N],tot; priority_queue\u0026lt;int,vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026gt;q; //也可以这样写 // priority_queue\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;q; void init(){//预处理函数 while(!q.empty()){ q.pop(); } memset(temp,0,sizeof temp); memset(nex,0,sizeof nex); memset(vis,0,sizeof vis); ans=0; } int main(){ int T; scanf(\u0026#34;%d\u0026#34;,\u0026amp;T); for(int l=1;l\u0026lt;=T;l++){ scanf(\u0026#34;%d%d%d\u0026#34;,\u0026amp;n,\u0026amp;k,\u0026amp;p); for(int i=1;i\u0026lt;=p;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); } for(int i=p;i\u0026gt;=1;i--){//预处理出nex数组 if(temp[a[i]]==0) nex[i]=1e6; else nex[i]=temp[a[i]]; temp[a[i]]=i; } for(int i=1;i\u0026lt;=p;i++){ if(!vis[a[i]]){ if(q.size()==k){ vis[q.top().second]=0; q.pop(); } q.push(make_pair(nex[i],a[i])); ans++; vis[a[i]]=1; }else{//因为有新的进入队列更新，而优先队列不好维护弹出，所以把范围开大一个，防止错误弹出 k++; q.push(make_pair(nex[i],a[i])); } } printf(\u0026#34;%d\\n\u0026#34;,ans); init(); } return 0; } ","date":"2023-06-12T14:10:00+08:00","permalink":"/post/sp688/","title":"SP688 SAM - Toy Cars题解"},{"content":"首先看到这道题的数据范围就可以想到是枚举暴力。\n而对于枚举我们有两种方法，一种是 $\\texttt{DFS}$，而另一种是通过二进制来进行枚举（反正我用的 $\\texttt{DFS}$）。\n而这题有两种情况的翻译并没有给出，一种是无解输出 $\\texttt{IMPOSSIBLE}$，另一种是解法不唯一输出 $\\texttt{NOT UNIQUE}$。\n而对于 $\\texttt{IMPOSSIBLE}$ 我们只需要判断 $\\texttt{DFS}$ 后是否找到了一种解就好了。\n对于 $\\texttt{NOT UNIQUE}$ 情况我们也只需要在判断是否找到了不唯一的合法解就可以了。\n复杂度 $\\mathrm{O}(2^{n}m)$，不完全估计 $\\mathrm{O}(104857600)$，$\\texttt{1s}$ 肯定是能过的啦。\ncode #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; int n,m,t[25],ans[25]; bool flag=false,judge; struct Query{ int a[25]; int num; }q[105]; bool check(){//判断解是否合法的函数 for(int i=1;i\u0026lt;=m;i++){ int temp=0; for(int j=1;j\u0026lt;=n;j++){ if(q[i].a[j]\u0026amp;\u0026amp;t[j]){ temp++;//记录搜到的结果的有草料的个数 } } if(temp!=q[i].num){//不符合条件直接返回 return false; } } for(int i=1;i\u0026lt;=n;i++){ ans[i]=t[i]; } if(flag==true){//是否有过合法解 judge=true; } return true; } void DFS(int dep){ if(dep==n){//搜完n个就检查是否合法 if(check()){ flag=true; } return; } t[dep+1]=1;//枚举每一种情况 DFS(dep+1); // if(flag) return; t[dep+1]=0; DFS(dep+1); // if(flag) return; } int main(){ scanf(\u0026#34;%d%d\u0026#34;,\u0026amp;n,\u0026amp;m); for(int i=1;i\u0026lt;=m;i++){ for(int j=1;j\u0026lt;=n;j++){ char s; cin\u0026gt;\u0026gt;s; if(s==\u0026#39; \u0026#39;||s==\u0026#39;\\n\u0026#39;){//防止玄学字符串出锅 j--; continue; } q[i].a[j]=s-\u0026#39;0\u0026#39;; } scanf(\u0026#34;%d\u0026#34;,\u0026amp;q[i].num); } //搜索 t[1]=1; DFS(1); t[1]=0; DFS(1); if(!flag){ printf(\u0026#34;IMPOSSIBLE\u0026#34;); return 0; } if(judge){ printf(\u0026#34;NOT UNIQUE\u0026#34;); return 0; } for(int i=1;i\u0026lt;=n;i++){ printf(\u0026#34;%d\u0026#34;,ans[i]); } return 0; } ","date":"2023-05-30T13:28:00+08:00","permalink":"/post/p3005/","title":"P3005 [USACO10DEC]The Trough Game S题解"},{"content":"题解 一句话：输出缩点后的出度为 $0$ 的点的个数和入度为 $0$ 的点的个数的最大值\n首先对这个矩阵进行建模，如果能滑到就建一条单向边。\nfor(int i=1;i\u0026lt;=n;i++){ for(int j=1;j\u0026lt;=m;j++){ if(i\u0026gt;1\u0026amp;\u0026amp;a[i-1][j]\u0026lt;=a[i][j]){ adding(cal(i,j),cal(i-1,j)); } if(i\u0026lt;n\u0026amp;\u0026amp;a[i+1][j]\u0026lt;=a[i][j]){ adding(cal(i,j),cal(i+1,j)); } if(j\u0026gt;1\u0026amp;\u0026amp;a[i][j-1]\u0026lt;=a[i][j]){ adding(cal(i,j),cal(i,j-1)); } if(j\u0026lt;m\u0026amp;\u0026amp;a[i][j+1]\u0026lt;=a[i][j]){ adding(cal(i,j),cal(i,j+1)); } } } 然后使用 Tarjan 进行缩点，最后记录缩点后的入度和出度就可以了，答案就是出度为 $0$ 的强连通分量的个数和入度为 $0$ 的强连通分量的个数取 $\\max$，最后不要忘了特判一下如果只有一个强连通分量的话就不需要建立任何一道缆车。\ncode #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; const int N=250005; int n,m,head[N],cnt,a[505][505]; struct Edge{ int to; int next; }e[4*N]; void adding(int u,int v){ e[++cnt].to=v; e[cnt].next=head[u]; head[u]=cnt; } int cal(int i,int j){ return (i-1)*m+j; } int dfn[N],low[N],id,vis[N],is[N],tot; stack\u0026lt;int\u0026gt; s; void Tarjan(int x){//缩点 dfn[x]=low[x]=++id; s.push(x); vis[x]=1; for(int i=head[x];i;i=e[i].next){ int v=e[i].to; if(!dfn[v]){ Tarjan(v); low[x]=min(low[v],low[x]); }else if(vis[v]){ low[x]=min(low[x],dfn[v]); } } if(dfn[x]==low[x]){ ++tot; while(s.top()!=x){ int temp=s.top(); s.pop(); is[temp]=tot; vis[temp]=0; } int temp=s.top(); s.pop(); is[temp]=tot; vis[temp]=0; } } int in[N],out[N],tt1,tt2; int main(){ scanf(\u0026#34;%d%d\u0026#34;,\u0026amp;m,\u0026amp;n); for(int i=1;i\u0026lt;=n;i++){ for(int j=1;j\u0026lt;=m;j++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i][j]); } } for(int i=1;i\u0026lt;=n;i++){//建边 for(int j=1;j\u0026lt;=m;j++){ if(i\u0026gt;1\u0026amp;\u0026amp;a[i-1][j]\u0026lt;=a[i][j]){ adding(cal(i,j),cal(i-1,j)); } if(i\u0026lt;n\u0026amp;\u0026amp;a[i+1][j]\u0026lt;=a[i][j]){ adding(cal(i,j),cal(i+1,j)); } if(j\u0026gt;1\u0026amp;\u0026amp;a[i][j-1]\u0026lt;=a[i][j]){ adding(cal(i,j),cal(i,j-1)); } if(j\u0026lt;m\u0026amp;\u0026amp;a[i][j+1]\u0026lt;=a[i][j]){ adding(cal(i,j),cal(i,j+1)); } } } for(int i=1;i\u0026lt;=n*m;i++){ if(!dfn[i]) Tarjan(i); } for(int k=1;k\u0026lt;=n*m;k++){//记录入度和出度 for(int i=head[k];i;i=e[i].next){ int v=e[i].to; if(is[v]!=is[k]){ in[is[v]]++; out[is[k]]++; } } } for(int k=1;k\u0026lt;=tot;k++){ if(!in[k]) tt1++; if(!out[k]) tt2++; } if(tot==1){//输出答案 printf(\u0026#34;0\u0026#34;); }else{ printf(\u0026#34;%d\u0026#34;,max(tt1,tt2)); } return 0; } ","date":"2023-05-11T21:01:00+08:00","permalink":"/post/p1653/","title":"P1653 [USACO04DEC] Cow Ski Area G"}]