Блог пользователя I_love_myself

Автор I_love_myself, 5 лет назад, По-русски

Добрый день, мой вклад упал я давно не писал ничего полезного, поэтому надо что-то написать.
0) Постановка задачи и реализация. Хотим лексикографически отсортировать все суффиксы (или циклические сдвиги) строки s по возрастанию. Суффиксный массив — это такая перестановка p чисел от 0 до n — 1, что p[i] — обозначает позицию начала лексикографически i-того суффикса строки, то есть это то, что позволит решить нашу задачу.
Для построения суффиксного массива мы будем использовать классы эквивалентности — c[i][len], которое обозначает, что циклический сдвиг, начиная с i-той позиции длины len имеет класс эквивалентности $$$c[i][len]$$$. При чем для двух классов эквивалентности $$$c[i][len]$$$ и $$$c[j][len]$$$ будет верно, что $$$c[i][len] == c[j][len]$$$ тогда и только тогда, когда соответствующие подстроки равны, а $$$c[i][len] < c[j][len]$$$ тогда и только тогда, когда i-тая подстрока лексикографически меньше j-той. Таким образом, если мы расставим классы для длины $$$len >= len(s)$$$, то мы очень просто сможем восстановить перестановку p.
Пусть мы расставили классы для длины len. Расставим для длины 2 * len. Но заметим, что класс c[i][2 * len] описывается парой $$$\{c[i][len], c[i + len][len]\}$$$, то есть какой результат сравнения будет у двух таких пар, такой будет и у соответствующих суффиксов. Поэтому мы можем отсортировать все пары и расставить новые классы для длины 2 * len, а сложность будет $$$O(\log n * n \log n) = O(n \log^2 n)$$$, так как мы будем увеличивать длину как степень двойки ($$$\log n$$$), а на каждой итерации мы будем сортировать n пар обычной сортировкой ($$$n \log n$$$).

Многие (в том числе и я) не любят суфмасс из-за его громоздкости и не очень понятной реализации, в которой легко набагать. На e-maxx, например, он занимает 50 строк и строки вида $$$p[--cnt[c[pn[i]]]] = pn[i];$$$ не очень крутые.

1) Моя реализация за $$$O(n \log^2 n)$$$:

vector<int> sufmas(vector<int> c) {
	int n = c.size();
	vector<pair<pair<int, int>, int>> t(n); //t[i] = { { first class, second class }, position }
	for (int i = 1; i < n; i <<= 1) {
		for (int j = 0; j < c.size(); j++)
			t[j] = { { c[j], c[(j + i) % n] }, j };
		sort(t.begin(), t.end());
		for (int cnt = 0, j = 0; j < n; j++) {
			if (j && t[j].first != t[j - 1].first)
				cnt++;
			c[t[j].second] = cnt;
		}
	} 

	vector<int> p(n);
	for (int i = 0; i < n; i++)
		p[c[i]] = i;
	return p;
}

2) Чтобы убрать лишний логарифм в сложности, можно воспользоваться цифровой сортировкой, которая лучше использует то, что на предыдущем шаге мы уже отсортировали суффиксы меньшей длины.
Тут задумывалась моя короткая реализация суффиксного массива за $$$O(n \log n)$$$, однако на практике мы выяснили, что она работает дольше первой, поэтому я оставлю здесь ссылку на короткую реализацию от Пядеркина за аналогичную асимптотику.
Также хочу сказать спасибо за помощь в написании этого поста Holidin, ismagilov.code и SpeedOfMagic

  • Проголосовать: нравится
  • -2
  • Проголосовать: не нравится

»
5 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

почему не заменить это pair<pair<int, int>, int> на tuple?

  • »
    »
    5 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится -10 Проголосовать: не нравится

    tuple очень медленно работает
    Upd: извините те, кого мы с Азатом могли ввести в заблуждение из-за времени работы typle. Я протестировал ее на серверах кф, оказалось, что работают почти одинаково (разница на 1e7 операций 15 мс)

  • »
    »
    5 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Ну, на самом деле я вообще не понимаю зачем в первой реализации нужен pair<pair<i, i>, i>, если можно просто обойтись pair<i, i> (заменив { c[j], c[(j + i) % n] } на c[j] * cnt + c[(j + i) % n] ) Да, возможно нам придется при этом использовать pair<ll, i>, но это в любом случае гораздо быстрее.

    А вообще, мне кажется pair работает сильно быстрее tuple, ибо более простая структура. Опять таки, лично мне просто приятнее писать .first.first чем .get.

    • »
      »
      »
      5 лет назад, # ^ |
        Проголосовать: нравится +3 Проголосовать: не нравится

      pair работает сильно быстрее tuple, ибо более простая структура

      Одинаковые по производительности структуры, все "сложности" разрешаются на этапе компиляции.

»
5 лет назад, # |
  Проголосовать: нравится +3 Проголосовать: не нравится

Ну, раз уж меня упомянули в посте, я обычно пишу что-то такое (красоту кода сильно ломает то, что иногда строка короче алфавита). Если нужно объяснение, как это работает, могу написать.

vector<int> suffix_array(string s) {
    s.push_back(0);
    int n = len(s);
    vector<int> c(n), new_c(n), suf(n), new_suf(n), starts(max(n, ALPHABET + 1));
    for (int i = 0; i < n; i++) {
        suf[i] = i;
        c[i] = s[i];
        starts[s[i] + 1]++;
    }
    int sum = 0;
    for (auto &v : starts) {
        sum += v, v = sum;
    }
    for (int l = 0; l < n; l = (l ? 2 * l : 1)) {
        for (auto v : suf) {
            int pos = (v - l + n) % n;
            new_suf[starts[c[pos]]++] = pos;
        }
        int type = -1;
        long long last = -1;
        for (int i = 0; i < n; i++) {
            int v = new_suf[i];
            if (last != (c[v] * 1ll * max(n, ALPHABET + 1) + c[(v + l) % n])) {
                last = (c[v] * 1ll * max(n, ALPHABET + 1) + c[(v + l) % n]);
                starts[++type] = i;
            }
            new_c[v] = type;
        }
        swap(c, new_c);
        swap(suf, new_suf);
    }
    return suf;
}

Понятно, что это код можно оптимизировать, просто заменив вектора на массивы (правда придется использовать memcpy вместо swap), а также заменив достаточно долгое взятие по модулю на тернарный оператор. Дальше можно избавиться от создания дополнительных переменных в циклах и получить практически молниеносный код.