[{"content":"Introduction 什么是拓扑顺序 想象你有一系列任务，其中一些任务必须在另一些任务之前完成。拓扑排序就是将这些任务排成一个线性序列，保证每个任务的所有“前置任务”都出现在它自己之前。它只适用于有向无环图（DAG）——即任务间的依赖关系不会形成一个循环，否则就永远找不到起点。\n意义 它的核心价值在于理清依赖，保证顺序。在复杂的依赖网络中，它能提供一个清晰、可执行的行动路线图，确保整个过程不会因为前置条件不满足而卡住。\n实践应用场景 任务调度：操作系统或分布式系统用它来决定多个进程或作业的执行顺序。\n依赖关系解析：软件包管理器（如 apt, npm）在安装软件时，用它来解析并安装所有必需的依赖库。\n课程先修计划：帮你规划大学选课顺序，确保在修读高级课程前，你已经完成了所有必修的先修课程。\n构建系统：像 Make 或 Gradle 这样的工具，用它来编译源代码，确定哪些模块需要先被编译，哪些可以并行编译。\nTopology Sorting Basics 拓扑排序为一系列存在依赖关系的任务找到一个可行的执行序列，使得对于任何两个任务 A 和 B，如果 A 必须在 B 之前完成，那么在序列中 A 就出现在 B 之前。\n它有一个黄金前提：这些任务和依赖必须构成一个 有向无环图（DAG）。\n有向：依赖关系是单向的（穿袜子 → 穿鞋）。\n无环：绝不能出现循环依赖。比如“穿鞋之前要穿袜子”和“穿袜子之前要穿鞋”同时存在，这就死锁了，永远无法开始。\nKahn\u0026rsquo;s算法 算法步骤 算法步骤 初始化：计算每个节点的入度（有多少边指向它）\n找起点：将所有入度为0的节点加入队列\n处理节点：\n从队列中取出节点，加入结果序列\n将该节点的所有邻居的入度减1\n如果某个邻居的入度变为0，将其加入队列\n检查结果：如果结果序列包含所有节点，成功；否则说明有环\n算法实现 题目 洛谷：https://www.luogu.com.cn/problem/P1807\n在有向无环图(DAG)中求最长路，拓扑排序是最高效的方法。我们按拓扑顺序递推，确保处理每个节点时，所有可能影响它的前驱节点都已经被计算完毕，从而正确更新最长距离\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int n, m; const int inf = -0x3f3f3f3f; const int N = 1505; vector\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; e[N]; int dist[N]; int degree[N]; int main(){ ios_base::sync_with_stdio(false); cin.tie(0); cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; fill(dist+1, dist+n+1, inf); for(int i = 1; i \u0026lt;= m; i++){ int u,v,w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; e[u].push_back({v, w}); degree[v]++; } dist[1] = 0; queue\u0026lt;int\u0026gt; q; for(int i = 1; i \u0026lt;= n; i++){ if(!degree[i]) q.push(i); } while(!q.empty()){ int u = q.front(); q.pop(); for(auto [v, w]: e[u]){ if(dist[u] != inf){ dist[v] = max(dist[v], dist[u]+w); } degree[v]--; if(!degree[v]) q.push(v); } } if(dist[n] == inf) cout \u0026lt;\u0026lt; \u0026#34;-1\u0026#34;; else cout \u0026lt;\u0026lt; dist[n]; } 基于DFS的拓扑排序 核心思想 想象你在规划一天的任务：有些任务必须在其他任务之前完成。DFS拓扑排序就像是用递归的方式探索这些依赖关系，沿着每条依赖链深入到底，然后逆序记录结果。\n算法流程 任选一个未访问节点开始DFS\n递归访问所有后继节点\n当某个节点没有未访问的后继时，将其加入结果集\n最终反转结果顺序，就得到了拓扑排序\n算法实现 我们还是用最长路来做题目，但是其实在最长路这个题目上dfs的优势不能很好表现，大家可以自己去探索到底什么时候用什么\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int n, m; const int inf = -0x3f3f3f3f; const int N = 1505; vector\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; e[N]; int dist[N]; bool visited[N]; vector\u0026lt;int\u0026gt; topo_order; void dfs(int u) { visited[u] = true; for (auto [v, w] : e[u]) { if (!visited[v]) { dfs(v); } } topo_order.push_back(u); } int main() { ios_base::sync_with_stdio(false); cin.tie(0); cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; fill(dist + 1, dist + n + 1, inf); for (int i = 1; i \u0026lt;= m; i++) { int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; e[u].push_back({v, w}); } // DFS拓扑排序 fill(visited + 1, visited + n + 1, false); for (int i = 1; i \u0026lt;= n; i++) { if (!visited[i]) { dfs(i); } } reverse(topo_order.begin(), topo_order.end()); // 动态规划求最长路 dist[1] = 0; for (int u : topo_order) { if (dist[u] != inf) { for (auto [v, w] : e[u]) { if (dist[v] \u0026lt; dist[u] + w) { dist[v] = dist[u] + w; } } } } if (dist[n] == inf) cout \u0026lt;\u0026lt; \u0026#34;-1\u0026#34;; else cout \u0026lt;\u0026lt; dist[n]; return 0; } END（这part和算法无关了） 拓扑对我来说还是很有哲学意义的。我们面对很多事情的时候总是喜欢想清楚去做，等我们花费很多时间终于找到了那些事自己想做且的做的事情，结果却发现许多自己想做的事情的前提是一系列自己不想做的事情。比如拿到好成绩是大家想得到的，加练等等是不想要的，但是后者确实前者的前提。或许我们不能仅仅想自己想要的，而是基于我们想要的去做一个拓扑排序，最后一个个去完成他们。\n","date":"2025-10-23T00:00:00Z","image":"https://xzyly.github.io/p/%E6%8B%93%E6%89%91/OIP_hu14673548058408144456.jpg","permalink":"https://xzyly.github.io/p/%E6%8B%93%E6%89%91/","title":"拓扑"},{"content":"什么是哈希 当我们在要进行查找，但是元素的关键码与储存地址不是一一对应的话，我们的效率就较低（顺序查找O(n), 树查找（O(logn)）），而哈希就是一个处理这类问题的一个算法。哈希通过函数把关键码“尽量均匀”地送到各个地址上，让我们在查找时通常能很快找到元素。至于“一一对应”？别想太多，冲突总会来，后面我们安排它。\n原理 我们在储存元素的时候根据设计的函数去分配它们的地址，这样在查找时我们就可以根据元素关键码去找到对应的地址来得到元素。\ne.g： hash(x) = x % capacity -\u0026gt; hash(5) = 5 % 7 = 5 (we assume that capacity is 7) -\u0026gt; hash(9) = 9 % 7 = 2\n但是你可能很快就会发现“不行啊，那9放在2的位置，那我2放在哪，桥洞下吗？”。别急，这就叫哈希冲突——不同关键码撞在了同一个地址上，我们不会丢弃它，毕竟“2”也要有个家。\n哈希冲突 哈希冲突就是多个元素的关键码对应的地址相同\n避免（其实是减少）哈希冲突的方法 面对哈希冲突的时候我们没办法丢弃可怜的2，那么不要着急，我们有很多方法帮它安家（不会是漏风的桥洞下）。\n重新设计函数 我们先想想为什么会出现冲突呢，首先可能我们的函数不太好，我们的“2”和太多人有同一个钥匙了，所以我们需要去重新设计一个函数，让拥有钥匙的人少一点（哈希表底层数组容量往往小于关键字的所以冲突在所难免），各位可以去查看常见的函数，自己来更具所需挑选，我们这里分享两个常见的：\n直接定址法 公式：Hash(key) = a * key + b 特点： ·思路最直接，在键空间小且连续时可以做到“不冲突”。\n·要求关键字分布基本连续、并且地址空间足够覆盖键空间。\n缺点：如果关键字分布不连续，会造成大量空间浪费；一旦需要取模或压缩地址，仍可能产生冲突\n除留余数法 - 最常用 公式：Hash(key) = key % p 特点： ·简单、高效，是实践中最常用的方法。\n·关键在于模数 p 的选择。通常 p 应该是一个质数，并且不能太接近2的幂次方。\n为什么p选质数？ 因为质数能使得关键字对p取模后的结果，更均匀地分布在[0, p-1]的区间内，减少冲突。如果p是一个合数，那么关键字中与p含有公共因子的部分会导致某些槽位永远无法被映射到。\n负载因子 负载因子 = 哈希表中已存储的元素数量 / 哈希表的总容量（槽位数）\n它衡量了哈希表的 \u0026ldquo;满的程度\u0026rdquo;。负载因子越高，意味着哈希表越满，发生哈希冲突的概率就越大，从而导致插入、查找等操作的性能下降。\n当负载因子超过某个 阈值 时，最直接有效的方法就是创建一个容量更大的新哈希表，然后将原哈希表中的所有键值对重新映射到新表中\n解决哈希冲突的方法 巴菲特说过“只有通过斗争，我们才能认识自己；只有通过冲突，我们才能发现自己的力量。”，所以我们不能为了给可怜的“2”一个家就去避免冲突，我们要战斗为千千万万个“2”打出一片天地。那么我们来分享如何解决哈希冲突。\n闭散列 我们这里分享两个冲突时寻找下一个地址的方法，不同方法的优缺点各有不同，各位具体情况具体分析\n线性探测 探测序列：H(key), H(key)+1, H(key)+2, \u0026hellip;, (H(key)+i) % TableSize\n优点：实现简单，对CPU缓存友好（顺序访问内存）。\n缺点：容易产生 初级聚集，即连续的位置被占用，形成长的冲突链，导致后续操作性能下降。\n二次探测 探测序列：H(key), H(key)+1², H(key)+2², H(key)+3², \u0026hellip;, (H(key)±i²) % TableSize\n优点：缓解了初级聚集，让冲突的元素分布得更分散。\n缺点：会产生 次级聚集（映射到同一初始地址的元素拥有相同的探测序列）。并且如果表长和负载控制不当，可能出现明明有空位却“兜兜转转找不到”的情况（通常选择表长为质数并控制负载因子可缓解）。\n双重散列（相比前两个应该是最优的） 探测序列：(H1(key) + i * H2(key)) % TableSize\n使用两个哈希函数。H1 确定初始位置，H2 确定探测的步长。\n要求：H2(key) 不能为0，且必须与表大小 m 互质，以保证探测序列能覆盖整个表。\n优点：是开放定址法中最好的方法之一，产生的探测序列最接近\u0026quot;随机\u0026quot;，能有效减少各种聚集现象。\n缺点：计算开销稍大。\n开散列 这个大家更熟悉的名称应该是哈希桶，而且我感觉这个概念大家应该也很好理解怎么实现的\n我们将相同地址的元素归于一个集合，也就是第一个桶，这个桶通过链表链接，那我们的哈希表就显而易见储存的是链表头的，这样我们查找元素的时候就只需要在一个更小的集合去查找，大大提高了效率而且也解决了冲突\n","date":"2025-10-21T00:00:00Z","image":"https://xzyly.github.io/p/%E5%93%88%E5%B8%8C/OIP_hu13373775048745788014.jpg","permalink":"https://xzyly.github.io/p/%E5%93%88%E5%B8%8C/","title":"哈希"},{"content":"Introduction What is RAG RAG(Retrieval Augment Generate)就是一种最优化LLMs的输出的一个方法，让大模型可以基于指定的语料库来进行生成。\nWhy RAG LLMs虽然拥有庞大的训练数据，但当我们向其提问时，可能遇到以下几个问题：\n对于常见的、开放性问题，LLMs的回答依赖于公开数据，有时虽能给出不错的答案，但也容易输出“幻觉”——即给出看似合理但实际并不存在的信息。 对于专业性强或企业内部的数据，比如公司政策、产品细节、历史故障记录等，传统的LLMs很难给出准确解答，因为这些内容往往没有包含在训练语料里。 RAG（Retrieval Augmented Generation）正好可以解决这个问题：通过检索公司内部文档、知识库等专有数据，把相关内容作为上下文补充到大模型前，使其结合最新、最相关的信息生成答案。这样既可以降低幻觉，提升回答的专业性和准确性，还能让LLMs灵活适应各种业务场景。这就是为什么需要用RAG来增强LLMs的理由。\n基础RAG的缺陷 基础的RAG主要流程是“检索-拼接-生成”，虽然可以借助外部知识提升答案的相关性，但在实际应用中仍存在以下几个问题：\n简单的检索策略\n多数基础RAG只使用向量检索，匹配文本内容的语义相似度。但检索模型容易受限于召回率和精度，导致检索到的内容不一定是真正相关、最重要的信息。\n检索粒度问题\n通常按段落、句子或者文档进行检索，检索粒度过粗会导致噪声上下文被引入，太细又可能丢失整体语义，从而影响生成质量。\n固定上下文窗口\n由于LLM的输入有长度上限，检索到的内容超出窗口时需要裁剪或截断，这容易丢失关键信息，也限制了大模型对多条复杂知识的融合能力。\n缺乏上下文理解和重排\n基础RAG一般没有进行二次筛选或重排，LLM只能被动接受检索内容，对上下文片段之间的关系和时序理解有限，可能导致生成答案时断章取义或者语义割裂。\n不处理检索噪声和错误\n若检索误召回了无关或错误的内容，基础RAG难以自动校正，容易把误导或错误信息直接带入生成，降低安全性与可靠性。\n因此，如果只是用最基础的RAG流程，很容易遇到“检索不到位、上下文无关、生成不准确”等尴尬，不能满足高质量AI问答或助手场景的需求，后续需要对RAG模块进行优化和增强。\nMake a Smart RAG 多模态检索 多模态检索指的是让RAG系统能够检索并处理文本、图片、表格等多种不同类型的数据，而不仅仅局限于纯文本。这样能够更全面地覆盖各种异构信息源，在实际应用中显著提升系统的知识获取能力。例如，在法律、医疗等领域，图片、表格同样是重要的知识载体，多模态检索让RAG应对真实场景更加得心应手。\n检索模型升级 检索模型的选择直接影响相关文档的召回质量。通过引入更强大的语义检索模型（比如ColBERT、Hybrid检索），同时结合关键词检索与向量检索，实现多路召回，可以大大提升检索的精准性和全面性。这为后续生成提供了更优质、更相关的上下文基础。\n段落/片段智能切分 智能分割文档内容是基础且关键的一步。利用知识分块、SBD算法等工具，将长文档合理切分为粒度合适的上下文片段，可有效避免信息噪声和表达割裂。同时，良好的切分粒度确保片段包含完整的语义，提升检索和生成阶段的质量。\n二次筛选与重排序 检索得到的内容往往存在相关性高低不同的问题。通过对初步检索结果进行二次打分、语义重排序（Rerank），可以结合文本语义、上下文连续性等更多维度，对召回片段进行优化排序，确保最终输入LLM的是最重要、最相关的信息段落，提高生成结果的专业性与准确性。\n上下文窗口优化 LLMs具有输入窗口长度的限制，原始检索内容往往超长。通过知识压缩、摘要融合、重要性排序等方式，可以最大程度保留核心信息，在有限窗口内传递尽可能多的有用知识，规避信息丢失，提升复杂问答场景下的系统表现。\n证据验证与降噪 检索内容可能掺杂错误、不相关或过时的信息。通过让LLM参与对片段的筛查、事实核验，可以自动过滤掉无关、虚假的内容，有效降低生成中“幻觉”的概率，提升最终答案的准确性和可信度。\n自适应召回策略 不同的业务需求和问题复杂度对检索数量和内容长度要求不一致。自适应召回策略能够根据具体任务动态调整召回文档的数量及片段长度，在保证信息覆盖的同时，兼顾高精度和省资源，实现更智能、更灵活的上下文选取。\n多轮检索和生成结合 面对连续对话、复杂需求场景，仅依赖单轮检索往往不足。多轮检索与生成结合的优化方法，可以在多轮对话中连续累积历史信息，动态调整检索和上下文输入，保证生成内容的上下文连贯，以及针对性的知识补充。\n领域知识增强 在专业场景（如医疗、金融等）下，需要更专业的知识支撑。通过训练和微调领域专用的检索模型，使其对专业词汇、知识点具有更敏锐的识别和检索能力，可以极大提升RAG在垂直领域的精准召回和知识覆盖。\n高质量数据和知识库建设 RAG系统的可靠性和覆盖性基础在于数据。通过定期清洗知识库、去除冗余和错误文档，并持续建设结构化、高可信度的知识底座，可以保障后续检索的高质量，同时为RAG系统的持续优化提供坚实支撑。\nHow to Evaluate Your RAG RAG性能评估方法 RAG系统开发完成后，评估其效果同样重要。科学的评测不仅能检验现有方案的优劣，也指导持续的系统优化。下面介绍常见的RAG评估思路、标准和实践。\n1. 检索阶段评估 召回率（Recall）与准确率（Precision）\n测测检索出的结果中包含多少真实相关内容（召回率），以及检索结果中有多少是与需求真正相关的（准确率）。 Top-K准确率\n通常统计检索返回的前K个片段中是否包含正确答案或高相关内容，典型指标如Top-1/Top-3/Top-5 Recall。 覆盖率\n检查知识库中与问题相关的信息是否有被检出，衡量知识库完整性和检索能力。 多样性\n分析检索到的片段类型、来源是否丰富，评估系统面对复杂问题时的信息广度。 2. 生成阶段评估 答案相关性/正确率\n让领域标注人员或专家判断生成答案是否覆盖关键信息、是否正确。 信息完整性与信噪比\n检验生成内容中包含多少有用信息，是否有无关赘述或错误\u0026quot;幻觉\u0026quot;。 可读性与连贯性\n评估输出流畅度、逻辑性，是否便于理解和采纳。 3. 端到端整体评估 人工标注打分\n制定标准问答集，对系统自动生成的答案进行主观标注打分，如相关性、完整性、信任度等。 自动化评测指标\n如ROUGE、BLEU等自动评分指标，适用于答案为结构化文本或标准答案较明确场景。 用户反馈/在线A/B测试\n在实际业务流中观察用户满意度、交互点击率、辅助决策效果等真实指标。 事实一致性验证\n检查LLM生成内容是否与检索证据保持一致，评估“幻觉”率。 4. 评测集构建建议 准备覆盖多业务场景、多问题类型的高质量问答对，明确标注标准答案及相关知识片段； 定期扩充和维护评测集，跟进业务变化和知识库更新； 引入专家复审机制，确保评测结论具有权威性和参考价值。 RAG系统的评估是多维度、系统化的工程。只有持续、科学评测，才能有效指导RAG的优化，实现高质量、可信赖的问答或知识助理服务。\n","date":"2025-10-16T00:00:00Z","image":"https://xzyly.github.io/p/how-to-make-a-smart-rag/OIP_hu12618926430399980339.jpg","permalink":"https://xzyly.github.io/p/how-to-make-a-smart-rag/","title":"How to make a smart RAG"},{"content":"前言 本文内容基于 LangChain 官方文档进行简化和提炼，主要为个人学习总结。如需更详尽的信息，建议直接查阅官方文档。\n什么是 LangChain LangChain 是一个基于大语言模型的应用程序开发框架，旨在简化 LLM 应用生命周期的各个阶段：开发、生产与部署。本文将重点介绍文档与文档加载器、文本分割、嵌入模型以及向量存储与检索的基本使用方法。\nDocuments and Document Loaders 1. Document Document 对象包含三个主要属性：\npage_content: 存储文本内容的字符串 metadata: 包含任意元数据的字典 id (可选): 文档的字符串标识符 1 2 3 4 5 6 7 8 9 10 11 12 from langchain_core.documents import Document documents = [ Document( page_content=\u0026#34;Dogs are great companions, known for their loyalty and friendliness.\u0026#34;, metadata={\u0026#34;source\u0026#34;: \u0026#34;mammal-pets-doc\u0026#34;}, ), Document( page_content=\u0026#34;Cats are independent pets that often enjoy their own space.\u0026#34;, metadata={\u0026#34;source\u0026#34;: \u0026#34;mammal-pets-doc\u0026#34;}, ), ] 2. Document Loader 这里以 PyPDFLoader 为例，它适用于处理结构化的 PDF 内容。若需处理非结构化内容（如扫描版 PDF），可考虑使用 UnstructuredPDFLoader。\nPyPDFLoader 将 PDF 的每一页存储为一个独立的 Document 对象，便于通过 page_content 和 metadata 访问内容。\n1 2 3 4 5 6 7 8 9 from langchain_community.document_loaders import PyPDFLoader file_path = \u0026#34;../example_data/nke-10k-2023.pdf\u0026#34; loader = PyPDFLoader(file_path) docs = loader.load() print(len(docs)) print(f\u0026#34;{docs[0].page_content[:200]}\\n\u0026#34;) print(docs[0].metadata) 输出示例：\n1 2 3 4 5 6 7 8 9 10 11 12 107 Table of Contents UNITED STATES SECURITIES AND EXCHANGE COMMISSION Washington, D.C. 20549 FORM 10-K (Mark One) ☑ ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(D) OF THE SECURITIES EXCHANGE ACT OF 1934 FO {\u0026#39;source\u0026#39;: \u0026#39;../example_data/nke-10k-2023.pdf\u0026#39;, \u0026#39;page\u0026#39;: 0} 文本分割 (Splitting) 直接将整个文档提供给 LLM 不仅效率低下，还会消耗大量 token。因此，我们需要将文档分割成小块，通过检索筛选出与问题最相关的内容片段，再提供给 LLM，从而获得更精准的回答。\n这里介绍一种常用的分割方法：RecursiveCharacterTextSplitter。它非常适合 RAG 应用场景，能够最大程度保持文本的语义完整性。该方法按照预设的分隔符列表递归地分割文本，直到每个块都符合设定的 chunk_size。与 CharacterTextSplitter 相比，虽然速度稍慢，但分割效果更智能。\n主要参数说明：\nchunk_size: 期望的文本块大小 chunk_overlap: 文本块之间的重叠部分大小 add_start_index: 是否为每个块添加在原文本中的起始位置索引 1 2 3 4 5 6 7 8 from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, add_start_index=True ) all_splits = text_splitter.split_documents(docs) len(all_splits) 1 514 嵌入 (Embedding) 嵌入的核心作用是将文本转换为高维向量表示，便于后续通过向量相似度计算来检索相关信息（特别是在 Q\u0026amp;A 场景中）。LangChain 支持多种嵌入模型，可以根据具体需求选择。\n以下示例使用 HuggingFace 的免费嵌入模型：\n1 2 3 4 5 from langchain_community.embeddings import HuggingFaceEmbeddings embeddings_model = HuggingFaceEmbeddings(model_name=\u0026#34;sentence-transformers/all-MiniLM-L6-v2\u0026#34;) texts = [doc.page_content for doc in split_docs] doc_embeddings = embeddings_model.embed_documents(texts) 向量存储与检索 生成文本嵌入后，需要将其存储起来以便后续查询。这里使用 Chroma 作为向量数据库：\n1 2 3 4 5 6 7 from langchain_chroma import Chroma vector_store = Chroma( collection_name=\u0026#34;example_collection\u0026#34;, embedding_function=embeddings_model, persist_directory=\u0026#34;./chroma_langchain_db\u0026#34;, # 本地存储路径，如不需要可移除 ) 存储完成后，即可通过 similarity_search 方法查找与查询问题最相关的文档片段。该方法通常基于余弦相似度、欧氏距离或点积等算法计算向量相似度，其中余弦相似度是最常用的方法。\n1 2 3 4 5 results = vector_store.similarity_search( \u0026#34;How many distribution centers does Nike have in the US?\u0026#34; ) print(results[0]) 1 2 3 4 5 6 7 8 page_content=\u0026#39;direct to consumer operations sell products through the following number of retail stores in the United States: U.S. RETAIL STORES NUMBER NIKE Brand factory stores 213 NIKE Brand in-line stores (including employee-only stores) 74 Converse stores (including factory stores) 82 TOTAL 369 In the United States, NIKE has eight significant distribution centers. Refer to Item 2. Properties for further information. 2023 FORM 10-K 2\u0026#39; metadata={\u0026#39;page\u0026#39;: 4, \u0026#39;source\u0026#39;: \u0026#39;../example_data/nke-10k-2023.pdf\u0026#39;, \u0026#39;start_index\u0026#39;: 3125} ","date":"2025-10-01T00:00:00Z","image":"https://xzyly.github.io/p/langchain-basics/OIP_hu16649314138537031086.jpg","permalink":"https://xzyly.github.io/p/langchain-basics/","title":"LangChain 核心概念入门：文档加载、分割与向量检索"},{"content":"为什么要求逆元 当我计算((a % p) (+/-/* ) (b % p)) % p的时候都可以直接这样使用，但是当我们处理除法的时候，比如((a%p)/(b%p))%p，这个方法就失效了，但在大数运算时，直接除法可能产生精度问题或溢出。\n什么是费马小定理 1.当a是p的倍数： a^p≡a(mod p) 2.当a与p互质，即gcd(a, p) = 1: a^p−1≡1(mod p)\n如何用费马小定理求逆元 逆元的定义 在模 p 运算中，如果存在整数 x 使得 b × x ≡ 1 (mod p)，则称 x 为 b 的模 p 逆元，记作 b⁻¹。\n除法转乘法的原理 在模运算中，我们定义除法为：a/b ≡ a × b⁻¹ (mod p)。\n因此，(a/b) % p = (a × b⁻¹) % p。\n费马小定理求逆元 当 p 是质数且 b 与 p 互质时，由费马小定理： b^(p-1) ≡ 1 (mod p)\n将上式两边同时乘以 b⁻¹： b^(p-1) × b⁻¹ ≡ 1 × b⁻¹ (mod p) b^(p-2) ≡ b⁻¹ (mod p)\n因此，逆元 x = b^(p-2) % p。\n完整计算过程 要求 (a/b) % p：\n验证 p 是质数且 gcd(b, p) = 1 计算逆元 x = b^(p-2) % p（使用快速幂） 结果 = (a × x) % p 例子 求4 % 5 的逆元 x = 4^(5-2) = 4^3 = 64 % 5 = 4 验证： (4*4)%5 = 1, 正确\n","date":"2025-09-22T00:00:00Z","image":"https://xzyly.github.io/p/%E8%B4%B9%E9%A9%AC%E5%B0%8F%E5%AE%9A%E7%90%86%E6%B1%82%E9%80%86%E5%85%83/OIP_hu10721549702287731090.jpg","permalink":"https://xzyly.github.io/p/%E8%B4%B9%E9%A9%AC%E5%B0%8F%E5%AE%9A%E7%90%86%E6%B1%82%E9%80%86%E5%85%83/","title":"费马小定理求逆元"},{"content":"背包问题我感觉可能好做一点，对于中等难度的题目可能只要判断好背包类型，可能再加上一点优化或 其他算法就差不多能解决了。\n这里的题目可能各位尝试感觉会说没多难，但是我还是感觉做题目比较难的是判断这是什么题目，要用什么方法解决，要不要优化。这里简单说一下如何判断背包问题。其实说起来很简单，对于一个在限制量的要求下去求最值的问题大多是可以用背包来解决的。\n这里总结一下几种背包类型和对应的题目，但是下面列出的大多数可能难度在绿题的样子或者低一级，各位熟悉后可以再去尝试更难的锻炼自己应用能力。\n01背包 简介 n个物品，m的容量，每个物品有对应的重量和价值，每个物品只能拿一个，问能得到的最大价值为多少\n分析 这个也是dp思想，首先我们很清楚这个问题可以由最优子问题解决，这个就不多说了，我们这里就说下一般的两种做法。\n二维DP矩阵说明 f[i][j]表示只考虑前i个物品，当容量为j时的最值\n1. 矩阵基本信息 行代表：物品（从0到n） 列代表：容量（从0到m） 单元格含义：f[i][j] 表示考虑前i个物品、容量为j时的最大价值 2. 矩阵结构示例 i\\j（物品\\容量） 0 1 2 \u0026hellip; m 0（无物品） 0 0 0 \u0026hellip; 0 1 计算值 计算值 计算值 \u0026hellip; 计算值 2 计算值 计算值 计算值 \u0026hellip; 计算值 \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; n 结果 结果 结果 \u0026hellip; 最终结果 一维DP数组说明 其实就是对二维dp的空间优化，用f[i]表示在容量为i时能达到的最值\n1. 数组基本信息 索引代表：容量（从0到m） 元素含义：f[j] 表示容量为j时能获得的最大价值（通过迭代更新实现空间优化） 2. 数组迭代过程示例 状态阶段 容量0 容量1 容量2 \u0026hellip; 容量m 初始状态 0 0 0 \u0026hellip; 0 处理第1个物品后 0 更新值1 更新值2 \u0026hellip; 更新值m 处理第2个物品后 0 再更新值1 再更新值2 \u0026hellip; 再更新值m \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; 处理完所有物品 0 最终结果1 最终结果2 \u0026hellip; 最终结果m 3. 核心特点 仅用一个一维数组存储中间结果，空间复杂度从O(n×m)优化为O(m) 迭代方向需从右到左（容量从m到0），避免单个物品被重复选择 每次迭代对应一个物品的处理，通过覆盖旧值实现状态更新 01背包例题——搬砖 题目描述 这天，小明在搬砖。\n他一共有 $n$ 块砖，他发现第 $i$ 砖的重量为 $w_{i}$，价值为 $v_{i}$。他突然想从这些砖中选一些出来从下到上堆成一座塔，并且对于塔中的每一块砖来说，它上面所有砖的重量和不能超过它自身的价值。\n他想知道这样堆成的塔的总价值（即塔中所有砖块的价值和）最大是多少。\n输入格式 输入共 $n+1$ 行, 第一行为一个正整数 $n$, 表示砖块的数量。\n后面 $n$ 行, 每行两个正整数 $w_{i}, v_{i}$ 分别表示每块砖的重量和价值。\n输出格式 一行，一个整数表示答案。\n输入输出样例 #1 输入 #1 1 2 3 4 5 6 5 4 4 1 1 5 2 5 5 4 3 输出 #1 1 10 说明/提示 【样例说明】\n选择第 $1$、$2$、$4$ 块砖，从上到下按照 $2$、$1$、$4$ 的顺序堆成一座塔，总价值为 $4+1+5=10$。\n【评测用例规模与约定】\n对于 $20 %$ 的数据，保证 $n \\leq 10$;\n对于 $100 %$ 的数据，保证 $n \\leq 1000 ; w_{i} \\leq 20 ; v_{i} \\leq 20000$ 。\n蓝桥杯 2022 国赛 B 组 J 题。\n思路 这道题比正常的01背包难一点，因为他让我们不能无脑遍历，为什么呢。因为每块转的承重能力不一样我们如果强行遍历会错失最优解，所以我们的首要任务就是想办法找出最好的顺序（各位正常解题要判断是否要用dp，用什么类型，这个分析就先省略了，可以看前面的博客或者自己多做题后会有感觉的）。\n为了找到这个排序，我们可以先减少数量来看，我们就思考两块砖i和j。我们想是不是存在一种可能如果i可以在j下面那么j也一定可以呢，这样我们排序后就不会错过最优解了，因为i能做的我j也能做（好热血啊）。那我们就试着推导一下。\n当i能在下面承担是会满足两个条件（我们假设二者中间的总重为m1,上面的总重为m2） ·v_i \u0026gt;= m1 + m2 + m_j ·v_j \u0026gt;= m2 而要j能在下面承担是也要满足两个条件 ·v_j \u0026gt;= m1 + m2 + m_i ·v_i \u0026gt;= m2 现在我们要用第一组条件推导出第二组，很明显第二组的第二个条件是满足的。我们就来证明第一个条件 观察到m1 + m2的部分是相同的，我们通过移项发现如果 v_j - m_i \u0026gt;= v_i - m_j 则满足，即v_j+m_j \u0026gt;= v_i+m_i。至此我们就得到了排序方案\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define endl \u0026#39;\\n\u0026#39; #define pii pair\u0026lt;int,int\u0026gt; #define int long long const int mod = 1e9+7; const int inf = 0x3f3f3f; int n; const int N = 20005; const int M = 1005; int f[N]; struct node{ int w, v; friend bool operator \u0026lt; (const node \u0026amp;a, const node \u0026amp;b){ return a.w + a.v \u0026lt; b.w + b.v; } }a[M]; void solve(){ cin \u0026gt;\u0026gt; n; for(int i = 1; i \u0026lt;= n; i++){ cin \u0026gt;\u0026gt; a[i].w \u0026gt;\u0026gt; a[i].v; } sort(a+1,a+1+n); int ans = -1; for(int i = 1; i \u0026lt;= n; i++){ for(int j = a[i].w+a[i].v; j \u0026gt;= a[i].w; j--){ f[j] = max(f[j], f[j-a[i].w]+a[i].v); ans = ans \u0026gt; f[j] ? ans : f[j]; } } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; endl; } signed main(){ ios_base::sync_with_stdio(false); cin.tie(0); int t = 1; // cin \u0026gt;\u0026gt; t; while(t--){ solve(); } return 0; } 完全背包 简介 一样的问题，但是完全背包的物品可以无限拿，处理方案和01背包相似，但是01背包是倒序背包容量避免一个物品拿多次，完全背包就正序就好了。\n完全背包例题——纪念品 题目描述 小伟突然获得一种超能力，他知道未来 $T$ 天 $N$ 种纪念品每天的价格。某个纪念品的价格是指购买一个该纪念品所需的金币数量，以及卖出一个该纪念品换回的金币数量。\n每天，小伟可以进行以下两种交易无限次：\n任选一个纪念品，若手上有足够金币，以当日价格购买该纪念品； 卖出持有的任意一个纪念品，以当日价格换回金币。 每天卖出纪念品换回的金币可以立即用于购买纪念品，当日购买的纪念品也可以当日卖出换回金币。当然，一直持有纪念品也是可以的。\n$T$ 天之后，小伟的超能力消失。因此他一定会在第 $T$ 天卖出所有纪念品换回金币。\n小伟现在有 $M$ 枚金币，他想要在超能力消失后拥有尽可能多的金币。\n输入格式 第一行包含三个正整数 $T, N, M$，相邻两数之间以一个空格分开，分别代表未来天数 $T$，纪念品数量 $N$，小伟现在拥有的金币数量 $M$。\n接下来 $T$ 行，每行包含 $N$ 个正整数，相邻两数之间以一个空格分隔。第 $i$ 行的 $N$ 个正整数分别为 $P_{i,1},P_{i,2},\\dots,P_{i,N}$，其中 $P_{i,j}$ 表示第 $i$ 天第 $j$ 种纪念品的价格。\n输出格式 输出仅一行，包含一个正整数，表示小伟在超能力消失后最多能拥有的金币数量。\n输入输出样例 #1 输入 #1 1 2 3 4 5 6 7 6 1 100 50 20 25 20 25 50 输出 #1 1 305 输入输出样例 #2 输入 #2 1 2 3 4 3 3 100 10 20 15 15 17 13 15 25 16 输出 #2 1 217 说明/提示 样例 1 说明\n最佳策略是：\n第二天花光所有 $100$ 枚金币买入 $5$ 个纪念品 $1$；\n第三天卖出 $5$ 个纪念品 $1$，获得金币 $125$ 枚；\n第四天买入 $6$ 个纪念品 $1$，剩余 $5$ 枚金币；\n第六天必须卖出所有纪念品换回 $300$ 枚金币，第四天剩余 $5$ 枚金币，共 $305$ 枚金币。\n超能力消失后，小伟最多拥有 $305$ 枚金币。\n样例 2 说明\n最佳策略是：\n第一天花光所有金币买入 $10$ 个纪念品 $1$；\n第二天卖出全部纪念品 $1$ 得到 $150$ 枚金币并买入 $8$ 个纪念品 $2$ 和 $1$ 个纪念品 $3$，剩余 $1$ 枚金币；\n第三天必须卖出所有纪念品换回 $216$ 枚金币，第二天剩余 $1$ 枚金币，共 $217$ 枚金币。\n超能力消失后，小伟最多拥有 $217$ 枚金币。\n数据规模与约定\n对于 $10%$ 的数据，$T = 1$。\n对于 $30%$ 的数据，$T \\leq 4, N \\leq 4, M \\leq 100$，所有价格 $10 \\leq P_{i,j} \\leq 100$。\n另有 $15%$ 的数据，$T \\leq 100, N = 1$。\n另有 $15%$ 的数据，$T = 2, N \\leq 100$。\n对于 $100%$ 的数据，$T \\leq 100, N \\leq 100, M \\leq 10^3$，所有价格 $1 \\leq P_{i,j} \\leq 10^4$，数据保证任意时刻，小伟手上的金币数不可能超过 $10^4$。\n思路 这个也不是板子题还是需要一些额外思考的，但是也不算太难因为也不需要额外的处理，属于一般题吧，毕竟除了学习的时候也不会做到板子题。\n来分析思路。对于我们正常来想，我们要思考的就是每一天投入多少去购买能最大化收益。题目提到纪念品可以买完就卖所以我们可以不要考虑持有量全部转化成金币就好，其余部分就是正常完全背包。\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define endl \u0026#39;\\n\u0026#39; #define pii pair\u0026lt;int,int\u0026gt; using i64 = long long; using u64 = unsigned long long; using u32 = unsigned; using u128 = unsigned __int128; using i128 = __int128; const int mod = 1e9+7; const int inf = 0x3f3f3f; int n,m,t; const int N = 105; const int M = 1e4+5; int v[N][N]; int f[M]; void solve(){ cin \u0026gt;\u0026gt; t \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; for(int i = 1; i \u0026lt;= t; i++){ for(int j = 1; j \u0026lt;= n; j++){ // 第i天第j个物品的价值 cin \u0026gt;\u0026gt; v[i][j]; } } int cur = m; for(int i = 1; i \u0026lt; t; i++){ for(int c = 0; c \u0026lt;= cur; c++) f[c] = c; for(int j = 1; j \u0026lt;= n; j++){ for(int k = v[i][j]; k \u0026lt;= cur; k++){ f[k] = max(f[k], f[k-v[i][j]]+v[i+1][j]); } } cur = f[cur]; } cout \u0026lt;\u0026lt; cur; } signed main(){ ios_base::sync_with_stdio(false); cin.tie(0); int t = 1; // cin \u0026gt;\u0026gt; t; while(t--){ solve(); } return 0; } 分组背包 简介 每一组只能挑一个物品，要求最值。理解起来也简单，就是01背包多了一个循环去跑一遍对应组里面的每一个物品\n分组背包例题——通天之分组背包 题目背景 直达通天路·小 A 历险记第二篇\n题目描述 自 $01$ 背包问世之后，小 A 对此深感兴趣。一天，小 A 去远游，却发现他的背包不同于 $01$ 背包，他的物品大致可分为 $k$ 组，每组中的物品相互冲突，现在，他想知道最大的利用价值是多少。\n输入格式 两个数 $m,n$，表示一共有 $n$ 件物品，背包能承受的最大重量为 $m$。\n接下来 $n$ 行，每行 $3$ 个数 $a_i,b_i,c_i$，表示物品的重量，利用价值，所属组数。\n输出格式 一个数，最大的利用价值。\n输入输出样例 #1 输入 #1 1 2 3 4 45 3 10 10 1 10 5 1 50 400 2 输出 #1 1 10 说明/提示 $0 \\leq m \\leq 1000$，$1 \\leq n \\leq 1000$，$1\\leq k\\leq 100$，$a_i, b_i, c_i$ 在 int 范围内。\n思路 纯板子，就阐明一下这个板子的思路吧。我们输入的时候比如输入x,y,z对应重量价值和租号,用吧b[z]存z组有多少个，用f[z][b[z]]存这个物品的索引好去调用他们的重量和价值\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define endl \u0026#39;\\n\u0026#39; #define pii pair\u0026lt;int,int\u0026gt; using i64 = long long; using u64 = unsigned long long; using u32 = unsigned; using u128 = unsigned __int128; using i128 = __int128; const int mod = 1e9+7; const int inf = 0x3f3f3f; const int N = 1005; const int K = 105; int n,m; int w[N], v[N], b[N],f[N][N],dp[N]; void solve(){ cin \u0026gt;\u0026gt; m \u0026gt;\u0026gt; n; int cat; int t = 0; for(int i = 1; i \u0026lt;= n; i++){ cin \u0026gt;\u0026gt; w[i] \u0026gt;\u0026gt; v[i] \u0026gt;\u0026gt; cat; t = t \u0026gt; cat ? t : cat; b[cat]++; f[cat][b[cat]] = i; } for(int i = 1; i \u0026lt;= t; i++){ for(int j = m; j \u0026gt;= 0; j--){ for(int k = 1; k \u0026lt;= b[i]; k++){ if(j \u0026gt;= w[f[i][k]]) dp[j] = max(dp[j], dp[j-w[f[i][k]]]+v[f[i][k]]); } } } cout \u0026lt;\u0026lt; dp[m]; } signed main(){ ios_base::sync_with_stdio(false); cin.tie(0); int t = 1; // cin \u0026gt;\u0026gt; t; while(t--){ solve(); } return 0; } 多重背包 简介 也是一个01背包变种，就是每个物品有确定的数量，去求最值，我们只需要把多个数量当成不同物品，那么又变成了01背包，但是可能复杂度高了点。我们这里就分享怎么通过二进制优化来处理\n我们对于有n个的物品，不用把他们存成n个不同物品，我们把他们存成 2^0 + 2^1 + \u0026hellip; + 2^i i个物品，这样就从O(n)变成了O(logn)。\n多重背包例题——板子题（后面写树上dp的时候那题用了分组背包，这里就用板子展示一下大致思想吧） 题目大意 有 N 种物品和一个容量是 V 的背包。\n第 i 种物品最多有 si 件，每件体积是 vi，价值是 wi。\n求解将哪些物品装入背包，可使物品体积总和不超过背包容量，且价值总和最大。输出最大价值。\n输入格式： 第一行两个整数，N 和 V，用空格隔开，分别表示物品种数和背包容积。\n接下来有 N 行，每行三个整数 vi, wi, si，用空格隔开，分别表示第 i 种物品的体积、价值和数量。\n求最大值\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;algorithm\u0026gt; using namespace std; const int MAX_V = 2005; struct Good { int v, w; }; int main() { int N, V; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; V; vector\u0026lt;Good\u0026gt; goods; // 二进制优化处理多重背包 for (int i = 0; i \u0026lt; N; i++) { int v, w, s; cin \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w \u0026gt;\u0026gt; s; // 将s个物品拆分成1,2,4,...,2^k,剩余数 的组别 for (int k = 1; k \u0026lt;= s; k *= 2) { s -= k; goods.push_back({v * k, w * k}); } if (s \u0026gt; 0) { goods.push_back({v * s, w * s}); } } // 01背包问题 vector\u0026lt;int\u0026gt; dp(V + 1, 0); for (auto good : goods) { for (int j = V; j \u0026gt;= good.v; j--) { dp[j] = max(dp[j], dp[j - good.v] + good.w); } } cout \u0026lt;\u0026lt; dp[V] \u0026lt;\u0026lt; endl; return 0; } ","date":"2025-09-20T00:00:00Z","image":"https://xzyly.github.io/p/dp%E4%B8%89%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98/OIP_hu8217367568331834919.jpg","permalink":"https://xzyly.github.io/p/dp%E4%B8%89%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98/","title":"dp(三)——背包问题"},{"content":"这里主要分享怎么用倍增法和Tarjan来解决LCA， 这两个方法也是在工作生活中应用基本最广的。\n这篇博客经历许多挫折，开始在写状压的题，开始直接用bfs爆搜，t了8个点，后面用LCA，用倍增法实现 ,t了5个点，我也为是算法问题，去搜了下tarjan，学了好半天还是没有搞懂，后面发现倍增法可以过，只要小小优化 一下就好了。后面ac后还是不甘心没看懂tarjan，又去看了好些博客和问了AI，得出来最基本的理解， 所以想要深入了解的，慎入。\n倍增法 核心思想 假设我们要找u和v的最近公共祖先，我们先判断u和v的深度，我们先让他们跳到同一个深度，再一起跳，直到他们公共祖先的下一层\n为什么叫倍增呢，因为我们跳的步幅是2^j, 比如s[i][j]就是从i开始，向上跳2^j，这样我们就可以更快到达目的地。\n这里要注意几个地方，就是比如我们要更新s[i][j]的状态时，我们先看s[i][j-1]，如果s[i][j-1]已经超出界限，那s[i][j]肯定也超过了，那么s[i][j]无法更新，则s[i][j] = s[i][j-1]。而如果未超呢，那么s[i][j] = s[s[i][j-1]][j-1],即从i先跳2^(j-1)，再跳2^(j-1)。\n例子 n个点，q次询问，问两个点的最近公共祖先是谁\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define endl \u0026#39;\\n\u0026#39; #define pii pair\u0026lt;int,int\u0026gt; #define int long long const int N = 1e5+5; const int LOG = 20; // enough since 2^20 \u0026gt; 1e5 int n, q, root; vector\u0026lt;int\u0026gt; G[N]; int up[N][LOG]; // up[v][j] = 2^j-th ancestor of v int depth[N]; // DFS to initialize parent and depth void dfs(int u, int p) { up[u][0] = p; for (int i = 1; i \u0026lt; LOG; i++) { if (up[u][i-1] != -1) up[u][i] = up[up[u][i-1]][i-1]; else up[u][i] = -1; } for (int v : G[u]) { if (v == p) continue; depth[v] = depth[u] + 1; dfs(v, u); } } // Lift node u up by k steps int lift(int u, int k) { for (int i = 0; i \u0026lt; LOG; i++) { if (k \u0026amp; (1 \u0026lt;\u0026lt; i)) { u = up[u][i]; if (u == -1) break; } } return u; } // Find LCA using binary lifting int lca(int u, int v) { if (depth[u] \u0026lt; depth[v]) swap(u, v); // lift u up to the same depth as v u = lift(u, depth[u] - depth[v]); if (u == v) return u; for (int i = LOG - 1; i \u0026gt;= 0; i--) { if (up[u][i] != up[v][i]) { u = up[u][i]; v = up[v][i]; } } return up[u][0]; } void solve() { cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; q \u0026gt;\u0026gt; root; for (int i = 1; i \u0026lt;= n; i++) { G[i].clear(); depth[i] = 0; for (int j = 0; j \u0026lt; LOG; j++) up[i][j] = -1; } for (int i = 1; i \u0026lt; n; i++) { int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; G[u].push_back(v); G[v].push_back(u); } depth[root] = 0; dfs(root, -1); for (int i = 0; i \u0026lt; q; i++) { int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; cout \u0026lt;\u0026lt; \u0026#34;LCA of query \u0026#34; \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; is \u0026#34; \u0026lt;\u0026lt; lca(u, v) \u0026lt;\u0026lt; endl; } } signed main() { ios_base::sync_with_stdio(false); cin.tie(0); int t = 1; // cin \u0026gt;\u0026gt; t; while (t--) { solve(); } return 0; } Tarjan 核心思想 想象一下，你是一个侦探，正在调查一宗案件（求解LCA）。这棵家谱树就是你的案发现场。你的调查方式是一种特殊的深度优先搜索（DFS）：你从老祖宗（根节点）开始，沿着家谱一支一支地调查，彻底查完一支家族（子树），才会返回上一级做汇报。\n在这个调查过程中，你有两个关键工具：\n你的笔记本（并查集）：记录每个家族分支目前是归谁管的。\n你的待办清单（查询列表）：记录你要查明的血缘关系，比如“小明和小红是什么关系？”。\n分步情景演绎 我们还是用那棵树来做例子：\n1（老祖宗） 2 4 5 3 查询：LCA(4, 5) 和 LCA(4, 3) 第一步：调查节点 1（老祖宗） 你（侦探）的行动：你站在了老祖宗 1 的位置。你在他的名字上画个圈（标记为灰色1），表示“我正在调查他”。\n你的待办清单：你看了一眼，发现没有关于 1 的直接查询。\n下一步：你决定按顺序调查，先调查他的大儿子 2 这一支。你暂时离开了 1，但你知道你最终会回到这里。\n第二步：调查节点 2 你的行动：你来到了 2 的家。你画个圈（标记2）。\n待办清单：没有关于 2 的查询。\n下一步：你继续深入，先去调查 2 的大儿子 4。\n第三步：调查节点 4 你的行动：你来到了 4 的家。你画个圈（标记4）。\n检查待办清单：你发现有一个查询是“4 和 5 的关系”。你看了看 5，5 你还没去调查过（状态是白色0），你没有任何线索，所以这个查询暂时无法解决。\n下一步：4 没有后代了，你对他家的调查完毕。你在 4 的名字上画个勾（标记为黑色2），这表示“这家我查完了，没问题”。\n关键操作：现在你要回去向你的上级 2 汇报。汇报的时候，你说：“老大，你儿子 4 这一支我查完了，以后他们家的事就直接归你管了！” 这个“归你管”的操作，就是并查集的 unite(4, 2)。现在，如果你问“4 的负责人是谁？”，答案不再是 4 自己，而是 2。\n第四步：回到节点 2，调查节点 5 你的行动：你回到了 2 这里。根据汇报，你把 4 的管理权收归己有。现在你开始调查 2 的二儿子 5。\n你来到 5 的家，画个圈（标记5）。\n检查待办清单：你发现有一个查询是“5 和 4 的关系”。你一看，4 的状态是已画勾（黑色2），说明 4 家已经被查完了。\n关键破案：你现在想知道 4 是归谁管的？你查了一下你的笔记本（调用 find(4)），笔记本告诉你：4 现在归 2 管！太好了，那么这个 2 就是同时管着 4 和 5 的人，他就是他俩的最近公共上级（LCA）！ 你立刻记录：LCA(4,5) = 2。\n下一步：5 没有后代了，调查完毕。你在 5 上画个勾（标记黑色2），然后回去向 2 汇报。汇报内容：“老二 5 这一支也查完了，也归你管了！” (unite(5, 2))。\n第五步：回到节点 2，最终汇报 你的行动：你回到了 2 这里。2 的所有儿子都调查完毕了，所以你自己也在 2 上画个勾（标记黑色2）。\n下一步：你回到你的上级，老祖宗 1 那里做汇报。你说：“老祖宗，您儿子 2 这一整支我都查完了，以后他们全家都归您直接管了！” (unite(2, 1))。现在，如果你问“2 的负责人是谁？”，答案会是 1；问“4 的负责人是谁？”，会先找到 2 的负责人是 1，所以答案也是 1（并查集的路径压缩效应）。\n第六步：回到节点 1，调查节点 3 你的行动：你回到 1 这里。你开始调查他的二儿子 3 这一支。\n你来到 3 的家，画个圈（标记3）。\n检查待办清单：你发现有一个查询是“3 和 4 的关系”。你一看，4 的状态是已画勾（黑色2）。\n关键破案：你马上查笔记本 4 归谁管（find(4)）。笔记本显示：4 所在的那一整支 (2 的家) 都已经归老祖宗 1 管了，所以 4 的负责人是 1！那么这个 1 就是同时管着 3 和 4 的人，他是他俩的最近公共祖先！ 你记录：LCA(4,3) = 1。\n下一步：3 调查完毕，画勾，然后向 1 汇报 (unite(3, 1))。\n第七步：全部调查完毕 你在 1 处画勾，整个调查结束。所有查询都已破案。\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define endl \u0026#39;\\n\u0026#39; #define pii pair\u0026lt;int,int\u0026gt; #define int long long const int mod = 1e9+7; const int inf = 0x3f3f3f; template\u0026lt;typename Ty\u0026gt; inline void read(Ty \u0026amp;x) { short c = getchar(), f = 1; for (; c \u0026lt; \u0026#39;0\u0026#39; || c \u0026gt; \u0026#39;9\u0026#39;; c = getchar()) if (c == \u0026#39;-\u0026#39;) f = -1; for (x = 0; c \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;9\u0026#39;; c = getchar()) x = (x \u0026lt;\u0026lt; 1) + (x \u0026lt;\u0026lt; 3) + (c ^ 48); x *= f; } int wr_l = 0, wr_s[1000005]; template\u0026lt;typename Ty\u0026gt; inline void write(Ty x) { if (x == 0) return (void)(putchar(\u0026#39;0\u0026#39;)); if (x \u0026lt; 0) putchar(\u0026#39;-\u0026#39;), x = -x; for (; x; x /= 10) wr_s[++wr_l] = x % 10 + \u0026#39;0\u0026#39;; for (; wr_l; wr_l--) putchar(wr_s[wr_l]); } // n points, q times query int n,q,root; // assume the max vertex is N const int N = 1e5+5; // store the tree vector\u0026lt;int\u0026gt; G[N]; // store the query vector\u0026lt;pii\u0026gt; query[N]; // store the father int parent[N]; // store the visited path bool vis[N]; // store the ans int ans[N]; int find(int x){ if(x != parent[x]){ parent[x] = find(parent[x]); } return parent[x]; } void tarjan(int u){ vis[u] = true; parent[u] = u; for(int v: G[u]){ if(!vis[v]){ tarjan(v); parent[v] = u; } } for(auto \u0026amp;q: query[u]){ int v = q.first; int id = q.second; if(vis[v]){ ans[id] = find(v); } } } void init(){ for (int i = 1; i \u0026lt;= n; i++){ G[i].clear(); query[i].clear(); vis[i] = false; } } void solve(){ cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; q \u0026gt;\u0026gt; root; init(); for (int i = 1; i \u0026lt; n; i++){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; G[u].push_back(v); G[v].push_back(u); } for (int i = 0; i \u0026lt; q; i++) { int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; query[u].push_back({v, i}); query[v].push_back({u, i}); } tarjan(root); for (int i = 0; i \u0026lt; q; i++){ cout \u0026lt;\u0026lt; \u0026#34;LCA of query \u0026#34; \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34; is \u0026#34; \u0026lt;\u0026lt; ans[i] \u0026lt;\u0026lt; endl; } } signed main(){ ios_base::sync_with_stdio(false); cin.tie(0); int t = 1; // cin \u0026gt;\u0026gt; t; while(t--){ solve(); } return 0; } ","date":"2025-09-13T00:00:00Z","image":"https://xzyly.github.io/p/lca%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/OIP_hu12832354338770344661.jpg","permalink":"https://xzyly.github.io/p/lca%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","title":"LCA(最近公共祖先)"},{"content":"花了许多天时间，写了大概10来道题，有几道简单，和两个作用属于提高+的吧。简单题没什么好说的，主要就是板子。从中等题开始，要么是对dp进行了优化，要么就是和其他的算法进行了结合，算是灵活性更高了。到了难的题目（对我来说），需要结合比较多数学知识，去自己手玩一下，去试和尝试证明一些方法再去进行DP，这里难的对我来说就是首先找到进行dp的状态是什么，然后用数学知识去进行一些推导，对于这种题目我觉得需要去多练习逐渐提升自己的数学思维，并且可以不用只练关于dp的，很多其他类型的也能做到锻炼数学思维。后面有机会写一篇关于数学思维题目的总结。这里抱歉很多题解都没写全，因为实在有些耗时，太杂了，我后面尽量写题的时候就把思路列好，后面写题解就方便点，还好这些题洛谷上都有，各位可以自行查看，这篇博客也主要是自己做一个总结还有挑一些题目尽量让各位多看几种，不要一直做重复的，当然练习的时候重复还是很重要的。\n总结 先分享一下做了些DP题后的思考。首先见识各种模型还是很重要，让我们能很快找到去处理一些子问题的方法。其次就是要理清怎么去解决这类题目，首先dp其实就是把一个问题分解成若干个子问题，对于每一个子问题得到最优解来得到问题的最优解。我来总结一下我做这些题的思考方向：\n·第一步是子问题是什么，因为我们要通过对子问题的最优决策来得到问题的最优解\n·第二步是找到状态，这一步在我的观点上是区分DP题的难度的一个点，有时候很难一下就找到，要多次尝试和思考 ·第三步是去寻找状态转移方程，我们已经确定状态后很自然就去推导状态转移方程 ·第四步是确定各种边界条件 ·最后一步是想是否需要优化和如何优化\n还有一个点就是我们要怎么确定这道题是否要用DP，因为我做的时候我是直接标签去找线性DP的题的，所以我很清楚就是要用DP解决，但是我们打比赛或做一些题的时候不可能有tag告诉我们是什么类型，所以我们要明白如何去判断是否使用DP。这里分享一下阅读一些大佬的文章和自己一些了解后做的总结： ·是否有重叠子问题。比如在在解决问题时候子问题被重复计算，这种我们通常会使用记忆化来解决 ·是否有最优子结构。就是这个问题是否能够由最优子结构构造出来 ·决策无后效性。就是我们的决策只会改变当前和未来的状态，与过去无关\n但是实际上最好的学习方法就是自己去选择题目去做并定期总结，接下来就展示几个各个难度的题\n简单 P12833 [蓝桥杯 2025 国 B] 斐波那契字符串 题目描述 斐波那契字符串 $S$ 是由 $\\tt 0$ 和 $\\tt 1$ 所组成的字符串，其生成规则如下：\n$S_1 = \\tt 0$。 $S_2 = \\tt 1$。 对于任意正整数 $n (n \\geq 3)$，$S_n = S_{n-2} + S_{n-1}$（“+”表示字符串拼接）。 例如：$S_3 = 01$、$S_4 = 101$、$S_5 = 01101$。\n在斐波那契字符串 $S$ 中，定义逆序对为满足以下条件的整数对 $(i, j)$:\n$1 \\leq i \u0026lt; j \\leq |S|$（其中 $|S|$ 表示 $S$ 的长度）。 $S[i] = 1$（第 $i$ 个字符为 $\\tt 1$）并且 $S[j] = 0$（第 $j$ 个字符为 $\\tt 0$）。 现在，给定一个正整数 $N$，请你计算出 $S_N$ 中所有逆序对 $(i, j)$ 的总数。由于结果可能很大，请输出其对 $10^9 + 7$ 取余后的值。\n输入格式 输入的第一行包含一个整数 $T$，表示测试用例的数量。\n接下来的 $T$ 行，每行包含一个整数 $N$，表示要计算的斐波那契字符串的序号。\n输出格式 对于每个测试用例，输出一行，包含一个整数，表示 $S_N$ 中所有逆序对的总数对 $10^9 + 7$ 取余后的结果。\n输入输出样例 #1 输入 #1 1 2 3 2 3 5 输出 #1 1 2 0 2 说明/提示 【样例说明】\n对于 $N = 3$，$S_3 = 01$，逆序对总数为 0。\n对于 $N = 5$，$S_5 = 01101$，逆序对为 $(2, 4)$、$(3, 4)$，总数为 2。\n【评测用例规模与约定】\n对于 20% 的评测用例，$1 \\leq T \\leq 20$，$3 \\leq N \\leq 35$。\n对于 100% 的评测用例，$1 \\leq T \\leq 10^5$，$3 \\leq N \\leq 10^5$。\n思路 我们那这道题来印证上面提到的思考方式。首先这道题存在子问题，并且可以由最优决策的子问题来解决问题，所以我们基本可以判断他是DP。 我们先来确定子问题，这题很明显，就是上两个字符串。 接下来确定状态，也不难看出状态就是逆序对数。 接下来推导状态转移方程，对于S_i = S_i-2 + S_i-1,S_i-2的逆序对不会变，而S_i-1的1与S_i-2的0组合会新增逆序对，那么我们的方程就很容易推导出来了： f[i] = f[i-2] + f[i-1] + one[i-1]*zero[i-2] 这道题的边界也很清晰，毕竟这是一个简单题，到此问题解决。 这里就不展示代码了，各位自己思考尝试一下。\n中等 P1020 [NOIP 1999 提高组] 导弹拦截 题目描述 某国为了防御敌国的导弹袭击，发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷：虽然它的第一发炮弹能够到达任意的高度，但是以后每一发炮弹都不能高于前一发的高度。某天，雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段，所以只有一套系统，因此有可能不能拦截所有的导弹。\n输入导弹依次飞来的高度，计算这套系统最多能拦截多少导弹，如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。\n输入格式 一行，若干个整数，中间由空格隔开。\n输出格式 两行，每行一个整数，第一个数字表示这套系统最多能拦截多少导弹，第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。\n输入输出样例 #1 输入 #1 1 389 207 155 300 299 170 158 65 输出 #1 1 2 6 2 说明/提示 对于前 $50%$ 数据（NOIP 原题数据），满足导弹的个数不超过 $10^4$ 个。该部分数据总分共 $100$ 分。可使用 $\\mathcal O(n^2)$ 做法通过。\n对于后 $50%$ 的数据，满足导弹的个数不超过 $10^5$ 个。该部分数据总分也为 $100$ 分。请使用 $\\mathcal O(n\\log n)$ 做法通过。\n对于全部数据，满足导弹的高度为正整数，且不超过 $5\\times 10^4$。\n此外本题开启 spj，每点两问，按问给分。\nNOIP1999 提高组 第一题\n$\\text{upd 2022.8.24}$：新增加一组 Hack 数据。\n思路 这道题的思路也比较清晰，这里就不像前一个一步步走了，讲一下大致的思路。我们要寻找一个最长非递增子序列，我们假设最长的结束在索引i，我们遍历前面j \u0026lt; i， 然后更新dp[i] = max(i, {dp[j]+1})。\n但是这道题要我们在O(nlogn)解决，说明这样单纯搞是不行的。看到logn我的第一个想法就是二分，结果确实可行。我们想要找到最大的dp[j],那么我们就假设dp[j] = x,令f[x]为长度为x的序列最大的最后一个数的大小，我们可以通过反证法去证明f[x]是一个单调不增的,由dp[j] \u0026lt;= f[x], dp[j] \u0026gt;= dp[i],得到dp[i] \u0026lt;= f[x]。因此我们要找到尽可能大的 x 满足 h(i)≤f[x]。考虑二分。\n接下来就是考虑第二问，这里用到了Dilworth\u0026rsquo;s theorem, 在之前的博客提到过，有兴趣可以去了解下。\nP1018 [NOIP 2000 提高组] 乘积最大 题目背景 NOIP2000 提高组 T2\n题目描述 今年是国际数学联盟确定的“2000——世界数学年”，又恰逢我国著名数学家华罗庚先生诞辰 90 周年。在华罗庚先生的家乡江苏金坛，组织了一场别开生面的数学智力竞赛的活动，你的一个好朋友 XZ 也有幸得以参加。活动中，主持人给所有参加活动的选手出了这样一道题目：\n设有一个长度为 $N$ 的数字串，要求选手使用 $K$ 个乘号将它分成 $K+1$ 个部分，找出一种分法，使得这 $K+1$ 个部分的乘积能够为最大。\n同时，为了帮助选手能够正确理解题意，主持人还举了如下的一个例子：\n有一个数字串：$312$，当 $N=3,K=1$ 时会有以下两种分法：\n$3 \\times 12=36$ $31 \\times 2=62$ 这时，符合题目要求的结果是：$31 \\times 2 = 62$。\n现在，请你帮助你的好朋友 XZ 设计一个程序，求得正确的答案。\n输入格式 程序的输入共有两行：\n第一行共有 $2$ 个自然数 $N,K$。\n第二行是一个长度为 $N$ 的数字串。\n输出格式 结果显示在屏幕上，相对于输入，应输出所求得的最大乘积（一个自然数）。\n输入输出样例 #1 输入 #1 1 2 4 2 1231 输出 #1 1 62 说明/提示 数据范围与约定\n对于 $60%$ 的测试数据满足 $6≤N≤20$。\n对于所有测试数据，$6≤N≤40,1≤K≤6$。\n思路 这道题大家可以自己思考试试，比较简单，就是加了个高精度，状态和方程都比较好想。\nP13871 [蓝桥杯 2024 省 Java/Python A] 吊坠 题目描述 小蓝想制作一个吊坠，他手上有 $n$ 个长度为 $m$ 的首尾相连的环形字符串 ${s_1, s_2, \\cdots, s_n}$，他想用 $n-1$ 条边将这 $n$ 个字符串连接起来做成吊坠，要求所有的字符串连完后形成一个整体。连接两个字符串 $s_i, s_j$ 的边的边权为这两个字符串的最长公共子串的长度（可以按环形旋转改变起始位置，但不能翻转），小蓝希望连完后的这 $n-1$ 条边的边权和最大，这样的吊坠他觉得最好看，请计算最大的边权和是多少。\n输入格式 输入的第一行包含两个正整数 $n, m$，用一个空格分隔。\n接下来 $n$ 行，每行包含一个长度为 $m$ 的字符串，分别表示 $s_1, s_2, \\cdots, s_n$。\n输出格式 输出一行包含一个整数表示答案。\n输入输出样例 #1 输入 #1 1 2 3 4 5 4 4 aabb abba acca abcd 输出 #1 1 8 说明/提示 【样例说明】\n连接 $\\langle 1,2\\rangle, \\langle 2,3\\rangle, \\langle 2,4\\rangle$，边权和为 $4 + 2 + 2 = 8$\n【评测用例规模与约定】\n对于 $20%$ 的评测用例，$1 \\leq n, m \\leq 10$；\n对于所有评测用例，$1 \\leq n \\leq 200$，$1 \\leq m \\leq 50$。所有字符串由小写英文字母组成。\n思路 这道题也是不难，主要就是给大家展示DP与其他算法的结合，这个加入的最大生成树，各位也可以练手\nP5124 [USACO18DEC] Teamwork G 题目描述 在 Farmer John 最喜欢的节日里，他想要给他的朋友们赠送一些礼物。由于他并不擅长包装礼物，他想要获得他的奶牛们的帮助。你可能能够想到，奶牛们本身也不是很擅长包装礼物，而 Farmer John 即将得到这一教训。\nFarmer John 的 $N$ 头奶牛（$1\\le N\\le 10^4$）排成一行，方便起见依次编号为 $1\\dots N$。奶牛 $i$ 的包装礼物的技能水平为 $s_i$。她们的技能水平可能参差不齐，所以 FJ 决定把她的奶牛们分成小组。每一组可以包含任意不超过 $K$ 头的连续的奶牛（$1\\le K\\le 10^3$），并且一头奶牛不能属于多于一个小组。由于奶牛们会互相学习，这一组中每一头奶牛的技能水平会变成这一组中水平最高的奶牛的技能水平。\n请帮助 FJ 求出，在他合理地安排分组的情况下，可以达到的技能水平之和的最大值。\n输入格式 输入的第一行包含 $N$ 和 $K$。以下 $N$ 行按照 $N$ 头奶牛的排列顺序依次给出她们的技能水平。技能水平是一个不超过 $10^5$ 的正整数。\n输出格式 输出 FJ 通过将连续的奶牛进行分组可以达到的最大技能水平和。\n输入输出样例 #1 输入 #1 1 2 3 4 5 6 7 8 7 3 1 15 7 9 2 5 10 输出 #1 1 84 说明/提示 在这个例子中，最优的方案是将前三头奶牛和后三头奶牛分别分为一组，中间的奶牛单独成为一组（注意一组的奶牛数量可以小于 $K$）。这样能够有效地将 $7$ 头奶牛的技能水平提高至 $15$、$15$、$15$、$9$、$10$、$10$、$10$，和为 $84$。\n思路 这道题的状态状态和状态转移方程算更难想一点的，多了一个考虑几个成一组的状态，也是一个我感觉比较好的中等题吧，虽然挺多大佬说是简单题，好想的DP，但是我没看过的就是好题嘛。\n结语 我们写了难的题但是没放上来，主要是我没能做到很好的理解透所以没办法去进行挑选和讲解，如果一股脑全加上来，各位自己去搜可以搜到更多的，所以这个留到以后我能去理解和有能力很好的解决后来补充。\n","date":"2025-09-10T00:00:00Z","image":"https://xzyly.github.io/p/dp%E4%BA%8C%E7%BA%BF%E6%80%A7dp/OIP_hu11092191784645836728.jpg","permalink":"https://xzyly.github.io/p/dp%E4%BA%8C%E7%BA%BF%E6%80%A7dp/","title":"dp(二)——线性DP"},{"content":"C 题目链接 https://codeforces.com/contest/2139/problem/C\n题目大意 一定数量的蛋糕（2^(k+1)），二人平分（分别叫a,b），现在有人想要特定数量的蛋糕，有两个操作，一个是a分一半给b，一个是b分一半给a，直到a拿到特定的x个，问要多少步，和具体方案\n思路 刚开始两人是平分的，我们来假设测试分过几次后的状态，a有m个，b有2^(k+1)-m个，如果m小于总数的一半说明 上一个操作一定是a分一半，反之亦然。所以我们可以反向操作。目标a有的数量为x，如果小于总数的一半说明上一个 操作是1，否则是2。\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define endl \u0026#39;\\n\u0026#39; #define pii pair\u0026lt;int,int\u0026gt; #define int long long const int mod = 1e9+7; const int inf = 0x3f3f3f; void solve(){ int k,x; cin \u0026gt;\u0026gt; k \u0026gt;\u0026gt; x; int s = 1LL \u0026lt;\u0026lt; (k+1); int h = 1LL \u0026lt;\u0026lt; k; int cur = x; vector\u0026lt;int\u0026gt; a; while(cur != h){ if(cur \u0026lt; h){ a.push_back(1); cur = cur * 2; }else{ a.push_back(2); cur = cur * 2 - s; } } reverse(a.begin(), a.end()); cout \u0026lt;\u0026lt; a.size() \u0026lt;\u0026lt; endl; for(int i = 0; i \u0026lt; (int)a.size(); i++){ cout \u0026lt;\u0026lt; a[i]; if(i + 1 != (int)a.size()) cout \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } cout \u0026lt;\u0026lt; endl; } signed main(){ ios_base::sync_with_stdio(false); cin.tie(0); int t = 1; cin \u0026gt;\u0026gt; t; while(t--){ solve(); } return 0; } 总结 中题考查就是一个反向思考来简化问题\nD 题目链接 https://codeforces.com/contest/2139/problem/D\n题目大意 有一个排列数组，我们有两个操作让他们变得非递减。操作一可以将第i位和i+1位换位置，操作二将 i和i+2换位置且只能最多用一次。我们每次询问会给出左右边界，要我们返回在这个范围内的元素是否 用操作二爷不会减少操作次数，即总次数和只用操作一是一样的。\n思路 我们那三个元素来进行分析，a，b，c（索引递增）。我们可以列出他们的大小关系，这里就不做证明，各位很容易就可以得到就是当a\u0026gt;b\u0026gt;c时，操作二会减少操作数量。\n然后我们来分析abc是否一定要相邻，结论是不要。因为我们可以通过操作一让他们相邻。\n那么我们要判断的就是被询问的左右边界是否包含了这样的三个数。方法也很简单，我们对于每一个i去先向前寻找一个大于a[i]的位置,设他为l，再向后寻找一个小于a[i]的位置设他为r，并存储在r位置对应的最大的l的位置（为什么要最大呢，因为往i后面继续遍历的时候可能找到同样的r但是l更大，此时记录更大的l这样后面的左右边界就更能包裹住）。\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; #define endl \u0026#39;\\n\u0026#39; #define pii pair\u0026lt;int,int\u0026gt; #define int long long const int mod = 1e9+7; const int inf = 0x3f3f3f; void solve(){ int n,q; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; q; vector\u0026lt;int\u0026gt; a(n+1, 0),mxl(n+1, 0), mxr(n+1, 0),L(n+1, 0), prev(n+1, 0); for(int i = 1; i \u0026lt;= n; i++) cin \u0026gt;\u0026gt; a[i]; for(int i = 1; i \u0026lt;= n; i++){ mxl[i] = i - 1; while(mxl[i] \u0026gt; 0 \u0026amp;\u0026amp; a[mxl[i]] \u0026lt; a[i]){ mxl[i] = mxl[mxl[i]]; } } for(int i = n; i \u0026gt;= 1; i--){ mxr[i] = i + 1; while(mxr[i] \u0026lt;= n \u0026amp;\u0026amp; a[mxr[i]] \u0026gt; a[i]){ mxr[i] = mxr[mxr[i]]; } } for(int i = 2; i \u0026lt; n; i++){ if(mxl[i] \u0026gt; 0 \u0026amp;\u0026amp; mxr[i] \u0026lt;= n){ L[mxr[i]] = max(L[mxr[i]], mxl[i]); } } for(int i = 1; i \u0026lt;= n; i++) L[i] = max(L[i], L[i-1]); for(int i = 0; i \u0026lt; q; i++){ int l, r; cin \u0026gt;\u0026gt; l \u0026gt;\u0026gt; r; if(l \u0026lt;= L[r]){ cout \u0026lt;\u0026lt; \u0026#34;NO\u0026#34; \u0026lt;\u0026lt; endl; }else{ cout \u0026lt;\u0026lt; \u0026#34;YES\u0026#34; \u0026lt;\u0026lt; endl; } } } signed main(){ ios_base::sync_with_stdio(false); cin.tie(0); int t = 1; cin \u0026gt;\u0026gt; t; while(t--){ solve(); } return 0; } 总结 这道题开始确实想过用线段树，现在也不太明白这个思路是不是很偏，这个就是去寻找不符合的点然后看看边界有没有覆盖到他们，线段树先存好各个的状态再去查询，感觉思路差不多但是线段树比较多余对这道题来说。\n","date":"2025-09-09T00:00:00Z","image":"https://xzyly.github.io/p/codeforces-round-1048-div.-2/OIP_hu2901737974964319729.jpg","permalink":"https://xzyly.github.io/p/codeforces-round-1048-div.-2/","title":"Codeforces Round 1048 (Div. 2)"},{"content":"在了解Dilworth\u0026rsquo;s theorem 之前，我们先了解一下偏序集（partiallly ordered set）的概念\npartially ordered set 正式定义 一个偏序集 （通常简写为 poset） 由一个集合 P 和一个二元关系 “≤” 组成，这个关系满足以下三个性质：\n自反性：对于任何元素 a ∈ P，都有 a ≤ a。\n解释：任何元素自己和自己都是有关系的。这很自然，就像数字5总是等于5一样。\n反对称性：如果 a ≤ b 且 b ≤ a，那么必须有 a = b。\n解释：不可能有两个不同的元素互相“小于等于”对方。如果它们互相“≤”，那它们只能是同一个元素。这防止了循环比较中的歧义。\n传递性：如果 a ≤ b 且 b ≤ c，那么一定有 a ≤ c。\n解释：次序关系可以传递。如果A是B的上司，B是C的上司，那么A一定是C的上司。\n这个“≤”关系不一定是我们熟悉的数字中的“小于等于”，它可以是任何满足上述三条规则的关系。\n关键点 其实如果感觉定义不太好理解可以想象他是一个“可以比较但不必全能比较”的次序\n例子 集合S = {2,3,4}，我们先考虑他子集形成的集合\nP(S) = {∅,{2},{3},{4},{2,3},{2,4},{3,4},{2,3,4}};\n这里的排序关系中的“\u0026lt;=”其实就是集合里面的属于关系，但P(S)里面的元素 可以用属于链接的时候他们便是可比的，否则就是不可比较的\nDilworth\u0026rsquo;s theorem 正式定义 狄尔沃斯定理亦称偏序集分解定理，是关于偏序集的极大极小的定理，该定理断言：对于任意有限偏序集，其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真，它断言：对于任意有限偏序集，其最长链中元素的数目必等于其最小反链划分中反链的数目，由偏序集P按如下方式产生的图G称为偏序集的可比图：G的节点集由P的元素组成，而e为G中的边，仅当e的两端点在P中是可比较的，有限全序集的可比图为完全图\n关键点 我们先理清几个概念：\n链: 偏序集的一个子集，其中元素可以比较\n反链： 偏序集的一个子集，其中元素不可比较\nDilworth\u0026rsquo;s theorem 告诉我们覆盖全部集合的链的个数等于反链的大小\n例子 假设大学里有一些课程，并且存在选修依赖关系（学B前必须先学A，即 A ≤ B）。有些课程则没有先后顺序（比如《音乐鉴赏》和《足球理论》，它们不可比）。\n反链：一堆没有任何先后顺序要求的课程（比如一堆同一级别的公共选修课）。这个集合能有多大？假设最多有 5 门这样的课。\n链：一条学习路径，比如《高等数学I》→《高等数学II》→《常微分方程》。\nDilworth定理告诉我们，你至少需要 5 条不同的学习路径（链），才能覆盖所有的课程。因为那5门互不依赖的课（反链）绝对不能出现在同一条路径上，必须被分开到5条路径中去。\n了解后想去练手可以尝试一下洛谷的导弹拦截（https://www.luogu.com.cn/problem/P1020）\n","date":"2025-09-08T00:00:00Z","image":"https://xzyly.github.io/p/dilworths-theorem/OIP_hu6017774572044733495.jpg","permalink":"https://xzyly.github.io/p/dilworths-theorem/","title":"Dilworth's theorem"},{"content":"这篇文章其实是为了这个刚搭好的博客写的，一直没想好要拿什么作为首个博客。因为现在水平还是非常有限，很难写出对大多数人 有用的博客，所以决定通过记录dp的学习路线来开篇，我会大致分为几个部分去学习，其中参考的是邓丝雨的dp进阶之路。dp我感觉是 需要花时间去熟悉各种模型，去练习自己的思考能力，才能不断进步的，而且总结还是比较重要的，所以以此来记录我的dp之路。\n简述 这里主要先记录基础dp的内容，比如：线性dp，背包问题，区间dp，树形dp，状压dp，数位dp 后面可能等再学习一些其他算法后会继续补充dp优化等内容。\n计划 我打算对于每一个类型去记录四道题目，一个板子，一个简单，一个中等，一个难点的，可以去对应洛谷的橙，绿，蓝。\n每一个题目主要是总结，可能有些比较基础的不会去赘述。每个中等题型开始我会在每一个类型后再推荐几个类似难度的， 毕竟做一道题目肯定不够的，题解的话可能会写一些。\n","date":"2025-09-04T00:00:00Z","image":"https://xzyly.github.io/p/dp%E4%B8%80%E5%89%8D%E8%A8%80/OIP_hu11092191784645836728.jpg","permalink":"https://xzyly.github.io/p/dp%E4%B8%80%E5%89%8D%E8%A8%80/","title":"dp(一)——前言"}]