saelcc03's blog

By saelcc03, history, 10 days ago, In English

A friendly explanation of the implementation of Sufix Array CP-alg. Constructive comments are welcome ^-^

Recap

The algorithm will sort the cycle shifts of a string. For this purpose, we'll maintain arrays p and c on each iteration, where p[i] is the index of the i-th substring and c[i] is the class of the i-th substring, meaning that a substring with a lower class will be lexicographically smaller.

The algorithm will perform $$$log(n) + 1$$$ iterations $$$n$$$ times => $$$O(nlogn)$$$

0-th iteration

Probably this is not so complicated but just in case. The main idea of counting sort here is array cnt. Once you have the frequencies of every character in the string, do a prefix sum with cnt. Then, $$$p[--cnt[s[i]]] = i$$$ will just mean that the characters with higher frequency will be placed later p.

    vector<int> p(n), c(n), cnt(max(alphabet, n), 0);
    for (int i = 0; i < n; i++)
        cnt[s[i]]++;
    for (int i = 1; i < alphabet; i++)
        cnt[i] += cnt[i-1];
    for (int i = 0; i < n; i++)
        p[--cnt[s[i]]] = i;
    c[p[0]] = 0;
    int classes = 1;
    for (int i = 1; i < n; i++) {
        if (s[p[i]] != s[p[i-1]])
            classes++;
        c[p[i]] = classes - 1;
    }

Next iterations

We have done the 0-th iteration to get $$$p$$$ and $$$c$$$ arrays for substrings of lenght $$$1 \ll 0 = 1$$$. Now we'll make the following steps h times so $$$(1 \ll h) < n$$$, being $$$(1 \ll h)$$$ the length of the substring in each step.

The idea is that we need a way to compare substrings of length $$$2^h$$$ for each k. We already have a permutation array $$$p$$$ and a class array $$$c$$$ for substrings of length 1 (what we did in the 0-th iteration). So now we want to do get the arrays $$$p$$$ and $$$c$$$ for substrings of length $$$2^1$$$. We can see that a substring of length $$$2^1$$$ consists of two substrings of length $$$2^0$$$. A string of length $$$2^2$$$ consists of two strings of length $$$2^1$$$, and so on. If we are in the $$$(h-1)-th$$$ step, length $$$2^{h-1}$$$ in the i-th index, the class of the string of length $$$2^h$$$ starting at i will be determined by $$$c[i]$$$ and $$$c[i+2^{h-1}]$$$, but what about the new position (the value of the new p[i])? As described in the blog, it can be used a simple sort or couting sort over pairs, but it's slow. The faster approach is described below.

    vector<int> pn(n), cn(n);
    for (int h = 0; (1 << h) < n; ++h) {
        for (int i = 0; i < n; i++) {
            pn[i] = p[i] - (1 << h);
            if (pn[i] < 0)
                pn[i] += n;
        }
        fill(cnt.begin(), cnt.begin() + classes, 0);
        for (int i = 0; i < n; i++)
            cnt[c[pn[i]]]++;
        for (int i = 1; i < classes; i++)
            cnt[i] += cnt[i-1];
        for (int i = n-1; i >= 0; i--)
            p[--cnt[c[pn[i]]]] = pn[i];
        cn[p[0]] = 0;
        classes = 1;
        for (int i = 1; i < n; i++) {
            pair<int, int> cur = {c[p[i]], c[(p[i] + (1 << h)) % n]};
            pair<int, int> prev = {c[p[i-1]], c[(p[i-1] + (1 << h)) % n]};
            if (cur != prev)
                ++classes;
            cn[p[i]] = classes - 1;
        }
        c.swap(cn);
    }
    return p;
}

Probably the most tricky line is $$$p[--cnt[c[pn[i]]]] = pn[i]$$$. We'll cover that soon.

First we create auxiliar arrays $$$pn$$$ and $$$cn$$$. $$$pn[i]$$$ is just the position of the i-th substring in the sorted order order, minus the current length of the substring: $$$pn[i] = p[i] - (1 « h)$$$. In that way, we are sorting the pairs by the second element by only subtracting.

The array of frequencies $$$cnt$$$ will be computed the same ways as the previous step with the current class array $$$c$$$. Until here, the idea of counting sort is the same as the first block of code.

Now we've got to the line $$$p[--cnt[c[pn[i]]]] = pn[i]$$$. Let's explain it from outside to inside. It's very important to notice we are iterating from i = n-1 to 0. We are assigning new values to p so now p[i] is the index where the i-th substring of length $$$2^h$$$ starts, considering the order of the two substrings of length $$$2^{k-1}$$$ consists of. This index will be $$$pn[i]$$$. Why? Here it's important to look at two things happening. First, $$$cnt[c[pn[i]]]$$$ is the frequency of the class of $$$pn[i]$$$ (the left substring of the pair). Second, we are assigning that position in p to $$$pn[i]$$$ because it's the index where this new substring of length $$$2^h$$$ will start. Here a little example:

Imagine our string is s = "aababc" and we have completed the step for substring of length 2, now we want to sort strings of length 4.

$$$\newline$$$
$$$ s = a, a, b, a, b, c $$$
$$$ p = 0, 1, 3, 2, 4, 5 $$$
$$$ c = 0, 1, 2, 1, 3, 4 $$$

then the $$$pn$$$ will be:

