Answer “length of the longest strictly increasing subsequence of ” for many ranges. One kernel array of length answers every range:

is computed in via seaweed combing and the unit-Monge (sticky braid) product. Queries are then 2D dominance counts: offline sweep + Fenwick in , or online with a wavelet matrix (count with ; note always).

Simpler cases first:

  • few queries: patience sorting per query, (LIS, LCS and Edit Distance);
  • all prefixes or all suffixes: one patience sort;
  • queries with fixed : sweep with one patience sort from ;
  • small values (): segment tree storing a table “LIS using values in ”, merged in or with monotone tricks.

Seaweeds

LIS of is the LCS of with the sorted list of distinct values (strict) or of all values with ties broken right-to-left (strict) / left-to-right (non-strict). Reduce to a permutation by ranking values, then draw the grid: rows = values (top to bottom: increasing), columns = positions. Cell is a match iff .

Send a strand (seaweed) into the top of every column and into the left of every row. In each cell, two strands meet: one going down, one going right.

  • At a match cell they bounce: the strand from the top leaves through the right side, the strand from the left leaves through the bottom.
  • At a mismatch cell they cross (the top strand continues down, the left strand continues right), unless the two strands have already crossed earlier, in which case they bounce.

is the column where the strand that entered column from the top leaves the bottom, or if it leaves through the right side.

Fast combing via the sticky product

Combing cell by cell is . Faster: split the values into the lower half and the upper half. Each half is a strip of rows; the columns whose value is in the other half are just straight vertical strands in that strip. Comb each strip recursively (as a permutation of half the size), lift the result to a braid on strands, and multiply the two braids with the sticky product (the unit-Monge distance product, min-plus product of the corresponding Monge matrices), which the steady ant algorithm computes in . Total , about 0.6 s at .

Implementation

