IOI 2003
IOI 2003

Comparing Substrings

Problem Statement Summary Given a string S of length N (N 10^5) and Q queries (Q 10^5), each query specifies two substrings S[a a + -1] and S[b b + -1] and asks for their lexicographic ordering (equal, less than, or g...

Updated May 21, 2026
Track IOI
Year 2003
Statement Not mirrored
TeXC++

Problem Statement

No standalone statement file is available for this entry.

A separate statement file is not available for this entry, so the page focuses on the editorial and implementation.

Editorial

The solution write-up is rendered from the LaTeX source, with equations kept live through MathJax.

Problem Statement Summary

Given a string $S$ of length $N$ ($N \le 10^5$) and $Q$ queries ($Q \le 10^5$), each query specifies two substrings $S[a \ldots a{+}\ell{-}1]$ and $S[b \ldots b{+}\ell{-}1]$ and asks for their lexicographic ordering (equal, less than, or greater than).

Solution: Suffix Array + LCP + Sparse Table

Suffix Array

Build the suffix array $\mathit{sa}$ using the $O(N \log^2 N)$ prefix-doubling algorithm. The rank array $\mathit{rank}[i]$ gives the position of suffix $i$ in the sorted order.

LCP Array (Kasai's Algorithm)

The LCP (longest common prefix) array $\mathit{lcp}[i]$ stores the length of the longest common prefix between $\mathit{sa}[i-1]$ and $\mathit{sa}[i]$ in the sorted order. Kasai's algorithm computes this in $O(N)$.

Sparse Table for RMQ

The LCP of two arbitrary suffixes starting at positions $a$ and $b$ equals the minimum value in $\mathit{lcp}[\mathit{rank}[a]+1 \ldots \mathit{rank}[b]]$ (assuming $\mathit{rank}[a] < \mathit{rank}[b]$). A sparse table provides $O(1)$ range-minimum queries after $O(N \log N)$ preprocessing.

Answering Queries

For substrings $S[a \ldots a{+}\ell{-}1]$ and $S[b \ldots b{+}\ell{-}1]$:

  1. Compute $L = \mathrm{LCP}(a, b)$ via the sparse table.

  2. If $L \ge \ell$: the substrings are equal.

  3. Otherwise: compare $S[a + L]$ and $S[b + L]$.

C++ Implementation

#include <bits/stdc++.h>
using namespace std;

struct SuffixArray {
    int n;
    string s;
    vector<int> sa, inv, lcp;
    vector<vector<int>> sparse;

    SuffixArray(const string& str) : s(str), n(str.size()) {
        buildSA();
        buildLCP();
        buildSparse();
    }

    void buildSA() {
        sa.resize(n);
        inv.resize(n);
        vector<int> tmp(n);
        iota(sa.begin(), sa.end(), 0);
        for (int i = 0; i < n; i++) inv[i] = s[i];

        for (int gap = 1; gap < n; gap <<= 1) {
            auto cmp = [&](int a, int b) -> bool {
                if (inv[a] != inv[b]) return inv[a] < inv[b];
                int ra = (a + gap < n) ? inv[a + gap] : -1;
                int rb = (b + gap < n) ? inv[b + gap] : -1;
                return ra < rb;
            };
            sort(sa.begin(), sa.end(), cmp);
            tmp[sa[0]] = 0;
            for (int i = 1; i < n; i++)
                tmp[sa[i]] = tmp[sa[i - 1]] + (cmp(sa[i - 1], sa[i]) ? 1 : 0);
            inv = tmp;
            if (inv[sa[n - 1]] == n - 1) break;
        }
    }

    void buildLCP() {
        lcp.assign(n, 0);
        // inv already stores the inverse suffix array
        int k = 0;
        for (int i = 0; i < n; i++) {
            if (inv[i] == 0) { k = 0; continue; }
            int j = sa[inv[i] - 1];
            while (i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
            lcp[inv[i]] = k;
            if (k > 0) k--;
        }
    }

    void buildSparse() {
        int LOG = 1;
        while ((1 << LOG) <= n) LOG++;
        sparse.assign(LOG, vector<int>(n, 0));
        for (int i = 0; i < n; i++) sparse[0][i] = lcp[i];
        for (int j = 1; j < LOG; j++)
            for (int i = 0; i + (1 << j) <= n; i++)
                sparse[j][i] = min(sparse[j - 1][i],
                                   sparse[j - 1][i + (1 << (j - 1))]);
    }

    int rmq(int l, int r) {
        if (l > r) return 0;
        int k = __lg(r - l + 1);
        return min(sparse[k][l], sparse[k][r - (1 << k) + 1]);
    }

    // LCP of suffixes starting at positions a and b.
    int lcpQuery(int a, int b) {
        if (a == b) return n - a;
        int ra = inv[a], rb = inv[b];
        if (ra > rb) swap(ra, rb);
        return rmq(ra + 1, rb);
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string S;
    cin >> S;

    SuffixArray sa(S);

    int Q;
    cin >> Q;
    while (Q--) {
        int a, b, len;
        cin >> a >> b >> len;
        a--; b--; // convert to 0-indexed

        int commonLen = sa.lcpQuery(a, b);
        if (commonLen >= len) {
            cout << "=\n";
        } else if (S[a + commonLen] < S[b + commonLen]) {
            cout << "<\n";
        } else {
            cout << ">\n";
        }
    }

    return 0;
}

Complexity Analysis

  • Suffix array construction: $O(N \log^2 N)$.

  • LCP array (Kasai): $O(N)$.

  • Sparse table: $O(N \log N)$ build, $O(1)$ per query.

  • Per query: $O(1)$.

  • Total: $O(N \log^2 N + Q)$.

  • Space: $O(N \log N)$.

Code

C++ solution used for this page.

C++

Clean code view with a raw-file link when you want the original source.

Raw file
// IOI 2003 - Comparing Substrings
// Given string S and Q queries (a, b, len), compare substrings
// S[a..a+len-1] vs S[b..b+len-1] lexicographically.
// Uses suffix array + LCP array + sparse table for O(1) per query.
// SA construction: O(N log^2 N). Queries: O(1) each.

#include <bits/stdc++.h>
using namespace std;

struct SuffixArray {
    int n;
    string s;
    vector<int> sa, rank_, lcp, inv;
    vector<vector<int>> sparse;

    SuffixArray(const string& str) : s(str), n((int)str.size()) {
        buildSA();
        buildLCP();
        buildInverse();
        buildSparse();
    }

    void buildSA() {
        sa.resize(n);
        rank_.resize(n);
        vector<int> tmp(n);
        iota(sa.begin(), sa.end(), 0);

        // Initial ranking by single character
        for (int i = 0; i < n; i++) rank_[i] = s[i];

        for (int gap = 1; gap < n; gap <<= 1) {
            auto cmp = [&](int a, int b) -> bool {
                if (rank_[a] != rank_[b]) return rank_[a] < rank_[b];
                int ra = (a + gap < n) ? rank_[a + gap] : -1;
                int rb = (b + gap < n) ? rank_[b + gap] : -1;
                return ra < rb;
            };
            sort(sa.begin(), sa.end(), cmp);

            tmp[sa[0]] = 0;
            for (int i = 1; i < n; i++) {
                tmp[sa[i]] = tmp[sa[i - 1]] + (cmp(sa[i - 1], sa[i]) ? 1 : 0);
            }
            rank_ = tmp;
            if (rank_[sa[n - 1]] == n - 1) break;
        }
    }

    // Kasai's algorithm for LCP array
    void buildLCP() {
        lcp.resize(n, 0);
        vector<int> inv_tmp(n);
        for (int i = 0; i < n; i++) inv_tmp[sa[i]] = i;

        int k = 0;
        for (int i = 0; i < n; i++) {
            if (inv_tmp[i] == 0) { k = 0; continue; }
            int j = sa[inv_tmp[i] - 1];
            while (i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
            lcp[inv_tmp[i]] = k;
            if (k > 0) k--;
        }
    }

    void buildInverse() {
        inv.resize(n);
        for (int i = 0; i < n; i++) inv[sa[i]] = i;
    }

    // Sparse table for range minimum query on LCP array
    void buildSparse() {
        int LOG = 1;
        while ((1 << LOG) <= n) LOG++;
        sparse.assign(LOG, vector<int>(n, 0));
        for (int i = 0; i < n; i++) sparse[0][i] = lcp[i];
        for (int j = 1; j < LOG; j++) {
            for (int i = 0; i + (1 << j) <= n; i++) {
                sparse[j][i] = min(sparse[j - 1][i],
                                   sparse[j - 1][i + (1 << (j - 1))]);
            }
        }
    }

    int rmq(int l, int r) {
        if (l > r) return 0;
        int k = __lg(r - l + 1);
        return min(sparse[k][l], sparse[k][r - (1 << k) + 1]);
    }

    // LCP of suffixes starting at positions a and b
    int lcpOf(int a, int b) {
        if (a == b) return n - a;
        int ra = inv[a], rb = inv[b];
        if (ra > rb) swap(ra, rb);
        return rmq(ra + 1, rb);
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string S;
    cin >> S;

    // Edge case: empty string
    if (S.empty()) {
        int Q; cin >> Q;
        while (Q--) cout << "=\n";
        return 0;
    }

    SuffixArray sa(S);

    int Q;
    cin >> Q;
    while (Q--) {
        int a, b, len;
        cin >> a >> b >> len;
        a--; b--; // convert to 0-indexed

        int commonLen = sa.lcpOf(a, b);
        if (commonLen >= len) {
            cout << "=\n";
        } else {
            if (S[a + commonLen] < S[b + commonLen])
                cout << "<\n";
            else
                cout << ">\n";
        }
    }

    return 0;
}

Source Files and Assets

Raw files are still available here when you want the original TeX, C++, or statement assets.

Show raw files