DSA DSA In Depth
0%
lesson karatsubastrassendivide-and-conquer

Algebraic Divide-and-Conquer: Karatsuba Multiplication & Strassen's Matrix Product

7 min read Module 2.3: Binary Search on Monotonic Spaces & Divide-and-Conquer Updated Oct 23, 2026
📄 Cheat Sheet
Karatsuba Sub-Quadratic Multiplication & Algebraic Divide-and-Conquer
Stanford Algorithms (Tim Roughgarden) Karatsuba Sub-Quadratic Multiplication & Algebraic Divide-and-Conquer Watch on YouTube ↗

Tim Roughgarden derives Gauss's 3-multiplication algebraic trick that reduces O(n²) grade-school multiplication to O(n^(log₂ 3)) ≈ O(n^1.585).

0:00 / --:--

Trading Expensive Multiplications for Cheap Additions

When we divide a problem of size $n$ into subproblems of size $n/2$, the Master Theorem tells us that for $T(n) = a T(n/2) + \Theta(n^d)$ with $a > 2^d$, the asymptotic complexity is governed purely by the number of leaves in the recursion tree: $\Theta(n^{\log_2 a})$. Even reducing $a$ by one single recursive call changes the exponent of the polynomial!

1. Karatsuba Fast Integer & Polynomial Multiplication (1960)

Given two $n$-limb big integers (or degree-$n$ polynomials) $X$ and $Y$, split each at $m = \lfloor n/2 \rfloor$ into high and low halves in base $B^m$:

$$X = x_1 B^m + x_0, \qquad Y = y_1 B^m + y_0$$

Their product is:

$$X \cdot Y = \underbrace{x_1 y_1}_{z_2} B^{2m} + \underbrace{(x_1 y_0 + x_0 y_1)}_{z_1} B^m + \underbrace{x_0 y_0}_{z_0}$$