$$$pn = 4, 5, 1, 0, 2, 3 $$$

and $$$cnt$$$:

$$$ cnt = 3, 5, 6 $$$

As we said, we'll iterate from i = n-1 to 0, because by doing that, the substring with higher frequencies will be assigned to lexicographically higher substrings. Let's look at the first assignment:

$$$\newline$$$
$$$ p[--cnt[c[3]]] = 3 $$$
$$$ p[2] = 3$$$

At position 3, starts "ab" substring of length 2, behind the highest substring of length 2 "ca". The frequency, (that is, the position in the sorted order) is given by "ab" (we used the array $$$c$$$ to get the frequency) and the index is given by the position of "ca" minus $$$2^{h-1} = 2$$$ (that is, position of "ab"). That's why it's also important to iterate from $$$i=n-1$$$ to $$$0$$$, because by doing that we are making sure that frequenciy/position of "ab" is assigned first to the highest lexicographically substring of length 2 "ca".

The final part is computing the new class array $$$cn$$$, very similar to the way it was computed in the 0-iteration.

Tbh, it took me a while to fully understand the algorithm, and explaining it was even more complicated. If there are any other points to mention, I'll be reading your feedback :>

Edit

This observation could be helpful: Suppose we are on len 2 and there are many substrings "ab". We are trying to sort substrings of len 4. How do we set the order indexes to substrings "ab" in such a way we assure that it will match the correct order for substrings of len 4?

Ofc if we have the options

$$$ab + ab$$$
$$$ab + cd$$$
$$$ab + bc$$$

We'll say that ab + cd has the latest index, so according to the frequency value of the class of "ab" in cnt, we'll set the last one (remembe we are itering in reversed order) as the ab whose next substring is cd

$$$\newline$$$

$$$p[--cnt[c[pn[i]$$$ //index of ab $$$]]] = pn[i]$$$ //index of ab whose next substr is cd

Full text and comments »

  • Vote: I like it
  • -9
  • Vote: I do not like it

By saelcc03, history, 2 months ago, In English

Contest: Link

A. No More Ties!

Tutorial
Pseudocode

B. Dividing

Tutorial
Pseudocode

C. Games with Queta

Tutorial
Pseudocode

D. Coins 3

Tutorial
Pseudocode

E. Separated Cells

Tutorial
Pseudocode

F. Painting Squares

Tutorial
Pseudocode
PSDT


I hope you found it useful ^-^

Full text and comments »

  • Vote: I like it
  • +6
  • Vote: I do not like it

By saelcc03, history, 3 months ago, In English

HI WORLD

I love cp algorithms articles but sometimes there are some proofnesses that I can't figure out by myself. I've just checked the proofness of transition of coefficients in extended euclidian algorithm and I'd like to share my explanation here.

Given $$$a$$$ and $$$b$$$, we need to calculate $$$x$$$ and $$$y$$$ such that satisfies Bézout's identity: $$$ax + by = gcd(a,b)$$$.

Starting with values $$$x=1$$$, $$$y=0$$$, $$$x_1=0$$$, $$$y_1=1$$$, $$$a_1=a$$$, $$$b_1=b$$$, after some iterations $$$b_1$$$ will become $$$0$$$ and $$$a_1$$$ will become $$$gcd(a,b)$$$:

// by cp algorithms
int gcd(int a, int b, int& x, int& y) {
    x = 1, y = 0;
    int x1 = 0, y1 = 1, a1 = a, b1 = b;
    while (b1) {
        int q = a1 / b1;
        tie(x, x1) = make_tuple(x1, x - q * x1);
        tie(y, y1) = make_tuple(y1, y - q * y1);
        tie(a1, b1) = make_tuple(b1, a1 - q * b1);
    }
    return a1;
}

We have these transitions:

$$$\begin{cases}x = x_1\\x_1 = x - x_1q\end{cases}$$$

$$$-$$$

$$$\begin{cases}y = y_1\\y_1 = y - y_1q\end{cases}$$$

$$$-$$$

$$$\begin{cases}a_1 = b_1\\b_1 = a_1 - b_1q\end{cases}$$$

The third transition is very clear. It's explained in the previous chapter about the normal Euclidean algorithm. But what is the reason that the first two transitions are correct as well?

At the beginnig we have these equalities:

  • $$$ax + by = a_1$$$
  • $$$ax_1 + by_1 = b_1$$$

On the following transition we'll have:

  • $$$ax_1 + by_1 = b_1$$$
  • $$$ax_2 + by_2 = a_1\%b_1 = a_1 - (a_1/b_1)*b_1 = a_1 - qb_1$$$

Replacing values of $$$a_1$$$ and $$$b_1$$$ on last equation we have:

$$$ax_2 + by_2 = a_1 - qb_1 = ax + by - q*(ax_1+by_1)$$$

Rearranging:

$$$ax_2 + by_2 = a*(x-x_1q) - b*(y-y1*q)$$$

By comparinson we have:

  • $$$x_2 = x - x_1q$$$
  • $$$y_2 = y - y_1q$$$

$$$x_2$$$ and $$$y_2$$$ are just the next values of x1 and y1. So we have checked the proofness of transition of coefficients of extended euclidean algorithm, iterative version.

Any observations will be really helpful. Thanks for reading ^-^

Full text and comments »

  • Vote: I like it
  • +24
  • Vote: I do not like it