// c = a then b as sticky braids, permutations of length N, O(N log N). pool: scratch of 24 N ints.
void stickyRec(const int* a, const int* b, int N, int* c, int* pool) {
    if (N == 1) { c[0] = 0; return; }
    int h = N / 2, m = N - h;
    int *aLoRows = pool, *aHiRows = aLoRows + h, *rk = aHiRows + m, *loCols = rk + N, *hiCols = loCols + h;
    int *aLo = hiCols + m, *aHi = aLo + h, *bLo = aHi + m, *bHi = bLo + h, *cLo = bHi + m, *cHi = cLo + h;
    int *rowCol = cHi + m, *colRow = rowCol + N, *bnd = colRow + N, *dAt = bnd + N + 1, *rest = dAt + N + 1;
    int nl = 0, nh = 0;
    for (int i = 0; i < N; i++) (a[i] < h ? aLoRows[nl++] : aHiRows[nh++]) = i;
    for (int k = 0; k < N; k++) rk[k] = -1;
    for (int j = 0; j < h; j++) rk[b[j]] = 0;
    nl = nh = 0;
    for (int k = 0; k < N; k++) { if (rk[k] == 0) rk[k] = nl, loCols[nl++] = k; else rk[k] = nh, hiCols[nh++] = k; }
    for (int t = 0; t < h; t++) aLo[t] = a[aLoRows[t]], bLo[t] = rk[b[t]];
    for (int t = 0; t < m; t++) aHi[t] = a[aHiRows[t]] - h, bHi[t] = rk[b[t + h]];
    stickyRec(aLo, bLo, h, cLo, rest);
    stickyRec(aHi, bHi, m, cHi, rest);
    for (int t = 0; t < h; t++) { int r = aLoRows[t], col = loCols[cLo[t]]; rowCol[r] = col, colRow[col] = r; }
    for (int t = 0; t < m; t++) { int r = aHiRows[t], col = hiCols[cHi[t]]; rowCol[r] = ~col, colRow[col] = ~r; }
    // delta(i, k) = #{high: row < i, col < k} - #{low: row >= i, col >= k}, monotone in i and k
    auto colStep = [&](int k, int r) { return colRow[k] < 0 ? ~colRow[k] < r : colRow[k] >= r; };
    auto rowStep = [&](int i, int k) { return rowCol[i] < 0 ? ~rowCol[i] < k : rowCol[i] >= k; };
    int k = 0, d = 0; bnd[N] = 0, dAt[N] = 0;
    for (int i = N - 1; i >= 0; i--) {                          // bnd[i] = first k with delta(i, k) >= 0
        d -= rowStep(i, k);
        while (d < 0) d += colStep(k, i), k++;
        bnd[i] = k, dAt[i] = d;
    }
    for (int i = 0; i < N; i++) {
        int kb = bnd[i + 1], ks = max(kb - 1, 0), own = rowCol[i];
        int e1 = kb >= 1 ? dAt[i + 1] - colStep(kb - 1, i + 1) : dAt[i + 1];
        int e0 = e1 - rowStep(i, ks);
        c[i] = -1;
        for (int x = ks; x < bnd[i]; x++) {                     // cells along the staircase
            int n1 = e1 + colStep(x, i + 1), n0 = e0 + colStep(x, i);
            if ((own == ~x) + min(n0, 0) - min(e0, 0) - min(n1, 0) + min(e1, 0) == 1) c[i] = x;
            e1 = n1, e0 = n0;
        }
        if (c[i] == -1) c[i] = own >= 0 ? own : ~own;            // away from the staircase the half keeps its point
    }
}
vector<int> stickyProduct(const vector<int>& a, const vector<int>& b) {
    int N = sz(a); vector<int> c(N), pool(24 * N + 64);
    if (N) stickyRec(a.data(), b.data(), N, c.data(), pool.data());
    return c;
}
// braid of permutation p on 2n strands. starts: s < n = left end of row n-1-s, s >= n = top of column s-n.
// ends: e < n = bottom of column e, e >= n = right end of row 2n-1-e.
vector<int> seaweedBraid(const vector<int>& p) {
    int n = sz(p);
    if (n == 0) return {};
    if (n == 1) return {0, 1};
    int h = n / 2, m = n - h;
    vector<int> colsLo, colsHi, qLo, qHi, idxLo(n, -1), idxHi(n, -1);
    for (int i = 0; i < n; i++) {
        if (p[i] < h) idxLo[i] = sz(colsLo), colsLo.push_back(i), qLo.push_back(p[i]);
        else idxHi[i] = sz(colsHi), colsHi.push_back(i), qHi.push_back(p[i] - h);
    }
    vector<int> bLo = seaweedBraid(qLo), bHi = seaweedBraid(qHi), top(2 * n), bot(2 * n);
    auto topEnd = [&](int e) { return e < h ? m + colsLo[e] : 2 * n - 2 * h + e; };
    auto botEnd = [&](int e) { return e < m ? colsHi[e] : n - m + e; };
    for (int s = 0; s < m; s++) top[s] = s;
    for (int s = m; s < n; s++) top[s] = topEnd(bLo[s - m]);
    for (int c = 0; c < n; c++) top[n + c] = idxLo[c] < 0 ? m + c : topEnd(bLo[h + idxLo[c]]);
    for (int s = 0; s < m; s++) bot[s] = botEnd(bHi[s]);
    for (int c = 0; c < n; c++) bot[m + c] = idxHi[c] < 0 ? c : botEnd(bHi[m + idxHi[c]]);
    for (int s = 2 * n - h; s < 2 * n; s++) bot[s] = s;
    return stickyProduct(top, bot);
}
template<class T> struct RangeLIS {
    int n; vector<int> K;
    RangeLIS(const vector<T>& a, bool strict = true) : n(sz(a)), K(n) {
        vector<int> ord(n), p(n); iota(all(ord), 0);
        stable_sort(all(ord), [&](int i, int j) { return a[i] != a[j] ? a[i] < a[j] : strict ? i > j : i < j; });
        for (int r = 0; r < n; r++) p[ord[r]] = r;
        vector<int> br = seaweedBraid(p);
        for (int j = 0; j < n; j++) K[j] = br[n + j] < n ? br[n + j] : n;
    }
    vector<int> query(const vector<pair<int, int>>& qs) const {   // offline, [l, r)
        vector<vector<int>> byR(n + 1), atK(n + 1);
        for (int i = 0; i < sz(qs); i++) byR[qs[i].second].push_back(i);
        for (int j = 0; j < n; j++) atK[K[j]].push_back(j);
        Fenwick<int> fw(n); vector<int> res(sz(qs));
        for (int r = 0; r <= n; r++) {
            if (r) for (int j : atK[r - 1]) fw.add(j, 1);           // now holds all j with K[j] < r
            for (int i : byR[r]) { auto [l, rr] = qs[i]; res[i] = (rr - l) - fw.sum(l, n); }
        }
        return res;
    }
};

Ties: for strictly increasing, equal values are ranked right-to-left, so two equal values can never both be in an increasing chain; for non-decreasing, left-to-right so they always can. Library Checker “Static Range LIS Query”.

  • Semi-local LCS of two strings (LCS of with every substring of ) uses the same seaweed kernel; combing a grid directly costs and gives all answers implicitly (Other Pattern Matching).
  • Unit-Monge product in general: the min-plus product of two unit-Monge matrices in ; also used for range LCS queries, “longest increasing subsequence in a cyclic shift”, and merging DP over strings.