Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit67e5aaf

Browse files
Update suffix-array.md for Linear Time approach (SA-IS Algo)
1 parent2bd61ca commit67e5aaf

File tree

1 file changed

+161
-96
lines changed

1 file changed

+161
-96
lines changed

‎src/string/suffix-array.md

Lines changed: 161 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -217,116 +217,181 @@ vector<int> suffix_array_construction(string s) {
217217
}
218218
```
219219

220-
###$O(n)$approach {data-toc-label="O(n) approach"}
220+
###$O(n)$Approach {data-toc-label="O(n) approach"}
221221

222-
Here we will use the**Skew algorithm**, also known as the**DC3 algorithm** (Difference Cover Modulo 3), which is a linear-time algorithm for constructing suffix arrays. It was developed by Kärkkäinen and Sanders in 2003 and is efficient for sorting the suffixes of a string in\(O(n)\) time.
223-
This algorithm is used to sort the suffixes of a string in**O(n)** time, where`n` is the length of the string.
222+
In this approach, we are using the**SA-IS algorithm**, a linear-time algorithm for constructing suffix arrays. SA-IS (Suffix Array Induced Sorting) is a highly efficient algorithm that builds suffix arrays by sorting induced substrings, enabling\(O(n)\) time complexity for suffix array construction. Developed by Nong, Zhang, and Chan in 2009, it is widely regarded for its simplicity and performance.
224223

225-
###Brief Explanation of the Algorithm:
224+
###Brief Explanation of theSA-ISAlgorithm:
226225

227-
1.**Divide Step (Triplet Naming)**:
228-
- The algorithm divides the suffixes into three groups: those starting at positions that are`0 mod 3`,`1 mod 3`, and`2 mod 3`. These groups help in organizing the suffixes into manageable parts.
229-
230-
2.**Recursive Sorting**:
231-
- The algorithm recursively sorts the suffixes that start at positions`1 mod 3` and`2 mod 3` using a simplified version of radix sort. Then, these two sorted groups are combined and their positions are lexicographically ranked (assigned unique "names").
232-
233-
3.**Merging**:
234-
- The suffixes starting at`0 mod 3` positions are then sorted based on their first character. The previously sorted`1 mod 3` and`2 mod 3` suffixes are merged with this group to form the final suffix array.
226+
1.**L-Type and S-Type Suffixes**:
227+
- The SA-IS algorithm classifies suffixes into two types:**L-type** (where the current suffix is lexicographically greater than the next one) and**S-type** (where the current suffix is smaller than the next one). This classification helps the algorithm sort and rank the suffixes efficiently.
235228

236-
4.**Lexicographic Order Comparison**:
237-
-The algorithm compares the suffixes lexicographically using helper functions like`leq` (for pairs and triples) to ensure that the suffix array is correctly sorted.
229+
2.**Identifying LMS Substrings**:
230+
-SA-IS identifies LMS (Leftmost S-type) positions, which are boundaries between L-type and S-type suffixes. LMS substrings are critical as they serve as anchor points for the sorting process.
238231

239-
###Example:
240-
Considerthestring`s = "banana"`.
232+
3.**Induced Sorting**:
233+
- The algorithm first sorts LMS substrings recursively, then inducestheorder of the remaining suffixes. Sorting the LMS substrings is the key part of the algorithm, and once sorted, the remaining suffixes are arranged by their lexicographical order relative to LMS substrings.
241234

242-
1. The algorithm first divides the suffixes into three groups based on their starting positions.
243-
2. It sorts the suffixes that start at positions`1 mod 3` and`2 mod 3` using radix sort.
244-
3. It recursively processes the string, ranks the sorted suffixes, and then merges them with the suffixes starting at`0 mod 3` to generate the full suffix array.
235+
4.**Recursive Call for LMS Substrings**:
236+
- If necessary, the algorithm further processes the LMS substrings recursively. After this, it merges the sorted LMS substrings with the other suffixes to construct the final suffix array.
245237

246-
The name**Skew algorithm** comes from the fact that it handles suffixes based on their positions in the original string (`0 mod 3`,`1 mod 3`,`2 mod 3`), creating a "skewed" partition of the suffixes. This partitioning allows for efficient sorting and merging of suffixes in linear time.
247-
The skew algorithm is a simple and asymptotically efficient direct algorithm for suffix array construction that is easy to adapt to various models of computation. We expect that it is a good starting point for actual implementations, in particular on parallel machines and for external memory.
248-
The key to the algorithm is the use of suffixes Si with i mod 3 ∈ {1, 2} in the first, recursive step, which enables simple merging in the third step. There are other choices of suffixes that would work. An interesting possibility, for example, is to take suffixes Si with i mod 7 ∈ {3, 5, 6}. Some adjustments to the algorithm are required (sorting the remaining suffixes in multiple groups and performing a multiway merge in the third step) but the main ideas still work. In general, a suitable choice is a periodic set of positions according to a difference cover. A difference cover D modulo v is a set of integers in the range[0, v) such that, for all i ∈[0, v), there exist j, k ∈ D such that i ≡ k−j (mod v). For example {1, 2} is a difference cover modulo 3 and {3, 5, 6} is a difference cover modulo 7, but {1} is not a difference cover modulo 2. Any nontrivial difference cover modulo a constant could be used to obtain a linear time algorithm.
249238

250-
####C++ Implementation of the Linear Suffix Array
251239

252-
```cpp
253-
inlineboolleq(int a1, int a2, int b1, int b2) {
254-
return (a1 < b1 || (a1 == b1 && a2 <= b2));
255-
}
240+
The SA-IS algorithm is highly efficient for handling large datasets, making it suitable for applications where memory and time efficiency are essential.
256241

