← All Posts
Combinatorics· Part 5 of 16

Young Tableaux — RSK Correspondence & Hook Length Formula

What Are Young Tableaux?

Young tableaux are combinatorial objects that sit at the crossroads of algebra, representation theory, and — surprisingly — competitive programming. They give us elegant ways to count permutations, compute longest increasing subsequences, and reason about symmetric functions.

Partitions

A partition of a positive integer n is a way to write n as an unordered sum of positive integers. We write partitions in weakly decreasing order. For example, the partitions of 5 are:

A partition λ = (λ₁, λ₂, …, λ_k) satisfies λ₁ ≥ λ₂ ≥ … ≥ λ_k > 0 and λ₁ + λ₂ + … + λ_k = n.

Young Diagrams

Every partition has a visual representation called a Young diagram (or Ferrers diagram): an array of boxes arranged in left-justified rows, where row i has λ_i boxes. Here is the Young diagram for λ = (4, 3, 1):

    
   
 

The conjugate partition λ' is obtained by transposing the diagram (reflecting across the main diagonal). For λ = (4, 3, 1), the conjugate is λ' = (3, 2, 2, 1):

   
  
  
 

Standard Young Tableaux (SYT)

A Standard Young Tableau of shape λ ⊢ n is a filling of the Young diagram with the numbers 1, 2, …, n (each used exactly once) such that entries increase along every row (left to right) and every column (top to bottom).

Here are the two standard Young tableaux of shape (3, 2):

Tableau 1

123
45

Tableau 2

124
35

Tableau 3

125
34

Tableau 4

134
25

Tableau 5

135
24

So f(3,2) = 5. We will soon see a beautiful formula that computes this without enumeration.

Semistandard Young Tableaux (SSYT)

A Semistandard Young Tableau relaxes the rules: entries are drawn from a set {1, 2, …, m} (repetition allowed), rows are weakly increasing, and columns are strictly increasing.

SSYT of shape (3,2) with entries in {1,2,3}

112
23

SSYT play a central role in the theory of symmetric functions and Schur polynomials, which we touch on later.

Key distinction: SYT use each number 1…n exactly once and require strict increase in both rows and columns. SSYT allow repeated entries, requiring weak increase in rows and strict increase in columns.

The Hook Length Formula

Counting the number of standard Young tableaux fλ for a given shape λ is a fundamental problem. The Frame–Robinson–Thrall hook length formula gives an elegant closed-form answer.

Hook Length Definition

For a cell u = (i, j) in a Young diagram of shape λ, the hook of u consists of:

The hook length h(u) = arm(u) + leg(u) + 1.

Here is λ = (4, 3, 1) with the hook of cell (1,1) highlighted:

● → → →
↓    
↓

h(1,1) = 3 (arm) + 2 (leg) + 1 = 6

The Formula

Hook Length Formula: The number of standard Young tableaux of shape λ ⊢ n is:

fλ = n! / ∏u ∈ λ h(u)

where the product runs over all cells u in the Young diagram.

Worked Example: λ = (3, 2)

Step 1: Draw the diagram and compute hook lengths for every cell.

431
21

Hook lengths for each cell in λ = (3, 2)

Let us verify each hook length:

Step 2: Compute fλ = 5! / (4 · 3 · 1 · 2 · 1) = 120 / 24 = 5.

This matches our enumeration of 5 standard Young tableaux above. ✓

Another Example: λ = (4, 3, 1)

6431
311
1

Hook lengths for λ = (4, 3, 1)

f(4,3,1) = 8! / (6 · 4 · 3 · 1 · 3 · 1 · 1 · 1) = 40320 / 216 = 70 × (40320/216 = 186.67)…

Wait — let me recompute. 6 × 4 × 3 × 1 × 3 × 1 × 1 × 1 = 216. And 40320 / 216 ≈ 186.67, which is not an integer. Let me re-check the hook lengths.

Common pitfall: Be extremely careful computing hook lengths. The leg of a cell (i, j) counts cells strictly below it in column j, i.e., the number of rows r > i such that λ_r ≥ j. Let me redo this.

