)
LeetCode-Go 题解753. Cracking the Safe——用贪心 DFS 构造最短开锁密码串de Bruijn 序列【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go本篇以 LeetCode-Go 仓库中 753. Cracking the Safe 题解文档 为主体深入讲解最短开锁密码串这一经典问题为什么暴力枚举可以做到最短、贪心思想如何与 DFS 回溯结合以及它与组合数学中 de Bruijn 序列的对应关系。读完本文你将掌握这类覆盖全部 n 位子串的最短序列问题的通用解法并能直接复现仓库中的 Go 实现与其测试。题目理解保险箱是如何记住密码的题目描述原始题面见 README.md有一个密码为n位数字的保险箱每位数字取自0, 1, ..., k-1共k个字符。输入密码时保险箱会自动把最后输入的n位与正确密码做匹配一旦匹配成功即打开。关键点在于最后 n 位而非完整的连续 n 位输入——也就是说你可以一直输入一串很长的字符只要这串字符的某个长度为 n 的后缀子串恰好等于密码箱子就会打开。因此问题转化为构造一个最短字符串使得它的所有长度为 n 的连续子串能覆盖全部 kⁿ 种可能的密码每种至少出现一次。来看题目给出的两个示例例 1n 1, k 2输出0110同样可接受。n1 时只需覆盖0、1两个单字符密码最短串长度为 2。例 2n 2, k 2输出0011001100、10011、11001等也均可接受。验证一下00110的长度为 2 的子串依次是00、01、11、10恰好是二进制下的全部 4 种 2 位密码且每种只出现一次。这也印证了最短串的长度公式kⁿ n − 1。对于例 2即 2² 2 − 1 5与输出00110的长度一致。数据范围为什么 DFS 暴力可行题目给出了严格的约束详见 README.md 的 Note 部分约束取值范围密码位数 n[1, 4]字符集大小 k[1, 10]密码总数 kⁿ最大 4096从源码看正是这些约束决定了算法的选型。753. Cracking the Safe.go 中直接使用int(math.Pow(float64(k), float64(n)))计算总状态数visit, total : map[string]bool{}, int(math.Pow(float64(k), float64(n)))kⁿ ≤ 4096意味着状态空间至多 4096 个节点无论用 DFS 回溯还是 BFS都完全在可控范围内。这也解释了题解采用 DFS 暴力枚举的合理性范围小到可以穷举难点反而在于如何让穷举出来的串最短。解题思路贪心 DFS 回溯核心观察复用前 n−1 位题解文档README.md 解题思路一节点出了本题的关键如果下一次递归能利用上一次已经输入的 n−1 位那么最终输出的字符串必然最短。直观理解每追加一个字符只会产生一个新的长度为 n 的子串即以刚追加字符结尾的那个窗口。前面 n−1 个字符是免费复用的不需要重新输入。因此从贪心的角度每一步都应优先尝试在当前串的基础上追加一个字符而不是另起炉灶。这样每追加一个字符就能解锁一个新密码用最少的字符数覆盖全部 kⁿ 个密码。以例 2 为线索00→01→11→10每个 2 位密码都通过复用前一个密码的后 1 位衔接而来最终拼成00110。与 de Bruijn 序列的对应关系这个问题本质上是在构造组合数学中的de Bruijn 序列 B(k, n)一个循环序列长度为 kⁿ包含字符集大小为 k 的所有长度为 n 的串恰好各一次作为其循环子串。将其展开成线性形式额外补上前 n−1 个字符就得到长度为 kⁿ n − 1 的答案。本仓库的实现正是用 DFS 在 de Bruijn 图上寻找一条覆盖所有 kⁿ 个 n 位状态的路径。为什么贪心能保证最短而非只能碰运气题解中明确说明原文未给出严格证明此处为基于算法结构的推断贪心策略保证了搜索树只在当前路径无法继续覆盖新状态时才回溯。由于每个状态至多被访问一次一旦 DFS 成功返回路径恰好经过 kⁿ 个状态、生成 kⁿ n − 1 个字符这正是理论下界。反例若每次都不复用前 n−1 位则每个密码平均需要 n 个字符串长会膨胀到接近 n·kⁿ远非最短。源码逐行拆解完整实现位于 753. Cracking the Safe.go与题解文档 README.md 中的代码一致共两个函数。入口函数crackSafeconst number 0123456789 func crackSafe(n int, k int) string { if n 1 { return number[:k] } visit, total : map[string]bool{}, int(math.Pow(float64(k), float64(n))) str : make([]byte, 0, totaln-1) for i : 1; i ! n; i { str append(str, 0) } dfsCrackSafe(total, n, k, str, visit) return string(str) }逐点说明number 0123456789是字符集常量。n 1时直接返回number[:k]即按序输出0, 1, ..., k-1覆盖全部 k 个单字符密码这与一般公式 k¹ 1 − 1 k 的长度完全吻合是一个轻量级特判。visit用 map 记录已经覆盖过的 n 位子串total kⁿ是需要覆盖的子串总数。str预分配容量totaln-1最终答案的精确长度并预填 n−1 个0。这一步至关重要DFS 首次取最后 n 位时需要这 n−1 个前导字符凑出第一个 n 位窗口000...0从而使后续每个状态都能无缝衔接。最后把 DFS 得到的字节切片转成字符串返回。递归函数dfsCrackSafefunc dfsCrackSafe(depth, n, k int, str *[]byte, visit *map[string]bool) bool { if depth 0 { return true } for i : 0; i ! k; i { *str append(*str, byte(0i)) cur : string((*str)[len(*str)-n:]) if _, ok : (*visit)[cur]; ok ! true { (*visit)[cur] true if dfsCrackSafe(depth-1, n, k, str, visit) { // 只有这里不需要删除 return true } delete(*visit, cur) } // 删除 *str (*str)[0 : len(*str)-1] } return false }递归逻辑拆解终止条件depth 0表示 kⁿ 个 n 位子串已全部覆盖返回true向上传递成功信号。贪心顺序内层循环i从 0 到 k−1 递增即优先尝试较小的数字。只要任意一个分支最终成功就立即return true不再尝试更大的数字——这正是贪心每一步都走能走的最小可行解的体现。状态判定追加字符后截取最后 n 位得到cur若cur尚未访问则标记并深入若已访问说明追加该字符没有产生新密码直接跳过。回溯细节当某次递归失败此路不通、继续走下去无法覆盖全部状态时需要delete(*visit, cur)撤销标记、并把刚追加的字符弹出*str (*str)[0 : len(*str)-1]恢复现场后尝试下一个数字。唯一的不删除代码注释只有这里不需要删除指的是成功路径——当dfsCrackSafe(depth-1, ...)返回true时当前字符属于最终答案的一部分不能回退直接层层返回。值得注意visit和str都以指针传递保证整个递归过程共享同一份状态避免拷贝开销得益于kⁿ ≤ 4096map 的内存占用也非常有限。正确性与复杂度分析正确性visit集合记录了所有已覆盖的长度为 n 的窗口DFS 终止条件depth 0等价于恰好覆盖了全部 kⁿ 个窗口。由于每个窗口只在首次出现时被标记整条路径天然满足每种密码至少出现一次且窗口之间通过复用前 n−1 位首尾相连长度达到理论下界 kⁿ n − 1。时间复杂度每个 n 位状态最多被访问一次每次访问尝试最多 k 个分支整体约为 O(kⁿ·k) 量级在最坏约束kⁿ 4096k 10下也只需约 4 万次尝试瞬间完成。空间复杂度visit表 O(kⁿ)递归栈深度最坏 O(kⁿ)总体 O(kⁿ)。测试验证仓库如何保证实现正确仓库为本题配备了单元测试 753. Cracking the Safe_test.go采用本仓库统一的表驱动测试风格用question753结构体组织输入para753{n, k}与期望输出ans753覆盖题目给出的两个示例n 1, k 2→ 期望01n 2, k 2→ 期望00110测试中逐个打印输入与crackSafe(p.n, p.k)的实际输出方便人工核对例如n2, k2时打印出的00110即覆盖全部 2 位二进制密码的最短串。本地复现验证方式仓库为只读仅需查看与运行cd leetcode/0753.Cracking-the-Safe go test -v -run Test_Problem753 .扩展思考n1 特例的普适性入口函数对n 1单独返回number[:k]这与 DFS 的通用流程结果一致只是省去了递归开销体现了实现层面的优化取舍。序列结构答案的本质是 de Bruijn 序列 B(k, n) 的线性展开同类型技巧可迁移到最短超串全覆盖滚码等一类问题在真实场景中类似的序列构造思想也常见于滚轮密码锁、条形码编码、DNA 片段拼接等领域此处为背景常识非本仓库功能。多种可行答案题目允许返回任意最短串例 1 的01/10、例 2 的四个答案均被接受因为贪心选择与回溯顺序不同会得到不同的等价解但长度一定同为 kⁿ n − 1。围绕本题读者还可以在仓库中横向对比同目录下其他题解的文档组织方式如 README.md 与各 leetcode 子目录体会题面 大意 思路 代码 测试这一统一的题解写作范式。【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考