Vocabulary and theory the string algorithms rest on (borders and periods, Fine–Wilf, runs), the tool-choice table, brackets, expression parsing, run-length encoding and compression ideas, and string DP. Uses prefixFunction / kmpAutomaton from Prefix Function and Z.

TermMeaning
substring / subsequencecontiguous block / any positions in order
prefix, suffix (proper: not the whole string),
borderboth a proper prefix and a proper suffix
period for all valid
rotation
palindromeequal to its reverse
runmaximal periodic substring with period $p \le \frac{

A string of length has substrings (not all distinct) and subsequences. Lexicographic order compares character by character, a proper prefix being smaller; it is not length order ("z" > "abc"). Smallest rotation: Booth or Duval (Lyndon Words); comparing substrings in : hashing + LCP binary search or a suffix array with ranks; -th substring: suffix automaton with path counts.

Border–period duality: has a border of length iff it has period . So the prefix function answers periodicity: all periods come from the border chain, the smallest is , and is a power of a shorter string iff and . Fine–Wilf: if has periods and and , it has period ; the bound is tight. It underlies constant-space matching (Galil–Seiferas, Crochemore–Perrin: Theoretical Algorithms) and the analysis of runs. Runs theorem (Bannai et al. 2015): a string has fewer than runs, all found in via Lyndon roots and LCE queries; Main–Lorentz’s divide and conquer is easier (Runs and Periodicity).

TaskTool
one pattern in a textKMP, Z, or hashing
many patternsAho–Corasick
compare arbitrary substringshashing, or suffix array + LCP
count distinct substringssuffix automaton or (hashing per length , trie of suffixes )
longest common substring of two stringssuffix automaton of one, run the other
all palindromic substringseertree or Manacher
periods and bordersprefix function
smallest rotationBooth / Duval
-th substring lexicographicallysuffix automaton
LZ factorisation, repetitivenesssuffix automaton (Suffix Automaton)
vector<int> allPeriods(const string& s) {                  // increasing, ends with n
    int n = sz(s); vector<int> res; if (n == 0) return res;
    vector<int> pi = prefixFunction(s);
    for (int b = pi[n - 1]; b > 0; b = pi[b - 1]) res.push_back(n - b);
    res.push_back(n); return res;
}
int smallestPeriod(const string& s) { return sz(s) - prefixFunction(s).back(); }
bool isRepetition(const string& s) { int p = smallestPeriod(s); return p != sz(s) && sz(s) % p == 0; }

Brackets

vector<int> matchBrackets(const string& s) {               // partner index, -1 if unmatched
    vector<int> mate(sz(s), -1), st;
    for (int i = 0; i < sz(s); i++) {
        if (s[i] == '(') st.push_back(i);
        else if (s[i] == ')' && !st.empty()) mate[i] = st.back(), mate[st.back()] = i, st.pop_back();
    }
    return mate;
}
bool isBalanced(const string& s) { int bal = 0; for (char c : s) { bal += c == '(' ? 1 : -1; if (bal < 0) return false; } return bal == 0; }
pair<vector<int>, vector<int>> bracketForest(const string& s) {   // par[i]: '(' index of the pair directly enclosing pair i
    vector<int> par(sz(s), -1), roots, st, mate = matchBrackets(s);
    for (int i = 0; i < sz(s); i++) {
        if (s[i] == '(' && mate[i] != -1) { if (!st.empty()) par[i] = st.back(); else roots.push_back(i); st.push_back(i); }
        else if (s[i] == ')' && mate[i] != -1) st.pop_back();
    }
    return {par, roots};
}
struct BracketInfo {                                        // segment tree node: longest correct subsequence (CF 380C)
    int matched = 0, open = 0, close = 0;
    static BracketInfo of(char c) { return c == '(' ? BracketInfo{0, 1, 0} : BracketInfo{0, 0, 1}; }
};
BracketInfo operator+(const BracketInfo& a, const BracketInfo& b) {
    int t = min(a.open, b.close);
    return {a.matched + b.matched + t, a.open + b.open - t, a.close + b.close - t};
}

A string is balanced iff all prefix balances are and the total is 0; “is balanced” is a range-minimum of prefix balances (lazy segment tree for flips). The longest correct bracket subsequence of a range is matched of the merged BracketInfo ( per query); bracket forests with subtree sizes appear in CF 1821E.

Expression parsing

Recursive descent with precedence low to high: + -, then * /, then unary + -, then ^ (right-associative: 2^3^2 = 2^(3^2), and -2^2 = -(2^2) = ). Grammar without left recursion: expr := term (('+'|'-') term)*, term := unary (('*'|'/') unary)*, unary := ('+'|'-') unary | power, power := atom ('^' unary)?, atom := number | '(' expr ')'.

struct ExprParser {                                         // O(length); asserts on malformed input
    string s; size_t pos = 0;
    explicit ExprParser(string str) : s(move(str)) {}
    long double parse() { long double v = parseExpr(); assert(peek() == '\0'); return v; }
    void skipSpaces() { while (pos < s.size() && isspace((unsigned char)s[pos])) pos++; }
    char peek() { skipSpaces(); return pos < s.size() ? s[pos] : '\0'; }
    long double parseExpr() { long double v = parseTerm(); for (char c; (c = peek()) == '+' || c == '-';) { pos++; long double t = parseTerm(); v = c == '+' ? v + t : v - t; } return v; }
    long double parseTerm() { long double v = parseUnary(); for (char c; (c = peek()) == '*' || c == '/';) { pos++; long double t = parseUnary(); v = c == '*' ? v * t : v / t; } return v; }
    long double parseUnary() { char c = peek(); if (c == '+') { pos++; return parseUnary(); } if (c == '-') { pos++; return -parseUnary(); } return parsePower(); }
    long double parsePower() { long double b = parseAtom(); if (peek() == '^') { pos++; return powl(b, parseUnary()); } return b; }
    long double parseAtom() {
        if (peek() == '(') { pos++; long double v = parseExpr(); assert(peek() == ')'); pos++; return v; }
        skipSpaces(); size_t st = pos;
        while (pos < s.size() && (isdigit((unsigned char)s[pos]) || s[pos] == '.')) pos++;
        assert(pos > st); return strtold(s.substr(st, pos - st).c_str(), nullptr);
    }
};

Compression ideas

Compression is rarely the goal in contests, but its ideas are constant problem structure.

IdeaContest form
run-length encoding characters but runs: solve on the runs; range-assign as an interval map (Chtholly tree, Chtholly Tree); a run of length contains constant substrings
LZ77 / LZ factorisationgreedy longest earlier-occurring factors; factors measure repetitiveness; basis of the runs algorithm; greedy minimises the number of factors; (BWT runs)
Huffmanmerge the two lightest repeatedly (“merge piles, cost = sum”: Greedy); with fixed leaf order use Hu–Tucker / Knuth-optimised interval DP (Knuth Optimization)
Burrows–Wheelerlast column of sorted rotations; from the suffix array; invertible by LF-mapping; FM-index with a wavelet tree (Burrows–Wheeler)
entropy lower bound for lossless codes (Huffman within 1 bit, arithmetic coding arbitrarily close); the same counting gives for comparison sorting
vector<pair<char, int>> rle(const string& s) {
    vector<pair<char, int>> r;
    for (int i = 0, j; i < sz(s); i = j) { for (j = i; j < sz(s) && s[j] == s[i]; j++) {} r.push_back({s[i], j - i}); }
    return r;
}
ll constantSubstrings(const string& s) { ll t = 0; for (auto [c, L] : rle(s)) t += (ll)L * (L + 1) / 2; return t; }

String DP

ShapeProblems
two sequences dp[i][j]LCS; edit distance; longest common substring (reset to 0 on mismatch); regex / wildcard (* = consume or skip); interleaving; distinct-subsequence counts; shortest common supersequence (Sequence DP, Hirschberg for linear space)
interval dp[l][r]longest palindromic subsequence; min insertions to a palindrome ; remove boxes / Zuma (extra “attached” dimension); counting palindromic subsequences; parenthesisation (Interval DP)
automaton dp[i][state]strings avoiding one pattern (KMP automaton) or several (Aho–Corasick); distinct substrings / -th substring (suffix automaton); dictionary strings (trie); any regular property (DFA); huge length: matrix power over the automaton
Building blockPrecompute
is a palindrome table, hashes, or Manacher
next occurrence of at or after nxt[i][c]: turns subsequence matching into jumps
, longest common extensionhashing, suffix array + LCP + sparse table
vector<array<int, 26>> nextOccurrence(const string& s) {   // nxt[i][c] = smallest j >= i with s[j] = c, or n
    int n = sz(s); vector<array<int, 26>> nxt(n + 1); nxt[n].fill(n);
    for (int i = n - 1; i >= 0; i--) { nxt[i] = nxt[i + 1]; nxt[i][s[i] - 'a'] = i; }
    return nxt;
}
ll distinctSubsequences(const string& s, ll mod) {          // including the empty one
    int n = sz(s); vector<ll> dp(n + 1, 0); dp[0] = 1; vector<int> last(26, -1);
    for (int i = 0; i < n; i++) {
        dp[i + 1] = 2 * dp[i] % mod;
        int c = s[i] - 'a'; if (last[c] != -1) dp[i + 1] = (dp[i + 1] - dp[last[c]] + mod) % mod;
        last[c] = i;
    }
    return dp[n];
}
ll countAvoiding(const string& pat, int L, int A, ll mod) { // strings of length L over 'a'..'a'+A-1 not containing pat
    int m = sz(pat); auto aut = kmpAutomaton(pat); vector<ll> dp(m, 0); dp[0] = 1;
    for (int i = 0; i < L; i++) {
        vector<ll> nd(m, 0);
        for (int st = 0; st < m; st++) if (dp[st]) for (int c = 0; c < A; c++) { int ns = aut[st][c]; if (ns < m) nd[ns] = (nd[ns] + dp[st]) % mod; }
        dp = nd;
    }
    ll r = 0; for (ll x : dp) r = (r + x) % mod; return r;
}