# Алгоритмы умножения матриц Штрассена

Этот модуль содержит реализации алгоритма умножения матриц Штрассена, включая оптимизированные версии с использованием Numba для повышения производительности.

## Теоретическая справка

Алгоритм Штрассена — это алгоритм типа «разделяй и властвуй» для умножения матриц. Он был предложен Фолькером Штрассеном в 1969 году и стал первым алгоритмом, асимптотическая сложность которого лучше, чем у стандартного алгоритма умножения матриц (O(n³) против O(n^log₂7) ≈ O(n².81)).

Основная идея алгоритма заключается в разбиении исходных матриц на подматрицы меньшего размера и рекурсивном выполнении умножений над ними. Вместо выполнения 8 умножений подматриц, как в стандартном алгоритме, алгоритм Штрассена выполняет только 7 умножений и несколько дополнительных сложений/вычитаний. Это приводит к снижению общей вычислительной сложности.

### Преимущества алгоритма Штрассена:

- **Асимптотическая эффективность:** Для достаточно больших матриц алгоритм Штрассена работает быстрее, чем стандартный алгоритм.
- **Применимость в других алгоритмах:** Алгоритм Штрассена может использоваться как строительный блок в других алгоритмах, работающих с матрицами.

### Ограничения алгоритма Штрассена:

- **Накладные расходы:** Для небольших матриц накладные расходы на разбиение матриц и дополнительные операции могут перевесить выигрыш от уменьшения количества умножений.
- **Численная устойчивость:** Алгоритм Штрассена может быть менее численно устойчивым, чем стандартный алгоритм, из-за большего количества операций сложения/вычитания.
- **Требования к памяти:** Алгоритм Штрассена требует дополнительной памяти для хранения временных матриц, возникающих в процессе рекурсивных вызовов.

## Функции модуля

Модуль предоставляет следующие функции для умножения матриц с использованием алгоритма Штрассена:

### 1. `strassen(A, B, leaf_size=64)`

Рекурсивная функция для умножения двух квадратных матриц `A` и `B` с использованием алгоритма Штрассена.

**Параметры:**

- `A` (np.ndarray): Первая квадратная матрица.
- `B` (np.ndarray): Вторая квадратная матрица.
- `leaf_size` (int, необязательный): Порог размерности, при котором рекурсия останавливается и используется стандартное умножение матриц. По умолчанию равен 64.

**Возвращает:**

- `C` (np.ndarray): Результирующая матрица, произведение `A` и `B`.

**Пример использования:**

```python
import numpy as np

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = strassen(A, B)
print(C)
# Вывод:
# [[19 22]
#  [43 50]]
```

**Примечание:** Эта функция предполагает, что входные матрицы квадратные и их размерность является степенью двойки.

### 2. `strassen_multiply(A, B, leaf_size=64)`

Функция для умножения двух матриц `A` и `B` (возможно, прямоугольных) с использованием алгоритма Штрассена.

**Параметры:**

- `A` (np.ndarray): Первая матрица.
- `B` (np.ndarray): Вторая матрица.
- `leaf_size` (int, необязательный): Порог размерности, при котором рекурсия останавливается и используется стандартное умножение матриц. По умолчанию равен 64.

**Возвращает:**

- `C` (np.ndarray): Результирующая матрица, произведение `A` и `B`.

**Ошибки:**

- `ValueError`: Возникает, если количество столбцов матрицы `A` не совпадает с количеством строк матрицы `B`.

**Пример использования:**

```python
import numpy as np

A = np.array([[1, 2, 3], [4, 5, 6]])
B = np.array([[7, 8], [9, 10], [11, 12]])
C = strassen_multiply(A, B)
print(C)
# Вывод:
# [[ 58  64]
#  [139 154]]
```

**Примечание:** Эта функция автоматически дополняет матрицы нулями до ближайшей степени двойки, если их размеры не соответствуют требованиям алгоритма Штрассена.

### 3. `numba_strassen(A, B, leaf_size=64)`

Рекурсивная функция для умножения двух квадратных матриц `A` и `B` с использованием алгоритма Штрассена, ускоренная с помощью Numba.

**Параметры:**

- `A` (np.ndarray): Первая квадратная матрица.
- `B` (np.ndarray): Вторая квадратная матрица.
- `leaf_size` (int, необязательный): Порог размерности, при котором рекурсия останавливается и используется стандартное умножение матриц. По умолчанию равен 64.

**Возвращает:**

- `C` (np.ndarray): Результирующая матрица, произведение `A` и `B`.

**Пример использования:**

```python
import numpy as np

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = numba_strassen(A, B)
print(C)
# Вывод:
# [[19 22]
#  [43 50]]
```

**Примечание:** Эта функция предполагает, что входные матрицы квадратные и их размерность является степенью двойки. Использование Numba позволяет значительно повысить производительность за счет JIT-компиляции кода.

### 4. `numba_strassen_multiply(A, B, leaf_size=64)`

Функция для умножения двух матриц `A` и `B` (возможно, прямоугольных) с использованием алгоритма Штрассена, ускоренная с помощью Numba.

**Параметры:**

- `A` (np.ndarray): Первая матрица.
- `B` (np.ndarray): Вторая матрица.
- `leaf_size` (int, необязательный): Порог размерности, при котором рекурсия останавливается и используется стандартное умножение матриц. По умолчанию равен 64.

**Возвращает:**

- `C` (np.ndarray): Результирующая матрица, произведение `A` и `B`.

**Ошибки:**

- `ValueError`: Возникает, если количество столбцов матрицы `A` не совпадает с количеством строк матрицы `B`.

**Пример использования:**

```python
import numpy as np

A = np.array([[1, 2, 3], [4, 5, 6]])
B = np.array([[7, 8], [9, 10], [11, 12]])
C = numba_strassen_multiply(A, B)
print(C)
# Вывод:
# [[ 58  64]
#  [139 154]]
```

**Примечание:** Эта функция автоматически дополняет матрицы нулями до ближайшей степени двойки, если их размеры не соответствуют требованиям алгоритма Штрассена. Использование Numba позволяет значительно повысить производительность за счет JIT-компиляции кода.

## Выбор оптимального `leaf_size`

Параметр `leaf_size` определяет размер подматриц, при котором алгоритм переключается с рекурсивного умножения Штрассена на стандартное умножение матриц. Оптимальное значение `leaf_size` зависит от аппаратной платформы и может быть определено экспериментально.

Рекомендуется провести тестирование с различными значениями `leaf_size` (например, 16, 32, 64, 128) для определения наиболее эффективного значения для конкретной системы и размеров матриц.

## Сравнение производительности

Для оценки производительности различных реализаций алгоритма Штрассена можно провести сравнение времени выполнения для матриц разных размеров.

Пример кода для сравнения:

```python
import numpy as np
import time
from your_module import strassen, strassen_multiply, numba_strassen, numba_strassen_multiply

def benchmark(func, A, B):
    start_time = time.time()
    C = func(A, B)
    end_time = time.time()
    return end_time - start_time

sizes = [128, 256, 512, 1024, 2048]
leaf_size = 64

for n in sizes:
    A = np.random.rand(n, n)
    B = np.random.rand(n, n)
  
    print(f"Matrix size: {n}x{n}")
  
    time_strassen = benchmark(lambda A, B: strassen(A, B, leaf_size), A[:n, :n], B[:n, :n])
    print(f"  strassen: {time_strassen:.4f} s")
  
    time_strassen_multiply = benchmark(lambda A, B: strassen_multiply(A, B, leaf_size), A[:n, :n], B[:n, :n])
    print(f"  strassen_multiply: {time_strassen_multiply:.4f} s")
  
    time_numba_strassen = benchmark(lambda A, B: numba_strassen(A, B, leaf_size), A[:n, :n], B[:n, :n])
    print(f"  numba_strassen: {time_numba_strassen:.4f} s")
  
    time_numba_strassen_multiply = benchmark(lambda A, B: numba_strassen_multiply(A, B, leaf_size), A[:n, :n], B[:n, :n])
    print(f"  numba_strassen_multiply: {time_numba_strassen_multiply:.4f} s")
  
    print("-" * 30)
```

Этот код сравнивает время выполнения различных реализаций алгоритма Штрассена для матриц разных размеров и выводит результаты в консоль.

## Заключение

Этот модуль предоставляет эффективные реализации алгоритма умножения матриц Штрассена, включая оптимизированные версии с использованием Numba. Выбор подходящей функции и оптимального значения `leaf_size` зависит от конкретной задачи и аппаратной платформы.