257-
inline bool leq(int a1, int a2, int a3, int b1, int b2, int b3) {
258-
return (a1 < b1 || (a1 == b1 && leq(a2, a3, b2, b3)));
259-
}
242+
###Understanding L-Type and S-Type Suffixes
260243

261-
static void radixPass(int* a, int* b, int* r, int n, int K) {
262-
int* c = new int[K + 1];
263-
for (int i = 0; i <= K; i++) c[i] = 0;
264-
for (int i = 0; i < n; i++) c[r[a[i]]]++;
265-
for (int i = 0, sum = 0; i <= K; i++) {
266-
int t = c[i]; c[i] = sum; sum += t;
267-
}
268-
for (int i = 0; i < n; i++) b[c[r[a[i]]]++] = a[i];
269-
delete[] c;
270-
}
244+
In the SA-IS algorithm, suffixes are classified as:
245+
-**L-type (Left)**: A suffix is L-type if it is lexicographically greater than the suffix immediately following it.
246+
-**S-type (Right)**: A suffix is S-type if it is lexicographically smaller than or equal to the suffix immediately following it.
271247

272-
void suffixArray(int* s, int* SA, int n, int K) {
273-
int n0 = (n + 2) / 3, n1 = (n + 1) / 3, n2 = n / 3, n02 = n0 + n2;
274-
int* s12 = new int[n02 + 3]; s12[n02] = s12[n02 + 1] = s12[n02 + 2] = 0;
275-
int* SA12 = new int[n02 + 3]; SA12[n02] = SA12[n02 + 1] = SA12[n02 + 2] = 0;
276-
int* s0 = new int[n0];
277-
int* SA0 = new int[n0];
248+
For the string`banana$`, let's classify each suffix:
249+
1. Starting from the end,`$` is considered S-type by definition.
250+
2. Moving leftward, the suffixes ending in`a`,`n`, etc., are classified as follows:
278251

279-
for (int i = 0, j = 0; i < n + (n0 - n1); i++)
280-
if (i % 3 != 0) s12[j++] = i;
252+
| Position| Suffix| Type|
253+
|----------|---------|------|
254+
| 6|`$`| S|
255+
| 5|`a$`| S|
256+
| 4|`na$`| L|
257+
| 3|`ana$`| S|
258+
| 2|`nana$`| L|
259+
| 1|`anana$`| S|
260+
| 0|`banana$`| S|
281261

282-
radixPass(s12, SA12, s + 2, n02, K);
283-
radixPass(SA12, s12, s + 1, n02, K);
284-
radixPass(s12, SA12, s, n02, K);
285-
286-
int name = 0, c0 = -1, c1 = -1, c2 = -1;
287-
for (int i = 0; i < n02; i++) {
288-
if (s[SA12[i]] != c0 || s[SA12[i] + 1] != c1 || s[SA12[i] + 2] != c2) {
289-
name++; c0 = s[SA12[i]]; c1 = s[SA12[i] + 1]; c2 = s[SA12[i] + 2];
290-
}
291-
if (SA12[i] % 3 == 1) s12[SA12[i] / 3] = name;
292-
else s12[SA12[i] / 3 + n0] = name;
293-
}
294-
295-
if (name < n02) {
296-
suffixArray(s12, SA12, n02, name);
297-
for (int i = 0; i < n02; i++) s12[SA12[i]] = i + 1;
298-
} else {
299-
for (int i = 0; i < n02; i++) SA12[s12[i] - 1] = i;
300-
}
301-
302-
for (int i = 0, j = 0; i < n02; i++)
303-
if (SA12[i] < n0) s0[j++] = 3 * SA12[i];
304-
radixPass(s0, SA0, s, n0, K);
305-
306-
for (int p = 0, t = n0 - n1, k = 0; k < n; k++) {
307-
#define GetI() (SA12[t] < n0 ? SA12[t] * 3 + 1 : (SA12[t] - n0) * 3 + 2)
308-
int i = GetI(), j = SA0[p];
309-
if (SA12[t] < n0 ? leq(s[i], s12[SA12[t] + n0], s[j], s12[j / 3]) :
310-
leq(s[i], s[i + 1], s12[SA12[t] - n0 + 1], s[j], s[j + 1], s12[j / 3 + n0])) {
311-
SA[k] = i; t++;
312-
if (t == n02) {
313-
for (k++; p < n0; p++, k++) SA[k] = SA0[p];
314-
}
315-
} else {
316-
SA[k] = j; p++;
317-
if (p == n0) {
318-
for (k++; t < n02; t++, k++) SA[k] = GetI();
319-
}
320-
}
321-
}
322-
323-
delete[] s12; delete[] SA12; delete[] SA0; delete[] s0;
324-
}
325-
```
326-
327-
The **Skew algorithm (DC3)** is a highly efficient method for constructing suffix arrays in linear time, providing a scalable solution for large string-processing problems. The above implementation strives for conciseness rather than for speed while maintaining its linear time complexity.
328-
329-
---
262+
The`L` and`S` types provide crucial information for sorting suffixes using induced sorting.
263+
###Example:
264+
For the string`s = "banana"`, The steps were :
265+
1. The algorithm first classifies suffixes into L-type and S-type.
266+
2. It identifies LMS positions based on the L and S classifications.
267+
3. It sorts the LMS substrings and uses them to induce the order of remaining suffixes.
268+
4. The final suffix array is constructed after all suffixes are sorted.
269+
270+
###C++ Implementation of the SA-IS Algorithm
271+
272+
Let's focus on the Implementation of the algorithm.
273+
274+
###1.**Suffix Array Initialization and Base Cases**
275+
```cpp
276+
std::vector<int>sa_is(const std::vector<int>& s, int upper) {
277+
int n = s.size();
278+
if (n == 0) return {};
279+
if (n == 1) return {0};
280+
if (n == 2) return (s[0] < s[1]) ? std::vector<int>{0, 1} : std::vector<int>{1, 0};
281+
```
282+
This part initializes the `sa_is` function, which constructs the suffix array for a given input vector `s`. It first handles edge cases:
283+
- If the string is empty (`n == 0`), it returns an empty array.
284+
- For one character (`n == 1`), it returns `{0}` since only one suffix exists.
285+
- For two characters (`n == 2`), it returns `{0, 1}` or `{1, 0}` depending on the lexicographic order.
286+
287+
### 2. **Classifying Suffix Types and Calculating Buckets**
288+
```cpp
289+
std::vector<int> sa(n, -1), ls(n);
290+
for (int i = n - 2; i >= 0; i--) {
291+
ls[i] = (s[i] < s[i + 1]) || (s[i] == s[i + 1] && ls[i + 1]);
292+
}
293+
294+
std::vector<int> sum_l(upper + 1), sum_s(upper + 1);
295+
for (int i = 0; i < n; i++) {
296+
if (!ls[i]) sum_s[s[i]]++;
297+
else sum_l[s[i] + 1]++;
298+
}
299+
for (int i = 0; i <= upper; i++) {
300+
sum_s[i] += sum_l[i];
301+
if (i < upper) sum_l[i + 1] += sum_s[i];
302+
}
303+
```
304+
This part:
305+
- Initializes the`sa` array to store suffix positions and the`ls` array to classify each suffix as S-type or L-type.
306+
- Classifies each suffix by comparing the current character with the next one.
307+
- Fills`sum_s` and`sum_l`, which hold counts of S-type and L-type suffixes respectively, to help with induced sorting later.
308+
309+
###3.**Induced Sorting with LMS Suffixes**
310+
```cpp
311+
auto induce = [&](const std::vector<int>& lms) {
312+
std::fill(sa.begin(), sa.end(), -1);
313+
std::vector<int> buf = sum_s;
314+
for (int d : lms) {
315+
if (d == n) continue;
316+
sa[buf[s[d]]++] = d;
317+
}
318+
buf = sum_l;
319+
sa[buf[s[n - 1]]++] = n - 1;
320+
for (int i = 0; i < n; i++) {
321+
int v = sa[i];
322+
if (v >= 1 && !ls[v - 1]) sa[buf[s[v - 1]]++] = v - 1;
323+
}
324+
buf = sum_l;
325+
for (int i = n - 1; i >= 0; i--) {
326+
int v = sa[i];
327+
if (v >= 1 && ls[v - 1]) sa[--buf[s[v - 1] + 1]] = v - 1;
328+
}
329+
};
330+
```
331+
Here, the `induce` function sorts suffixes by first placing LMS (leftmost S-type) suffixes in the correct order, and then inducing the order for L-type and S-type suffixes based on LMS positions. The buffers `sum_l` and `sum_s` are used for efficiently placing suffixes within their buckets.
332+
333+
### 4. **Recursive Sorting of LMS Substring and Final Output**
334+
```cpp
335+
std::vector<int> lms_map(n + 1, -1), lms;
336+
int m = 0;
337+
for (int i = 1; i < n; i++) {
338+
if (!ls[i - 1] && ls[i]) {
339+
lms_map[i] = m++;
340+
lms.push_back(i);
341+
}
342+
}
343+
induce(lms);
344+
345+
if (m) {
346+
std::vector<int> sorted_lms, rec_s(m), rec_sa;
347+
for (int i : sa) if (lms_map[i] != -1) sorted_lms.push_back(i);
348+
int rec_upper = 0;
349+
rec_s[lms_map[sorted_lms[0]]] = 0;
350+
for (int i = 1; i < m; i++) {
351+
int l = sorted_lms[i - 1], r = sorted_lms[i];
352+
int end_l = (lms_map[l] + 1 < m) ? lms[lms_map[l] + 1] : n;
353+
int end_r = (lms_map[r] + 1 < m) ? lms[lms_map[r] + 1] : n;
354+
bool same = (end_l - l == end_r - r);
355+
for (int j = 0; same && j < end_l - l; j++) {
356+
if (s[l + j] != s[r + j]) same = false;
357+
}
358+
if (!same) rec_upper++;
359+
rec_s[lms_map[sorted_lms[i]]] = rec_upper;
360+
}
361+
rec_sa = sa_is(rec_s, rec_upper);
362+
363+
for (int i = 0; i < m; i++) sorted_lms[i] = lms[rec_sa[i]];
364+
induce(sorted_lms);
365+
}
366+
return sa;
367+
}
368+
```
369+
- Constructs the suffix array of LMS substrings recursively.
370+
- Sorts LMS suffixes by generating`rec_s`, a reduced string representation, and calls`sa_is` recursively on it.
371+
- After sorting LMS substrings, the`induce` function finalizes the order for all suffixes.
372+
Here's an addition to the breakdown, covering the missing part:
373+
374+
###5.**Wrapper for Suffix Array and Main Function**
375+
```cpp
376+
std::vector<int>suffix_array(const std::string& s) {
377+
std::vector<int> s_vec(s.size() + 1);
378+
for (size_t i = 0; i < s.size(); i++) s_vec[i] = s[i];
379+
s_vec[s.size()] = 0;
380+
return sa_is(s_vec, 255);
381+
}
382+
383+
int main() {
384+
std::string text;
385+
std::cin >> text;
386+
auto suffix_arr = suffix_array(text);
387+
for (size_t i = 1; i < suffix_arr.size(); i++) {
388+
std::cout << suffix_arr[i] << (i + 1 < suffix_arr.size() ? " " : "\n");
389+
}
390+
return 0;
391+
}
392+
```
393+
- **`suffix_array` Function**: This wrapper function prepares the input string `s` by converting it into a vector of integers (`s_vec`), where each character is represented as an integer. An additional `0` is appended to mark the end of the string. The function then calls `sa_is` to construct and return the suffix array.
394+
- **`main` Function**: The main function reads a string `text` from standard input, computes its suffix array using the `suffix_array` function, and then outputs the suffix array (starting from index 1) in a space-separated format. This provides the lexicographically sorted order of suffixes for the input text.
330395
331396
332397
## Applications

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp