news 2026/3/16 10:28:24

补题日记:26-1-12

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
补题日记:26-1-12

Table of Contents

  1. 前言:
  2. Problem B:P1032 NOIP 2002 字符变换
    1. 题目大意:
    2. WriteUp:
  3. Problem D:P1120 [CERC 1995] 小木棍
    1. 题目大意:
    2. WriteUp:
  4. 后记:

Powered by GhostFace's Emacs.

前言:

Ctorch诞生的第178天,小小的庆祝一下。

厚颜无耻的宣传我们团队的项目:https://github.com/ShengFlow/CTorch/tree/feature-nn.Module

这是一次比赛的赛后补题记录&题解(Part 1,因为全部的题目太多了,我准备分两篇)。

主要涉及到的算法就是搜索,但是需要一些技巧才能全部AC。

推荐几篇本人的文章:

1.设计LUTM时与Deepseek的对话:https://chat.deepseek.com/share/wednovjlp5sa013j4q
2.文章:《恭喜自己,挑战成功》————CSP-J省一获奖感言:https://www.cnblogs.com/SilverGo/p/19223328
3.文章:《逆袭导论·初中生的宝书》:https://www.cnblogs.com/SilverGo/p/19284999
4.文章:《矩阵乘法优化》:https://www.cnblogs.com/SilverGo/p/19019364
5.Ctorch的github:https://github.com/ShengFlow/CTorch/tree/feature-nn.Module?tab=contributing-ov-file

本文由 Emacs 29.0 Org-mode 写成,在此感谢GNU做出的开源软件运动。

Problem B:P1032 NOIP 2002 字符变换

难度:普及+

题目大意:

给出\(A,B\)两个字符串,\(N\)个变换规则,其中\(A.len,B.len \leq 20\)\(N \leq 6\).

WriteUp:

其实赛时跳过这道题的主要原因是本蒟蒻看到了几道更可做的题目,因此就先跳过了。

以下是题解:
显然,由于我们有深度限制,使用广搜是更好的选择,我们只需要使用一个变量记录当前深度就可以,而且不会丢解。

又因为BFS的特性,第一次找到的解一定是最优解

简单做一下复杂度分析,我们就会发现:
1.朴素BFS的复杂度为\(O((L \cdot M)^{10})\)(当然,不精确,精确的复杂度如下:首先,我们定义\(b = \Theta(L \cdot M)\),随后,整体的复杂度就是\(O(b^{10})\)).

2.上述的上界是很悲观的,事实上,由于种种限制,我们的复杂度将远低于\(O(b^{10})\),而是近似于\(O(可达状态数 \times L \times M)\),其中的\(可达状态数 << b^{10}\).

3.双向BFS的上界为\(O((L \cdot M)^{5})\),显然使用双向BFS会更优,然而,在该题的低强度数据下,朴素BFS加上一些卡常是可以AC的。

常见的技巧:

  1. 使用STL可以大幅缩短代码复杂度(另外,貌似普遍认为STL的性能不如手写的好,实际上,STL的算法都是经过精细打磨的,除非是非常小的数据规模,一些常数因子几乎可以忽略不计)。
  2. 在使用双向BFS时,一般是选择较小一侧扩展(这一点在《信息学奥赛一本通·提高篇》中有详细的讲解,很推荐大家阅读)。
  3. 推荐大家研究一些STL的代码,比较出名的就是STL的 sort 函数,这不是普通的排序,它结合了快排、堆排、插入排序等算法,确保不会出现纯快排的最坏情况。
  4. 良好的码风很重要,具体来讲:
    变量名清晰、注释完善、易读、冷热代码分区(这一点在OI Wiki中有讲到,这样做可以便于编译器优化)、简化实现(如使用lambda函数)、“一个函数只做好一件事”(俗称的「GNU开发准则」)。
  5. 基于范围的 for 很好用,尤其是在STL容器的遍历中,省时省力,但是请注意,仅适用于0-based下标(即从0开始的下标)。

代码如下:

/* by 01130.hk - online tools website : 01130.hk/zh/urlthunder.html */ #include <bits/stdc++.h> using namespace std; struct Node { string s; int step; }; queue<Node> q[2]; unordered_map<string, int> vis[2]; int extend(int dir, vector<pair<string,string>>& rules) { Node cur = q[dir].front(); q[dir].pop(); for (auto& r : rules) { string from = dir ? r.second : r.first; string to = dir ? r.first : r.second; size_t pos = 0; while ((pos = cur.s.find(from, pos)) != string::npos) { string nxt = cur.s.substr(0, pos) + to + cur.s.substr(pos + from.length()); if (nxt.length() > 20) { ++pos; continue; } if (vis[dir].count(nxt)) { ++pos; continue; } if (vis[1-dir].count(nxt)) { return cur.step + 1 + vis[1-dir][nxt]; } vis[dir][nxt] = cur.step + 1; q[dir].push({nxt, cur.step + 1}); ++pos; } } return -1; } int main() { string A, B; cin >> A >> B; vector<pair<string,string>> rules; string a, b; while (cin >> a >> b) rules.push_back({a, b}); q[0].push({A, 0}); vis[0][A] = 0; q[1].push({B, 0}); vis[1][B] = 0; while (!q[0].empty() && !q[1].empty()) { // 选择较小一侧扩展 if (q[0].size() > q[1].size()) { int ans = extend(1, rules); if (ans != -1 && ans <= 10) { cout << ans; return 0; } else if (ans > 10) break; } else { int ans = extend(0, rules); if (ans != -1 && ans <= 10) { cout << ans; return 0; } else if (ans > 10) break; } } cout << "NO ANSWER!"; return 0; }

Problem D:P1120 [CERC 1995] 小木棍

难度:提高+/省选(但是本人觉得也就是个普及)

题目大意:

乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过 50 。
现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。

WriteUp:

一些简要的经验、分析:

  1. 这道题目的正解就是DFS(但是需要很多剪枝)。
  2. 纯DFS会TLE(我之前曾经在loj上做过这道题目)。
  3. 这道题在《一本通提高篇》中有,而且是例题。
  4. 即便是加入了各种优化,也不会AC(不知道有没有大佬做出AC的解法,至少本蒟蒻的没有AC)。

一般来说,一道搜索的题目会先考虑问题的维度。
为什么呢,我们入门搜索时,一般都是什么「子集」、「马的遍历」等题目。

很明显,我们事实上是在移动一个“高维空间中的点”(你也可以理解为高维向量),一旦这个点坐标满足我们所说的「解的要求」,那么这就是一个解。

子集就是在移动一个可变集合,里面的元素就是已经选择的,每次更新其中的一个元素,就是移动了该点,改变了这个点的坐标。

对于这道题,我们就是在移动一个点,它的坐标就是选择的各个木棍(做一下离散化,事实上,坐标是选择的各个木棍的序号)。

那么解的条件自然就是:得到的原始木棍一样长,我们需要在这些解中找到最短的一个。

那么是否每个点都应该探索一下呢,显然不是的。
我们可以排除一些不会影响最终答案的点(或者说「空间」),降低复杂度。
这就是「剪枝」:剪去搜索树中一些不会产生最优解的枝条(子树),同样,也是去除一些不必要的搜索状态空间。
这两种理解方式没有优劣,但是结合起来能更好的理解搜索的本质,也方便对搜索进行改造。

