字符串-最小表示法

最小表示法


基础问题

对于一个长度为n的字符串s,我们把它首尾相连形成一个,然后在任一位置断开得到的字符串ts循环同构

从某一位置开始循环读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...i1],1in
上面的+指的是字符串拼接

在这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+k1)]=s[j..(j+k1)](kn)
如果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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值