For λ = (4, 3, 1), the rows have lengths 4, 3, 1:

6431
421
1

Corrected hook lengths for λ = (4, 3, 1)

f(4,3,1) = 8! / (6 · 4 · 3 · 1 · 4 · 2 · 1 · 1) = 40320 / 576 = 70. ✓

RSK Correspondence

The Robinson–Schensted–Knuth (RSK) correspondence is one of the most important bijections in combinatorics. It maps a permutation σ of {1, …, n} to a pair (P, Q) of standard Young tableaux of the same shape λ ⊢ n.

RSK Theorem: The RSK correspondence is a bijection:
S_n  ↔  ⊔λ⊢n SYT(λ) × SYT(λ)

Summing over all partitions: n! = Σλ⊢n (fλ)². This identity counts permutations by summing squares of SYT counts — a consequence of the representation theory of S_n.

Row Insertion Algorithm

The algorithm builds two tableaux simultaneously: the insertion tableau P and the recording tableau Q. For each element of the permutation (read left to right), we perform row insertion into P, and record where the new cell was created in Q.

Row-insert(P, x):

function RowInsert(P, x):
  row ← 1
  while row ≤ number_of_rows(P):
    // Find the leftmost element in this row that is strictly > x
    j ← position of smallest entry in P[row] that is > x
    if no such entry exists:
      append x to end of P[row]
      return (row, len(P[row]))
    else:
      swap x with P[row][j]   // x "bumps" P[row][j]
      row ← row + 1
  // x was bumped out of all existing rows — create a new row
  P[row] ← [x]
  return (row, 1)

Step-by-Step Example

Let σ = (3, 1, 4, 1, 5, 9, 2, 6) — but since we need a permutation of {1,…,n}, let us use σ = (4, 2, 7, 3, 6, 1, 5) (a permutation of {1,…,7}).

Insert 4: P is empty, so P = [[4]]. New cell at (1,1), so Q records 1 at (1,1).

P

4

Q

1

Insert 2: 2 < 4, so 2 bumps 4 from row 1. Then 4 goes to row 2 (new row).

P

2
4

Q

1
2

Insert 7: 7 > 2 in row 1, append to row 1. New cell at (1,2).

P

27
4

Q

13
2

Insert 3: 3 bumps 7 from row 1 (smallest element > 3 is 7). Then 7 > 4 in row 2, so append. New cell at (2,2).

P

23
47

Q

13
24

Insert 6: 6 > 3 in row 1, so check: smallest entry > 6 in row 1 {2,3}? Neither is > 6. Append 6 to row 1. New cell at (1,3).

P

236
47

Q

135
24

Insert 1: 1 bumps 2 from row 1. Then 2 bumps 4 from row 2. Then 4 starts row 3.

P

136
27
4

Q

135
24
6

Insert 5: In row 1 {1,3,6}: smallest > 5 is 6. So 5 bumps 6. In row 2 {2,7}: smallest > 6 is 7. So 6 bumps 7. In row 3 {4}: 7 > 4, append. New cell at (3,2).

P (final)

135
26
47

Q (final)

135
24
67

Both P and Q have shape (3, 2, 2) ⊢ 7. Notice that P is a valid SYT (rows and columns increase) and Q is also a valid SYT recording the order in which cells were created.

Properties of RSK

LIS via RSK

One of the most celebrated connections in combinatorics links the RSK correspondence to the Longest Increasing Subsequence (LIS) problem — a staple of competitive programming.

Schensted's Theorem (1961): If a permutation σ maps to tableaux (P, Q) of shape λ = (λ₁, λ₂, …) under RSK, then:
  • λ₁ (length of the first row) = length of the longest increasing subsequence of σ
  • λ'₁ (length of the first column, i.e., number of rows) = length of the longest decreasing subsequence of σ

Greene's Theorem (Generalization)

Greene's theorem extends Schensted's result. Let σ be a permutation and λ the shape of its RSK insertion tableau. Then for every k ≥ 1:

So the entire shape λ encodes a rich decomposition structure of the permutation.

Connection to Patience Sorting

The patience sorting algorithm (the card game analogy for computing LIS) is essentially the first-row projection of RSK. When you place cards into piles:

This gives us the well-known O(n log n) LIS algorithm: for each element, binary search for the leftmost pile whose top is ≥ the current element, or create a new pile.

Example: Verifying LIS

For our permutation σ = (4, 2, 7, 3, 6, 1, 5), the RSK insertion tableau P has shape (3, 2, 2).

CP insight: While RSK gives deep structural information, the O(n log n) patience-sorting / binary-search approach is almost always preferred in contests for the raw LIS problem. RSK becomes valuable when you need to count permutations with a given LIS length, or reason about the full decomposition into increasing/decreasing subsequences.

C++ Implementation

RSK Row Insertion

The following implements the full RSK correspondence, producing both the insertion tableau P and recording tableau Q:

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

// Row-insert value x into tableau P.
// Returns (row, col) of the newly created cell.
pair<int,int> rsk_insert(vector<vector<int>>& P, int x) {
    for (int i = 0; i < (int)P.size(); i++) {
        // Find leftmost element strictly greater than x
        auto it = upper_bound(P[i].begin(), P[i].end(), x);
        if (it == P[i].end()) {
            // x is larger than all elements in this row — append
            P[i].push_back(x);
            return {i, (int)P[i].size() - 1};
        }
        // Bump: swap x with *it, then continue inserting x into next row
        swap(x, *it);
    }
    // x was bumped out of every row — create a new row
    P.push_back({x});
    return {(int)P.size() - 1, 0};
}

// Full RSK: permutation sigma -> (P, Q)
pair<vector<vector<int>>, vector<vector<int>>>
rsk(const vector<int>& sigma) {
    vector<vector<int>> P, Q;
    for (int k = 0; k < (int)sigma.size(); k++) {
        auto [r, c] = rsk_insert(P, sigma[k]);
        // Extend Q if needed
        while ((int)Q.size() <= r)
            Q.push_back({});
        Q[r].push_back(k + 1);  // 1-indexed recording
    }
    return {P, Q};
}

void print_tableau(const vector<vector<int>>& T) {
    for (auto& row : T) {
        for (int v : row) cout << v << "\t";
        cout << "\n";
    }
}

int main() {
    vector<int> sigma = {4, 2, 7, 3, 6, 1, 5};
    auto [P, Q] = rsk(sigma);

    cout << "Insertion Tableau P:\n";
    print_tableau(P);
    cout << "\nRecording Tableau Q:\n";
    print_tableau(Q);
    cout << "\nLIS length = " << P[0].size() << "\n";
    cout << "LDS length = " << P.size() << "\n";
}

Output:

Insertion Tableau P:
1	3	5
2	6
4	7

Recording Tableau Q:
1	3	5
2	4
6	7

LIS length = 3
LDS length = 3

Complexity: Each insertion processes at most k rows where k is the number of rows in the tableau. With binary search in each row, the total RSK construction runs in O(n · k · log n) in the worst case. Since k ≤ √(2n) (as the number of cells is n and the shape is a partition), this is O(n1.5 log n) worst case, but typically much faster.

Hook Length Formula

Given a partition λ, compute fλ = n! / ∏ h(u). For competitive programming we often work modulo a prime, so we need modular inverse.

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

const long long MOD = 1e9 + 7;

