相关概念:
- 在介绍后续算法前,首先要理解循环同构的概念。
- 定义太拗口了,可以直接看例子:字符串
abcd
的循环同构有bcda
、cdab
和dabc
。
- 定义太拗口了,可以直接看例子:字符串
- 最小表示法是用来解决字符串最小表示问题的方法。
- 而字符串的最小表示指的是字符串和它的所有循环同构中字典序最小的字符串。
- 算法最后只需要返回循环同构中字典序最小的字符串的起始位置即可。
核心思路:
- 可以把字符串看作是可循环的字符串,通常循环串的题目,都可以对循环串进行相应的处理:复制一倍。
- 如字符串
abcd
复制一倍即变成abcdabcd
,如此在新的字符串中就可以访问到原字符串的所有循环同构。 - 实际代码中不需要真的开辟两倍的数组空间,只需要通过取模运算进行访问数组即可。【假设访问循环串
S
的当前索引为i
,而i > n
,其中n
为字符串大小,则可以通过S[i%n]
来访问到对应字符】
- 如字符串
- 最小表示法的暴力解法很直观,因为已经进行复制一倍的操作,所以循环字符串的所有可能性都可以线性访问。
- 则可以维护三个变量
i
、j
和k
,i
和j
表示两个字符串的起始索引,k
表示两个字符串同步访问的距离。 - 暴力解法只需要以
O(n)
的时间复杂度遍历所有的两对字符串,再在每一次遍历中以O(n)
的时间复杂度对比两个字符串的字典序,对比完成后保留字典序更小的字符串起始位置即可(另一个位置向后移动)。
- 则可以维护三个变量
- 可以从具体例子中理解暴力法的遍历过程:
-
假设字符串
s
为abcd
,字符串长度n = 4
,且初始化i = 0
、j = 1
和k = 0
:
-
移动
k
之后,此时i = 0
、j = 1
和k = 1
:
-
在上一步的比较结束后,保留字典序更小的字符串的起始位置
i
,而将第二个字符串的起始位置j
向后移动,此时需要进行下一次比较,因而重置k = 0
:
-
后续的遍历同理,所有遍历完之后返回
min(i, j)
即可。
-
- 上述暴力解法其实是
O(n^2)
的时间复杂度,但是整个过程中有些比较可以避免,经过优化可以使得时间复杂度降为O(n)
:-
考虑以下的情况,字符串
s
为acacab
,且i = 0
、j = 2
和k = 3
,在本次比较完成后对i
更新为i + k + 1
以进行下一次比较(而在暴力法中会将i
更新为i + 1
):
-
优化的关键在于此:当以
i
为起始位置字符串的字典序更大时,会将i
更新为i + k + 1
,因为所有以i + p
为起始位置的字符串均无需再进行比较(p
的取值范围为[0, k]
)。- 以
i + p
为起始位置的字符串,一定存在以j + p
为起始位置的字符串比它更优。
- 以
-
事实上还避免了很多其他情况,但上述情况已经可以说明优化后解法的正确性。
-
算法步骤:
- 优化后解法步骤如下:
- 将字符串复制一倍。【此操作非必要】
- 初始化指针
i = 0
、j = 1
,初始化匹配长度k = 0
。 - 根据匹配位置
k
进行比较,根据比较结果移动相应指针,同时重置匹配长度k
;若指针移动后,两指针相同,则移动指针j
。 - 重复上一步骤直到比较结束。
- 最后的结果为
min(i, j)
。【返回结果是字符串的起始索引】
代码实现:
int minimal_representation(string& s)
{
int n = s.size();
int k = 0, i = 0, j = 1;
while (k < n && i < n && j < n)
{
if (s[(i + k) % n] == s[(j + k) % n]) ++k;
else
{
s[(i + k) % n] > s[(j + k) % n] ? i = i + k + 1 : j = j + k + 1;
if (i == j) j++;
k = 0;
}
}
i = min(i, j);
return i;
}
部分题目:
- leetcode - 796.旋转字符串【比较两个字符串的最小表示结果是否相等】
- leetcode - 899.有序队列