最小表示法
基础问题
对于一个长度为n的字符串s
,我们把它首尾相连形成一个环,然后在任一位置断开得到的字符串t
与s
循环同构。
从某一位置开始循环读n位
t
=
s
[
i
.
.
.
n
]
+
s
[
1...
i
−
1
]
,
1
≤
i
≤
n
t = s[i...n] + s[1...i-1], 1 \le i \le n
t=s[i...n]+s[1...i−1],1≤i≤n
上面的+
指的是字符串拼接
在这n
个与s
循环同构的字符串中,字典序最小的那个称为s
的最小表示
-
暴力做法
考虑最暴力的做法,我们把这
n
个与s
循环同构的字符串都求出来,然后选个字典序最小的就可以啦两个字符串进行比较的时间复杂度是 O ( n ) O(n) O(n), 要求出字典序最小的, 则需要比
n
次, 所以总的时间复杂度为: O ( n 2 ) O(n^2) O(n2) -
稍微优化一下
我们可以将s 复制一遍加到原串后面,这时与s 循环同构的字符串都是新串的长度为n的子串, 每次只看n位
仅仅优化了字符串表示, 但时间复杂度与上面暴力一样
-
使用二分+哈希,我们可以做到 O ( n l o g n ) O(nlog n) O(nlogn)
线性算法
线性的时间复杂度解决最小表示法
我们用两个指针 i, j
,分别指向到目前为止两个可能是答案串的串的起始位置
初始 i = 1, j = 2
,随着算法进行两者逐步增大。
复制扩展后的字符串(s + s)中的一个位置即确定了一个最小表示
假设现在i < j
,要求k最大, 并且从 i
开始的 k
位字符和从 j
开始的 k
位字符是一样的, 即:
s
[
i
.
.
.
(
i
+
k
−
1
)
]
=
s
[
j
.
.
(
j
+
k
−
1
)
]
(
k
≤
n
)
s [i... (i + k - 1)]= s[j ..(j + k - 1)] (k \le n)
s[i...(i+k−1)]=s[j..(j+k−1)](k≤n)
如果s[i + k] != s[j + k]
,我们可以得到哪些信息?
在这里插入图片描述
如果s[i + k] > s[j + k]
:
- 位置 i 显然不可能是最终答案了( i 开始的第 k 位比从 j 开始的第 k 位大)
- 注意
s[i ... i + k - 1]
和s[i... j + k - 1]
完全相等,且s[i+k] > s[j+k]
,那么[i, i + k]
都不可能是答案, 从[i, i + k]
开始的串在[j, j + k]
中对应的位置都比它字典序小, 因为它们都会包含相同位置的s[i + k]
和s[j + k]
- 所以
i
可以直接挪到i + k + 1
的位置,注意此时i
可能会大于等于j
,两者相等时(i == j
)我们可以随便选择一个指针把它向后挪一位(两个指针在同一位置则无法排除一段)
如果s[i + k] < s[j + k]
:
- 此时
j
的情况与上面的i
的情况一样 - 此时 j 位置不可能是最终答案了, j 指针需要往后挪
- j 到 j + k 都不可能是答案,j 可以直接挪到
j+k+1
的位置 j
向后移动后仍会大于i
如果s[i + 1] == s[j + 1]
- 使其中一个后挪一位, 这里我们让
j
向后移动一个位置
当i, j
两个指针中有一个的位置大于n的时候算法结束
此时仍然留在字符串范围内([1, n]
)的位置所对应的循环同构最是字符串的最小表示
我们会保证算法执行过程中i
始终小于j
核心思想
-
每次排除一段不可能为循环同构起点的位置
-
时间复杂度
注意到每次k的值增加的时候,i或j会向后移动相应的步数。i和j最多向后移动O(n)步,所以算法的时间复杂度为O(n)。
为什么
j
已经跳出n
了, 最终答案仍在n里面?因为会出现位置为
i
和位置j
相对的情况, 此时假设j
向后移动, 则最后一次两个位置相等的位置就是答案, 此时i
肯定还在
模板代码
string getMinPre(string& s) {
int n = s.size();
s += s; // 复制一份到末尾
s = "#" + s; // 1-base
int i = 1, j = 2;
while (j <= n) {
int k = 0;
// 先暴力求k
while (k < n && s[i + k] == s[j + k]) k++;
// 如果从 i 开始的第 k+1 位不相等
if (s[i + k] > s[j + k]) i += k + 1; // i向后启动k+1位
else j += k + 1;
// 移动后保证位置满足: i < j
if (i == j) j++;
if (i > j) swap(i, j);
}
string ans = s.substr(i, n);
return ans;
}
循环同构判断
给你两个字符串 a, b, 字符串均由小写字母组成,你需要判断这两个字符串是否循环同构。
是的话输出
Yes
,否则输出No
。
如果与a循环同构的字符串集合为A, 与b循环同构的字符串集合为B
如果两个集合A, B相同, 则a, b也是循环同构的
只有A, B集合中有一个字符串相同, 则整个集合就是相同的
因为不知道要比较A集合中的哪一个和B集合中的哪一个字符串, 如果暴力的匹配集合A与集合B中的字符串, 则时间复杂度为 O ( n 2 ) O(n^2) O(n2)
因为一个字符串的最小表示是唯一的, 所以只需比较最小表示是否相等就可以了
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5+5;
const int INF = 0x3f3f3f3f;
string getMinPre(string& s) {
int n = s.size();
s += s; // 复制一份到末尾
s = "#" + s; // 1-base
int i = 1, j = 2;
while (j <= n) {
int k = 0;
// 先暴力求k
while (k < n && s[i + k] == s[j + k]) k++;
// 如果从 i 开始的第 k+1 位不相等
if (s[i + k] > s[j + k]) i += k + 1; // i向后启动k+1位
else j += k + 1;
// 移动后保证位置满足: i < j
if (i == j) j++;
if (i > j) swap(i, j);
}
string ans = s.substr(i, n);
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
string a, b;
cin >> a >> b;
cout << (getMinPre(a) == getMinPre(b) ? "Yes" : "No") << endl;
return 0;
}
最小循环覆盖2
给你一个字符串 a,你需要求出这个字符串的字典序最小的最小循环覆盖。
b 是 a 的最小循环覆盖,当且仅当 a 是通过 b 复制多次并连接后得到的字符串的子串,且 b 是满足条件的字符串中长度最小的。你需要找出字典序最小的最小循环覆盖。
输入一个字符串 a,输出一个字符串表示字典序最小的最小循环覆盖。
由最小循环覆盖的定义可知, 最小循环覆盖不止有一个
最小循环覆盖的循环同构都是最小循环覆盖
先用KMP求出最小循环覆盖
然后最该最小循环覆盖求最小表示
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5+5;
const int INF = 0x3f3f3f3f;
int nxt[2 * N];
string getMinPre(string s) {
int n = s.size();
s += s; // 复制一份到末尾
s = "#" + s; // 1-base
int i = 1, j = 2;
while (j <= n) {
int k = 0;
// 先暴力求k
while (k < n && s[i + k] == s[j + k]) k++;
// 如果从 i 开始的第 k+1 位不相等
if (s[i + k] > s[j + k]) i += k + 1; // i向后启动k+1位
else j += k + 1;
// 移动后保证位置满足: i < j
if (i == j) j++;
if (i > j) swap(i, j);
}
string ans = s.substr(i, n);
return ans;
}
void kmp(string s) {
s = "." + s;
int n = s.size() - 1;
nxt[1] = 0;
int j = 0;
for (int i = 2; i <= n; i++) {
while (j && s[i] != s[j + 1]) j = nxt[j];
if (s[i] == s[j + 1]) j++;
nxt[i] = j;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
string s;
cin >> s;
kmp(s);
int n = s.size();
int len = n - nxt[n];
string minCover = s.substr(0, len);
cout << getMinPre(minCover) << endl;
return 0;
}