C++

Suffix array の構成ライブラリを整備する

ISO_C++_Logo

ABC362 G問題解説記事)で suffix array を紹介しました。これを効率よく構成するアルゴリズムを紹介します。

Suffix array

Suffix array(接尾辞配列)とは、与えられた文字列 $S$ に対して、すべての接尾辞を辞書順にソートしたものです。配列には、接尾辞そのものではなく、接尾辞の開始位置を格納します。

Suffix array を構成すると、文字列の検索が効率的に行えます。ABC362 G問題自体がその例となっています。

素朴な実装

最初に素朴な実装を紹介します。部分文字列とインデックスの pair を sort します。このインデックスを suffix array として返します。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

vector<int> suffix_array(string s)
{
	int n = s.length();
	vector<int> result(n);
	vector<pair<string, int>> buffer(n);

	for (int i = 0; i < n; ++i) {
		buffer[i] = make_pair(s.substr(i), i);
	}
	sort(buffer.begin(), buffer.end());
	for (int i = 0; i < n; ++i) {
		result[i] = buffer[i].second;
	}

	return result;
}

int main()
{
	string s;
	cin >> s;

	vector<int> sa = suffix_array(s);

	for (int i = 0; i < sa.size(); ++i) {
		cout.width(4);
		cout << sa[i] << ":" << s.substr(sa[i]) << endl;
	}

	return 0;
}

例として、”abracadabra” という文字をよく使います。この入力に対して、プログラムの出力は以下となります。

$ ./suffix_array_sort.exe
abracadabra
  10:a
   7:abra
   0:abracadabra
   3:acadabra
   5:adabra
   8:bra
   1:bracadabra
   4:cadabra
   6:dabra
   9:ra
   2:racadabra

実践的な suffix array 実装

素朴な実装は、長さ $N$ の文字列のソートに $N \log N$ の計算量が必要なため、合わせた計算量は、$N^2 \log N$ となります。

Manber & Myers が考案した、ダブリングを使った計算量 $N \log^2 N$ のプログラムを紹介します。最初は1文字分だけでソートして、次に2文字分、4文字分とソートで比較する範囲を倍々に増やしていきます。比較する文字数は、配列 r で管理します。

この実装は「蟻本」(「プログラミングコンテストチャレンジブック」第2版 秋葉拓哉、岩田陽一、北川宣稔著、マイナビ 2012年)を参考にしました。このため、Vector コンテナではなく配列を対象としました。コードは以下となります。

参考までに、AtCoder Library の suffix array 構築は、SA-ISという線形時間で処理するアルゴリズムが採用されていました。

#define MAX_N 500000

int n, k;
int r[MAX_N + 1];
int t[MAX_N + 1];
int sa[MAX_N + 1];

bool compare_sa(int i, int j)
{
	if (r[i] != r[j]) {
		return r[i] < r[j];
	} else {
		int ri;
		if (i + k < n) {
			ri = r[i + k];
		} else {
			ri = -1;
		}
		int rj;
		if (j + k < n) {
			rj = r[j + k];
		} else {
			rj = -1;
		}
		return ri < rj;
	}
}

void suffix_array(string s, int *sa)
{
	n = s.length();

	for (int i = 0; i < n; ++i) {
		sa[i] = i;
		r[i] = s[i];
	}

	for (k = 1; k < n; k *= 2) {
		sort(sa, sa + n, compare_sa);

		t[sa[0]] = 0;
		for (int i = 1; i < n; ++i) {
			t[sa[i]] = t[sa[i - 1]] + (compare_sa(sa[i - 1], sa[i]) ? 1 : 0);
		}
		for (int i = 0; i < n; ++i) {
			r[i] = t[i];
		}
	}
}

ライブラリのテスト

Library Checker でアルゴリズムをテストします。以下がアルゴリズムのリンク先です。

Suffix Array

テストしたプログラムは以下です。AC(Accepted)と判定されました。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;
#define MAX_N 500000

int n, k;
int r[MAX_N + 1];
int t[MAX_N + 1];
int sa[MAX_N + 1];

bool compare_sa(int i, int j)
{
	if (r[i] != r[j]) {
		return r[i] < r[j];
	} else {
		int ri;
		if (i + k < n) {
			ri = r[i + k];
		} else {
			ri = -1;
		}
		int rj;
		if (j + k < n) {
			rj = r[j + k];
		} else {
			rj = -1;
		}
		return ri < rj;
	}
}

void suffix_array(string s, int *sa)
{
	n = s.length();

	for (int i = 0; i < n; ++i) {
		sa[i] = i;
		r[i] = s[i];
	}

	for (k = 1; k < n; k *= 2) {
		sort(sa, sa + n, compare_sa);

		t[sa[0]] = 0;
		for (int i = 1; i < n; ++i) {
			t[sa[i]] = t[sa[i - 1]] + (compare_sa(sa[i - 1], sa[i]) ? 1 : 0);
		}
		for (int i = 0; i < n; ++i) {
			r[i] = t[i];
		}
	}
}

int main()
{
	string s;
	cin >> s;

	suffix_array(s, sa);

	for (int i = 0; i < n; ++i) {
		cout << sa[i] << " \n"[i == n - 1];
	}

	return 0;
}

最後に

Suffix array について整理しました。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA