medvedkov_andrey_lab_6 is ready

This commit is contained in:
MaD
2025-12-11 22:59:14 +04:00
parent 716d205ebb
commit b8b5d9e6f4
2 changed files with 356 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""
Лабораторная работа:
Вычисление детерминанта квадратной матрицы (последовательный и параллельный алгоритмы).
При запуске файла:
- автоматически выполняется бенчмарк для матриц 100x100, 300x300, 500x500,
- используется последовательный и параллельный алгоритмы,
- для параллельного алгоритма тестируются числа потоков: 1, 2, 4, 8.
"""
import random
import time
from concurrent.futures import ThreadPoolExecutor
from typing import List, Optional, Tuple
Matrix = List[List[float]]
# ==============================
# Вспомогательные функции
# ==============================
def generate_matrix(n: int,
seed: Optional[int] = None,
low: float = -10.0,
high: float = 10.0) -> Matrix:
"""
Генерация случайной квадратной матрицы n x n.
Элементы — вещественные числа в диапазоне [low, high].
"""
if seed is not None:
random.seed(seed)
return [[random.uniform(low, high) for _ in range(n)] for _ in range(n)]
def upper_triangular_and_sign(matrix: Matrix) -> Tuple[Matrix, int]:
"""
Приведение матрицы к верхнетреугольному виду методом Гаусса
с частичным выбором главного элемента.
Возвращает:
- новую матрицу (верхнетреугольную),
- sign: +1 или -1 в зависимости от числа перестановок строк,
либо 0, если найден нулевой столбец (det == 0).
"""
n = len(matrix)
a = [row[:] for row in matrix] # копия, исходную матрицу не портим
sign = 1
eps = 1e-12
for i in range(n):
# Поиск ведущего элемента (pivot) в столбце i
pivot = i
for j in range(i + 1, n):
if abs(a[j][i]) > abs(a[pivot][i]):
pivot = j
# Если весь столбец почти нулевой — детерминант = 0
if abs(a[pivot][i]) < eps:
return a, 0
# Если нужно — меняем строки местами
if pivot != i:
a[i], a[pivot] = a[pivot], a[i]
sign *= -1
pivot_val = a[i][i]
# Обнуляем элементы ниже главной диагонали
for j in range(i + 1, n):
factor = a[j][i] / pivot_val
row_j = a[j]
row_i = a[i]
for k in range(i, n):
row_j[k] -= factor * row_i[k]
return a, sign
# ==============================
# Последовательный алгоритм
# ==============================
def determinant_sequential(matrix: Matrix) -> float:
"""
Последовательное вычисление детерминанта:
1) приводим к верхнетреугольной матрице,
2) det = sign * произведение диагональных элементов.
"""
a, sign = upper_triangular_and_sign(matrix)
if sign == 0:
return 0.0
det = float(sign)
n = len(a)
for i in range(n):
det *= a[i][i]
return det
# ==============================
# Параллельный алгоритм
# ==============================
def _product_chunk(diag: List[float], start: int, end: int) -> float:
"""Произведение части диагонали diag[start:end]."""
p = 1.0
for i in range(start, end):
p *= diag[i]
return p
def determinant_parallel(matrix: Matrix, num_threads: int = 4) -> float:
"""
"Параллельный" алгоритм:
- так же приводим матрицу к верхнетреугольной (последовательно),
- произведение диагональных элементов считаем в нескольких потоках.
При num_threads == 1 ведёт себя как последовательный алгоритм
(задание со звёздочкой: один и тот же алгоритм для 1 и N потоков).
"""
a, sign = upper_triangular_and_sign(matrix)
if sign == 0:
return 0.0
n = len(a)
diag = [a[i][i] for i in range(n)]
# Если пользователь указал 1 поток — выполняем произведение последовательно
if num_threads <= 1 or n == 0:
det = float(sign)
for x in diag:
det *= x
return det
# Делим диагональ на блоки по числу потоков
chunk_size = (n + num_threads - 1) // num_threads
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = []
for start in range(0, n, chunk_size):
end = min(start + chunk_size, n)
futures.append(executor.submit(_product_chunk, diag, start, end))
det = float(sign)
for f in futures:
det *= f.result()
return det
# ==============================
# Бенчмарк
# ==============================
def benchmark() -> None:
"""
Прогон бенчмарка для матриц 100x100, 300x300, 500x500.
Число потоков для параллельного алгоритма: 1, 2, 4, 8.
"""
sizes = [100, 300, 500]
thread_counts = [1, 2, 4, 8]
print("Бенчмарк детерминанта случайных матриц\n")
for n in sizes:
print(f"=== Размер матрицы: {n}x{n} ===")
# фиксируем seed, чтобы последовательный и параллельный считали одну и ту же матрицу
matrix = generate_matrix(n, seed=42)
# Последовательный алгоритм
t0 = time.perf_counter()
det_seq = determinant_sequential(matrix)
t1 = time.perf_counter()
seq_time = t1 - t0
print(f"Последовательный: det = {det_seq:.3e}, "
f"время = {seq_time:.4f} с")
# Параллельный алгоритм для разных чисел потоков
for threads in thread_counts:
t0 = time.perf_counter()
det_par = determinant_parallel(matrix, num_threads=threads)
t1 = time.perf_counter()
par_time = t1 - t0
# Разница нужна только для проверки корректности (когда нет переполнения)
diff = abs(det_par - det_seq) if (
abs(det_seq) != float("inf")
and abs(det_par) != float("inf")
) else float("nan")
print(
f"Параллельный (threads={threads}): det = {det_par:.3e}, "
f"разность = {diff:.3e}, время = {par_time:.4f} с"
)
print()
def main() -> None:
"""
Точка входа: сразу запускаем бенчмарк.
При проверке достаточно выполнить: python determinant_lab.py
"""
benchmark()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,146 @@
## Используемые технологии
- Язык программирования: **Python 3**
- Стандартная библиотека Python:
- `concurrent.futures.ThreadPoolExecutor` — создание пула потоков и ручное управление их количеством;
- `time` — измерение времени выполнения;
- `random` — генерация случайных матриц.
Дополнительные внешние библиотеки **не используются**, достаточно установленного Python 3.
## Как запустить лабораторную работу
### Требования
- Установленный **Python 3** (3.8+ достаточно).
- Файлы `determinant_lab.py` и `README.md` лежат в одной директории.
### Запуск
Просто выполнить:
```bash
python determinant_lab.py
```
При запуске автоматически:
- генерируются случайные матрицы размером 100×100, 300×300 и 500×500;
- вычисляется детерминант:
- последовательным алгоритмом;
- параллельным алгоритмом для числа потоков 1, 2, 4, 8;
- на экран выводятся:
- значение детерминанта (в научной нотации),
- время работы для каждого варианта,
- (при отсутствии переполнения) разница между детерминантами последовательного и параллельного вычислений.
Если нужно изменить набор размеров или числа потоков,
это можно сделать, отредактировав списки `sizes` и `thread_counts`
в функции `benchmark()`.
---
## Описание реализованных алгоритмов
### Последовательный алгоритм
Последовательная функция: `determinant_sequential(matrix: Matrix) -> float`
Идея:
1. Используется метод **Гаусса** с частичным выбором главного элемента:
- матрица копируется во внутренний массив;
- последовательно обрабатываются столбцы, выбирается максимальный по модулю элемент в текущем столбце — pivot;
- при необходимости строки меняются местами, счётчик перестановок учитывается через знак `sign = ±1`;
- ниже главной диагонали элементы зануляются.
2. После приведения к **верхнетреугольному виду**:
- детерминант равен произведению диагональных элементов,
- умноженному на знак `sign` (зависящий от числа перестановок строк).
Формула:
\[
\det(A) = \mathrm{sign} \cdot \prod_{i=1}^{n} u_{ii},
\]
где \(u_{ii}\) — элементы на диагонали верхнетреугольной матрицы.
Алгоритмическая сложность: **O(n³)**.
---
### Параллельный алгоритм
Параллельная функция: `determinant_parallel(matrix: Matrix, num_threads: int) -> float`
Особенности:
1. Шаг приведения к верхнетреугольной матрице выполняется **так же, как в последовательной версии**
он остаётся последовательным (это основная трудоёмкость O(n³)).
2. Отличие в том, как считается произведение диагональных элементов:
- собирается список диагональных элементов `diag`;
- он делится на `num_threads` примерно равных блоков;
- каждый блок обрабатывается в отдельном потоке (`ThreadPoolExecutor`);
- результаты (частичные произведения) перемножаются в один итоговый детерминант.
3. При `num_threads = 1`:
- используется тот же код, но без распараллеливания;
- таким образом **один и тот же алгоритм** реализует и последовательный, и параллельный режим
(соответствие варианту задания со звёздочкой).
---
## Результаты бенчмарка
Бенчмарк выполнялся функцией `benchmark()` при запуске `determinant_lab.py`.
Использовались случайные матрицы с элементами в диапазоне [-10, 10] и фиксированным `seed = 42`
(для воспроизводимости результатов).
Ниже приведены **фактические** времена выполнения (в секундах), полученные при запуске:
### Время работы
| Размер матрицы | Алгоритм | Число потоков | Время, с |
|----------------|-----------------|---------------|------------:|
| 100×100 | последовательный| 1 | 0.0183 |
| 100×100 | параллельный | 1 | 0.0179 |
| 100×100 | параллельный | 2 | 0.0189 |
| 100×100 | параллельный | 4 | 0.0195 |
| 100×100 | параллельный | 8 | 0.0196 |
| 300×300 | последовательный| 1 | 0.5635 |
| 300×300 | параллельный | 1 | 0.5327 |
| 300×300 | параллельный | 2 | 0.5677 |
| 300×300 | параллельный | 4 | 0.5204 |
| 300×300 | параллельный | 8 | 0.5358 |
| 500×500 | последовательный| 1 | 2.6207 |
| 500×500 | параллельный | 1 | 2.4511 |
| 500×500 | параллельный | 2 | 2.4260 |
| 500×500 | параллельный | 4 | 2.6239 |
| 500×500 | параллельный | 8 | 2.3660 |
Для сравнения корректности можно отметить:
- Для матрицы 100×100 детерминант по модулю имеет разумный порядок (`≈ 1.8 · 10^153`),
и последовательный и параллельный алгоритмы дают очень близкие значения
(разность либо ноль, либо на уровне погрешности представления `float`).
- Для матриц 300×300 и 500×500 значение детерминанта по модулю становится настолько большим,
что во время эксперимента оно выходило за диапазон представимых значений типа `float`,
и в выводе отображалось как `inf` или `-inf`. Это ожидаемое поведение
для случайных матриц с элементами порядка 10 и не является ошибкой алгоритма.
---
## Анализ результатов
1. **Основное время** работы алгоритма тратится на приведение матрицы к верхнетреугольному виду,
которое выполняется последовательно и имеет сложность O(n³).
2. Параллелизация реализована на этапе **перемножения диагональных элементов**, который по трудоёмкости
значительно проще (O(n)). Поэтому существенного ускорения при увеличении числа потоков не наблюдается:
накладные расходы на создание потоков и их синхронизацию сопоставимы с выигрышем.
3. Для размера 100×100 время параллельного алгоритма с 18 потоками практически совпадает
с последовательным вариантом (отличия в пределах нескольких миллисекунд).
Для размеров 300×300 и 500×500 также наблюдаются лишь небольшие колебания времени
без устойчивого существенного ускорения.
## ссылка на видео
https://vkvideo.ru/video-234070899_456239022?list=ln-XP9xzFbV2KUljy9ewx