什么样的点不需要探索呢?

  1. 我们需要在dfs外枚举可能的原始木棍长度,那么它的范围是什么呢(这种通过减少搜索范围的剪枝,称作「上下界剪枝」)。
    显然,最小是最长的一根木棍(此时这根木棍恰好是原来的一根木棍);而最多是所有木棍的长度之和。
  2. 当原始长度不能被所有木棍的长度之和整除时,我们跳过;(这种通过跳过一些不可行的方案的剪枝,称作「可行性剪枝」)。
  3. 短的木棍更灵活,更容易凑出目标长度,因此我们将木棍降序排序;(这个不算是剪枝,优化的一种,称为「优化搜索顺序」)。
  4. 假如目前的dfs刚刚尝试了长度为\(m\)的木棍无法完成拼接,那么下一个长度为\(m\)的木棍仍然无法完成,跳过所有的这个长度的木棍;(这种方法称为「排除等效冗余」)
  5. 由于我们的目标长度是由小到大枚举的,因此只要找到解就可以直接返回,此时一定是最优解(这种跳过一些不优的解的剪枝,称作「最优性剪枝」)。
  6. 输入时忽略长度大于50的木棍。

加上如上的6个剪枝后,便能够通过本题,代码如下:

/* by 01130.hk - online tools website : 01130.hk/zh/urlthunder.html */ #include <bits/stdc++.h> int totalSticks; // 木棍总数 std::vector<int> stickLengths; // 木棍长度列表 std::vector<bool> used; // 使用标记 std::vector<int> nextIdx; // 跳过相同长度的优化数组 int totalLength = 0; // 木棍总长度 int maxLength = 0; // 最长单根木棍 int numTargetSticks = 0; // 目标棒数量 int bestAnswer = INT_MAX; // 最优解 bool foundSolution = false; // 是否已找到解 // DFS搜索 // @param completed: 已完成的棒子数量 // @param lastIdx: 从哪个木棍索引开始尝试 // @param targetLen: 目标棒长度 // @param currentLen: 当前棒已拼长度 void dfs(int completed, int lastIdx, int targetLen, int currentLen) { // 剪枝1: 成功完成所有棒子 if (completed == numTargetSticks - 1) { bestAnswer = std::min(bestAnswer, targetLen); foundSolution = true; return; } // 剪枝2: 当前棒已拼完,开始下一根 if (currentLen == targetLen) { dfs(completed + 1, 0, targetLen, 0); return; // 重要:必须return,避免继续循环 } // 尝试拼接 for (int i = lastIdx; i < totalSticks && !foundSolution; ) { if (!used[i] && currentLen + stickLengths[i] <= targetLen) { // 选择当前木棍 used[i] = true; dfs(completed, i + 1, targetLen, currentLen + stickLengths[i]); // 回溯 used[i] = false; if (foundSolution) return; // 剪枝3: // - 如果当前棒为空,这根木棍无法用,后面的更短也无法用 // - 如果这根木棍正好填满当前棒但失败,说明此路不通 if (currentLen == 0 || currentLen + stickLengths[i] == targetLen) { return; } i = nextIdx[i]; // 跳过相同长度的木棍 } else { i++; } } } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int n; if (!(std::cin >> n)) return 0; stickLengths.reserve(70); // 读取输入,过滤过长木棍 for (int i = 0; i < n; i++) { int len; std::cin >> len; if (len > 50) continue; // 单根木棍不超过50 stickLengths.push_back(len); totalLength += len; maxLength = std::max(maxLength, len); } totalSticks = stickLengths.size(); used.resize(totalSticks, false); // 从长到短排序,优先尝试长木棍 std::sort(stickLengths.begin(), stickLengths.end(), std::greater<int>()); // 预处理:构建跳过相同长度的next数组 nextIdx.resize(totalSticks); for (int i = 0; i < totalSticks; i++) { int j = i; while (j < totalSticks && stickLengths[j] == stickLengths[i]) { j++; } for (int k = i; k < j; k++) { nextIdx[k] = j; } i = j - 1; // 跳过已处理区间 } // 枚举目标棒长度(必须是totalLength的约数) for (int targetLen = maxLength; targetLen <= totalLength; targetLen++) { if (totalLength % targetLen != 0) continue; numTargetSticks = totalLength / targetLen; std::fill(used.begin(), used.end(), false); foundSolution = false; dfs(0, 0, targetLen, 0); if (foundSolution) break; } std::cout << bestAnswer << '\n'; return 0; }

P.S. 上面代码中使用了一些奇怪的注释:如“// @param completed:”,这是Doxygen注释,广泛使用于大型软件项目,用于自动生成项目文档,不过这种代码没必要使用,这是作者习惯。

另外,这种风格的代码极其类似AI的代码风格,因此,锻炼码风的一个好方法就是跟着AI学习,这也是本蒟蒻使用的训练方法。

后记:

由于这次比赛的题目很多,所以本文仅仅分析了其中的两道题目,剩余的需要等明后天继续。

不得不说,这些题目的质量都很高。
最后的最后,祝各位RP++。

2026.1.12.

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/13 5:50:19

(内联数组内存布局深度剖析):从缓存对齐到零拷贝的进阶之路

第一章&#xff1a;内联数组内存优化 在现代高性能计算与系统级编程中&#xff0c;内存访问效率直接影响程序的整体性能。内联数组作为一种将数据直接嵌入结构体或对象中的技术&#xff0c;能够显著减少内存碎片和间接寻址开销&#xff0c;从而提升缓存命中率。 内联数组的优势…

作者头像 李华
网站建设 2026/3/14 7:19:47

Figma中文插件:5步实现界面精准汉化,设计师必备工具

Figma中文插件&#xff1a;5步实现界面精准汉化&#xff0c;设计师必备工具 【免费下载链接】figmaCN 中文 Figma 插件&#xff0c;设计师人工翻译校验 项目地址: https://gitcode.com/gh_mirrors/fi/figmaCN 还在为Figma的英文界面而头疼吗&#xff1f;每次操作都要反复…

作者头像 李华
网站建设 2026/3/13 11:23:54

AR眼镜骨骼点方案:云端计算+边缘端显示最佳实践

AR眼镜骨骼点方案&#xff1a;云端计算边缘端显示最佳实践 引言&#xff1a;为什么需要云边协同的AR骨骼点方案&#xff1f; 想象一下&#xff0c;当你戴着AR眼镜玩体感游戏时&#xff0c;设备需要实时追踪你的每一个动作——从抬手到踢腿&#xff0c;从转头到弯腰。传统方案…

作者头像 李华
网站建设 2026/3/13 21:49:45

AI太极拳教学系统:骨骼角度分析,云端GPU支持百人并发

AI太极拳教学系统&#xff1a;骨骼角度分析&#xff0c;云端GPU支持百人并发 引言&#xff1a;当传统武术遇上AI科技 疫情期间&#xff0c;线上教育平台迎来爆发式增长&#xff0c;太极拳这类需要专业指导的健身课程尤其受欢迎。但传统视频教学存在明显痛点&#xff1a;学员动…

作者头像 李华
网站建设 2026/3/15 12:55:39

【JVM底层揭秘】:Lambda与方法重载的碰撞,为何不支持默认参数?

第一章&#xff1a;Lambda默认参数重载在现代编程语言中&#xff0c;Lambda 表达式已成为函数式编程的核心特性之一。当 Lambda 支持默认参数时&#xff0c;开发者可以在定义匿名函数时为参数指定默认值&#xff0c;从而提升代码的灵活性与可读性。尽管多数语言不直接支持对 La…

作者头像 李华
网站建设 2026/3/14 7:51:16

SD-PPP终极指南:Photoshop与AI绘图的革命性融合方案

SD-PPP终极指南&#xff1a;Photoshop与AI绘图的革命性融合方案 【免费下载链接】sd-ppp Getting/sending picture from/to Photoshop in ComfyUI or SD 项目地址: https://gitcode.com/gh_mirrors/sd/sd-ppp 还在为传统设计流程中创意实现与工具切换的割裂感而困扰吗&a…

作者头像 李华