Алгоритми · Математика · Високопродуктивні обчислення
📅 Квітень 2026 ⏱ ≈ 12 хв читання 🎯 Середній рівень · Останнє оновлення: 28 травня 2026 р.

Множення матриць за Штрассеном — від O(n³) до субкубічних алгоритмів

Шкільний алгоритм множення n×n матриць потребує n³ множень. У 1969 Штрассен показав, що матриці 2×2 можна перемножити лише 7 множеннями замість 8, а рекурсія дає складність O(nlog₂7) ≈ O(n2.807) — субкубічну економію, на якій ґрунтуються сучасні бібліотеки числових обчислень.

1. Наївний алгоритм O(n³)

Множення матриць C = A·B для матриць n×n: C[i][j] = Σₖ A[i][k] · B[k][j]. Трициклова реалізація — це O(n³) множень і O(n³) додавань:

function matmul(A, B, n) {
  const C = Array.from({length: n}, () => new Float64Array(n));
  for (let i = 0; i < n; i++)
    for (let k = 0; k < n; k++)             // k-цикл усередині задля кешу
      for (let j = 0; j < n; j++)
        C[i][j] += A[i][k] * B[k][j];
  return C;
}

Зверніть увагу на порядок циклів i-k-j (зовнішній-k-внутрішній-j): він звертається до B рядок за рядком (дружньо до кешу), тоді як i-j-k звертається до B стовпець за стовпцем (вороже до кешу). Для n = 1000 наївний алгоритм виконує 10⁹ множень — на що йде близько 1 секунди на скалярному процесорі.

2. «Розділяй і володарюй»

Розбиваємо кожну матрицю n×n на чотири блочні матриці (n/2)×(n/2):

A = | A₁₁ A₁₂ | B = | B₁₁ B₁₂ | C = | C₁₁ C₁₂ | | A₂₁ A₂₂ | | B₂₁ B₂₂ | | C₂₁ C₂₂ | C₁₁ = A₁₁·B₁₁ + A₁₂·B₂₁ C₁₂ = A₁₁·B₁₂ + A₁₂·B₂₂ C₂₁ = A₂₁·B₁₁ + A₂₂·B₂₁ C₂₂ = A₂₁·B₁₂ + A₂₂·B₂₂ → 8 рекурсивних множень матриць (n/2)×(n/2) → 4 додавання матриць (кожне O((n/2)²)) Рекурентне співвідношення: T(n) = 8T(n/2) + O(n²) Розв'язок: T(n) = O(n^log₂8) = O(n³) ← поки що без покращення!

Розбиття на 4 блоки з рекурсією структурно еквівалентне наївному підходу. Ключова ідея в роботі Штрассена: зменшити кількість рекурсивних підзадач з 8 до 7.

3. Трюк Штрассена із 7 множеннями

Штрассен показав, що чотири добутки C₁₁, C₁₂, C₂₁, C₂₂ можна обчислити лише з 7 проміжних добутків M₁…M₇:

M₁ = (A₁₁ + A₂₂)(B₁₁ + B₂₂) M₂ = (A₂₁ + A₂₂) B₁₁ M₃ = A₁₁ (B₁₂ - B₂₂) M₄ = A₂₂ (B₂₁ - B₁₁) M₅ = (A₁₁ + A₁₂) B₂₂ M₆ = (A₂₁ - A₁₁)(B₁₁ + B₁₂) M₇ = (A₁₂ - A₂₂)(B₂₁ + B₂₂) C₁₁ = M₁ + M₄ - M₅ + M₇ C₁₂ = M₃ + M₅ C₂₁ = M₂ + M₄ C₂₂ = M₁ - M₂ + M₃ + M₆

Ці тотожності можна перевірити прямим розкриттям. Вартість — 7 множень і 18 додавань на рівень. Оскільки додавання є Θ(n²) і дешеві порівняно з множеннями (на великих матрицях), сукупний результат загалом швидший.

4. Аналіз складності

Рекурентне співвідношення: T(n) = 7·T(n/2) + O(n²) Основна теорема (випадок 1: log_b(a) = log₂7 ≈ 2.807 > 2): T(n) = Θ(n^log₂7) ≈ Θ(n^2.807) Економія порівняно з наївним при n = 4096: Наївний: 4096³ = 68,7 млрд множень Штрассен: 4096^2.807 ≈ 15,5 млрд множень Прискорення: ~4,4× При n = 16384: Наївний: 4,4 × 10¹² Штрассен: 3,5 × 10¹¹ Прискорення: ~12,7× Точка перетину (де Штрассен випереджає наївний, з урахуванням додавань): Практичний поріг: n ≈ 256–512 залежно від обладнання
Числова стабільність: алгоритм Штрассена виконує більше додавань, ніж наївний алгоритм, дещо посилюючи похибки округлення з рухомою комою. Для подвійної точності зростання похибки в O(n^0.585) разів більше — прийнятно для більшості застосувань, але важливо в погано обумовлених системах.

5. Кеш-нечутливі алгоритми

Сучасні процесори з ієрархіями кешу L1/L2/L3 штрафують випадковий доступ до пам'яті. Блочне (плиткове) множення матриць покращує локальність даних, працюючи з малими підблоками B×B, що повністю вміщаються в кеш L1 або L2:

Блочне matmul (розмір блока B): for ii = 0..n step B: for kk = 0..n step B: for jj = 0..n step B: // множимо A[ii:ii+B][kk:kk+B] × B[kk:kk+B][jj:jj+B] for i = ii..ii+B: for k = kk..kk+B: for j = jj..jj+B: C[i][j] += A[i][k] * B[k][j]; Промахи кешу: Θ(n³ / B) проти Θ(n³) для наївного Виберіть B = √(M/3), де M — розмір кешу L1 у елементах.

Кеш-нечутливий алгоритм досягає оптимальної продуктивності кешу, не знаючи його розміру, шляхом рекурсивного поділу доти, доки підзадача не вміститься в кеш. Формулювання Штрассена у стилі «розділяй і володарюй» природно є кеш-нечутливим.

6. BLAS і практична реалізація

Стандарт Basic Linear Algebra Subprograms (BLAS) визначає інтерфейс для операцій з матрицями. DGEMM (загальне множення матриць подвійної точності) — це базова процедура:

Множення матриць у глибинному навчанні (пакетне, змішаної точності) обробляється cuBLAS-lt та тензорними ядрами — спеціалізованими блоками множення матриць 4×4 у графічних процесорах NVIDIA, що досягають у 2–8× вищої пропускної здатності для операцій FP16/BF16, критичної для навчання трансформерів.

7. Гонка до ω = 2

Показник множення матриць ω визначається як інфімум усіх дійсних чисел α таких, що дві матриці n×n можна перемножити за O(n^α) операцій у полі. Прогрес:

1969: Штрассен ω ≤ log₂7 ≈ 2.8074 1979: Пан ω ≤ 2.796 1981: Шенхаге ω ≤ 2.548 1987: Копперсміт-Віноград ω ≤ 2.3755 2010: Стотерс ω ≤ 2.3737 2012: Вільямс ω ≤ 2.3727 2020: Алман-Вільямс ω ≤ 2.3728596 2024: Вільямс та ін. ω ≤ 2.371866 Нижня межа: ω ≥ 2 (тривіально — потрібно прочитати n² елементів) Гіпотеза: ω = 2 (відкрита проблема)

Родина алгоритмів Копперсміт-Віноград (та її нащадки) використовує витончений алгебраїчний апарат — розклад тензорного рангу, лазерний метод, теоретико-групові техніки — породжуючи алгоритми настільки складні, що їх рідко реалізують на практиці. Точка перетину зі Штрассеном астрономічно велика (n ~ 1010 або більше). Але теоретично вони підтверджують, що множення матриць ближче до «лінійної» складності, ніж до «кубічної».

🔢 Дослідити алгоритми →