ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

将二进制字符串划分为最小数量的“漂亮子串“:基于 codeforces-go 仓库的 DP 题解与源码剖析

将二进制字符串划分为最小数量的“漂亮子串“:基于 codeforces-go 仓库的 DP 题解与源码剖析 科学计算【免费下载链接】codeforces-go算法竞赛模板库 by 灵茶山艾府 项目地址https://gitcode.com/GitHub_Trending/co/codeforces-go点击查看免费下载本文围绕 LeetCode 双周赛第 108 场第 3 题「将字符串拆分为最少漂亮子字符串」Partition String Into Minimum Beautiful Substrings展开以本仓库 leetcode/biweekly/108/c/README.md 中的官方题解为骨架结合仓库内配套的 Go 实现、测试样例与测试框架源码系统讲解从记忆化搜索到递推的完整推导过程以及该解法在 Python 与 Go 双语言下的落地方式。读完本文你将掌握如何用十进制幂的二进制表示这一关键观察完成状态压缩、如何借助倒序递推天然规避前导零约束以及如何利用本仓库的 LeetCode 测试基础设施验证任意实现。题目背景与核心约束本题来自力扣双周赛第 108 场题目编号biweekly-contest-108对应题目partition-string-into-minimum-beautiful-substrings。任务如下输入一个二进制字符串s仅含0和1且不包含前导零。需要将s划分成若干段子串每一段都必须是一个漂亮子串。漂亮子串定义为其十进制表示是 5 的幂即数值形如 $5^k, k \ge 0$。求最小划分段数若无法划分返回-1。例如字符串1011的十进制值为 11而 5 的幂依次为 1、5、25、125…。二进制下101对应十进制 51对应 1因此1011 101 1可以划分成 2 段漂亮子串答案为 2。理解这道题的关键在于两个观察值域上界由于s长度为 $n$其十进制数值严格小于 $2^n$。因此只需要考虑小于 $2^n$ 的 5 的幂。漂亮子串集合极小在 $2^{15} 32768$ 以内5 的幂只有 $1, 5, 25, 125, 625, 3125, 15625$ 共7 个。也就是说s中可能出现的漂亮子串的二进制形式总共只有 7 种候选。正是由于候选集合如此之小预处理全部候选串后问题就退化为一个非常朴素的字符串划分 DP。预处理5 的幂的二进制表示原文档给出的预处理逻辑是枚举 $5^i$从 $i0$ 开始只要值小于 $2^{15}$就把它的二进制字符串形式存入全局变量pow5。仓库配套 Go 实现位于 leetcode/biweekly/108/c/c.go完整代码如下package main import strconv var pow5 []string func init() { // 预处理 2**15 以内的 5 的幂 for p5 : 1; p5 115; p5 * 5 { pow5 append(pow5, strconv.FormatUint(uint64(p5), 2)) } }这段代码有两点值得说明循环条件p5 115与 README 中预处理 $2^{15}$ 以内的 5 的幂完全一致115是 Go 的移位运算即 $2^{15}32768$。循环从p5 1即 $5^0$开始每次p5 * 5直到下一个幂不小于 $2^{15}$ 为止共得到 7 个值。进制转换strconv.FormatUint(uint64(p5), 2)是标准库的进制格式化函数第二个参数2表示按二进制输出。因此pow5中存放的是形如1、101、11001、1111101、1001110001、110000110101、11110100001001的字符串。从仓库源码结构看之所以把pow5设计成包级全局变量并在init()中填充是因为测试框架leetcode/testutil/leetcode.go 中的RunLeetCodeFuncWithFile、RunFuncWithRandomInput会反复调用minimumBeautifulSubstrings执行多组用例预处理只做一次即可被所有用例共享避免重复计算。为什么是 $2^{15}$题目没有直接给出 $2^{15}$ 这个数字它来自对数据规模的推断README 中明确说明由于测试数据很多可以用全局变量预处理 $2^{15}$ 以内的 5 的幂。可以推断该题数据范围中 $n \le 15$即输入二进制串长度至多 15因此任何漂亮子串的数值都不可能达到 $2^n$枚举小于 $2^{15}$ 的 5 的幂即覆盖了全部候选。如果你的输入规模更大这一上限需要相应放大但候选数量增长极为缓慢10 的幂增长对应到二进制串个数也极少不影响整体思路。记忆化搜索自顶向下的最小划分状态设计定义 $\textit{dfs}(i)$ 表示将后缀s[i:]从s[i]开始到末尾的子串划分成若干漂亮子串的最小段数。那么递归边界$\textit{dfs}(n) 0$空后缀不需要划分。递归入口$\textit{dfs}(0)$即整个字符串的最小划分段数。不可行情况若s[i] 0该段以 0 开头会形成前导零而 5 的幂的二进制表示不可能有前导零也不允许前导零的子串或者s[i:]无法匹配任何候选串则 $\textit{dfs}(i) \infty$Python 中用infGo 中用n1这类上界值。状态转移枚举pow5中的每个候选串t长度为 $m$若s[i:im] t则可以把这一段切下来剩下部分递归求解因此有$$ \textit{dfs}(i) \textit{dfs}(im) 1 $$对所有可行候选取最小值即可。Python 实现原文档代码# 预处理 2**15 以内的 5 的幂 pow5 [bin(5 ** i)[2:] for i in range(7)] class Solution: def minimumBeautifulSubstrings(self, s: str) - int: n len(s) cache def dfs(i: int) - int: if i n: return 0 if s[i] 0: return inf # 不能包含前导 0 res inf for t in pow5: if i len(t) n: break if s[i: i len(t)] t: # 忽略切片的时间这里的比较视作均摊 O(1) res min(res, dfs(i len(t)) 1) return res ans dfs(0) return ans if ans inf else -1实现细节pow5 [bin(5 ** i)[2:] for i in range(7)]与 Go 版init()等价bin(x)[2:]去掉0b前缀即得二进制字符串range(7)恰好覆盖 $i0..6$对应 7 个小于 $2^{15}$ 的 5 的幂。内层循环在i len(t) n时break是因为pow5中的字符串按长度递增排列一旦当前候选超出后缀长度后续更长的候选必然也超出可以提前终止。cachePython 3.9 的functools.cache负责记忆化保证每个dfs(i)只被计算一次。返回时判断ans infinf与任意整数比较时整数值一定更小因此该条件等价于存在可行划分。递推版倒序遍历规避前导零README 中强调按照视频中的做法1:1 翻译成递推。倒着遍历的好处是方便判断是否有前导零。为什么倒序记忆化搜索天然从位置 0 向尾部推进而递推若正序从左到右填表会面临一个问题划分的第一段以s[0]开头而题目保证输入本身没有前导零正序填表时状态f[i]表示s[0:i]的划分处理起来并不直观且当前段能否以 0 开头的判断分散在转移中。倒序填表则让f[i]直接对应后缀s[i:]与 $\textit{dfs}(i)$ 一一对应从i n-1一路算到i 0每一轮只需检查s[i]是否为零零则直接跳过不可行否则枚举候选串尝试匹配。这与原题解的递归语义完全同构翻译成本最低。Python 递推实现原文档代码# 预处理 2**15 以内的 5 的幂 pow5 [bin(5 ** i)[2:] for i in range(7)] class Solution: def minimumBeautifulSubstrings(self, s: str) - int: n len(s) f [inf] * n [0] for i in range(n - 1, -1, -1): if s[i] 0: continue # 不能包含前导 0 for t in pow5: if i len(t) n: break if s[i: i len(t)] t: # 忽略切片的时间这里的比较视作均摊 O(1) f[i] min(f[i], f[i len(t)] 1) return f[0] if f[0] inf else -1注意f [inf] * n [0]的写法[inf] * n得到长度 $n$ 的列表再拼接[0]恰好构造出长度为 $n1$ 的数组其中f[n] 0正是递归边界 $\textit{dfs}(n)0$ 的递推对应物。Go 递推实现仓库源码仓库中的 leetcode/biweekly/108/c/c.go 给出了与 README 完全一致的 Go 实现func minimumBeautifulSubstrings(s string) int { n : len(s) f : make([]int, n1) for i : n - 1; i 0; i-- { f[i] n 1 if s[i] 0 { continue } for _, t : range pow5 { if ilen(t) n { break } if s[i:ilen(t)] t { f[i] min(f[i], f[ilen(t)]1) } } } if f[0] n { return -1 } return f[0] } func min(a, b int) int { if b a { return b }; return a }Go 版与 Python 版有三处语言差异需要留意无穷大的表示Python 用infGo 用n 1作为不可行标记。由于最多划分 $n$ 段每段一个字符n1必然大于任何合法答案因此f[0] n与 Python 的f[0] inf判断等价。字符串切片比较s[i:ilen(t)] t是 Go 的字节串比较ilen(t)保证不越界内层循环在ilen(t) n时已break。min函数该仓库使用go 1.23见 go.modmin已是内置函数这里手写min(a, b)是为了兼容旧版本 Go 编译环境的写法。复杂度分析README 中给出的复杂度结论如下时间复杂度$\mathcal{O}(n^2)$其中 $n$ 为s的长度。动态规划的时间复杂度 状态个数 × 单个状态的计算时间。本题状态个数为 $\mathcal{O}(n)$每个位置一个状态单个状态需要枚举 7 个候选串并逐一比较枚举过程本身是 $\mathcal{O}(7) \mathcal{O}(1)$ 级别的常数开销字符串比较中由于pow5内各候选串的公共前缀很短绝大多数比较在很短的位数内就失配可视为均摊 $\mathcal{O}(1)$因此单个状态计算时间为 $\mathcal{O}(n)$最坏情况是某次匹配成功需要比较完整长度总时间复杂度为 $\mathcal{O}(n^2)$。空间复杂度$\mathcal{O}(n)$仅需一个长度为 $n1$ 的 DP 数组pow5是固定大小的预处理数据不随 $n$ 增长。值得补充的是若不使用失配即退出的均摊论证把每次字符串比较都算作 $\mathcal{O}(n)$则复杂度会上升为 $\mathcal{O}(n^2 \cdot \sum|t|)$但 7 个候选串的最大长度为 14常数很小实际运行仍然非常快。仓库中的测试与验证体系测试用例文件仓库为本题提供了手写测试数据文件 leetcode/biweekly/108/c/c.txt内容是输入字符串 期望输出成对排列共 3 组1011 2 111 3 0 -1这 3 组数据正好覆盖了三种典型情形1011→ 2可行划分101 1对应 $5 1$答案为 2111→ 3二进制 111 即十进制 7不是 5 的幂且 7 以内的 5 的幂只有 1 和 5111无法匹配任何候选11、111都不是 5 的幂的二进制只能拆成 3 个1答案为 30→ -1以 0 开头无法形成漂亮子串返回-1。测试驱动代码leetcode/biweekly/108/c/c_test.go 通过仓库封装的测试框架驱动func Test_c(t *testing.T) { targetCaseNum : 0 // -1 if err : testutil.RunLeetCodeFuncWithFile(t, minimumBeautifulSubstrings, c.txt, targetCaseNum); err ! nil { t.Fatal(err) } if err : testutil.RunFuncWithRandomInput(t, minimumBeautifulSubstrings); err ! nil { t.Fatal(err) } }其中RunLeetCodeFuncWithFile的实现位于 leetcode/testutil/leetcode.goL340-L370它读取c.txt按每 2 行一组解析成 (输入, 期望输出) 用例再用反射调用被测试函数逐一比对。targetCaseNum 0表示运行全部用例若设为正数则只运行指定用例设为-1则运行最后一组且单用例通过后会自动继续跑完全部用例对应源码L320-L323的递归逻辑。第二行的RunFuncWithRandomInput是随机对拍随机生成输入并调用函数用于在函数具备输入 → 输出单调可验证性质时做补充回归对本题而言主要起到跑通、防 panic 的作用。题目来源c_test.go末尾注释标明了本题的两条链接双周赛 108 场第三题及题目独立页均为 LeetCode 官方地址本文不再重复给出外部链接。核心思路复盘最后把整道题的解题链条浓缩为四步便于在同类字符串划分 数值性质问题中复用压缩候选集利用数值上界二进制串长度 $n$ ⇒ 数值 $2^n$大幅缩小合法段的枚举范围本题中 5 的幂只有 7 个二进制候选串。定义后缀状态$\textit{dfs}(i)$ 后缀s[i:]的最小划分段数边界 $\textit{dfs}(n)0$不可行记为无穷大。枚举转移逐个匹配候选串t匹配成功则 $\textit{dfs}(i) \min(\textit{dfs}(im)1)$。倒序翻译成递推f[n] 0从i n-1倒推到i 0天然处理前导零约束最终f[0] n时返回-1。这套记忆化搜索 → 1:1 翻译递推的方法正是该仓库题解 READMEleetcode/biweekly/108/c/README.md强调的通用训练路径配合仓库自带的 Go 实现与测试框架可以随时go test验证任意一步改写例如把pow5上限改为 $2^{20}$、把匹配方式换成字符串哈希的正确性。赞分享科学计算【免费下载链接】codeforces-go算法竞赛模板库 by 灵茶山艾府 项目地址https://gitcode.com/GitHub_Trending/co/codeforces-go点击查看免费下载相关推荐codeforces-go中的字符串处理子串查找算法codeforces go中的字符串处理子串查找算法 在日常编程和算法竞赛中我们经常需要处理各种字符串问题其中子串查找是一个非常基础且重要的操作。无论是在科学计算StarRocks hex_decode_binary 函数详解将十六进制字符串解码为 VARBINARY 二进制数据StarRocks hex_decode_binary 函数详解将十六进制字符串解码为 VARBINARY 二进制数据 hex_decode_binary 是数据库OLAP数据仓库大数据湖仓一体数据分析AI-Trader智能交易平台体验5分钟让AI代理自动开始交易新手也能一键复制高手操作AI Trader智能交易平台体验5分钟让AI代理自动开始交易新手也能一键复制高手操作 你是不是也遇到过这样的尴尬满心欢喜地下载了一款号称AI炒股神器后端前端金融科技AI AgentAI 技能上一篇Emscripten中的信号处理性能信号传递延迟测试下一篇如何使用Zellij打造终极终端测试自动化工作流从入门到精通创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表