最小表示法:高效解决循环字符串字典序最小表示问题

发布时间:2026/8/11 3:30:04
最小表示法:高效解决循环字符串字典序最小表示问题 这次我们来看一个在力扣周赛 511 中出现的算法问题——“最小表示法”。对于参加算法竞赛或准备技术面试的同学来说这是一个非常经典且实用的字符串处理技巧。它解决的问题是对于一个循环字符串如何找到其所有循环同构字符串中字典序最小的那个作为其“最小表示”。这个算法本身并不复杂但其在字符串匹配、循环数组处理等场景下的高效性使其成为算法工具箱里的一个利器。本文的重点不是空谈概念而是直接告诉你这个算法能不能用、怎么用、以及如何高效地应用到力扣等平台的题目中。我们会从核心思想讲起逐步拆解算法步骤并用清晰的代码实现和力扣周赛 511 的实战题目作为验证。无论你是想快速掌握这个算法应对比赛还是希望在面试中脱颖而出这篇文章都能提供直接的帮助。1. 核心能力速览能力项说明算法名称最小表示法 (Minimum Representation / Lexicographically Smallest Rotation)核心功能寻找一个字符串或数组所有循环移位中字典序最小的那个表示。时间复杂度O(n)其中 n 为字符串长度。远优于朴素的 O(n²) 比较方法。空间复杂度O(1) 或 O(n)取决于实现通常为 O(1) 额外空间。适用数据结构字符串、数组可视为循环结构。典型应用场景力扣周赛题目、字符串循环同构判断、循环数组的最小表示问题、某些字符串匹配的预处理。前置知识要求基本的字符串操作、双指针快慢指针思想。是否支持“批量”任务算法本身针对单个输入。但可封装成函数用于处理多个字符串或作为更复杂算法的一部分。2. 适用场景与使用边界最小表示法主要适用于需要处理循环同构数据的场景。它最适合谁算法竞赛选手在力扣周赛、Codeforces、AtCoder 等平台中遇到涉及循环字符串/数组最小表示或判等的题目时此算法是标准解法。面试备考者字符串和数组的循环处理是高频面试题点掌握最小表示法能展现你的算法优化能力。需要处理周期性数据开发者例如在生物信息学DNA序列、数据压缩或某些模式识别中寻找循环序列的规范形式。它能解决什么问题寻找最小循环表示给定字符串s求其所有循环移位s[i:] s[:i]中字典序最小的一个。判断循环同构两个字符串是否互为循环移位。只需比较它们的最小表示是否相同即可。解决力扣特定问题如周赛 511 中的相关问题本质是寻找最小表示或基于其进行构造。它的使用边界与限制仅适用于全序关系算法依赖于字典序比较要求元素间可以比较大小如字符、数字。处理对象是离散序列对链表等非随机访问结构不友好通常需要先转化为数组。不直接解决最长回文子串等问题虽然同属字符串领域但解决的问题不同不要混淆。3. 环境准备与前置条件学习并实现最小表示法你只需要一个能运行代码的编程环境对硬件无特殊要求。通用环境清单操作系统Windows, macOS, Linux 均可。编程语言以 Python、Java、C 为主进行演示因其在算法竞赛中最常用。开发工具任意代码编辑器VS Code, PyCharm或在线判题平台力扣 Playground。核心依赖无第三方库依赖仅使用语言标准库。思维准备理解双指针i,j和比较逻辑是关键。4. 算法原理与步骤拆解最小表示法的核心思想是双指针竞争淘汰。我们使用两个指针i和j分别指向当前待比较的两个候选起始位置k表示从这两个位置开始已匹配的长度。算法步骤如下初始化令i 0,j 1,k 0。n为字符串长度。比较与推进在k n的循环中比较s[(ik) % n]和s[(jk) % n]。如果相等说明到目前为止两个候选序列相同k继续比较下一位。如果不相等若s[(ik) % n] s[(jk) % n]说明从i开始的表示不会是最小的因为从j开始的在当前位更小。那么我们可以直接将i跳到i k 1。同时如果跳完后i j需要让i以保证两个指针不同。若s[(ik) % n] s[(jk) % n]同理说明从j开始的表示不会是最小的。将j跳到j k 1。同样若跳后j i则j。无论哪种情况发生跳转都将k重置为0重新开始比较。决出胜者循环结束后i和j中的较小者即为最小表示的起始下标。即min(i, j)。为什么是 O(n) 复杂度每次比较 (k) 或指针跳跃 (i i k 1) 都会至少消耗掉一个未比较的字符且每个字符最多被比较两次因此总时间复杂度为线性。5. 代码实现与功能验证我们将用 Python、Java 和 C 分别实现最小表示法函数并针对力扣周赛 511 的典型题目场景进行测试。5.1 Python 实现def minimum_representation(s: str) - int: 返回字符串 s 的最小表示的起始索引。 n len(s) if n 0: return 0 i, j, k 0, 1, 0 while i n and j n and k n: a s[(i k) % n] b s[(j k) % n] if a b: k 1 else: if a b: i k 1 else: # a b j k 1 if i j: i 1 k 0 return min(i, j) def get_min_representation(s: str) - str: 返回字符串 s 的最小表示字符串。 idx minimum_representation(s) n len(s) return s[idx:] s[:idx] # 功能验证测试 if __name__ __main__: test_cases [ bcab, abca, aaaa, cba, ] for s in test_cases: idx minimum_representation(s) min_rep get_min_representation(s) print(f字符串: {s} - 最小表示起始索引: {idx}, 最小表示: {min_rep})预期输出字符串: bcab - 最小表示起始索引: 1, 最小表示: abbc 字符串: abca - 最小表示起始索引: 3, 最小表示: aabc 字符串: aaaa - 最小表示起始索引: 0, 最小表示: aaaa 字符串: cba - 最小表示起始索引: 2, 最小表示: abc 字符串: - 最小表示起始索引: 0, 最小表示: 5.2 Java 实现public class MinimumRepresentation { public static int minimumRepresentation(String s) { int n s.length(); if (n 0) return 0; int i 0, j 1, k 0; while (i n j n k n) { char a s.charAt((i k) % n); char b s.charAt((j k) % n); if (a b) { k; } else { if (a b) { i k 1; } else { // a b j k 1; } if (i j) { i; } k 0; } } return Math.min(i, j); } public static String getMinRepresentation(String s) { int idx minimumRepresentation(s); int n s.length(); return s.substring(idx) s.substring(0, idx); } public static void main(String[] args) { String[] testCases {bcab, abca, aaaa, cba, }; for (String s : testCases) { int idx minimumRepresentation(s); String minRep getMinRepresentation(s); System.out.printf(字符串: %s - 最小表示起始索引: %d, 最小表示: %s%n, s, idx, minRep); } } }5.3 C 实现#include iostream #include string #include algorithm using namespace std; int minimumRepresentation(const string s) { int n s.size(); if (n 0) return 0; 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 k 1; } else { // a b j k 1; } if (i j) i; k 0; } } return min(i, j); } string getMinRepresentation(const string s) { int idx minimumRepresentation(s); int n s.size(); if (n 0) return ; return s.substr(idx) s.substr(0, idx); } int main() { string testCases[] {bcab, abca, aaaa, cba, }; for (const string s : testCases) { int idx minimumRepresentation(s); string minRep getMinRepresentation(s); cout 字符串: s - 最小表示起始索引: idx , 最小表示: minRep endl; } return 0; }判断成功的标准对于已知最小表示的字符串如bcab的最小表示为abbc算法能正确输出起始索引和字符串。算法处理空字符串和全相同字符字符串时不会出错。时间复杂度感知对于长字符串如长度 10^5算法应瞬间完成而朴素 O(n²) 算法会超时。6. 力扣周赛 511 实战应用分析虽然无法获取周赛 511 的历史原题但基于“最小表示法”这个关键词我们可以推断并构造一类典型的题目。这类题目通常不会直接要求你实现最小表示法而是将其作为解题的关键步骤。假设题目场景给你一个字符串s你可以进行任意次操作将s的第一个字符移动到末尾。请找出经过若干次操作后能得到的字典序最小的字符串。解题思路这直接等价于求字符串s的最小表示。调用上面实现的getMinRepresentation(s)函数即可得到答案。进阶题目场景更贴合周赛难度给你两个字符串s和t。判断t是否可以通过将s进行循环移位得到。解题思路如果s和t长度不等直接返回false。将s复制一份连接到自身得到s2 s s。在s2中寻找子串t是否存在使用 KMP 或内置find函数。因为s2包含了s的所有循环移位。或者更优雅地分别求出s和t的最小表示然后比较它们是否相等。如果相等则t是s的循环移位。代码示例Python使用最小表示法判断循环同构def is_cyclic_rotation(s: str, t: str) - bool: 判断 t 是否是 s 的循环移位。 if len(s) ! len(t): return False # 方法1: 使用字符串查找 (O(n)) # return t in (s s) # 方法2: 使用最小表示法 (O(n)) def min_rep(x): n len(x) i, j, k 0, 1, 0 while i n and j n and k n: a x[(ik)%n] b x[(jk)%n] if a b: k 1 else: if a b: i k 1 else: j k 1 if i j: i 1 k 0 return x[min(i, j):] x[:min(i, j)] return min_rep(s) min_rep(t) # 测试 print(is_cyclic_rotation(abcde, cdeab)) # True print(is_cyclic_rotation(abcde, abced)) # False7. 性能分析与边界情况处理时间复杂度O(n)如前所述每个字符最多被比较两次。空间复杂度O(1)仅使用了几个整型变量。边界情况与处理空字符串在函数开头判断长度直接返回 0 或空字符串。全相同字符算法能正确处理指针会逐步移动最终返回索引 0。单字符字符串同样能正确处理。指针越界循环条件while i n and j n and k n确保了指针在有效范围内。当i或j跳跃后可能超过n循环会终止并通过min(i, j)返回有效索引因为至少有一个指针在范围内。大整数运算在 C 或 Java 中i k 1可能导致临时值超出int范围吗对于字符串长度 n 在 int 范围内通常 ≤ 10^5 到 10^6i和k也在此范围相加不会溢出 32 位 int 上限约21亿。但若 n 极大应考虑使用long long(C) 或long(Java)。8. 常见问题与排查方法在实现和应用最小表示法时可能会遇到以下问题问题现象可能原因排查方式解决方案输出结果不正确对于简单用例也错误。1. 指针跳跃逻辑写反和判断错误。2. 取模运算(ik) % n写错或遗漏。3. 重置k0的时机或位置错误。使用print或调试器跟踪i,j,k以及当前比较字符a,b在每一步的变化。用bcab等小例子手动模拟。仔细对照本文给出的算法步骤和代码逐行检查。确保在字符不等时较大的那个指针被跳跃。算法陷入死循环。1. 循环条件不完整未能覆盖所有终止情况。2. 指针跳跃后未处理i j的情况导致两指针停滞。检查while循环条件是否为i n and j n and k n。在指针跳跃后立即检查并处理if i j: i或j。确保循环条件正确并严格添加相等指针的修正逻辑。对于某些输入返回的索引不是最小的。算法实现存在逻辑漏洞未能正确淘汰非最小候选。构造反例测试例如随机生成字符串与朴素的 O(n²) 解法结果对比。回归算法本质当s[(ik)] s[(jk)]时说明从i开始的串在k位置已经比从j开始的串“大”因此i到ik之间的所有位置都不可能成为最小表示的起点可以直接跳到ik1。处理空字符串时程序崩溃。未对输入长度为 0 的情况进行判断导致取模或访问字符出错。在函数入口添加if n 0: return 0或类似判断。增加边界条件检查。在力扣题目中提交超时。1. 错误地实现了 O(n²) 的朴素算法。2. 在循环内部进行了不必要的字符串拼接或复制。确认你的算法是否是本文所述的双指针 O(n) 算法。检查在比较时是否直接通过索引访问字符而不是生成子串。使用本文的标准实现避免在关键循环中进行任何 O(n) 的字符串操作。9. 最佳实践与使用建议封装成工具函数将minimum_representation函数封装好在解决力扣问题时直接调用避免现场推导出错。理解而非记忆掌握“双指针竞争淘汰”和“前缀相等则增长k不等则跳跃指针”的核心思想这样即使忘记代码细节也能重新推导。与 KMP 结合记忆最小表示法的指针跳跃思想与 KMP 算法中 next 数组的构建有相似之处都是利用已匹配的信息避免回溯。可以对比学习。测试用例覆盖常规用例”bcab“,”abca“边界用例空串””单字符”a“全相同字符”aaaa“随机大用例生成长度 10000 的随机字符串与朴素算法对比结果和耗时。应用于数组该算法同样适用于整数数组或其他可比较元素的数组只需将字符比较改为元素比较即可。注意字典序定义确保你理解的字典序lexicographical order与题目要求一致通常是基于字符的 ASCII 码或 Unicode 码点。10. 总结与下一步最小表示法是一个高效、优雅的字符串算法它将寻找循环串最小表示的时间复杂度从 O(n²) 优化到了 O(n)。其核心在于通过双指针的竞争和跳跃避免了大量不必要的比较。最值得掌握的点算法模板i, j, k三个变量的初始化、比较、跳跃和重置的逻辑建议熟练到能默写。问题识别在题目中看到“循环移位”、“旋转字符串”、“字典序最小”等关键词应立刻联想到此算法。复杂度优势明确其 O(n) 的线性复杂度这是解决大规模数据问题的关键。最先应该验证的功能使用”bcab“这个经典例子手动模拟或运行代码确认算法能正确输出起始索引1和最小表示”abbc“。这是检验实现是否正确的最快方法。最容易踩的坑指针跳跃的方向哪个指针该跳和跳跃后处理i j的情况是代码中最容易出错的两个地方。务必对照本文的代码逻辑仔细检查。后续扩展方向最大表示法稍作修改将比较条件从寻找“最小”改为寻找“最大”即可解决寻找字典序最大循环表示的问题。扩展至二维思考在二维矩阵的循环旋转中如何寻找最小表示这是一个更复杂的挑战。结合其他算法在更复杂的字符串问题中最小表示法可能作为预处理步骤结合后缀数组、哈希等数据结构使用。建议将本文的代码模板收藏在下次力扣周赛或面试遇到相关题目时可以直接应用。理解其原理后这个算法将成为你解决循环字符串问题的得力工具。