long long power(long long base, long long exp, long long mod) {
    long long result = 1;
    base %= mod;
    while (exp > 0) {
        if (exp & 1) result = result * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return result;
}

long long mod_inv(long long a, long long mod) {
    return power(a, mod - 2, mod);
}

// Compute f^lambda mod p using the hook length formula.
// lambda: partition in decreasing order, e.g. {4, 3, 1}
long long count_syt(const vector<int>& lambda) {
    int n = 0;
    for (int x : lambda) n += x;

    // Precompute factorials
    vector<long long> fact(n + 1);
    fact[0] = 1;
    for (int i = 1; i <= n; i++)
        fact[i] = fact[i - 1] * i % MOD;

    // Compute product of hook lengths
    long long hook_prod = 1;
    int rows = lambda.size();
    // Conjugate partition for computing leg lengths
    int max_col = lambda[0];
    vector<int> conj(max_col + 1, 0);
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < lambda[i]; j++)
            conj[j]++;

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < lambda[i]; j++) {
            int arm = lambda[i] - j - 1;    // cells to the right
            int leg = conj[j] - i - 1;      // cells below
            int hook = arm + leg + 1;
            hook_prod = hook_prod * hook % MOD;
        }
    }

    return fact[n] % MOD * mod_inv(hook_prod, MOD) % MOD;
}

int main() {
    // f^{(3,2)} should be 5
    cout << "f^(3,2) = " << count_syt({3, 2}) << "\n";

    // f^{(4,3,1)} should be 70
    cout << "f^(4,3,1) = " << count_syt({4, 3, 1}) << "\n";

    // f^{(4,2,1)} = 7! / (5*3*1*3*1*1) = 5040 / 45 = 112
    // Let's verify: hooks for (4,2,1):
    //   (1,1): arm=3, leg=2 -> 6... let me just trust the code
    cout << "f^(4,2,1) = " << count_syt({4, 2, 1}) << "\n";

    // f^{(n)} = 1 (only one row, only one SYT)
    cout << "f^(5) = " << count_syt({5}) << "\n";

    // f^{(1,1,1,1)} = 1 (only one column, only one SYT)
    cout << "f^(1,1,1,1) = " << count_syt({1, 1, 1, 1}) << "\n";
}

LIS via Binary Search (Classic O(n log n))

For comparison, here is the standard LIS algorithm that mirrors the patience sorting / first-row-of-RSK logic:

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

// Returns LIS length for a sequence.
// This mirrors patience sorting, which is the first-row projection of RSK.
int lis_length(const vector<int>& a) {
    vector<int> tails;  // tails[i] = smallest tail of IS of length i+1
    for (int x : a) {
        auto it = lower_bound(tails.begin(), tails.end(), x);
        if (it == tails.end())
            tails.push_back(x);
        else
            *it = x;
    }
    return tails.size();
}

// RSK-based LIS: insert all elements, return first row length.
int lis_rsk(const vector<int>& a) {
    vector<vector<int>> P;
    for (int x : a) {
        int val = x;
        bool placed = false;
        for (auto& row : P) {
            auto it = upper_bound(row.begin(), row.end(), val);
            if (it == row.end()) {
                row.push_back(val);
                placed = true;
                break;
            }
            swap(val, *it);
        }
        if (!placed) P.push_back({val});
    }
    return P.empty() ? 0 : (int)P[0].size();
}

int main() {
    vector<int> a = {4, 2, 7, 3, 6, 1, 5};
    cout << "LIS (binary search): " << lis_length(a) << "\n";
    cout << "LIS (RSK):           " << lis_rsk(a) << "\n";

    // Larger random test
    srand(42);
    vector<int> big(100000);
    iota(big.begin(), big.end(), 1);
    shuffle(big.begin(), big.end(), mt19937(42));
    cout << "LIS of random perm of 100000: " << lis_length(big) << "\n";
}

O(n log n) Binary Search

  • Only computes LIS length
  • O(n log n) time, O(n) space
  • Preferred in contests for raw LIS
  • Easily adapted to LIS recovery

RSK Correspondence

  • Computes full insertion + recording tableaux
  • O(n√n log n) worst case
  • Gives LIS and LDS and full shape
  • Needed for counting/structural problems

Schur Polynomials

Young tableaux are the combinatorial backbone of Schur polynomials, a class of symmetric polynomials with deep connections to algebra, geometry, and (occasionally) competitive programming.

Definition via SSYT

The Schur polynomial sλ(x₁, x₂, …, x_m) is defined as:

