Z-функция¶
Пусть дана строка \(s\) длины \(n\). Тогда Z-функция от этой строки — это массив длины \(n\), \(i\)-ый элемент которого равен наибольшему числу символов, начиная с позиции \(i\), совпадающих с первыми символами строки \(s\).
Иными словами, \(z[i]\) — это наибольший общий префикс строки \(s\) и её \(i\)-го суффикса.
Примеры¶
string | z-func |
---|---|
aaaaa | 0, 4, 3, 2, 1 |
aaabaab | 0, 2, 1, 0, 2, 1, 0 |
abacaba | 0, 0, 1, 0, 3, 0, 1 |
Тривиальный алгоритм¶
Формальное определение можно представить в виде следующей элементарной реализации за \(O (n^2)\):
vector<int> z_function_trivial (string s) {
int n = (int) s.length();
vector<int> z (n);
for (int i = 1; i < n; i++)
while (i + z[i] < n && s[z[i]] == s[i+z[i]])
z[i]++;
return z;
}
Эффективный алгоритм вычисления Z-функции \(O(n)\)¶
vector<ll> zf(string s) {
vector<ll> z(s.size());
for (int i = 1, l = 0, r = 0; i < s.size(); ++i) {
if (i <= r)
z[i] = min((ll)r - i + 1, z[i - l]);
while (i + z[i] < s.size() && s[z[i]] == s[i + z[i]])
z[i]++;
if (i + z[i] - 1 > r)
l = i, r = i + z[i] - 1;
}
return z;
}
Применения¶
Поиск подстроки в строке \(O(n)\)¶
Во избежании путаницы, назовём одну строку текстом \(t\), другую — образцом \(p\). Таким образом, задача заключается в том, чтобы найти все вхождения образца \(p\) в текст \(t\).
Для решения этой задачи образуем строку \(s = p + \# + t\), т.е. к образцу припишем текст через символ-разделитель (который не встречается нигде в самих строках).
Посчитаем для полученной строки Z-функцию. Тогда для любого \(i\) в отрезке \([0; length(t)-1]\) по соответствующему значению \(z[i + length(p) + 1]\) можно понять, входит ли образец \(p\) в текст \(t\), начиная с позиции \(i\): если это значение Z-функции равно \(length(p)\), то да, входит, иначе — нет.
Таким образом, асимптотика решения получилась \(O (length(t) + length(p))\). Потребление памяти имеет ту же асимптотику.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
vector<ll> zf(string s) {
vector<ll> z(s.size());
for (int i = 1, l = 0, r = 0; i < s.size(); ++i) {
if (i <= r)
z[i] = min((ll) r - i + 1, z[i - l]);
while (i + z[i] < s.size() && s[z[i]] == s[i + z[i]])
z[i]++;
if (i + z[i] - 1 > r)
l = i, r = i + z[i] - 1;
}
return z;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
string p, t;
cin >> p >> t;
auto zfv = zf(p + "#" + t);
vector<ll> ans;
for (int i = p.size() + 1; i < p.size() + t.size() + 1; i++) {
if (zfv[i] == p.size()) {
ans.push_back(i - p.size());
}
}
cout << ans.size() << "\n";
for (auto i : ans) {
cout << i << " ";
}
return 0;
}
Сжатие строки¶
Дана строка \(s\) длины \(n\). Требуется найти самое короткое её «сжатое» представление, т.е. найти такую строку \(t\) наименьшей длины, что \(s\) можно представить в виде конкатенации одной или нескольких копий \(t\).
Для решения посчитаем Z-функцию строки \(s\), и найдём первую позицию \(i\) такую, что \(i + z[i] = n\), и при этом \(n\) делится на \(i\). Тогда строку \(s\) можно сжать до строки длины \(i\).
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
vector<ll> zf(string s) {
vector<ll> z(s.size());
for (int i = 1, l = 0, r = 0; i < s.size(); ++i) {
if (i <= r)
z[i] = min((ll)r - i + 1, z[i - l]);
while (i + z[i] < s.size() && s[z[i]] == s[i + z[i]])
z[i]++;
if (i + z[i] - 1 > r)
l = i, r = i + z[i] - 1;
}
return z;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
string s;
cin >> s;
auto z = zf(s);
ll n = z.size();
for (int i = 1; i < n; i++) {
if (i + z[i] == n && n % i == 0) {
cout << i;
return 0;
}
}
cout << s.size();
return 0;
}