多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

LeetCode-Go 1048 Longest String Chain:排序加逆序动态规划的词链求解(Go 实现全解析)

LeetCode-Go 1048 Longest String Chain:排序加逆序动态规划的词链求解(Go 实现全解析) LeetCode-Go 1048 Longest String Chain排序加逆序动态规划的词链求解Go 实现全解析【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go本篇以 LeetCode-Go 仓库中第 1048 题Longest String Chain的题解为骨架完整讲解“前身关系 词链”问题的题目结构、为什么从暴力枚举转向动态规划、排序与poss数组的设计意图以及isPredecessor双指针判定的实现细节并结合仓库中的完整 Go 源码与测试用例给出可运行、可复核的复杂度分析与验证方式。一、题目描述与约束原题收录于 leetcode/1048.Longest-String-Chain/README.md题面如下Given a list of words, each word consists of English lowercase letters.Lets sayword1is apredecessorofword2if and only if we can add exactly one letter anywhere inword1to make it equal toword2. For example,abcis a predecessor ofabac.Aword chainis a sequence of words[word_1, word_2, ..., word_k]withk 1, whereword_1is a predecessor ofword_2,word_2is a predecessor ofword_3, and so on.Return the longest possible length of a word chain with words chosen from the given list ofwords.题目大意给出一个由小写英文字母组成的单词列表。若可以在word1的任意位置恰好插入一个字母使其等于word2则称word1是word2的“前身”例如abc是abac的前身。词链是单词序列[word_1, word_2, ..., word_k]k 1其中相邻两词均满足前身关系。要求从给定列表中选取单词组成词链返回词链的最大可能长度。样例输入: words [a,b,ba,bca,bda,bdca] 输出: 4 说明: 最长词链之一是 a → ba → bda → bdca输入: words [xbc,pcxbcf,xb,cxbc,pcxbc] 输出: 5数据约束约束项取值范围words.length1 words.length 1000words[i].length1 words[i].length 16字符集仅小写英文字母仓库主目录 README.md 的题单表格中1048 题的难度标注为Medium该题解属于题单的“动态规划”主题范畴。二、为什么是动态规划从数据规模出发的分析原解题思路给出的第一条判断依据非常值得借鉴——“从数据规模上分析可以猜出此题是 DFS 或者 DP 的题”。n 1000这个规模下对任意一对词做O(L)的前身判定L 16都很廉价真正的问题是链条的枚举如果以每个字符串为起点做深度优先搜索或两两枚举链条数量可能指数级膨胀。关键观察在于题目存在明显的重叠子问题以词a和b构成的前身关系可能被“以c开头的链条”用到也可能被“以d开头的链条”用到。同一个子结构某个词能延伸出的最长链会被反复计算这正是引入 DP 的动机为每个词只计算一次“以它为起点的最长链长度”。三、算法设计排序、poss 数组与逆序递推完整实现位于 1048. Longest String Chain.go注意文件名与目录名均含空格核心由三部分构成。3.1 按词长排序建立“长度单调”的递推基础sort.Slice(words, func(i, j int) bool { return len(words[i]) len(words[j]) })前身关系要求len(word2) len(word1) 1因此只有长度恰好相差 1 的相邻长度层之间才可能存在转移边。按词长升序排序后所有同长度的词在数组中构成连续块这就为下面“按长度定位”提供了前提。3.2 poss 数组长度 → 起始下标的索引表poss, res : make([]int, 162), 0 for i, w : range words { if poss[len(w)] 0 { poss[len(w)] i } }poss[l]记录排序后长度为l的词块的首个下标。数组大小为162 18因为约束中词长最大 16而递推时需要访问len(words[i])1最大 17多留一位防止越界。这里有一个从源码结构看颇具巧思的默认值行为poss零值初始化为全 0而“某长度不存在”与“某长度块恰好从下标 0 开始”都会表现为poss[l] 0。这两种情况都不影响正确性若长度L根本不存在内层循环从下标 0 开始扫描时条件len(words[j]) len(words[i])1立即失败除非words[i]本身是最短词但那时len(words[i])1必然存在否则不构成“不存在”的情形循环空转等价于没有转移若长度块恰从下标 0 开始即最短长度层poss保持 0 恰好就是正确的起始位置。3.3 逆序递推dp[i] 表示以 words[i] 为起点的链长dp : make([]int, len(words)) for i : len(words) - 1; i 0; i-- { dp[i] 1 for j : poss[len(words[i])1]; j len(words) len(words[j]) len(words[i])1; j { if isPredecessor(words[j], words[i]) { dp[i] max(dp[i], 1dp[j]) } } res max(res, dp[i]) }状态定义dp[i]是以words[i]为词链起点头、后续每步都向更长的词转移时能得到的最长链长。状态初值为 1词链至少含自身对应题面k 1。转移方向决定了遍历方向dp[i]依赖所有长度len1的候选后继dp[j]而后继一定排在排序数组的后面更长的词在下标更大处。因此从i len(words)-1逆序递推到 0保证用到dp[j]时它已经完成计算——这与原解题思路中“逆序往前递推因为初始条件只能得到以最长字符串为起始的字符串链长度为 1”完全一致。内层循环的两个边界也值得注意j从poss[len(words[i])1]起步直接跳到目标长度块的起点跳过所有更短的词终止条件len(words[j]) len(words[i])1在越过长度块后自动为假因此无需额外记录每块的结束下标。3.4 isPredecessor双指针一次判定func isPredecessor(long, short string) bool { i, j : 0, 0 wasMismatch : false for j len(short) { if long[i] ! short[j] { if wasMismatch { return false } wasMismatch true i continue } i j } return true }该函数判断long是否比short恰好多一个字母即short是long删去一个字符的结果。思路是双指针 至多一次失配容忍i走长串、j走短串字符相等时同步前进第一次不等时只推进i视作长串多出的那个字符第二次再不等则说明多出的字符不止一个立即返回false。循环以短串走完为结束条件长串末尾可能还剩余 0 或 1 个字符都合法。时间复杂度O(len(long))、空间O(1)比“枚举插入位置”或“排序去重后比较”的写法更简洁且不需要分配任何临时字符串。四、完整实现与配套函数仓库中该题的完整解法源码package leetcode import sort func longestStrChain(words []string) int { sort.Slice(words, func(i, j int) bool { return len(words[i]) len(words[j]) }) poss, res : make([]int, 162), 0 for i, w : range words { if poss[len(w)] 0 { poss[len(w)] i } } dp : make([]int, len(words)) for i : len(words) - 1; i 0; i-- { dp[i] 1 for j : poss[len(words[i])1]; j len(words) len(words[j]) len(words[i])1; j { if isPredecessor(words[j], words[i]) { dp[i] max(dp[i], 1dp[j]) } } res max(res, dp[i]) } return res }两个配套函数func max(a, b int) int { // 源码 if a b { return a } return b }func isPredecessor(long, short string) bool { // 源码 i, j : 0, 0 wasMismatch : false for j len(short) { if long[i] ! short[j] { if wasMismatch { return false } wasMismatch true i continue } i j } return true }实现细节说明所有题解统一置于package leetcode非main与仓库其余题目保持一致的包组织仓库 go.mod 声明go 1.19因此解法自行定义了max辅助函数而非依赖 Go 1.21 才引入的内建max在当前模块版本下可编译sort.Slice的比较器只按长度排序不做字典序排序——对正确性而言同层内部顺序无关紧要这是刻意为之的简化res在每次更新dp[i]后取全局最大值等价于max(dp[0..n-1])但只需 O(1) 额外扫描。五、样例推演逐状态跟踪 Example 1以words [a,b,ba,bca,bda,bdca]为例排序后下标 : 词为下标012345词abbabcabdabdca长度112334poss索引表poss[1]0、poss[2]2、poss[3]3、poss[4]5。逆序递推dp[i]依赖右侧已算好的更长词iwords[i]扫描的长度层前身判定命中dp[i]5bdca长度 5不存在空转—14bda长度 4bdca ✔1dp[5]23bca长度 4bdca ✔1dp[5]22ba长度 3bca ✔、bda ✔1dp[3]31b长度 2ba ✔1dp[2]40a长度 1 的词无更短前身层检查长度… 此处 i0 词长 1扫描长度 2ba ✔1dp[2]4最终res max(...) 4对应题面给出的链a → ba → bda → bdcadp[1]与dp[0]同得 4链头可取a或b。Example 2[xbc,pcxbcf,xb,cxbc,pcxbc]排序后为[xb,xbc,cxbc,pcxbc,pcxbcf]恰好形成一条完整链xb → xbc → cxbc → pcxbc → pcxbcf逆序递推得到dp[0] 5与题面输出一致。六、复杂度分析设n len(words) 1000L为词长 16排序O(n log n)比较每次比较O(1)构建 poss单趟线性扫描O(n)递推外层每个词一次内层扫描“长度 1”的整个词块最多n个词每次调用isPredecessor为O(L)最坏合计O(n^2 * L)。代入约束即O(16 × 10^6)量级的常数操作在L 16这一固定小常数下实际很快空间dp数组O(n)poss定长O(1)18 个 int。相比“以每个词为起点 DFS 记忆化”的等价写法本实现用排序把“找所有长度 1 的候选词”从O(n)全表扫描降为直接跳块poss数组即该优化的体现相比用哈希表按长度分组的写法定长数组利用了“词长上限 16”这一约束避免了哈希开销。七、测试与验证仓库对每题维护同目录测试文件本题为 1048. Longest String Chain_test.go。测试沿用仓库统一范式para1048结构体承载输入参数words []stringans1048承载期望答案one int外层question1048将二者组合测试文件 L8-L23Test_Problem1048共内置4 组用例测试文件 L25-L69[a,b,ab,bac]→ 2题面 Example 2[xbc,pcxbcf,xb,cxbc,pcxbc]→ 5题面 Example 1[a,b,ba,bca,bda,bdca]→ 4一组约 170 个词的长随机大样例期望链长 15用于在更接近n 1000量级的输入上检验实现从测试代码看fmt.Printf逐组打印输入与实际输出测试文件 L71-L77便于运行核对。在仓库根目录下可以这样运行# 只跑本题 go test ./leetcode/1048.Longest-String-Chain/ # 或按仓库自带脚本跑全量题解并生成覆盖率文件 bash gotest.shgotest.sh 使用go test -covermodeatomic -coverprofilecoverage.txt ./leetcode/...一次性对全部leetcode/...包生成单一合法的覆盖率文件脚本注释说明了合并多 profile 会导致新版 Codecov 解析为 0% 的坑。仓库主 README.md 亦声明了 100% 测试覆盖率与“runtime beats 100%”的整体目标本题解法与测试文件共同构成这一体系中的一个单元。八、小结1048 题的解法浓缩了一条可复用的模板当状态转移只发生在“长度相差 1 的相邻层”时先按长度排序再按长度建索引最后从长到短逆序 DP。poss定长数组大小由词长上限 16 推出与isPredecessor的“至多一次失配”双指针是两个低开销的关键构件分别把候选定位和前身判定都压到接近常数级别。整份实现无哈希、无额外字符串分配是 Go 语言下处理此类“插入单字符”判定问题同族问题如词梯的简洁参考完整代码与测试可直接在 leetcode/1048.Longest-String-Chain/ 目录中查看复现。【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表