Three DPs on sequences with a lot of structure: LIS has an greedy-DP, LCS reduces to LIS for permutations and runs in bit-parallel, and edit distance has banded and variants. All three can produce the actual answer in linear memory with Hirschberg’s divide and conquer. For LIS of every subarray see Range LIS.

LIS in

tail[k] = smallest possible last element of an increasing subsequence of length . It is strictly increasing, so each new element replaces the first tail (strict LIS) or (non-decreasing). A mental model: this is literally the card game patience — deal each card onto the leftmost pile whose top is it; the number of piles is the LIS length, which is why the greedy replacement never loses an achievable length.

WantSearch
strictly increasinglower_bound
non-decreasingupper_bound
strictly decreasingreverse the array (or negate values), then strictly increasing
non-increasingreverse, then non-decreasing

tail is not an LIS

tail holds the best last-value per length, not an actual achievable subsequence — the values at different indices were never simultaneously the tail at the same time. Reconstructing a real LIS needs the separate predecessor/length bookkeeping the code above already does (par, replayed from tailIdx.back()).

vector<int> lis(const vector<int>& a) {                  // returns indices of one strictly increasing LIS
    vector<int> tailVal, tailIdx, par(sz(a), -1);
    for (int i = 0; i < sz(a); i++) {
        int k = lower_bound(all(tailVal), a[i]) - tailVal.begin();   // upper_bound: non-decreasing
        if (k) par[i] = tailIdx[k - 1];
        if (k == sz(tailVal)) tailVal.push_back(a[i]), tailIdx.push_back(i);
        else tailVal[k] = a[i], tailIdx[k] = i;
    }
    vector<int> res;
    for (int i = sz(tailIdx) ? tailIdx.back() : -1; i != -1; i = par[i]) res.push_back(i);
    reverse(all(res)); return res;
}
VariantHow
number of LISFenwick over values storing (best length, count), merged by max then sum
weighted LIS (max sum increasing)Fenwick over values storing max
LIS with segment tree max over a value window
longest bitonicLIS ending at from the left plus from the right
2D chains (envelopes)sort by ascending, ties by descending, then strict LIS on
3D chainsCDQ (CDQ),
LCS of two permutationsmap to positions in , then LIS
min deletions to sort
min number of increasing sequences coveringlongest non-increasing subsequence
LIS of all cyclic shifts / all subarraysseaweed / unit-Monge (Range LIS)
// number of strictly increasing subsequences of maximum length (values compressed to [0, V))
struct Info { int len = 0; mint cnt = 0; };
Info comb(Info a, Info b) { if (a.len != b.len) return a.len > b.len ? a : b; return {a.len, a.cnt + b.cnt}; }
// for each i in order: Info best = prefixQuery(a[i]) (values < a[i]); if (!best.len) best.cnt = 1;
//                      update(a[i], {best.len + 1, best.cnt}); the answer is the query over all values

LCS

if , else . ; reconstruct by walking back.

Counting the number of maximum-length LCSs (not distinct LCS strings, the simpler “how many ways” question): carry a count alongside the length, but guard against double-counting when and , tie — both branches may share alignments already counted in , so subtract cnt[i-1][j-1] by inclusion–exclusion in that case.

Bit-parallel LCS (, about 0.2 s for ): the row differences are 0/1, so the row is a bit vector, and one row update is a few word operations with a carry (Hyyrö / Crochemore et al.).

struct BitLCS {                                          // b fixed; stream a; LCS(a, b[0, j)) queries
    int m, W; map<int, vector<ull>> mask; vector<ull> v; // v bit j = 0 iff the row increases at j
    BitLCS(const vector<int>& b) : m(sz(b)), W((sz(b) + 63) / 64), v(W, ~0ULL) {
        for (int j = 0; j < m; j++) { auto& mk = mask[b[j]]; if (mk.empty()) mk.assign(W, 0); mk[j >> 6] |= 1ULL << (j & 63); }
    }
    void push(int c) {
        auto it = mask.find(c); if (it == mask.end()) return;
        ull carry = 0;
        for (int w = 0; w < W; w++) {                   // v = (v + (v & M)) | (v & ~M)
            ull x = v[w], y = x & it->second[w], s = x + y, over = s < x;
            s += carry; carry = over | (s < carry);
            v[w] = s | (x & ~it->second[w]);
        }
    }
    int prefix(int j) const {
        int r = 0;
        for (int w = 0; w < j >> 6; w++) r += 64 - __builtin_popcountll(v[w]);
        if (j & 63) r += (j & 63) - __builtin_popcountll(v[j >> 6] & ((1ULL << (j & 63)) - 1));
        return r;
    }
};

Other LCS facts:

  • Hunt–Szymanski: where = number of matching pairs, via LIS over match lists processed right-to-left within a row.
  • LCS of strings is (NP-hard for unbounded ); no for two strings under SETH.
  • Longest common substring is a different problem: suffix automaton or suffix array (Suffix Array).
  • Longest palindromic subsequence = LCS, or interval DP.
  • Shortest common supersequence = .
  • Longest common increasing subsequence: for each , sweep keeping the best with : .
  • Count distinct LCS strings: next-occurrence DP from each over the alphabet, .

Edit distance

— this is the Levenshtein distance. Applications: spell checking, DNA alignment, diff/version control, OCR post-correction, fuzzy search, plagiarism detection, speech-recognition scoring.

VariantChange
insert/delete only (LCS distance)
Hamming (replace only)requires , no insert/delete branches
weighted operationsreplace the 1s by costs
alphabet-dependent costscost[a[i]][b[j]] instead of a flat
Damerau (adjacent transpositions)extra branch if
affine gaps (Gotoh)three layers: match, gap in , gap in
local alignment (Smith–Waterman)clamp at 0, take the global max
pattern occurrences with errorsfree start in the text:
answer knownband :
distance small, long stringsdiagonal furthest-reaching (Ukkonen/Myers ; with LCP queries, Landau–Vishkin )
need the alignment in linear memoryHirschberg

Hirschberg: the alignment in memory

The same divide-at-the-midpoint skeleton, reused for speed rather than memory, is exactly Knuth’s optimization and D&C DP optimization — worth noticing once you’ve internalised this version.

Split at , compute the last row of the forward DP for against all prefixes of and of the backward DP for against all suffixes, pick the column maximising the sum, and recurse on both halves.

vector<int> lcsRow(const string& a, const string& b) {
    vector<int> p(sz(b) + 1), c(sz(b) + 1);
    for (char x : a) { for (int j = 1; j <= sz(b); j++) c[j] = x == b[j - 1] ? p[j - 1] + 1 : max(p[j], c[j - 1]); swap(p, c); }
    return p;
}
string hirschberg(const string& a, const string& b) {
    if (a.empty() || b.empty()) return "";
    if (sz(a) == 1) return b.find(a[0]) != string::npos ? a : "";
    int i = sz(a) / 2;
    string ra(a.rbegin(), a.rend() - i), rb(b.rbegin(), b.rend());
    auto F = lcsRow(a.substr(0, i), b), G = lcsRow(ra, rb);
    int k = 0;
    for (int j = 0; j <= sz(b); j++) if (F[j] + G[sz(b) - j] > F[k] + G[sz(b) - k]) k = j;
    return hirschberg(a.substr(0, i), b.substr(0, k)) + hirschberg(a.substr(i), b.substr(k));
}