Перейти до основного вмісту

Суфіксне дерево. Алгоритм Укконена

Ця стаття є заготовкою й не містить детального опису. За описом алгоритму звертайтеся до інших джерел, наприклад до книги Algorithms on Strings, Trees, and Sequences Дена Ґасфілда (Dan Gusfield).

Цей алгоритм будує суфіксне дерево для заданого рядка ss довжини nn за час O(nlog(k))O(n\log(k))), де kkрозмір алфавіту (якщо вважати kk константою, асимптотика стає лінійною).

На вхід алгоритму подаються рядок ss та його довжина nn, які передаються як глобальні змінні.

Головна функція build_tree будує суфіксне дерево. Воно зберігається у вигляді масиву структур node, де node[0] — це корінь дерева.

Щоб спростити код, ребра зберігаються в тих самих структурах: для кожної вершини її структура node містить інформацію про ребро між нею та її батьком. Загалом кожна node зберігає таку інформацію:

  • (l, r) — ліва і права межі підрядка s[l..r-1], що відповідає ребру до цієї вершини,
  • par — батьківська вершина,
  • linkсуфіксне посилання,
  • next — список ребер, що виходять із цієї вершини.
string s;
int n;

struct node {
int l, r, par, link;
map<char,int> next;

node (int l=0, int r=0, int par=-1)
: l(l), r(r), par(par), link(-1) {}
int len() { return r - l; }
int &get (char c) {
if (!next.count(c)) next[c] = -1;
return next[c];
}
};
node t[MAXN];
int sz;

struct state {
int v, pos;
state (int v, int pos) : v(v), pos(pos) {}
};
state ptr (0, 0);

state go (state st, int l, int r) {
while (l < r)
if (st.pos == t[st.v].len()) {
st = state (t[st.v].get( s[l] ), 0);
if (st.v == -1) return st;
}
else {
if (s[ t[st.v].l + st.pos ] != s[l])
return state (-1, -1);
if (r-l < t[st.v].len() - st.pos)
return state (st.v, st.pos + r-l);
l += t[st.v].len() - st.pos;
st.pos = t[st.v].len();
}
return st;
}

int split (state st) {
if (st.pos == t[st.v].len())
return st.v;
if (st.pos == 0)
return t[st.v].par;
node v = t[st.v];
int id = sz++;
t[id] = node (v.l, v.l+st.pos, v.par);
t[v.par].get( s[v.l] ) = id;
t[id].get( s[v.l+st.pos] ) = st.v;
t[st.v].par = id;
t[st.v].l += st.pos;
return id;
}

int get_link (int v) {
if (t[v].link != -1) return t[v].link;
if (t[v].par == -1) return 0;
int to = get_link (t[v].par);
return t[v].link = split (go (state(to,t[to].len()), t[v].l + (t[v].par==0), t[v].r));
}

void tree_extend (int pos) {
for(;;) {
state nptr = go (ptr, pos, pos+1);
if (nptr.v != -1) {
ptr = nptr;
return;
}

int mid = split (ptr);
int leaf = sz++;
t[leaf] = node (pos, n, mid);
t[mid].get( s[pos] ) = leaf;

ptr.v = get_link (mid);
ptr.pos = t[ptr.v].len();
if (!mid) break;
}
}

void build_tree() {
sz = 1;
for (int i=0; i<n; ++i)
tree_extend (i);
}

Стиснена реалізація

Цю стиснену реалізацію запропонував freopen.

const int N=1000000,INF=1000000000;
string a;
int t[N][26],l[N],r[N],p[N],s[N],tv,tp,ts,la;

void ukkadd (int c) {
suff:;
if (r[tv]<tp) {
if (t[tv][c]==-1) { t[tv][c]=ts; l[ts]=la;
p[ts++]=tv; tv=s[tv]; tp=r[tv]+1; goto suff; }
tv=t[tv][c]; tp=l[tv];
}
if (tp==-1 || c==a[tp]-'a') tp++; else {
l[ts+1]=la; p[ts+1]=ts;
l[ts]=l[tv]; r[ts]=tp-1; p[ts]=p[tv]; t[ts][c]=ts+1; t[ts][a[tp]-'a']=tv;
l[tv]=tp; p[tv]=ts; t[p[ts]][a[l[ts]]-'a']=ts; ts+=2;
tv=s[p[ts-2]]; tp=l[ts-2];
while (tp<=r[ts-2]) { tv=t[tv][a[tp]-'a']; tp+=r[tv]-l[tv]+1;}
if (tp==r[ts-2]+1) s[ts-2]=tv; else s[ts-2]=ts;
tp=r[tv]-(tp-r[ts-2])+2; goto suff;
}
}

void build() {
ts=2;
tv=0;
tp=0;
fill(r,r+N,(int)a.size()-1);
s[0]=1;
l[0]=-1;
r[0]=-1;
l[1]=-1;
r[1]=-1;
memset (t, -1, sizeof t);
fill(t[1],t[1]+26,0);
for (la=0; la<(int)a.size(); ++la)
ukkadd (a[la]-'a');
}

Той самий код із коментарями:

const int N=1000000, // максимально можлива кількість вершин у суфіксному дереві
INF=1000000000; // константа нескінченності
string a; // вхідний рядок, для якого будується суфіксне дерево
int t[N][26], // масив переходів (стан, літера)
l[N], // ліва...
r[N], // ...та права межі підрядка a, що відповідає вхідному ребру
p[N], // батько вершини
s[N], // суфіксне посилання
tv, // вершина поточного суфікса (якщо ми посеред ребра, то нижня вершина ребра)
tp, // позиція в рядку, що відповідає позиції на ребрі (між l[tv] і r[tv] включно)
ts, // кількість вершин
la; // поточний символ у рядку

void ukkadd(int c) { // додати символ s до дерева
suff:; // сюди ми повертатимемося після кожного переходу до суфікса (і знову додаватимемо символ)
if (r[tv]<tp) { // перевіряємо, чи ми ще в межах поточного ребра
// якщо ні, знаходимо наступне ребро. Якщо його не існує, створюємо листок і додаємо його до дерева
if (t[tv][c]==-1) {t[tv][c]=ts;l[ts]=la;p[ts++]=tv;tv=s[tv];tp=r[tv]+1;goto suff;}
tv=t[tv][c];tp=l[tv];
} // інакше просто переходимо до наступного ребра
if (tp==-1 || c==a[tp]-'a')
tp++; // якщо літера на ребрі дорівнює c, спускаємося цим ребром
else {
// інакше розбиваємо ребро на два із серединою у вершині ts
l[ts]=l[tv];r[ts]=tp-1;p[ts]=p[tv];t[ts][a[tp]-'a']=tv;
// додаємо листок ts+1. Він відповідає переходу через c.
t[ts][c]=ts+1;l[ts+1]=la;p[ts+1]=ts;
// оновлюємо інформацію про поточну вершину — не забуваємо позначити ts як батька tv
l[tv]=tp;p[tv]=ts;t[p[ts]][a[l[ts]]-'a']=ts;ts+=2;
// готуємося до спуску
// tp позначатиме, де ми перебуваємо в поточному суфіксі
tv=s[p[ts-2]];tp=l[ts-2];
// поки поточний суфікс не вичерпано, спускаємося
while (tp<=r[ts-2]) {tv=t[tv][a[tp]-'a'];tp+=r[tv]-l[tv]+1;}
// якщо ми у вершині, додаємо до неї суфіксне посилання, інакше додаємо посилання до ts
// (ts ми створимо на наступній ітерації).
if (tp==r[ts-2]+1) s[ts-2]=tv; else s[ts-2]=ts;
// додаємо tp до нового ребра й повертаємося, щоб додати літеру до суфікса
tp=r[tv]-(tp-r[ts-2])+2;goto suff;
}
}

void build() {
ts=2;
tv=0;
tp=0;
fill(r,r+N,(int)a.size()-1);
// ініціалізуємо дані для кореня дерева
s[0]=1;
l[0]=-1;
r[0]=-1;
l[1]=-1;
r[1]=-1;
memset (t, -1, sizeof t);
fill(t[1],t[1]+26,0);
// додаємо текст до дерева, літера за літерою
for (la=0; la<(int)a.size(); ++la)
ukkadd (a[la]-'a');
}

Задачі для практики

Відеоматеріали