Naively computing $x_1 y_1, x_1 y_0, x_0 y_1, x_0 y_0$ requires $a = 4$ multiplications of size $n/2$, giving $T(n) = 4T(n/2) + \Theta(n) = \Theta(n^{\log_2 4}) = \Theta(n^2)$. Anatolii Karatsuba observed (following Gauss's complex multiplication trick) that $z_1$ can be recovered using only one additional multiplication plus two subtractions:

$$z_2 = x_1 y_1, \qquad z_0 = x_0 y_0, \qquad z_1 = (x_1 + x_0)(y_1 + y_0) - z_2 - z_0$$

Now $a = 3$, so $T(n) = 3T(n/2) + \Theta(n) = \Theta\!\left(n^{\log_2 3}\right) \approx \Theta\!\left(n^{1.58496}\right)$.

2. Strassen's Sub-Cubic Matrix Multiplication (1969)

Given two $n \times n$ matrices $A$ and $B$, partition each into four $\frac{n}{2} \times \frac{n}{2}$ quadrant blocks:

$$\begin{bmatrix} C_{11} & C_{12} \\ C_{21} & C_{22} \end{bmatrix} = \begin{bmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{bmatrix} \begin{bmatrix} B_{11} & B_{12} \\ B_{21} & B_{22} \end{bmatrix}$$

Standard block multiplication requires $8$ recursive $\frac{n}{2} \times \frac{n}{2}$ matrix products, giving $T(n) = 8T(n/2) + \Theta(n^2) = \Theta(n^3)$. Volker Strassen shocked the mathematical community by defining 7 intermediate block products $M_1, \dots, M_7$ using $18$ matrix additions/subtractions:

  • $M_1 = (A_{11} + A_{22})(B_{11} + B_{22})$
  • $M_2 = (A_{21} + A_{22}) B_{11}$
  • $M_3 = A_{11} (B_{12} - B_{22})$
  • $M_4 = A_{22} (B_{21} - B_{11})$
  • $M_5 = (A_{11} + A_{12}) B_{22}$
  • $M_6 = (A_{21} - A_{11})(B_{11} + B_{12})$
  • $M_7 = (A_{12} - A_{22})(B_{21} + B_{22})$

From which the four output quadrants are reconstructed in $\Theta(n^2)$ additions:

$$C_{11} = M_1 + M_4 - M_5 + M_7, \quad C_{12} = M_3 + M_5, \quad C_{21} = M_2 + M_4, \quad C_{22} = M_1 - M_2 + M_3 + M_6$$

This yields $T(n) = 7T(n/2) + \Theta(n^2) = \Theta\!\left(n^{\log_2 7}\right) \approx \Theta\!\left(n^{2.80735}\right)$.

Cooley-Tukey Radix-2 FFT Butterfly, Karatsuba & CDQ Divide-and-Conquer
O(N log N) FFT / O(N log² N) CDQ

Inspect the Radix-2 Cooley-Tukey FFT butterfly y_k = E_k + ω_n^k O_k, Karatsuba 3-multiplication algebraic identity, and CDQ Divide-and-Conquer left-to-right contribution transfer.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

Halving Lemma: Because ω_n^(k + n/2) = −ω_n^k and (ω_n^k)² = ω_{n/2}^k, a single product t = ω_n^k · O_k yields both y_k = E_k + t and y_{k+n/2} = E_k − t.
E_k O_k × ω_n^k y_k = E_k + ω_n^k·O_k y_{k+n/2} = E_k − ω_n^k·O_k
Butterfly Operations
1 Complex Multiply + 2 Adds
Stages / Width
log₂(N) stages × (N/2) butterflies
Polynomial Multiply
O(N log N) (vs O(N²) naive)
[Step 1] Combined even DFT E_k and twiddle-multiplied odd DFT ω_n^k·O_k across Radix-2 butterfly.
Algorithm Recurrence Asymptotic Time Practical Crossover Threshold
Schoolbook Big-Int / Poly Multiply $4T(n/2) + O(n)$ $\Theta(n^2)$ $n < 32$ limbs ($64$-bit words)
Karatsuba Multiplication $3T(n/2) + O(n)$ $\Theta(n^{1.585})$ $32 \le n \le 2000$ limbs
Toom-Cook (Toom-3) / FFT Multiplication $5T(n/3) + O(n)$ / FFT $\Theta(n^{1.465})$ / $\Theta(n \log n)$ $n > 2000$ limbs (GMP library)
Cache-Blocked GEMM vs. Strassen $8T(n/2)$ vs. $7T(n/2) + 18(n/2)^2$ $\Theta(n^3)$ vs. $\Theta(n^{2.807})$ Strassen wins for $n \ge 512\text{–}1024$
Master Theorem Case 1: Leaf-Dominated Recurrences

In both Karatsuba ($a=3, b=2, d=1$) and Strassen ($a=7, b=2, d=2$), the branching factor $a$ strictly exceeds $b^d$ ($3 > 2^1$ and $7 > 2^2$). Consequently, work grows geometrically down the recursion tree by a factor of $3/2$ or $7/4$ per level. Stopping the recursion at a tuned base-case leaf size $n_0$ (e.g., $n_0 = 32$ for Karatsuba, $n_0 = 64$ for Strassen) replaces the bottom $\log_2 n_0$ levels of recursive allocation overhead with vectorizable SIMD loops, speeding up total runtime by $5\times\text{–}15\times$!

Karatsuba Gauss Trick: Replacing 2 Cross-Multiplications with 1 Sum-Product Naive Divide & Conquer (4 Calls) 1. x_1 · y_1 (High × High) 2. x_1 · y_0 (High × Low) 3. x_0 · y_1 (Low × High) 4. x_0 · y_0 (Low × Low) Leaves at depth k: 4^k = n^2 Karatsuba Divide & Conquer (3 Calls) 1. z_2 = x_1 · y_1 2. z_0 = x_0 · y_0 3. z_m = (x_1 + x_0) · (y_1 + y_0) Recover z_1 = z_m - z_2 - z_0 (2 Subtractions) Leaves at depth k: 3^k = n^{1.585}
flowchart TD A["Multiply Polynomials A[0..n), B[0..n)"] --> B{"n <= 32?"} B -- "Yes (Base Case)" --> C["Schoolbook O(n^2) Nested Loop (L1 Cache + SIMD Auto-Vectorized)"] B -- "No (n > 32)" --> D["Split at m = n / 2: A = A_1 x^m + A_0, B = B_1 x^m + B_0"] D --> E["3 Recursive Calls of Size m: z_0 = A_0*B_0, z_2 = A_1*B_1, z_m = (A_0+A_1)*(B_0+B_1)"] E --> F["Combine in O(n): z_1 = z_m - z_2 - z_0 Add into Result at Offsets 0, m, 2m"]
Hardware Reality: Arena Scratch Buffers & IEEE-754 Stability in Strassen

Two practical factors govern algebraic D&C in production:

  1. Heap Allocation Thrashing: If each Karatsuba call allocates three std::vectors on the heap, malloc/free lock contention destroys performance. Passing a single pre-allocated contiguous scratch arena bump-pointer reduces runtime by over $4\times$.
  2. Floating-Point Catastrophic Cancellation in Strassen: Because Strassen computes $C_{11} = M_1 + M_4 - M_5 + M_7$ by adding and subtracting quadrants of vastly different magnitudes, IEEE-754 floating-point error is bounded only in the normwise sense ($\|\Delta C\| \le c_n u \|A\| \|B\|$), not the componentwise sense ($|\Delta C_{ij}| \le c_n u (|A||B|)_{ij}$). If $A$ and $B$ have poorly scaled rows/columns, small entries in $C$ can lose all significant digits!

Production C++20 Implementation: Arena-Backed Karatsuba Polynomial Multiplier

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <vector>

// Multiplies two polynomials a[0..n-1] and b[0..n-1] (n = power of 2) into res[0..2n-1]
// Uses a pre-allocated scratch buffer to perform ZERO heap allocations during recursion!
void karatsuba_rec(const std::int64_t* a, const std::int64_t* b,
                   std::int64_t* res, std::int64_t* scratch, std::size_t n) noexcept {
    std::fill_n(res, 2 * n, 0);

    // Base case: Schoolbook O(n^2) loop auto-vectorizes cleanly for n <= 32
    if (n <= 32) {
        for (std::size_t i = 0; i < n; ++i) {
            for (std::size_t j = 0; j < n; ++j) {
                res[i + j] += a[i] * b[j];
            }
        }
        return;
    }

    const std::size_t m = n / 2;
    const std::int64_t* a0 = a;
    const std::int64_t* a1 = a + m;
    const std::int64_t* b0 = b;
    const std::int64_t* b1 = b + m;

    // Carve out workspace from linear scratch arena:
    // sum_a[m], sum_b[m], z_m[2m], next_scratch[...]
    std::int64_t* sum_a = scratch;
    std::int64_t* sum_b = scratch + m;
    std::int64_t* z_m   = scratch + 2 * m;
    std::int64_t* next_scratch = scratch + 4 * m;

    for (std::size_t i = 0; i < m; ++i) {
        sum_a[i] = a0[i] + a1[i];
        sum_b[i] = b0[i] + b1[i];
    }

    // 1. z0 = a0 * b0 stored directly in res[0 .. 2m-1]
    karatsuba_rec(a0, b0, res, next_scratch, m);
    // 2. z2 = a1 * b1 stored directly in res[2m .. 4m-1]
    karatsuba_rec(a1, b1, res + 2 * m, next_scratch, m);
    // 3. z_m = (a0 + a1) * (b0 + b1) stored in z_m[0 .. 2m-1]
    karatsuba_rec(sum_a, sum_b, z_m, next_scratch, m);

    // Combine: z1 = z_m - z0 - z2 added into res[m .. 3m-1]
    for (std::size_t i = 0; i < 2 * m; ++i) {
        z_m[i] -= (res[i] + res[2 * m + i]);
    }
    for (std::size_t i = 0; i < 2 * m; ++i) {
        res[m + i] += z_m[i];
    }
}

std::vector<std::int64_t> karatsuba_multiply(const std::vector<std::int64_t>& A,
                                             const std::vector<std::int64_t>& B) {
    std::size_t n = 1;
    const std::size_t max_len = std::max(A.size(), B.size());
    while (n < max_len) n <<= 1;

    std::vector<std::int64_t> a_pad(n, 0), b_pad(n, 0);
    std::copy(A.begin(), A.end(), a_pad.begin());
    std::copy(B.begin(), B.end(), b_pad.begin());

    std::vector<std::int64_t> res(2 * n, 0);
    std::vector<std::int64_t> scratch(6 * n, 0); // 4m + 4(m/2) + ... <= 4n scratch space
    karatsuba_rec(a_pad.data(), b_pad.data(), res.data(), scratch.data(), n);
    res.resize(A.size() + B.size() - 1);
    return res;
}
Carry Propagation in Big-Integer Karatsuba

When multiplying polynomials, $a_0 + a_1$ has the exact same degree $m-1$ as $a_0$ and $a_1$. When multiplying base-$2^{64}$ big integers, however, the sum $x_0 + x_1$ can produce a $1$-bit carry into limb $m+1$, making the recursive operands $(m+1)$ limbs wide! Moreover, subtracting $z_2$ and $z_0$ requires signed borrow handling. Production big-int libraries (like GMP) either work with redundant limb representations or handle the top carry bit explicitly outside the $m$-limb recursion.

Check yourself Question 1 of 4
Concept Check Karatsuba's Gauss Identity & Master Theorem Exponent recall

To multiply two n-limb numbers X = x_1 B^m + x_0 and Y = y_1 B^m + y_0 (where m = n/2), we need z_2 = x_1*y_1, z_0 = x_0*y_0, and the middle cross-term z_1 = x_1*y_0 + x_0*y_1.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

What fundamental numerical stability property does standard O(n^3) matrix multiplication satisfy that Strassen's algorithm violates?

Key Takeaways

Karatsuba replaces 4 half-size multiplications with 3 using z_1 = (x_1 + x_0)(y_1 + y_0) - x_1*y_1 - x_0*y_0, reducing complexity from O(n^2) to O(n^{log_2 3}) = O(n^{1.585}).
Strassen replaces 8 block matrix multiplications with 7 and 18 block additions/subtractions, reducing complexity from O(n^3) to O(n^{log_2 7}) = O(n^{2.807}).
Because both recurrences fall into Master Theorem Case 1 (leaf-dominated), cutting off recursion at a base-case size (n <= 32 or 64) and switching to a flat SIMD loop eliminates 90%+ of recursion overhead.
Passing a pre-allocated linear scratch buffer avoids O(n^{1.585}) heap allocations during recursion.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue