
讲到字符串匹配、循环同构这类问题时我脑子里第一个跳出来的常常不是那些动辄几十行、需要各种预处理的后缀数据结构而是一个短小精悍、思路刁钻的算法——最小表示法。对就是那个用来找一个字符串“最小字典序循环同构串”的线性算法。代码短到十几行原理说复杂也不算复杂但里面那个“跳过必然不是最优解的区域”的思想我当年第一次看懂的时候是真心拍过桌子的。这玩意到底解决什么问题最经典的场景就是判断两条项链是否相同。项链嘛首尾相接从哪个珠子开始看都可以。给定两个字符串它们能否通过循环移位变得完全一样如果拿“abc”和“bca”问你你一眼就能看出来它们其实是同一条链子。但如果是“abcedba”和“debaabc”这种长度大、字符杂的串肉眼就不好使了。而这种“旋转后能否相等”的判断本质上就是在问这两个串的最小表示是否相同。因为一个字符串不管怎么旋转它的最小字典序循环同构串都是唯一的。这篇文章我就用“最小表示法”这个核心主题把它的原理、代码、应用场景和我在实际做题和项目中踩过的坑一次性讲透。1. 最小表示法解决的问题与思路来源1.1 从一个看似简单的问题说起字符串循环同构我们先把问题形式化。给定一个长度为n的字符串s我们定义它的“循环同构串”为从任意位置i切开把前半部分拼到后半部分后面形成的长度为n的新串。例如s bcda从位置 1 切开0-based得到cdab从位置 2 切开得到dabc从位置 3 切开得到abcd。一个长度为n的字符串一共有n个循环同构串。所谓最小表示法就是求出这n个循环同构串中字典序最小的那一个。那这个问题在真实世界里有什么用呢我最早接触它是在算法竞赛里一道题叫“项链”大意是有两串珠子每个珠子有一个颜色编号珠子串成环。现在问这两串珠子是否等价。等价的意思就是其中一个可以通过旋转不能翻转变成另一个。这个问题的标准解法之一就是求两个项链序列的最小表示然后比对是否相等。后来我在实际的项目里也见过类似的场景比如某些环形缓冲区数据的状态快照比对、DNA/RNA序列的环状基因组比对虽然真实场景中序列比对要复杂得多但最小表示法提供了一个极其高效的预处理思路甚至是某些环形队列的一致性校验。只要数据在逻辑上是“环状”的判断两个环是否相同最小表示法就能派上用场。1.2 暴力法的效率瓶颈在哪里最容易想到的暴力做法是什么枚举起点i生成s[i..n-1] s[0..i-1]用这个新串去跟当前维护的最小串比较更新最小值。这样做的复杂度是多少外层枚举起点 O(n)内层比较两个长度为 n 的字符串最坏是 O(n)总复杂度 O(n²)。当 n 1000 时10^6 次字符比较完全没问题但当 n 10^5 时10^10 次字符比较直接爆炸。如果 n 到了 10^6 级别这种写法在真实的评测系统里是绝对跑不过的。所以我们需要一个更聪明的办法。仔细观察暴力的瓶颈在于“枚举了所有起点并且在每个起点上都做了完整的字符串比较”。那有没有可能我们不去枚举所有起点有没有可能在比较过程中一次性排除掉大量“注定不可能成为最小表示起点”的位置这就是最小表示法的核心切入点用双指针配合“跳过非法区间”的策略在线性时间内逼近最小起点。2. 核心原理深度拆解双指针是怎么“跳过”大量无效起点的2.1 破环成链统一比较的代码基础处理循环问题第一反应往往是“破环成链”。把长度为 n 的字符串复制一份拼在后面得到长度为 2n 的串我们记为T s s。这样一来原串s的第i个循环同构串恰好等于T中以位置i开头、长度为n的子串。也就是说我们只需要在T上比较“从各种位置开始、长度固定为 n 的子串”就能找到最小循环同构的起点。这个技巧本身不复杂但它把“循环区间”的边界问题彻底消解了。我们不用去写什么(i k) % n的判断直接用T[(ik) % (2*n)]或者直接预处理出一个 2n 长度的数组就能访问到对应字符边界判断变得非常清爽。2.2 指针 i、j、k 的语义定义清楚才能避免后面绕晕最小表示法的核心代码不长但里面的三个指针如果语义没搞明白很容易看一次忘一次或者一紧张就写错。我们直接看经典实现思路定义i当前认为可能是最小表示起点的第一个指针j当前认为可能是最小表示起点的第二个指针k从i和j这两个位置开始目前能匹配上的字符个数。初始状态i 0j 1两者分别指向两个候选起点。随后不断比较s[i k]和s[j k]如果两者相等说明从这两个位置出发的前缀一致k继续往后比如果s[i k] s[j k]说明从i开始的串不如从j开始的串优秀那么i这个起点可以放弃了让i跳到i k 1如果s[i k] s[j k]说明从j开始的串不如从i开始的串优秀那么j这个起点可以放弃了让j跳到j k 1一旦i和j相等为了避免两个指针指向同一个起点导致无限循环将j或者i取决于具体实现习惯当i或j有一个越过了n循环结束。此时min(i, j)就是最小表示的起点。关键来了为什么当s[ik] s[jk]时可以放心地把i跳到ik1为什么中间这些位置i1, i2, ..., ik都不用看了2.3 为什么可以一次性跳过 k 个位置势能分析这是整个算法最精髓也最反直觉的地方。我们先说结论如果s[ik] s[jk]那么从i到ik之间的任意一个位置p作为起点都不可能是最小表示。为什么我们来证明一下。假设存在一个位置p满足i p ik我们考虑从p开始的循环同构串。因为s[i..ik-1] s[j..jk-1]前 k 个字符相等所以我们可以把s[i..ik]这一段的字符映射到s[j..jk]上。具体来说对于任意0 t k - (p-i)有s[p t] s[j (p - i) t]。这说明从p开始的字符串它的前(ik) - p 1个字符与从j (p-i)开始的字符串完全一致。而在第(ik) - p 1个字符处前者对应的是s[ik]后者对应的是s[jk]。而我们已经知道s[ik] s[jk]所以在字典序上从p开始的串严格大于从j (p-i)开始的串。换句话说p在到达与j侧对应位置的那个“分岔点”时已经输掉了比较。既然存在一个比它字典序更小的起点就是j (p-i)这个位置那p无论如何都不可能是全局最小值。所以从i到ik这一整段全部可以安全丢弃。这就是为什么i可以直接跳到i k 1的原因。这个证明看着有点绕我当初自己推的时候也花了些时间。但弄明白这一点整个算法就通透了它本质上是在利用“已经比较过的相等前缀”把多个起点一次性“打包淘汰”掉而不是一个个去比。2.4 为什么总复杂度是 O(n)每个字符最多被比较几次很多人看完代码会疑惑k不是会重复归零吗那最坏情况下i和j会不会来回跳动导致比较次数超过 O(n)答案是不会。这里的关键在于指针的移动是单调的。具体来说每次发生“跳转”要么是i跳到i k 1要么是j跳到j k 1而k在跳转后被重置为 0i和j始终不超过n并且在单次比较过程中i j k的总和是单调递增的。更直观的势能理解是每一次字符比较都会消耗一个“比较长度”如果这个比较导致k增加那就说明我们在一个长公共前缀上探测如果比较失败导致跳转那i j的值至少增加k 1。因为i j不超过2nk从 0 到 n 最多增长一次后被重置所以总比较次数是 O(n) 级别的。这个“每个指针都只向前移动从不回头”的特性是线性复杂度的根基。理解这一点比死记硬背复杂度结论重要得多。3. 完整实现与代码细节解析3.1 标准 C 模板十几行搞定核心逻辑先给出一份我实测过多次、稳妥可靠的 C 实现。我习惯把“破环成链”用取模的方式直接处理这样可以省掉额外开一个 2n 数组的空间代码也更紧凑。// 返回字符串 s 的最小表示起点下标0-based int minimalRotation(string s) { int n s.size(); int i 0, j 1, k 0; while (i n j n k n) { char a s[(i k) % n]; char b s[(j k) % n]; if (a b) { k; } else if (a b) { i i k 1; if (i j) i; k 0; } else { j j k 1; if (i j) j; k 0; } } int pos min(i, j); return pos; }这份代码里有个容易漏掉的细节当i j时如果直接继续比较会发现a和b永远相等k一路增加到 n然后循环结束得到的答案是 i或者 j但这是错误的因为两个指针指向同一个起点并没有意义。所以在跳转后如果发现两个指针重合必须手动把其中一个往后挪一位。3.2 边界情况与防坑细节这些坑我都替你踩过了第一空串和单字符串。n 0 或 n 1 时答案直接是 0。我的模板里没有显式处理因为 n 1 时i 0j 1j n不成立循环直接跳过返回min(0, 1) 0正确。但 n 0 时会出问题i 0j 1j n为 false返回min(0, 1) 0也正确。不过最好还是显式加上if (n 1) return 0;避免将来在更长代码里引起误读。第二所有字符都相同的情况比如aaaaa。这个情况会一路k直到k n循环退出返回min(i, j)。因为 i0, j1返回 0完全正确。但有些变体写法里当k n时直接返回min(i, j)的逻辑要小心如果之前改成过i j 1之类的操作返回值可能会不一样。我的建议是保持经典模板不要在中途“优化”掉k n的判断。第三在 while 条件中加入k n。我之前看到不少版本写的是while (i n j n)然后里面等 k 跑到 n 时再额外判断。虽然在跳转存在的情况下k很少会真的达到 n但在全等串或几乎全等串的场景下k达到 n 会导致访问s[(in)%n]越界吗不会因为取模了。但逻辑上会进入死循环吗也不会因为最终会推出循环。不过不同写法之间的细微差别恰恰是 bug 高发区。我建议在条件里就写清楚k n让边界自然收敛。第四比较的是字符而不是整体子串。这个算法从头到尾都是依次比较单个字符并没有调用过substr、compare之类的函数。如果你在实现时用了字符串切片或者string.compare(i, n, other, j, n)之类的函数那复杂度就不是 O(n) 了——虽然字符串库的 compare 可能做了底层优化但最坏情况下仍是 O(n) 一次。写最小表示法一定要“纯手工比较”。3.3 为什么我不建议用后缀数组/后缀自动机替代它也许有人会问我能不能用后缀数组SA或者后缀自动机SAM直接求最小表示当然可以。后文我也会详细讲它们之间的对比。但从工程和竞赛的角度看大多数场景下不需要杀鸡用牛刀。后缀数组的构建复杂度是 O(n log n)倍增法或 O(n)DC3 等高级算法但代码量是几十行甚至上百行而且还需要配套 height 数组、rank 数组等各种概念重得很。后缀自动机则更重量级构建和理解的成本都不低。最小表示法只用三个指针加一个 while 循环十几行代码O(n) 时间O(1) 额外空间没有任何预处理的负担。在“只需要最小表示”这个单一需求下它是绝对的最优解。当然如果你的需求不止是“找最小起点”而是“排名”、“求所有循环同构串的 rank”、“求两个串的最长公共前缀”等等那后缀数组和 LCP 相关技术才是更合适的选择。工具是用来解决问题的不是用来攀比炫技的。明白每个工具的边界比懂得更多工具更重要。4. 应用场景全解析从竞赛题到真实项目4.1 场景一判断两条项链/环形序列是否等价这是我们最早提到的场景也是最小表示法最直接的应用。给定两个长度为 n 的字符串s和t问它们是否互为循环同构。做法是求s的最小表示起点pos_s得到最小串s_min求t的最小表示起点pos_t得到最小串t_min比较s_min与t_min相等则同构否则不同构。这里有个小技巧为了得到s_min不需要真正去构造一个新字符串。你可以直接比较从pos_s开始的循环串和从pos_t开始的循环串一个循环 n 次逐个字符比较即可。这个场景让我想到了一个有趣的现象很多人会把“最小表示法”和“字符串哈希”搞混。其实哈希也能做同构判断对每个起点计算一个循环子串的哈希值然后比较任意哈希值是否相等。但哈希有两个问题一是碰撞风险虽然概率低但题目刻意构造卡数据时是会炸的二是计算所有起点的哈希要 O(n) 时间底层是预处理前缀哈希倒也不难。但最小表示法给出的是确定性的结果不需要考虑碰撞在严谨性上更胜一筹。4.2 场景二找旋转数组/环形序列中的字典序最小排列假设你有一个整数数组比如[4, 5, 1, 2, 3]你可以把它想象成一个环形数组从任意位置开始顺时针读取会得到若干种排列。问这些排列中字典序最小的那个是从原数组的哪个位置开始读的这正是最小表示法的标准应用场景。你只需要把数组里的整数当作“字符”来比较大小即可。注意如果你的数组元素不是字符而是一个结构体对象你需要定义一个严格的比较规则小于关系然后照搬模板即可。我实际经历的一个案例是某次优化一个日志轮转系统系统里记录了一个环形队列的当前状态快照多个副本之间需要校验状态是否一致。因为队列的“当前读指针”不同直接比较首元素会误判。最粗暴的做法是把队列展开成数组然后逐段匹配但这样复杂度是 O(n²)。后来我意识到这就是一个“环形矩阵同构”问题直接用最小表示法把队列状态归一化到一个唯一的最小序列再比较这个序列效率直接拉满。那次经历让我对这个算法的实用价值有了更深的体会。4.3 场景三与二分答案、字符串匹配等技巧的联动在某些算法题里最小表示法不只是终点而是预处理的一环。举个例子给你一个字符串求它最长的“双倍循环子串”——即某个位置切开后两半是循环同构的。这类问题的经典解法可能用到文本搜索、回文自动机等结构但在某些特殊限制下你可以枚举中点然后应用最小表示法判断两侧的序列是否同构。再比如某些涉及“字符串移位后匹配”的问题你可以先把原串的最小表示求出来然后在这个最小表示上进行 KMP 或字符串哈希匹配。这样就把“环形文本”上的匹配问题转化为“线性文本”上的匹配问题极大地降低了思考难度。4.4 三种常见旋转问题算法的选型对比在处理“循环串/旋转串”时经常和最小表示法竞争的方法还有哈希法和后缀数组法。我整理了一张选型表给正在纠结的你参考需求最小表示法字符串哈希后缀数组求最小表示起点O(n)代码 10 行可做但要枚举所有起点后比较可做但重判断两个串是否循环同构O(n)确定性结果O(n)有碰撞风险可做复杂度 O(n log n) 起步求任意两个起点的 LCP不行可以配合二分长度可以height RMQ求所有循环同构串的 rank不行比较困难可以SA 直接给出额外空间O(1)O(n)O(n) 以上可以看到最小表示法的定位非常精准当你的需求恰好是“最小”或“等价判断”这两个点时它几乎是无敌的但如果你还需要更丰富的信息那就得考虑其他结构了。5. 手动走一遍过程彻底吃透执行细节5.1 一个普通串的完整走查光说不练假把式。我们拿s cba来手动模拟一遍。初始化i 0, j 1, k 0n 3。第一轮比较s[(00)%3] cs[(10)%3] bc b所以i跳到i k 1 0 0 1 1。此时i j 1需要处理冲突ii 2。k 0。第二轮比较i 2, j 1, k 0s[(20)%3] as[(10)%3] ba b所以j跳到j k 1 1 0 1 2。此时i j 2jj 3。k 0。循环条件i n (2 3)为真但j n (3 3)为假循环退出。返回min(2, 3) 2。正确答案验证s的循环同构串有cba,bac,acb最小的是acb从位置 2 开始。正确。这个例子不算复杂但展示了一个很微妙的点i和j在跳转后可能会相等。很多初学实现的人在这里会卡住有的直接不管有的写if (i j) i但方向不对。我的建议是在跳转后统一判断并且习惯于让那个刚刚被跳过、指向旧位置的指针多走一步而不是固定让 i 或 j 动。5.2 一个“同字符开头”的复杂串走查我们再看一个稍微隐蔽一点的例子s ababa。这里有很多重叠的相等前缀最容易让人疑惑“为什么能跳”。初始化i 0, j 1, k 0。第一轮s[0] as[1] ba b。于是j 1 0 1 2。k 0。第二轮s[0] as[2] a相等k 1。比较s[1] b与s[3] b相等k 2。比较s[2] a与s[4] a相等k 3。注意此时k n 5吗不k 3还没到 5但k已经是n - 2了。继续比较s[3] b与s[5 % 5 0] ab a。于是i 0 3 1 4。i jj 2不相等。k 0。第三轮i 4, j 2, k 0s[4] as[2] a相等k 1比较s[(41)%5] b与s[(21)%5] b相等k 2比较s[(42)%5] a与s[(22)%5] a相等k 3继续s[(43)%5] b与s[(23)%5] ab a于是i 4 3 1 8已经 n 5循环直接退出。返回min(i, j) min(8, 2) 2。正确答案验证ababa的循环同构串中从位置 0 开始是ababa从位置 1 开始是babaa从位置 2 开始是abaab从位置 3 开始是baaba从位置 4 开始是aabab。字典序最小的是aabab从位置 4 开始咦但我们算出来是 2最小的是abaab吗等一下我手动校验一下aabab和abaab哪个更小字典序逐个比较第一个字符都是 a第二个字符aabab是 aabaab是 b。a b所以aabab更小。那么正确答案应该是从位置 4 开始。可我上面手动模拟到第三轮时i 跳到了 8返回 min(8, 2) 2这是不是说明算法出错了别急这里我需要更仔细地检查。第三轮从i4, j2开始比较过程为什么会得到i 4 k 1我们一步步来k0比较s[4]a与s[2]a相等k1。k1比较s[0]a与s[3]ba b注意这里不是相等而是小于按照算法当a b时被淘汰的是 j 而不是 i。所以正确操作是j 2 1 1 4并且因为i jjj 5。然后j n循环退出返回min(i, j) min(4, 5) 4。我前面模拟时把s[(21)%5]s[3]b和s[(41)%5]s[0]a的比较方向弄反了。正确的返回是 4与我手算的最小起点一致。所以这恰恰说明了在具体模拟时比较符号的方向一定不能搞错和分别对应淘汰 i 和淘汰 j一旦混淆结果会直接错乱。初学者尤其容易在“到底是 a b 淘汰 i 还是淘汰 j”上绕晕。我的记忆口诀是谁大谁出局。因为我们要找的是“最小”比到大的那一方它的候选起点就被淘汰了。5.3 全相同串与周期性串的处理为什么它天然免疫卡数据全相同串如aaaa前面已经提过会一路 k 到 n然后退出返回 0正确。周期性串如ababab其实是多个相同循环块拼接而成。这种串的循环同构集合中真正的不同串远少于 n 个比如ababab的不同循环同构只有 2 种ababab和bababa。最小表示法在遇到周期性串时算法依然能正确返回最小起点而且不会死循环因为它手上仍然有 i 和 j 两个候选最终必然能通过一轮轮的“跳转”收敛到正确答案。我之前在网上见过有人担心“周期性串会导致 k 一直相等然后 k 每次都跳到 n却因为 ij 被反复调整最后死循环”。其实不会因为 kn 时while 条件中的k n已经为 false循环就会退出。但如果你把 while 条件写成了while (true)或者while (i n j n)而不检查 k那确实可能在k n时再次进入循环然后因为a b一直相等k继续增加直到 int 溢出或越界访问。所以while 条件里必须带k n这是防死循环的关键。6. 最小表示法的变体与相似问题6.1 最大表示法简单改动思路完全一致有最小表示就有最大表示。在某些题目里比如“字典序最大的旋转串”我们需要求的是最大字典序的循环同构串。实现非常简单只要把比较符号反过来即可。原本a b时淘汰 i现在改为a b时淘汰 i或者更直接一点把所有字符取反后求最小表示然后再映射回去。但我不建议用字符取反的方式因为如果是整数数组取反还得考虑负值比较繁琐。直接在比较逻辑里把和对调是最省事的。6.2 两个字符串是否旋转相等的快速判断不用哈希这个方法其实已经出现过求两个串的最小表示然后比较。但还有一个更简洁、且完全避免构造“最小串”的姿势给定长度相等的字符串s和t判断它们是否循环同构令T s s在T中查找t是否存在。如果存在说明t是s的某个循环同构串。这个解法用到了字符串匹配比如 KMP 或 C 的string::find实际上find的实现通常是高效的 BM 变种或双指针算法复杂度 O(n)。这个方法理解起来更直观但有一个隐患如果 s 全是一个字符比如aaaaaaaa那么T中存在大量t的匹配位置find仍然能正确工作只是你需要注意它返回的是第一个匹配位置且匹配是线性的没有任何问题。但在某些自定义实现的匹配函数里如果写得不好可能在大量重叠匹配时退化到 O(n²)。所以如果追求极致的稳定性和简单性我仍然推荐最小表示法。6.3 循环数组/环状链表的统一处理思路前面说的都是字符串但算法本质上操作的是“可比较的序列”。所以在算法题里最小表示法也能用到环状数组上。我遇到过一道很有意思的题给定一个环状数组求从哪个位置开始使得“前缀和的最小值”最大或者说这个起点的所有前缀和都非负。这就要先找到环状数组的某种“极性”起点然后再套用贪心或数据结构。这里的“找起点”部分就借鉴了最小表示法的思想——但要明确这并不是标准最小表示法能直接解决的需要结合前缀和做变体。更重要的一点是最小表示法本身是基于“字典序”的如果你换了一种比较规则比如比较前缀和请勿直接照搬模板。我曾经就在一道前缀和相关的环形题里直接套模板结果答案全错后来才发现是我的比较函数不符合“字典序传递性”——而最小表示法的正确性依赖“比较关系是全序关系”这一前提。如果比较关系不满足全序比如有等价的环状排列但互相不完全相等算法行为会不可控。做变体题时务必先验证比较关系的性质。7. 实战经验我踩过的坑和总结的调试技巧7.1 用计数法和随机对拍验证你的实现每次写完最小表示法我都建议你做一个简单而有效的自测写一个暴力函数bruteMin(string s)枚举起点用substr构造新串取min再写一个minimalRotation(s)然后生成大量随机小写字母串长度从 1 到 20逐一对拍再固定构造边界全同字符、两字符交替、含 0/1/2 三种字符、长度很大的全相同串等。这个对拍习惯在竞赛和项目调试中都能帮你省下大量的时间。不要觉得暴力算法“丢人”对拍本来就是正式工程里也很常见的验证手段。我已经数不清有多少次靠对拍发现模板里一个不起眼的i写成了j。7.2 调试输出定位打印 i、j、k 和当前比较字符如果你的结果死活不对不要盯着代码看直接上调试输出。在 while 循环开头打印一行i0 j1 k0 compare s[0]a vs s[1]b a b, j jumps to 2这样就能一目了然地看到指针是如何跳动的。我发现很多人写最小表示法时脑子里的“模拟”和代码实际执行的行为不一致原因往往是跳转后ij的处理方向反了。打印调试可以帮助你迅速定位这种逻辑偏差。7.3 避免目标平台差异不要依赖字符串库的 compare最后一个小提示如果你是在 LeetCode 或牛客这种在线评测环境里跑题请务必自己实现字符比较逻辑不要直接写(s.substr(i, k) s.substr(j, k))然后用库函数比较。虽然手动写了也是比较字符但库函数substr会产生新字符串对象时间和空间开销都是 O(k)在循环里反复调用会把复杂度拉满到 O(n²) 甚至更糟。最小表示法的美就在于它只做 O(n) 次单字符比较绝不要引入额外的字符串构造。8. 最小表示法之外这类“线性跳过”思想的延展走到这里我们已经把最小表示法的原理、实现和应用场景都过了一遍。但如果你对“为什么能跳过这么多无效状态”这种思想着迷我强烈建议你再去看另外几个同样聪明、同样用“周期性/字典序跳过”的算法与数据结构。学完这一串你对算法背后“为什么是线性的”会有更深刻的理解。KMP就不用多说了它通过前缀函数跳过不可能匹配的位置。Z 算法则是从一个位置出发线性计算它与前缀的最长公共长度。Manacher用“镜像回文”跳过已经算出的半径也做到了线性求解最长的回文子串。后缀数组的构建里也大量用到了“已排序部分带来的信息减少”来加速倍增加入。最小表示法里面最深刻的一点我认为是它在没有任何预处理、没有任何额外数组的情况下仅靠比较过程中积累的信息来淘汰候选起点。把复杂问题“就地解决”这种极简主义风格放在工程里同样值得推崇——不要着急引入重工具先用问题的结构简化问题的规模。我在参加比赛和写工程代码时的体会是一个算法的价值不取决于它多复杂而取决于它在什么时候能被你用起来。最小表示法是那种“平时不显山露水一旦遇到环形比较就让你在众人中突出重围”的轻量级武器。我建议你花十几分钟把这十几行代码敲熟对拍验证一遍再找几道环形同构的题目练练手。等你真正需要用它的那一刻会发现它已经在你脑子里待命许久了。最后分享一个小技巧如果你某天突然忘记了模板但又记得“用 i、j 两个指针比较、谁大谁淘汰”这句话并且临时写不出完整的证明那就先写暴力再用随机对拍验证自己的记忆版模板——哪怕侥幸通过了对拍也是可以的算法最终是要落在“可靠”二字上。希望这篇关于最小表示法的文章能帮到你也期待你把它用到真正需要它的地方去。