Schur polynomial:
sλ(x₁, …, x_m) = ΣT xT

where the sum is over all semistandard Young tableaux T of shape λ with entries in {1, …, m}, and xT = ∏i x_i(number of i's in T).

Example: For λ = (2, 1) and m = 2, the SSYT with entries in {1, 2} are:

T₁: x₁²x₂

11
2

T₂: x₁x₂²

12
2

So s(2,1)(x₁, x₂) = x₁²x₂ + x₁x₂² = x₁x₂(x₁ + x₂).

Key Properties

CP Relevance

Schur polynomials rarely appear directly in contests, but the underlying ideas surface in:

CP Applications

1. LIS Variants

The most direct application. The RSK connection tells us that the number of permutations of {1,…,n} with LIS length ≤ k equals the number of pairs of SYT of the same shape λ where λ has at most k columns. Combined with the hook length formula, we can count such permutations.

Problem pattern: "Count permutations of length n with LIS exactly k." This requires computing Σ (fλ)² over partitions λ of n whose first row has length exactly k.

2. Involutions and Fixed-Point-Free Involutions

An involution is a permutation σ with σ² = id (equivalently, σ = σ⁻¹). Under RSK, σ = σ⁻¹ means P = Q. So involutions biject to single SYT:

This gives an independent derivation of the involution count formula. The number of involutions satisfies a(n) = a(n−1) + (n−1)·a(n−2).

3. Descent Sets and Permutation Statistics

The descent set of a permutation σ is Des(σ) = {i : σ(i) > σ(i+1)}. The recording tableau Q encodes the descent set: Des(Q) = Des(σ). This lets us count permutations with a given descent set using SYT enumeration.

4. Counting Standard Young Tableaux of Specific Shapes

Many counting problems reduce to computing fλ for specific families of partitions:

5. Selected Problems

Problem Concept Used Key Idea
CF 1621G LIS + Counting Count permutations with given LIS structure via RSK shape analysis
CF 1439E Young diagrams Partition manipulation, hook formula for counting
USACO Platinum — LIS counting Patience sorting Count number of LIS via patience sorting + DP on tableau shape
IOI 2005 — Mountains Dilworth's theorem Decompose into minimum number of decreasing subsequences = LIS length (dual of RSK first row)
Atcoder ARC 149E SYT counting Hook length formula applied to restricted partition shapes
Contest reality check: Pure Young tableaux problems are rare in standard CP. However, the ideas — LIS via patience sorting, counting permutations with given structure, partition-based enumeration — appear frequently. Understanding the RSK framework gives you a powerful mental model even when you don't implement it directly.

Summary

Concept Key Result CP Use
Young diagram Visual representation of partition λ Foundation for all tableau-based reasoning
SYT / SSYT Fillings with increasing rows/columns Counting structures, symmetric functions
Hook length formula fλ = n! / ∏ h(u) Count SYT for given shape in O(n) per partition
RSK correspondence σ ↔ (P, Q), n! = Σ (fλ)² Full permutation decomposition
Schensted's theorem First row of P = LIS length Why patience sorting works
Greene's theorem Shape encodes k-increasing subsequences Dilworth-style decompositions
Schur polynomials Generating function over SSYT Lattice path counting, non-intersecting paths

Quick Reference: Key Formulas

1. Hook Length Formula: fλ = n! / ∏u∈λ h(u)

2. RSK identity: n! = Σλ⊢n (fλ)²

3. Hook-Content Formula (SSYT count): |SSYT(λ, m)| = ∏u=(i,j)∈λ (m + j − i) / h(u)

4. Involution count: Inv(n) = Σλ⊢n fλ

5. Hook shape SYT: f(a, 1b) = C(a + b − 1, b)

Young tableaux bridge abstract algebra and algorithmic thinking. Even if you never directly implement RSK in a contest, understanding why patience sorting works, how LIS length connects to partition shapes, and how to count standard tableaux via hooks gives you a richer toolkit for attacking combinatorial problems.