Загрузка данных
# # Численные методы - шаблоны конструкторы v8
#
# эта версия сделана для быстрого копирования ячеек на экзамене.
#
# главная идея:
# - открываешь ноутбук
# - находишь нужный метод через ctrl+f
# - копируешь конкретную ячейку под свой случай
# - например не общий ньютон, а вариант 2 на 2 или 3 на 3
# - меняешь только блок `менять только тут`
# - запускаешь
#
# важно:
# - блок 0 оставлен как аварийная база и как справочник функций
# - в готовый ответ блок 0 целиком лучше не копировать
# - перед каждым методом теперь есть своя минимальная база: импорты и функции, которые нужны именно этой ячейке
# - если в задаче другой случай, ищи отдельный вариант в таблице, а не переделывай универсальный код
# - поэтому отдельную ячейку можно копировать без большого блока 0
#
# внутри каждой ячейки есть:
# - как понять, что нужен этот метод
# - что значит каждая переменная
# - где искать это значение в условии
# - код с короткими комментариями
# - типовые выводы и теория
# %%
# # Общая таблица выбора метода
#
# Сначала ищи эти слова в задании. Потом через Ctrl+F ищи номер блока в файле.
#
# | Ключевые слова в задании | Какой блок искать |
# |---|---|
# | метод бисекции, деления пополам, отрезок [a,b], f(a)f(b)<0 | 2.1 Бисекция |
# | метод Ньютона, касательные, дана f'(x) | 2.2 Ньютон |
# | модифицированный Ньютон, производная фиксируется | 2.3 Модифицированный Ньютон |
# | метод секущих, даны x0 и x1 | 2.4 Секущие |
# | простые итерации, x = phi(x) | 2.5 Простые итерации |
# | минимум функции, метод дихотомии, минимизировать на [a,b] | 2.6 Дихотомия |
# | система из 2 нелинейных уравнений, Якобиан | 3.1а Ньютон 2 на 2 |
# | модифицированный Ньютон для системы, фиксированный Якобиан | 3.1б фиксированный Якобиан 2 на 2 |
# | система из 3 нелинейных уравнений, x1 x2 x3 | 3.1в Ньютон 3 на 3 |
# | система x=g(x), функциональная итерация | 3.2 Итерации для системы |
# | линейная интерполяция, значение между двумя точками | 4.1 Линейная интерполяция |
# | полином Лагранжа | 4.2 Лагранж |
# | кубический сплайн, сплайн | 4.3 Кубический сплайн |
# | доминирующее собственное значение, степенной метод | 5.1 Степенной метод |
# | собственное значение около sigma, со сдвигом | 5.2 Обратный степенной со сдвигом |
# | QR-разложение, A=QR | 5.3 QR-разложение |
# | QR-алгоритм, спектр матрицы | 5.4 QR-алгоритм |
# | разложение Шура, A=UTU^T | 5.5 Шур |
# | круги Гершгорина, локализация собственных значений | 5.6 Гершгорин |
# | умножение матриц, Штрассен | 5.7 Матрицы / Штрассен |
# | производная численно, конечные разности | 6.1 Численное дифференцирование |
# | задача Коши, метод Эйлера | 6.2 Эйлер |
# | предиктор-корректор, улучшенный Эйлер | 6.3 Предиктор-корректор Эйлера |
# | метод Рунге-Кутты 4 порядка | 6.4 Рунге-Кутта 4 |
# | система ОДУ, метод Эйлера, фазовый портрет | 6.5а Система ОДУ Эйлером |
# | система ОДУ, метод Рунге-Кутты 4 порядка, фазовый портрет | 6.5б Система ОДУ Рунге-Куттой |
# | сравнить Эйлера и метод Рунге-Кутты 4 порядка | 6.6 Сравнение Эйлера и Рунге-Кутты 4 |
# | Адамс, Адамс-Мултон, Адамс-Башфорт | 6.7 Адамс |
# | ДПФ, дискретное преобразование Фурье, спектр | 7.1 ДПФ |
# | БПФ, быстрое преобразование Фурье | 7.2 БПФ |
#
#
# ## дополнительные пункты для необычных заданий
#
# эти блоки не всегда нужны, но если формулировка странная, сначала проверь их.
#
# | Ключевые слова в задании | Какой блок искать |
# |---|---|
# | интеграл, метод трапеций, площадь | 9.1а Метод трапеций |
# | интеграл, метод Симпсона, четное N | 9.1б Метод Симпсона |
# | система линейных уравнений, итерационный метод, Гаусс-Зейдель | 9.2 Гаусс-Зейдель |
# | вторая производная, центральная разность | 9.3 Вторая производная |
# | симметричная матрица, вращения, Якоби | 9.4 Вращения Якоби |
# | убрать шум, частоты шума, фильтрация сигнала | 9.5 Фильтрация через ДПФ |
# | частота дискретизации, сетка частот, Найквист | 9.6 Частотная сетка |
# | прямая наилучшего приближения, метод наименьших квадратов | 9.7 МНК |
# | сколько итераций бисекции нужно | 9.8 Оценка итераций бисекции |
# | сравнить бисекцию, Ньютона и секущие | 9.9 Сравнение методов корня |
# | ОДУ с контролем ошибки, менять шаг | 9.10 Адаптивный шаг |
# | PageRank, важность страниц, граф ссылок | 9.11 PageRank |
# | QR через отражения, Хаусхолдер | 9.12 QR Хаусхолдер |
# | SVD, сингулярные числа, A^T A | 10.1 SVD через A^T A |
# | PCA, главные компоненты, ковариация | 10.2 PCA для маленькой таблицы |
# | разреженная матрица, CSR, умножить на вектор | 10.3 CSR |
# | COO, координатный формат | 10.4 COO |
# | характеристический многочлен, np.poly, np.roots | 10.5 Характеристический многочлен |
# | нормальная матрица, A A^T = A^T A | 10.6 Проверка нормальности |
# | гессенбергова форма | 10.7 Проверка формы Гессенберга |
# | отношение Релея | 10.8 Отношение Релея |
# | невязка Ax=b, проверка решения СЛАУ | 10.9 Невязка СЛАУ |
# | невязка собственного значения | 10.10 Невязка собственного значения |
# | метод золотого сечения | 10.11 Золотое сечение |
# | прямоугольники, левый, правый, средний | 10.12 Метод прямоугольников |
# | правило Рунге, оценка ошибки по h и h/2 | 10.13 Правило Рунге |
# | неявный Эйлер, жесткая задача, устойчивость | 10.14 Неявный Эйлер |
# | низкочастотный фильтр, частотный порог | 10.15 Фильтр Фурье |
# | парабола МНК, квадратичная аппроксимация | 10.16 МНК параболой |
#
# ## Как пользоваться таблицей
#
# 1. Прочитай задание.
# 2. Найди похожие слова в левом столбце.
# 3. В правом столбце возьми номер блока.
# 4. Нажми Ctrl+F и найди этот блок.
# 5. Скопируй ячейку.
# 6. Сначала выбери конкретный вариант блока: 2 переменные, 3 переменные, метод Эйлера, метод Рунге-Кутты и т.д.
# 7. Внутри ячейки меняй только блок `менять только тут`. В начале этого блока обычно есть список значений, которые надо взять из условия.
#
# ## если метод не написан прямо
#
# - есть отрезок и корень - скорее всего бисекция
# - есть производная и стартовая точка - скорее всего Ньютон
# - есть две стартовые точки - скорее всего секущие
# - есть таблица x,y - интерполяция
# - есть матрица A - смотри блоки 5
# - есть y' и y(t0) - смотри блоки 6
# - есть сигнал или спектр - смотри блоки 7
#
#
# ## дополнительные слова для поиска
#
# если обычная таблица не помогла, ищи по этим словам. строки специально частично повторяются, чтобы проще было найти нужный блок через ctrl+f.
#
# | слова из задания | куда идти |
# |---|---|
# | абсолютная и относительная погрешность, округление, верные цифры | 1.1 Ошибки и суммирование |
# | накопление ошибки, сумма с компенсацией, Кэхэн | 1.1 Алгоритм Кэхэна |
# | f(x)=0, найти корень, уточнить корень | 2.1-2.5 Корни уравнений |
# | касательные, x_{k+1}=x_k-f/df | 2.2 Ньютон |
# | производная одна и та же, df(x0), фиксированная производная | 2.3 Модифицированный Ньютон |
# | без производной, две точки, хорды, секущие | 2.4 Секущие |
# | x=phi(x), сжимающее отображение, q<1 | 2.5 Простые итерации |
# | минимум на отрезке, unimodal, дихотомия | 2.6 Дихотомия |
# | система F(x)=0, нелинейная система, матрица Якоби | 3.1а-3.1в Ньютон для системы |
# | фиксированный Якобиан, модифицированный Ньютон для системы | 3.1б Фиксированный Якобиан |
# | интерполяция, таблица x y, значение между узлами | 4.1-4.3 Интерполяция |
# | Лагранж, полином Лагранжа, базисные многочлены | 4.2 Лагранж |
# | Ньютон для интерполяции, разделенные разности | 4.3 Интерполяция Ньютона |
# | производная по таблице, конечные разности | 5.1-5.2 Численное дифференцирование |
# | интеграл по таблице, квадратурная формула | 6.1, 9.1а, 9.1б Интегрирование |
# | трапеции, формула трапеций | 9.1а Метод трапеций |
# | симпсон, параболы, четное число разбиений | 9.1б Метод Симпсона |
# | ОДУ, задача Коши, y'=f(x,y), шаг h | 7.1-7.4 ОДУ |
# | метод Эйлера, явный Эйлер | 7.1 Эйлер |
# | улучшенный Эйлер, предиктор-корректор | 7.2 Улучшенный Эйлер |
# | метод Рунге-Кутты 4-го порядка, k1 k2 k3 k4 | 7.3 Рунге-Кутта 4 |
# | подобрать шаг, контроль ошибки, правило Рунге | 7.4 или 10.13 |
# | СЛАУ Ax=b, итерационный метод, простые итерации | 8.1 Итерации для СЛАУ |
# | Гаусс-Зейдель, использовать новые значения сразу | 9.2 Гаусс-Зейдель |
# | собственные значения, степенной метод, максимальное собственное | 8.2 Степенной метод |
# | Якоби для собственных, вращения, симметрическая матрица | 9.4 Вращения Якоби |
# | ДПФ, Фурье, спектр, гармоники, частоты, шум | 9.5 Фильтрация через ДПФ |
# | частота дискретизации, Найквист, шаг частоты | 9.6 Частотная сетка |
# | МНК, аппроксимация, нормальные уравнения, линия тренда | 9.7 или 10.16 |
# | сколько итераций нужно, оценить число шагов бисекции | 9.8 Оценка бисекции |
# | сравнить методы, какой быстрее, какой надежнее | 9.9 Сравнение методов |
# | адаптивный шаг, один шаг и два полушага | 9.10 Адаптивный шаг |
# | граф, страницы, ранжирование, PageRank | 9.11 PageRank |
# | QR, ортогональная матрица, верхнетреугольная, Хаусхолдер | 9.12 QR Хаусхолдер |
# | сингулярные числа, SVD, A^T A, ранг, сжатие | 10.1 SVD |
# | PCA, главные компоненты, уменьшение размерности | 10.2 PCA |
# | sparse, CSR, COO, разреженная матрица | 10.3-10.4 Разреженные матрицы |
# | характеристический многочлен, poly, roots | 10.5 Характеристический многочлен |
# | нормальная матрица, AA^T=A^TA | 10.6 Нормальная матрица |
# | гессенбергова матрица, Hessenberg | 10.7 Гессенберг |
# | отношение Релея, оценка собственного значения | 10.8 Отношение Релея |
# | невязка, проверить ответ, Ax-b | 10.9 Невязка СЛАУ |
# | невязка собственного значения | 10.10 Невязка собственного значения |
# | золотое сечение, минимум без производной | 10.11 Золотое сечение |
# | прямоугольники, левый прямоугольник, правый прямоугольник | 10.12 Метод прямоугольников |
# | неявный Эйлер, жесткая задача, устойчивость | 10.14 Неявный Эйлер |
# | фильтр нижних частот, убрать высокие частоты | 10.15 Низкочастотный фильтр |
# %%
# # быстрый поиск редких тем
#
# если в билете встретилось что-то странное и этого нет в основных блоках, смотри сюда.
#
# | слова в задании | какой блок искать |
# |---|---|
# | SVD, сингулярные числа, сингулярное разложение, A^T A | 10.1 SVD через A^T A |
# | PCA, главные компоненты, ковариация, изображение, сжатие | 10.2 PCA для маленькой таблицы |
# | разреженная матрица, sparse, CSR, data indices indptr | 10.3 CSR |
# | COO, координатный формат, rows cols values | 10.4 COO |
# | характеристический многочлен, собственные через poly roots | 10.5 Характеристический многочлен |
# | нормальная матрица, A A^T = A^T A, Шур диагональная | 10.6 Проверка нормальности |
# | верхне-гессенбергова форма, QR-алгоритм быстрее | 10.7 Проверка Гессенберга |
# | отношение Релея, (Ax,x)/(x,x) | 10.8 Отношение Релея |
# | невязка Ax-b, проверить СЛАУ | 10.9 Невязка СЛАУ |
# | невязка Ax-lambda*x, проверить собственный вектор | 10.10 Невязка собственного значения |
# | золотое сечение, минимум без производной | 10.11 Золотое сечение |
# | левые прямоугольники, правые, средние | 10.12 Метод прямоугольников |
# | правило Рунге, шаг h и h/2, оценка ошибки | 10.13 Правило Рунге |
# | неявный Эйлер, жесткая задача, устойчивость | 10.14 Неявный Эйлер |
# | низкочастотный фильтр, убрать высокие частоты, шум | 10.15 Фильтр Фурье |
# | МНК параболой, квадратичная аппроксимация | 10.16 МНК параболой |
#
# правило: эти блоки запасные. если попалась обычная задача, сначала бери основной блок из общей таблицы.
# %%
# # подробный поиск по формулировкам из билета
#
# этот блок нужен, если в билете формулировка не совпадает с названием метода.
#
# - если в задании есть **найти корень**, **уточнить корень**, **f(x)=0**, сначала смотри блоки 2.1-2.5. если дан отрезок и смена знака - бисекция. если дана производная - ньютон. если даны две начальные точки - секущие.
# - если есть **система нелинейных уравнений**, **якобиан**, **несколько неизвестных**, смотри блоки 3.1а, 3.1б, 3.1в. выбирай по количеству неизвестных.
# - если есть **таблица значений** и надо найти значение функции между узлами, это интерполяция. если про производную по таблице - численное дифференцирование. если про площадь или интеграл - квадратурные формулы.
# - если есть **задача коши**, **оду**, **y'=f(x,y)**, **шаг h**, смотри блоки 7.1-7.4. если требуют точность через сравнение шагов, смотри адаптивный шаг и правило Рунге.
# - если есть **матрица A и вектор b**, **Ax=b**, это СЛАУ. если про итерации - простые итерации или гаусс-зейдель. если про невязку - блок 10.9.
# - если есть **собственные значения**, **собственный вектор**, **максимальное собственное**, смотри степенной метод, вращения якоби, отношение релея или невязку собственного значения.
# - если есть **фурье**, **гармоники**, **частоты**, **шум**, **wav**, смотри фильтрацию через дпф и частотную сетку.
# - если есть **аппроксимация**, **мНК**, **линия тренда**, смотри мНК для прямой или параболы.
# - если есть **SVD**, **PCA**, **sparse**, **CSR**, **COO**, это редкие темы из блока 10.
# %%
# # как писать вывод после расчета
#
# используй этот блок как шаблон, когда надо написать общий вывод по задаче.
#
# 1. сначала напиши, что именно нашли: корень, интеграл, решение системы, собственное значение, график, спектр и т.д.
# 2. потом напиши главный численный контроль: невязка, ошибка, число итераций, шаг h, разница двух методов или устойчивость результата.
# 3. потом сравни число с ориентиром:
# - ошибка или невязка меньше 1e-6 - обычно хороший точный ответ;
# - от 1e-6 до 1e-3 - приближенно нормально, если в условии нет жесткой точности;
# - больше 1e-3 - лучше написать, что точность невысокая или нужен меньший шаг;
# - 1-10 итераций - быстро;
# - 10-30 итераций - нормально для большинства методов;
# - 30-100 итераций - много, но допустимо для медленных методов типа бисекции или простых итераций;
# - больше 100 итераций - метод сходится плохо или параметры выбраны неудачно.
# 4. если диапазон зависит от данных, используй формулу из подсказки в конкретном блоке. например для бисекции число итераций зависит от длины отрезка и eps: примерно log2((b-a)/eps).
# 5. в конце напиши смысл: метод сошелся, шаг подходит, решение устойчивое, шум уменьшился, матрица подходит под метод и т.д.
#
# короткая заготовка:
#
# получено значение ... . контрольная величина равна ... . так как она меньше ..., результат можно считать достаточно точным. метод подходит для этой задачи, потому что ... .
# %%
########################################################################
# как использовать блок 0
# это аварийная база и справочник функций.
# в готовый ответ блок 0 целиком лучше не копировать.
# перед каждым методом ниже уже добавлена своя минимальная база.
# если что-то сломалось или не хватает функции, можно запустить этот блок.
# обычно здесь ничего менять не надо.
########################################################################
# ============================================================
# блок 0. общие функции - запасной вариант
# ============================================================
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция считает евклидову норму вектора
def vector_norm(x): # норма через обычный цикл
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
for value in x:
s += float(value) * float(value)
# возвращаем результат
return math.sqrt(s)
# функция считает максимум модулей
def max_norm(x): # максимум модулей через обычный цикл
# приводим данные к массиву
x = np.array(x, dtype=float)
best = 0.0
for value in x:
cur = abs(float(value))
if cur > best:
best = cur
# возвращаем результат
return best
# функция считает скалярное произведение
def dot(u, v): # скалярное произведение через обычный цикл
# приводим данные к массиву
u = np.array(u, dtype=float)
# приводим данные к массиву
v = np.array(v, dtype=float)
# считаем итоговое значение
s = 0.0
# основной цикл
for i in range(len(u)):
s += float(u[i]) * float(v[i])
# возвращаем результат
return s
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8): # округление вектора для вывода
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция достает диагональ матрицы
def diag_values(A): # диагональ матрицы для вывода
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
d = []
# основной цикл
for i in range(min(n, m)):
d.append(float(A[i, i]))
# возвращаем результат
return d
# функция умножает матрицу на вектор
def mat_vec(A, x):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
x = np.array(x, dtype=float)
# определяем размеры
n, m = A.shape
y = np.zeros(n)
# основной цикл
for i in range(n):
# считаем итоговое значение
s = 0.0
# основной цикл
for j in range(m):
s += A[i, j] * x[j]
y[i] = s
# возвращаем результат
return y
# функция умножает две матрицы
def mat_mul(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# определяем размеры
n, m = A.shape
m2, p = B.shape
if m != m2:
raise ValueError("Размеры матриц не подходят для умножения")
C = np.zeros((n, p))
# основной цикл
for i in range(n):
# основной цикл
for j in range(p):
# считаем итоговое значение
s = 0.0
# основной цикл
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
# возвращаем результат
return C
# функция транспонирует матрицу
def transpose(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
T = np.zeros((m, n))
# основной цикл
for i in range(n):
# основной цикл
for j in range(m):
T[j, i] = A[i, j]
# возвращаем результат
return T
# функция создает единичную матрицу
def identity(n):
I = np.zeros((n, n))
# основной цикл
for i in range(n):
I[i, i] = 1.0
# возвращаем результат
return I
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("блок 0 загружен как запасная база")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция решает систему линейных уравнений методом гаусса
def gaussian_solve(A, b):
# Ручной метод Гаусса с частичным выбором главного элемента.
# Решает A x = b. Нужен вместо готового решателя.
A = np.array(A, dtype=float)
# приводим данные к массиву
b = np.array(b, dtype=float)
# определяем размеры
n = len(b)
# основной цикл
for k in range(n):
pivot = k
# основной цикл
for i in range(k + 1, n):
# проверяем деление на маленькое число
if abs(A[i, k]) > abs(A[pivot, k]):
pivot = i
# проверяем деление на маленькое число
if abs(A[pivot, k]) < EPS_DIV:
raise ValueError("Матрица вырождена или почти вырождена")
if pivot != k:
A[[k, pivot]] = A[[pivot, k]]
b[k], b[pivot] = b[pivot], b[k]
# основной цикл
for i in range(k + 1, n):
factor = A[i, k] / A[k, k]
A[i, k] = 0.0
# основной цикл
for j in range(k + 1, n):
A[i, j] -= factor * A[k, j]
b[i] -= factor * b[k]
x = np.zeros(n)
# основной цикл
for i in range(n - 1, -1, -1):
# считаем итоговое значение
s = 0.0
# основной цикл
for j in range(i + 1, n):
s += A[i, j] * x[j]
x[i] = (b[i] - s) / A[i, i]
# возвращаем результат
return x
# функция считает производную центральной разностью
def numerical_derivative(f, x, h=1e-6):
# возвращаем результат
return (f(x + h) - f(x - h)) / (2 * h)
# выводим результат
print("Блок 0 загружен. Теперь можно запускать нужный шаблон.")
# %%
# ## 1. Ошибки и суммирование
# %%
########################################################################
# как понять, что этот блок нужен
# ищи в условии слова: сумма, суммирование, ошибка округления, потеря точности, Кэхэн.
#
# что менять
# numbers - это список чисел, которые надо сложить.
# если в условии дана последовательность чисел, просто вставь ее сюда.
#
# пример
# если в условии: сложить 0.1 десять раз
# пиши:
# numbers = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
#
# что смотреть в ответе
# Обычная сумма - как считает обычный цикл сложения.
# Сумма Кэхэна - более аккуратная сумма.
# если они разные, значит накопление ошибки реально заметно.
########################################################################
# ============================================================
# 1.1. АЛГОРИТМ КЭХЭНА ДЛЯ СУММИРОВАНИЯ
# если в задании про накопление ошибки
# ============================================================
# === минимальная база для этой ячейки ===
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
numbers = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
# === дальше не трогать ===
def kahan_sum(numbers):
# считаем итоговое значение
s = 0.0
c = 0.0
# создаем историю итераций
history = []
for i, x in enumerate(numbers):
y = x - c
t = s + y
c = (t - s) - y
# считаем итоговое значение
s = t
history.append({
"i": i,
"x": x,
"sum": s,
"correction": c
})
# возвращаем результат
return s, history
ordinary = 0.0
for value in numbers:
ordinary += value
kahan, history = kahan_sum(numbers)
# выводим результат
print("Обычная сумма:", ordinary)
# выводим результат
print("Сумма Кэхэна:", kahan)
print_history(history, max_rows=15)
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# сравни обычную сумму и сумму кэхэна.
# разница меньше 1e-10 - ошибка почти не влияет.
# разница от 1e-8 до 1e-4 - ошибка заметная.
# разница больше 1e-4 - обычная сумма может быть плохой.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если суммы отличаются:
Обычная сумма и сумма Кэхэна получились разными. Значит, при обычном суммировании появилась ошибка округления. Алгоритм Кэхэна уменьшил эту ошибку.
Вариант 2 - если суммы почти одинаковые:
Обычная сумма и сумма Кэхэна почти совпали. На этих данных ошибка округления почти не проявилась.
Вариант 3 - если нужно написать общий вывод:
Алгоритм Кэхэна нужен для более аккуратного суммирования чисел. Он хранит поправку и уменьшает накопление ошибки.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод Кэхэна нужен для более точного суммирования чисел.
#
# Проблема: при сложении большого и маленького числа маленькая часть может потеряться из-за округления.
# Идея: хранить не только сумму, но и поправку c.
#
# Формулы:
# y = x_i - c
# t = sum + y
# c = (t - sum) - y
# sum = t
#
# Данные: список чисел.
# Остановка: метод просто проходит по списку один раз.
# Плюс: уменьшает накопление ошибки.
# Минус: не убирает ошибку полностью.
#
# Короткий ответ: алгоритм Кэхэна уменьшает ошибку округления при суммировании за счет хранения поправки.
# ====================================================================
# %%
# ## 2. Корни нелинейных уравнений
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: бисекция, деление пополам, корень на отрезке, отрезок [a,b].
# Часто в условии есть фраза, что на концах отрезка функция меняет знак.
#
# что менять
# f(x) - функция из уравнения. Уравнение надо привести к виду f(x)=0.
# a - левый конец отрезка.
# b - правый конец отрезка.
# tol - точность. Если написано 10^-6, пиши 1e-6.
# max_iter - максимум итераций. Обычно не трогай.
# график идет отдельным блоком ниже. если он не нужен, удали блок графика.
#
# куда смотреть в условии
# если написано: найти корень x^3 - x - 2 = 0 на [1,2]
# то:
# f(x) = x**3 - x - 2
# a = 1
# b = 2
#
# что смотреть в ответе
# корень - это найденное x.
# значение функции в найденном корне должно быть близко к нулю.
# количество итераций можно написать в ответе.
########################################################################
# ============================================================
# 2.1. МЕТОД БИСЕКЦИИ
# Когда дан отрезок [a, b] и f(a)*f(b)<0
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
# возвращаем результат
return x**3 - x - 2
# левый конец отрезка или нижний предел
a = 1.0
# правый конец отрезка или верхний предел
b = 2.0
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def bisection(f, a, b, tol=1e-6, max_iter=100):
fa = f(a)
fb = f(b)
if fa * fb > 0:
raise ValueError("На концах отрезка нет смены знака: f(a)*f(b)>0")
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
c = (a + b) / 2
fc = f(c)
history.append({
"k": k,
"a": round(a, 8),
"b": round(b, 8),
"c": round(c, 8),
"f(c)": round(fc, 8),
"length": round(abs(b - a), 8)
})
# проверяем деление на маленькое число
if abs(fc) < tol or abs(b - a) / 2 < tol:
# возвращаем результат
return c, history
if fa * fc <= 0:
b = c
fb = fc
else:
a = c
fa = fc
# возвращаем результат
return c, history
root, history = bisection(f, a, b, tol, max_iter)
# выводим результат
print("Метод бисекции")
# выводим результат
print("Корень:", root)
# выводим результат
print("f(root):", f(root))
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# график
# если график не нужен, удали этот блок
xs = np.linspace(a - 1, b + 1, 300)
ys = [f(x) for x in xs]
# строим график
plt.figure()
# строим график
plt.axhline(0)
# строим график
plt.plot(xs, ys)
# строим график
plt.scatter([root], [f(root)])
# строим график
plt.title("График функции и найденный корень")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри количество итераций k и |f(корень)|.
# для eps=1e-6 обычно 20-30 итераций - нормально.
# точная оценка: k примерно log2((b-a)/eps).
# если k больше 50 при eps=1e-6, метод сработал, но из-за точности или большого отрезка вышло долго.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если f(root) близко к 0:
Метод бисекции сошелся. Найден корень на заданном отрезке. Значение f(root) близко к нулю, поэтому ответ можно считать подходящим.
Вариант 2 - если итераций много:
Метод бисекции дал правильный результат, но потребовал много итераций. Это нормально, потому что метод надежный, но сходится не очень быстро.
Вариант 3 - если нужно написать про отрезок:
На каждом шаге отрезок уменьшался в два раза. В конце был получен маленький отрезок, внутри которого находится корень.
Вариант 4 - если есть график:
По графику видно, что функция пересекает ось Ox около найденного значения root.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод бисекции решает уравнение f(x)=0 на отрезке [a,b].
#
# Главное условие: f(a)*f(b) < 0. Тогда на отрезке есть смена знака.
# Идея: делить отрезок пополам и оставлять половину, где есть смена знака.
#
# Формула:
# c = (a + b) / 2
#
# Данные: f(x), a, b, eps.
# Остановка: abs(f(c)) < eps или abs(b-a) < eps.
# Плюс: надежный метод, не нужна производная.
# Минус: сходится медленно.
#
# Короткий ответ: метод последовательно сужает отрезок, внутри которого находится корень.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: метод Ньютона, касательные, начальное приближение, производная.
#
# что менять
# f(x) - функция из уравнения f(x)=0.
# df(x) - производная этой функции.
# x0 - начальное приближение. Обычно прямо дано в условии.
# tol - точность.
# max_iter - максимум итераций. Обычно не трогай.
# график идет отдельным блоком ниже. если он не нужен, удали блок графика.
#
# куда смотреть в условии
# если написано: решить x^3 - x - 2 = 0 методом Ньютона, x0=1.5
# то:
# f(x) = x**3 - x - 2
# df(x) = 3*x**2 - 1
# x0 = 1.5
#
# что смотреть в ответе
# корень - это найденное решение.
# значение функции в найденном корне должно быть близко к нулю.
# если итераций мало, метод сошелся быстро.
########################################################################
# ============================================================
# 2.2. МЕТОД НЬЮТОНА ДЛЯ ОДНОГО УРАВНЕНИЯ
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
# возвращаем результат
return x**3 - x - 2
# производная функции из условия
def df(x):
# возвращаем результат
return 3*x**2 - 1
# начальное приближение из условия
x0 = 1.5
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def newton_scalar(f, df, x0, tol=1e-6, max_iter=100):
x = float(x0)
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
fx = f(x)
dfx = df(x)
# проверяем деление на маленькое число
if abs(dfx) < EPS_DIV:
raise ZeroDivisionError("Производная почти ноль")
# считаем новое приближение
x_new = x - fx / dfx
# считаем ошибку
err = abs(x_new - x)
history.append({
"k": k,
"x": round(x, 10),
"f(x)": round(fx, 10),
"x_new": round(x_new, 10),
"error": round(err, 10)
})
# проверяем условие остановки
if err < tol or abs(f(x_new)) < tol:
# возвращаем результат
return x_new, history
x = x_new
# возвращаем результат
return x, history
root, history = newton_scalar(f, df, x0, tol, max_iter)
# выводим результат
print("Метод Ньютона")
# выводим результат
print("Корень:", root)
# выводим результат
print("f(root):", f(root))
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# график
# если график не нужен, удали этот блок
xs = np.linspace(root - 2, root + 2, 300)
ys = [f(x) for x in xs]
iter_x = [row["x"] for row in history]
# строим график
plt.figure()
# строим график
plt.axhline(0)
# строим график
plt.plot(xs, ys)
# строим график
plt.scatter(iter_x, [f(x) for x in iter_x])
# строим график
plt.title("Итерации метода Ньютона")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри количество итераций и |f(корень)|.
# 1-7 итераций - быстро и хорошо.
# 8-15 итераций - нормально, старт был не совсем близко.
# больше 15 - для Ньютона многовато, проверь старт или производную.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если f(root) близко к 0:
Метод Ньютона сошелся. Полученное значение root является приближенным корнем уравнения.
Вариант 2 - если итераций мало:
Метод Ньютона сошелся быстро. Это ожидаемо, потому что около корня метод Ньютона обычно имеет быструю сходимость.
Вариант 3 - если итераций много:
Метод Ньютона сошелся, но потребовал много итераций. Возможно, начальное приближение было не очень близко к корню.
Вариант 4 - если метод не сошелся или появилась ошибка:
Метод Ньютона не дал устойчивого результата. Возможно, производная около нуля или начальное приближение выбрано плохо.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод Ньютона решает уравнение f(x)=0.
#
# Идея: в текущей точке строится касательная, а ее пересечение с осью Ox дает новое приближение.
#
# Формула:
# x_next = x - f(x) / f_prime(x)
#
# Данные: f(x), f_prime(x), x0, eps.
# Остановка: abs(x_next-x) < eps или abs(f(x_next)) < eps.
# Плюс: часто сходится очень быстро.
# Минус: нужна производная, метод зависит от x0, нельзя делить на почти нулевую производную.
#
# Короткий ответ: метод Ньютона заменяет функцию касательной и уточняет корень по формуле x_next.
# ====================================================================
# %%
import numpy as np
import matplotlib.pyplot as plt
########################################################################
# как понять, что этот блок нужен
# ищи слова: модифицированный Ньютон, фиксированная производная, производная в начальной точке.
#
# что менять
# f(x) - функция из уравнения.
# df(x) - производная функции.
# x0 - начальное приближение.
# tol - точность.
# max_iter - обычно не трогай.
#
# куда смотреть в условии
# если написано x0=1 и eps=10^-5, то:
# x0 = 1
# tol = 1e-5
#
# что смотреть в ответе
# корень - найденное решение.
# значение функции в найденном корне должно быть близко к нулю.
# Этот метод может идти медленнее обычного Ньютона.
########################################################################
# ============================================================
# 2.3. МОДИФИЦИРОВАННЫЙ МЕТОД НЬЮТОНА
# Производная берется только в начальной точке x0
# ============================================================
# === минимальная база для этой ячейки ===
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
# возвращаем результат
return x**3 - x - 2
# производная функции из условия
def df(x):
# возвращаем результат
return 3*x**2 - 1
# начальное приближение из условия
x0 = 1.5
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def modified_newton_scalar(f, df, x0, tol=1e-6, max_iter=100):
x = float(x0)
dfx0 = df(x0)
# проверяем деление на маленькое число
if abs(dfx0) < EPS_DIV:
raise ZeroDivisionError("Производная в x0 почти ноль")
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
fx = f(x)
# считаем новое приближение
x_new = x - fx / dfx0
# считаем ошибку
err = abs(x_new - x)
history.append({
"k": k,
"x": round(x, 10),
"f(x)": round(fx, 10),
"x_new": round(x_new, 10),
"error": round(err, 10)
})
# проверяем условие остановки
if err < tol or abs(f(x_new)) < tol:
# возвращаем результат
return x_new, history
x = x_new
# возвращаем результат
return x, history
root, history = modified_newton_scalar(f, df, x0, tol, max_iter)
# выводим результат
print("Модифицированный метод Ньютона")
# выводим результат
print("Корень:", root)
# выводим результат
print("f(root):", f(root))
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# выводим результат
# ====================================================================
# график функции и найденного корня
# если график не нужен, удали этот блок
graph_left = root - 2
graph_right = root + 2
xs = np.linspace(graph_left, graph_right, 300)
ys = [f(x) for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим ось x
plt.axhline(0, color="black", linewidth=1)
# отмечаем корень
plt.scatter([root], [f(root)])
# строим график
plt.title("график функции и найденный корень")
# строим график
plt.grid(True)
# строим график
plt.show()
# график ошибки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
errors_plot = []
for row in history:
if "k" in row and "error" in row:
ks_plot.append(row["k"])
errors_plot.append(row["error"])
if errors_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, errors_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("ошибка")
# строим график
plt.title("ошибка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри количество итераций и |f(корень)|.
# 5-20 итераций - нормально.
# больше 30 - фиксированная производная сильно замедлила метод.
# вывод: дешевле один шаг, но итераций обычно больше, чем у обычного Ньютона.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если f(root) близко к 0:
Модифицированный метод Ньютона сошелся. Найдено приближенное решение уравнения.
Вариант 2 - если итераций больше, чем у обычного Ньютона:
Метод сошелся медленнее обычного Ньютона, потому что производная фиксировалась в начальной точке и не обновлялась.
Вариант 3 - если нужно описать отличие метода:
В отличие от обычного метода Ньютона, здесь производная считается только один раз в точке x0.
Вариант 4 - если ошибка большая:
Метод не дал хорошей точности. Нужно проверить функцию, производную или начальное приближение.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Модифицированный метод Ньютона похож на обычный Ньютон.
#
# Отличие: производная считается один раз в начальной точке x0.
#
# Формула:
# x_next = x - f(x) / f_prime(x0)
#
# Данные: f(x), f_prime(x), x0, eps.
# Остановка: abs(x_next-x) < eps или abs(f(x_next)) < eps.
# Плюс: меньше вычислений производной.
# Минус: обычно медленнее обычного Ньютона.
#
# Короткий ответ: производная фиксируется в начальной точке, поэтому метод проще, но может сходиться медленнее.
# ====================================================================
# %%
import numpy as np
import matplotlib.pyplot as plt
########################################################################
# как понять, что этот блок нужен
# ищи слова: метод секущих, две начальные точки, x0 и x1.
#
# что менять
# f(x) - функция из уравнения f(x)=0.
# x0 - первое начальное приближение.
# x1 - второе начальное приближение.
# tol - точность.
# max_iter - обычно не трогай.
#
# куда смотреть в условии
# если написано: начальные приближения 1 и 2
# то:
# x0 = 1
# x1 = 2
#
# что смотреть в ответе
# корень - найденное решение.
# значение функции в найденном корне должно быть близко к нулю.
# количество итераций можно написать в выводе.
########################################################################
# ============================================================
# 2.4. МЕТОД СЕКУЩИХ
# ============================================================
# === минимальная база для этой ячейки ===
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
# возвращаем результат
return x**3 - x - 2
# начальное приближение из условия
x0 = 1.0
x1 = 2.0
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def secant_method(f, x0, x1, tol=1e-6, max_iter=100):
x_prev = float(x0)
x_curr = float(x1)
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
f_prev = f(x_prev)
f_curr = f(x_curr)
denom = f_curr - f_prev
# проверяем деление на маленькое число
if abs(denom) < EPS_DIV:
raise ZeroDivisionError("Деление на почти ноль")
x_next = x_curr - f_curr * (x_curr - x_prev) / denom
# считаем ошибку
err = abs(x_next - x_curr)
history.append({
"k": k,
"x_prev": round(x_prev, 10),
"x_curr": round(x_curr, 10),
"x_next": round(x_next, 10),
"f(x_next)": round(f(x_next), 10),
"error": round(err, 10)
})
# проверяем условие остановки
if err < tol or abs(f(x_next)) < tol:
# возвращаем результат
return x_next, history
x_prev = x_curr
x_curr = x_next
# возвращаем результат
return x_curr, history
root, history = secant_method(f, x0, x1, tol, max_iter)
# выводим результат
print("Метод секущих")
# выводим результат
print("Корень:", root)
# выводим результат
print("f(root):", f(root))
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# выводим результат
# ====================================================================
# график функции и найденного корня
# если график не нужен, удали этот блок
graph_left = root - 2
graph_right = root + 2
xs = np.linspace(graph_left, graph_right, 300)
ys = [f(x) for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим ось x
plt.axhline(0, color="black", linewidth=1)
# отмечаем корень
plt.scatter([root], [f(root)])
# строим график
plt.title("график функции и найденный корень")
# строим график
plt.grid(True)
# строим график
plt.show()
# график ошибки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
errors_plot = []
for row in history:
if "k" in row and "error" in row:
ks_plot.append(row["k"])
errors_plot.append(row["error"])
if errors_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, errors_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("ошибка")
# строим график
plt.title("ошибка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри количество итераций и |f(корень)|.
# 5-15 итераций - нормально.
# больше 20 - начальные точки не очень удачные.
# если не сходится, бери x0 и x1 ближе к корню.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если f(root) близко к 0:
Метод секущих сошелся. Полученное значение root является приближенным корнем уравнения.
Вариант 2 - если итераций мало:
Метод секущих быстро нашел корень, потому что использует две последние точки и не требует явной производной.
Вариант 3 - если итераций много:
Метод секущих сошелся, но потребовал много итераций. Начальные точки могли быть не очень удачными.
Вариант 4 - если метод не сошелся:
Метод секущих не дал хорошего результата. Нужно проверить начальные точки x0 и x1.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод секущих решает уравнение f(x)=0 без явной производной.
#
# Идея: производная заменяется наклоном секущей через две последние точки.
#
# Формула:
# x_next = x_curr - f(x_curr)*(x_curr-x_prev)/(f(x_curr)-f(x_prev))
#
# Данные: f(x), x0, x1, eps.
# Остановка: abs(x_next-x_curr) < eps или abs(f(x_next)) < eps.
# Плюс: не нужна производная.
# Минус: нужны две стартовые точки, метод может не сойтись.
#
# Короткий ответ: метод секущих использует две последние точки, чтобы приблизить производную.
# ====================================================================
# %%
import numpy as np
import matplotlib.pyplot as plt
########################################################################
# как понять, что этот блок нужен
# ищи слова: простые итерации, функциональные итерации, x = phi(x).
#
# что менять
# phi(x) - формула, по которой считается новое x.
# f(x) - исходная функция для проверки. Если есть уравнение f(x)=0, вставь его.
# x0 - начальное приближение.
# tol - точность.
# max_iter - обычно не трогай.
#
# куда смотреть в условии
# если написано x = (x+2)^(1/3), то:
# phi(x) = (x + 2)**(1/3)
#
# что смотреть в ответе
# решение - это последняя итерация.
# ошибка должна стать маленькой.
# значение функции в найденном корне должно быть близко к нулю, если f задана.
########################################################################
# ============================================================
# 2.5. МЕТОД ПРОСТЫХ ИТЕРАЦИЙ x = phi(x)
# ============================================================
# === минимальная база для этой ячейки ===
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def phi(x):
# возвращаем результат
return (x + 2)**(1/3)
# функция из условия
def f(x):
# возвращаем результат
return x**3 - x - 2
# начальное приближение из условия
x0 = 1.0
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def fixed_point_iteration(phi, x0, tol=1e-6, max_iter=100, f=None):
x = float(x0)
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
# считаем новое приближение
x_new = phi(x)
# считаем ошибку
err = abs(x_new - x)
row = {
"k": k,
"x": round(x, 10),
"x_new": round(x_new, 10),
"error": round(err, 10)
}
if f is not None:
row["f(x_new)"] = round(f(x_new), 10)
history.append(row)
# проверяем условие остановки
if err < tol:
# возвращаем результат
return x_new, history
x = x_new
# возвращаем результат
return x, history
root, history = fixed_point_iteration(phi, x0, tol, max_iter, f=f)
# выводим результат
print("Метод простых итераций")
# выводим результат
print("Решение:", root)
# выводим результат
print("f(root):", f(root))
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# выводим результат
# ====================================================================
# график функции и найденного решения
# если график не нужен, удали этот блок
graph_left = root - 2
graph_right = root + 2
xs = np.linspace(graph_left, graph_right, 300)
ys = [f(value) for value in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим ось x
plt.axhline(0, color="black", linewidth=1)
# отмечаем решение
plt.scatter([root], [f(root)])
# строим график
plt.title("график функции и найденное решение")
# строим график
plt.grid(True)
# строим график
plt.show()
# график ошибки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
errors_plot = []
for row in history:
if "k" in row and "error" in row:
ks_plot.append(row["k"])
errors_plot.append(row["error"])
if errors_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, errors_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("ошибка")
# строим график
plt.title("ошибка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри ошибку между соседними итерациями и |f(решение)|.
# до 20-30 итераций - нормально.
# 30-80 - медленно, но возможно.
# если ошибка не уменьшается, phi выбрана плохо или |phi'| >= 1.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если error маленький:
Метод простых итераций сошелся. Последнее значение можно считать приближенным решением.
Вариант 2 - если f(root) близко к 0:
Проверка через f(root) показывает, что найденное значение подходит для исходного уравнения.
Вариант 3 - если error уменьшается медленно:
Итерации сходятся медленно. Это может быть связано с тем, что phi(x) слабо сжимает значения.
Вариант 4 - если error не уменьшается:
Метод простых итераций не сходится. Возможно, формула phi(x) выбрана неудачно.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод простых итераций решает уравнение, записанное как x = phi(x).
#
# Идея: много раз считать x_next = phi(x), пока значения не перестанут сильно меняться.
#
# Формула:
# x_next = phi(x)
#
# Данные: phi(x), x0, eps.
# Остановка: abs(x_next-x) < eps.
# Условие хорошей сходимости: abs(phi_prime(x)) < 1 около решения.
# Плюс: простая схема.
# Минус: нужно правильно выбрать phi, иначе метод не сойдется.
#
# Короткий ответ: метод ищет неподвижную точку функции phi.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: минимум, минимизация, метод дихотомии, отрезок [a,b].
#
# что менять
# f(x) - функция, минимум которой надо найти.
# a - левый конец отрезка.
# b - правый конец отрезка.
# tol - точность.
# delta - маленькое число для сравнения точек. Обычно не трогай.
# график идет отдельным блоком ниже. если он не нужен, удали блок графика.
#
# куда смотреть в условии
# если написано: минимизировать (x-2)^2+1 на [0,5]
# то:
# f(x) = (x - 2)**2 + 1
# a = 0
# b = 5
#
# что смотреть в ответе
# x_min - точка минимума.
# f(x_min) - минимальное значение функции.
# если график есть, минимум должен быть в нижней точке.
########################################################################
# ============================================================
# 2.6. МИНИМИЗАЦИЯ МЕТОДОМ ДИХОТОМИИ
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
# возвращаем результат
return (x - 2)**2 + 1
# левый конец отрезка или нижний предел
a = 0.0
# правый конец отрезка или верхний предел
b = 5.0
# точность из условия
tol = 1e-5
# маленькое число для метода дихотомии, обычно можно не менять
delta = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 200
# === дальше не трогать ===
def dichotomy_minimize(f, a, b, tol=1e-5, delta=1e-6, max_iter=200):
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
mid = (a + b) / 2
x1 = mid - delta
x2 = mid + delta
f1 = f(x1)
f2 = f(x2)
history.append({
"k": k,
"a": round(a, 8),
"b": round(b, 8),
"x1": round(x1, 8),
"x2": round(x2, 8),
"f1": round(f1, 8),
"f2": round(f2, 8)
})
if f1 <= f2:
b = x2
else:
a = x1
# проверяем деление на маленькое число
if abs(b - a) < tol:
break
x_min = (a + b) / 2
# возвращаем результат
return x_min, f(x_min), history
x_min, y_min, history = dichotomy_minimize(f, a, b, tol, delta, max_iter)
# выводим результат
print("Метод дихотомии")
# выводим результат
print("x_min:", x_min)
# выводим результат
print("f(x_min):", y_min)
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# график
# если график не нужен, удали этот блок
xs = np.linspace(a, b, 300)
ys = [f(x) for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим график
plt.scatter([x_min], [y_min])
# строим график
plt.title("Минимум функции")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри длину итогового отрезка и f(x_min).
# если длина меньше tol, минимум найден с нужной точностью.
# итераций может быть много, если tol маленькая.
# метод надежный, но не самый быстрый.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если x_min внутри отрезка:
Метод дихотомии нашел приближенную точку минимума на заданном отрезке.
Вариант 2 - если нужен ответ с функцией:
В точке x_min функция принимает значение f(x_min). Это приближенное минимальное значение на отрезке.
Вариант 3 - если есть график:
По графику видно, что найденная точка расположена около нижней точки функции.
Вариант 4 - если минимум на границе:
Минимум оказался близко к границе отрезка. Нужно проверить, допускает ли условие минимум на границе.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод дихотомии ищет минимум функции на отрезке.
#
# Идея: около середины отрезка берутся две близкие точки. По значениям функции выбирается половина, где может быть минимум.
#
# Данные: f(x), a, b, eps, delta.
# Остановка: abs(b-a) < eps.
# Плюс: не нужна производная.
# Минус: нужен отрезок и метод может быть медленным.
#
# Короткий ответ: метод постепенно сужает отрезок, в котором находится точка минимума.
# ====================================================================
# %%
# ## 3. Системы нелинейных уравнений
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: система нелинейных уравнений, метод Ньютона для системы, якобиан.
# этот вариант для двух неизвестных x1 и x2.
#
# что из условия куда вставлять
# пункт 1 - первое уравнение вставь в F1, но перенеси все в левую часть, чтобы справа был 0.
# пункт 2 - второе уравнение вставь в F2, тоже в виде левая часть минус правая часть.
# пункт 3 - производные F1 по x1 и x2 вставь в первую строку J.
# пункт 4 - производные F2 по x1 и x2 вставь во вторую строку J.
# пункт 5 - начальные x1 и x2 вставь в x1_start и x2_start.
#
# пример: x1^2 + x2^2 = 4 и x1 - x2 = 1
# F1 = x1**2 + x2**2 - 4
# F2 = x1 - x2 - 1
########################################################################
# ============================================================
# 3.1а. метод ньютона для системы из двух уравнений
# якобиан пересчитывается на каждом шаге
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
# функция решает систему линейных уравнений методом гаусса
def gaussian_solve(A, b):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
b = np.array(b, dtype=float)
# определяем размеры
n = len(b)
# основной цикл
for k in range(n):
pivot = k
# основной цикл
for i in range(k + 1, n):
# проверяем деление на маленькое число
if abs(A[i, k]) > abs(A[pivot, k]):
pivot = i
# проверяем деление на маленькое число
if abs(A[pivot, k]) < EPS_DIV:
raise ValueError("матрица вырождена или почти вырождена")
if pivot != k:
A[[k, pivot]] = A[[pivot, k]]
b[k], b[pivot] = b[pivot], b[k]
# основной цикл
for i in range(k + 1, n):
factor = A[i, k] / A[k, k]
A[i, k] = 0.0
# основной цикл
for j in range(k + 1, n):
A[i, j] -= factor * A[k, j]
b[i] -= factor * b[k]
x = np.zeros(n)
# основной цикл
for i in range(n - 1, -1, -1):
# считаем итоговое значение
s = b[i]
# основной цикл
for j in range(i + 1, n):
s -= A[i, j] * x[j]
x[i] = s / A[i, i]
# возвращаем результат
return x
# === конец минимальной базы ===
# === менять только тут ===
# начальные приближения
# x1_start - начальное значение x1 из условия
x1_start = 1.5
# x2_start - начальное значение x2 из условия
x2_start = 0.5
# точность из условия
# если написано eps=10^-6, пиши 1e-6
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 50
# первое уравнение системы
# сюда вставь первое уравнение, но все перенеси в левую часть
def F1(x1, x2):
# возвращаем результат
return x1**2 + x2**2 - 4
# второе уравнение системы
# сюда вставь второе уравнение, тоже в виде = 0
def F2(x1, x2):
# возвращаем результат
return x1 - x2 - 1
# якобиан
# первая строка - производные F1 по x1 и x2
# вторая строка - производные F2 по x1 и x2
def J_matrix(x1, x2):
# возвращаем результат
return np.array([
[2*x1, 2*x2],
[1, -1]
], dtype=float)
# === дальше не трогать ===
# подготовка начального приближения
x = np.array([x1_start, x2_start], dtype=float)
# создаем историю итераций
history = []
# итерации метода ньютона
for k in range(1, max_iter + 1):
x1 = x[0]
x2 = x[1]
F_vec = np.array([F1(x1, x2), F2(x1, x2)], dtype=float)
J = J_matrix(x1, x2)
# находим поправку
delta = gaussian_solve(J, -F_vec)
# считаем новое приближение
x_new = x + delta
# считаем ошибку
error = vector_norm(x_new - x)
# считаем ошибку
residual = vector_norm(np.array([F1(x_new[0], x_new[1]), F2(x_new[0], x_new[1])], dtype=float))
history.append({"k": k, "x": round_vector(x, 8), "error": round(error, 10), "residual": round(residual, 10)})
x = x_new
# проверяем условие остановки
if error < tol or residual < tol:
break
# вывод результата
print("метод ньютона для системы 2 на 2")
# выводим результат
print("x1 =", x[0])
# выводим результат
print("x2 =", x[1])
# выводим результат
print("невязка =", vector_norm(np.array([F1(x[0], x[1]), F2(x[0], x[1])], dtype=float)))
print_history(history)
# выводим результат
print("""
вывод:
метод ньютона сошелся к решению системы. значения x1 и x2 являются приближенным решением, а невязка показывает, насколько хорошо они подходят в исходные уравнения.
""")
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри главное число ошибки или невязки, которое печатает блок.
# если ошибка меньше заданной точности tol или eps, результат можно считать хорошим.
# если ошибка больше 1e-3, ответ лучше проверить или уменьшить шаг.
# в выводе всегда пиши: что нашли, насколько точный ответ и почему метод подходит.
# ====================================================================
print("""
типовые выводы:
- метод ньютона для системы использует матрицу якобиана.
- ответ проверяется по невязке, то есть по близости F1 и F2 к нулю.
- если начальное приближение хорошее, метод обычно сходится быстро.
""")
# ====================================================================
# теория по методу
# ====================================================================
# для системы нелинейных уравнений метод ньютона на каждом шаге решает линейную систему J*delta = -F.
# матрица J называется якобианом и состоит из частных производных функций.
# новое приближение считается как x_new = x + delta.
# метод быстро сходится около корня, если якобиан не вырожден и старт выбран нормально.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: модифицированный метод ньютона для системы, фиксированный якобиан.
# этот вариант для двух неизвестных x1 и x2.
#
# отличие от обычного метода
# здесь матрица якоби считается один раз в начальной точке и дальше не меняется.
# это надо брать только если в задании прямо написано про фиксированный якобиан.
########################################################################
# ============================================================
# 3.1б. модифицированный ньютон для системы 2 на 2
# якобиан фиксируется в начальной точке
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
rows = history[:max_rows]
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
# функция решает систему линейных уравнений методом гаусса
def gaussian_solve(A, b):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
b = np.array(b, dtype=float)
# определяем размеры
n = len(b)
# основной цикл
for k in range(n):
pivot = k
# основной цикл
for i in range(k + 1, n):
# проверяем деление на маленькое число
if abs(A[i, k]) > abs(A[pivot, k]):
pivot = i
# проверяем деление на маленькое число
if abs(A[pivot, k]) < EPS_DIV:
raise ValueError("матрица вырождена или почти вырождена")
if pivot != k:
A[[k, pivot]] = A[[pivot, k]]
b[k], b[pivot] = b[pivot], b[k]
# основной цикл
for i in range(k + 1, n):
factor = A[i, k] / A[k, k]
A[i, k] = 0.0
# основной цикл
for j in range(k + 1, n):
A[i, j] -= factor * A[k, j]
b[i] -= factor * b[k]
x = np.zeros(n)
# основной цикл
for i in range(n - 1, -1, -1):
# считаем итоговое значение
s = b[i]
# основной цикл
for j in range(i + 1, n):
s -= A[i, j] * x[j]
x[i] = s / A[i, i]
# возвращаем результат
return x
# === конец минимальной базы ===
# === менять только тут ===
# начальные приближения
# x1_start - начальное значение x1 из условия
x1_start = 1.5
# x2_start - начальное значение x2 из условия
x2_start = 0.5
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 50
# первое уравнение системы
# вставь сюда первое уравнение после переноса всего в левую часть
def F1(x1, x2):
# возвращаем результат
return x1**2 + x2**2 - 4
# второе уравнение системы
# вставь сюда второе уравнение после переноса всего в левую часть
def F2(x1, x2):
# возвращаем результат
return x1 - x2 - 1
# якобиан
# тут производные такие же, как в обычном ньютоне, но матрица считается один раз
def J_matrix(x1, x2):
# возвращаем результат
return np.array([
[2*x1, 2*x2],
[1, -1]
], dtype=float)
# === дальше не трогать ===
# подготовка начального приближения и фиксированного якобиана
x = np.array([x1_start, x2_start], dtype=float)
J_fixed = J_matrix(x1_start, x2_start)
# создаем историю итераций
history = []
# итерации модифицированного метода ньютона
for k in range(1, max_iter + 1):
F_vec = np.array([F1(x[0], x[1]), F2(x[0], x[1])], dtype=float)
# находим поправку
delta = gaussian_solve(J_fixed, -F_vec)
# считаем новое приближение
x_new = x + delta
# считаем ошибку
error = vector_norm(x_new - x)
# считаем ошибку
residual = vector_norm(np.array([F1(x_new[0], x_new[1]), F2(x_new[0], x_new[1])], dtype=float))
history.append({"k": k, "x": round_vector(x, 8), "error": round(error, 10), "residual": round(residual, 10)})
x = x_new
# проверяем условие остановки
if error < tol or residual < tol:
break
# вывод результата
print("модифицированный метод ньютона для системы 2 на 2")
# выводим результат
print("x1 =", x[0])
# выводим результат
print("x2 =", x[1])
# выводим результат
print("невязка =", vector_norm(np.array([F1(x[0], x[1]), F2(x[0], x[1])], dtype=float)))
print_history(history)
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри норму невязки системы.
# если невязка меньше 1e-6, ответ можно считать хорошим для eps=1e-6.
# если невязка от 1e-6 до 1e-4, результат примерный, но часто приемлемый.
# если невязка больше 1e-3, решение слабое: проверь якобиан, старт или число итераций.
# ====================================================================
print("""
типовые выводы:
- в модифицированном методе якобиан считается только один раз.
- из-за этого один шаг дешевле, но сходимость обычно медленнее.
- метод стоит использовать, если в условии прямо сказано про фиксированный якобиан.
""")
# ====================================================================
# теория по методу
# ====================================================================
# модифицированный метод ньютона для системы отличается тем, что якобиан фиксируется в начальной точке.
# на всех итерациях решается система с одной и той же матрицей J(x0).
# это уменьшает объем вычислений, потому что не надо каждый раз считать производные заново.
# минус в том, что метод обычно теряет квадратичную сходимость и становится медленнее.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: система нелинейных уравнений с тремя неизвестными.
# этот вариант для x1, x2, x3.
#
# что из условия куда вставлять
# пункт 1 - три уравнения вставь в F1, F2, F3, каждое в виде = 0.
# пункт 2 - начальные значения вставь в x1_start, x2_start, x3_start.
# пункт 3 - в J_matrix вставь производные: первая строка для F1, вторая для F2, третья для F3.
########################################################################
# ============================================================
# 3.1в. метод ньютона для системы из трех уравнений
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
rows = history[:max_rows]
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
# функция решает систему линейных уравнений методом гаусса
def gaussian_solve(A, b):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
b = np.array(b, dtype=float)
# определяем размеры
n = len(b)
# основной цикл
for k in range(n):
pivot = k
# основной цикл
for i in range(k + 1, n):
# проверяем деление на маленькое число
if abs(A[i, k]) > abs(A[pivot, k]):
pivot = i
# проверяем деление на маленькое число
if abs(A[pivot, k]) < EPS_DIV:
raise ValueError("матрица вырождена или почти вырождена")
if pivot != k:
A[[k, pivot]] = A[[pivot, k]]
b[k], b[pivot] = b[pivot], b[k]
# основной цикл
for i in range(k + 1, n):
factor = A[i, k] / A[k, k]
A[i, k] = 0.0
# основной цикл
for j in range(k + 1, n):
A[i, j] -= factor * A[k, j]
b[i] -= factor * b[k]
x = np.zeros(n)
# основной цикл
for i in range(n - 1, -1, -1):
# считаем итоговое значение
s = b[i]
# основной цикл
for j in range(i + 1, n):
s -= A[i, j] * x[j]
x[i] = s / A[i, i]
# возвращаем результат
return x
# === конец минимальной базы ===
# === менять только тут ===
# начальные приближения
# если в условии x0=(1,1,1), эти три числа идут сюда
x1_start = 1.2
x2_start = 1.0
x3_start = 1.0
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 50
# первое уравнение системы, перенесенное в вид = 0
def F1(x1, x2, x3):
# возвращаем результат
return x1 + x2 + x3 - 6
# второе уравнение системы, перенесенное в вид = 0
def F2(x1, x2, x3):
# возвращаем результат
return x1**2 + x2 - 5
# третье уравнение системы, перенесенное в вид = 0
def F3(x1, x2, x3):
# возвращаем результат
return x3 - x1 - 1
# якобиан
# строка 1 - производные F1 по x1, x2, x3
# строка 2 - производные F2 по x1, x2, x3
# строка 3 - производные F3 по x1, x2, x3
def J_matrix(x1, x2, x3):
# возвращаем результат
return np.array([
[1, 1, 1],
[2*x1, 1, 0],
[-1, 0, 1]
], dtype=float)
# === дальше не трогать ===
# подготовка начального приближения
x = np.array([x1_start, x2_start, x3_start], dtype=float)
# создаем историю итераций
history = []
# итерации метода ньютона
for k in range(1, max_iter + 1):
F_vec = np.array([F1(x[0], x[1], x[2]), F2(x[0], x[1], x[2]), F3(x[0], x[1], x[2])], dtype=float)
J = J_matrix(x[0], x[1], x[2])
# находим поправку
delta = gaussian_solve(J, -F_vec)
# считаем новое приближение
x_new = x + delta
# считаем ошибку
error = vector_norm(x_new - x)
# считаем ошибку
residual = vector_norm(np.array([F1(x_new[0], x_new[1], x_new[2]), F2(x_new[0], x_new[1], x_new[2]), F3(x_new[0], x_new[1], x_new[2])], dtype=float))
history.append({"k": k, "x": round_vector(x, 8), "error": round(error, 10), "residual": round(residual, 10)})
x = x_new
# проверяем условие остановки
if error < tol or residual < tol:
break
# вывод результата
print("метод ньютона для системы 3 на 3")
# выводим результат
print("x1 =", x[0])
# выводим результат
print("x2 =", x[1])
# выводим результат
print("x3 =", x[2])
# выводим результат
print("невязка =", vector_norm(np.array([F1(x[0], x[1], x[2]), F2(x[0], x[1], x[2]), F3(x[0], x[1], x[2])], dtype=float)))
print_history(history)
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри главное число ошибки или невязки, которое печатает блок.
# если ошибка меньше заданной точности tol или eps, результат можно считать хорошим.
# если ошибка больше 1e-3, ответ лучше проверить или уменьшить шаг.
# в выводе всегда пиши: что нашли, насколько точный ответ и почему метод подходит.
# ====================================================================
print("""
типовые выводы:
- для трех неизвестных идея такая же, как для системы 2 на 2.
- надо правильно составить три функции и матрицу якобиана 3 на 3.
- качество ответа проверяется по невязке всех трех уравнений.
""")
# ====================================================================
# теория по методу
# ====================================================================
# метод ньютона для системы из трех уравнений строится так же, как для двух уравнений.
# вектор F содержит три функции, а якобиан содержит все частные производные по x1, x2, x3.
# на каждом шаге решается линейная система для поправки delta.
# если невязка стала маленькой, найденные x1, x2, x3 можно считать решением.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: простые итерации для системы, векторная итерация, x=g(x).
#
# что менять
# g(x) - формулы для следующего приближения.
# F_check(x) - исходные уравнения для проверки.
# x0 - начальный вектор.
#
# как записывать переменные
# x[0] - первая неизвестная.
# x[1] - вторая неизвестная.
#
# куда смотреть в условии
# если дана формула:
# u_new = sqrt(4 - v^2)
# v_new = u - 1
# то:
# g[0] = math.sqrt(4 - x[1]**2)
# g[1] = x[0] - 1
#
# что смотреть в ответе
# решение - последняя итерация.
# ошибка должна стать маленькой.
# невязка должна быть маленькой, если проверка задана.
########################################################################
# ============================================================
# 3.2. ПРОСТЫЕ ИТЕРАЦИИ ДЛЯ СИСТЕМЫ x = g(x)
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# === конец минимальной базы ===
# === менять только тут ===
def g(x):
# возвращаем результат
return np.array([
math.sqrt(max(0, 4 - x[1]**2)),
x[0] - 1
], dtype=float)
# вспомогательная функция
def F_check(x):
# возвращаем результат
return np.array([
x[0]**2 + x[1]**2 - 4,
x[0] - x[1] - 1
], dtype=float)
# начальное приближение из условия
x0 = np.array([1.5, 0.5], dtype=float)
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def fixed_point_system(g, x0, tol=1e-6, max_iter=100, F_check=None):
# приводим данные к массиву
x = np.array(x0, dtype=float)
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
# считаем новое приближение
x_new = np.array(g(x), dtype=float)
# считаем ошибку
err = vector_norm(x_new - x)
row = {
"k": k,
"x": round_vector(x, 8),
"x_new": round_vector(x_new, 8),
"error": round(err, 10)
}
if F_check is not None:
row["residual"] = round(vector_norm(F_check(x_new)), 10)
history.append(row)
# проверяем условие остановки
if err < tol:
# возвращаем результат
return x_new, history
x = x_new
# возвращаем результат
return x, history
solution, history = fixed_point_system(g, x0, tol, max_iter, F_check)
# выводим результат
print("Простые итерации для системы")
# выводим результат
print("Решение:", solution)
# выводим результат
print("F(solution):", F_check(solution))
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# выводим результат
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри норму изменения x и невязку.
# до 30 итераций - хорошо.
# 30-100 - медленно, но допустимо.
# если значения расходятся, отображение g не сжимающее.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если error маленький:
Метод простых итераций для системы сошелся. Полученный вектор можно считать приближенным решением.
Вариант 2 - если residual маленький:
Проверочная невязка мала, значит найденный вектор подходит для исходной системы.
Вариант 3 - если error маленький, но residual большой:
Итерации сошлись, но не к решению исходной системы. Нужно проверить формулу g(x).
Вариант 4 - если error не уменьшается:
Метод не сходится. Возможно, отображение g(x) не является сжимающим.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод простых итераций для системы решает систему вида x = g(x).
#
# Идея: строится последовательность векторов x_next = g(x).
#
# Формула:
# x_next = g(x)
#
# Данные: g(x), x0, eps, исходная F(x) для проверки.
# Остановка: norm(x_next-x) < eps.
# Плюс: не нужен Якобиан.
# Минус: метод может не сойтись, если g выбрана плохо.
#
# Короткий ответ: метод повторяет векторную формулу g до стабилизации решения.
# ====================================================================
# %%
# ## 4. Интерполяция
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: линейная интерполяция, таблица значений, найти значение между точками.
#
# что менять
# x_points - все x из таблицы.
# y_points - все y из таблицы.
# x_star - точка, где нужно найти значение функции.
#
# куда смотреть в условии
# если таблица:
# x: 0, 1, 2
# y: 1, 3, 5
# и надо найти значение при x=1.5
# то:
# x_points = [0, 1, 2]
# y_points = [1, 3, 5]
# x_star = 1.5
#
# что смотреть в ответе
# y* - найденное значение в точке x_star.
# Интервал показывает, между какими точками считали.
########################################################################
# ============================================================
# 4.1. ЛИНЕЙНАЯ ИНТЕРПОЛЯЦИЯ
# ============================================================
# === минимальная база для этой ячейки ===
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
x_points = [0, 1, 2, 3]
# значения y из таблицы
y_points = [1, 2, 0, 4]
# точка, где надо найти значение
x_star = 1.5
# === дальше не трогать ===
def linear_interpolation(x_points, y_points, x_star):
xs = list(map(float, x_points))
ys = list(map(float, y_points))
# основной цикл
for i in range(len(xs) - 1):
if xs[i] <= x_star <= xs[i + 1]:
x0, x1 = xs[i], xs[i + 1]
y0, y1 = ys[i], ys[i + 1]
y_star = y0 + (y1 - y0) * (x_star - x0) / (x1 - x0)
# возвращаем результат
return y_star, i
raise ValueError("x_star не попал ни в один интервал")
y_star, interval_index = linear_interpolation(x_points, y_points, x_star)
# выводим результат
print("Линейная интерполяция")
# выводим результат
print("x*:", x_star)
# выводим результат
print("y*:", y_star)
# выводим результат
print("Интервал:", interval_index)
# график
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(x_points, y_points, marker="o")
# строим график
plt.scatter([x_star], [y_star])
# строим график
plt.title("Линейная интерполяция")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри положение x0 относительно узлов.
# если x0 внутри таблицы, это нормальная интерполяция.
# если x0 вне таблицы, это экстраполяция и точность хуже.
# много узлов для одного полинома может дать колебания.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
Методом линейной интерполяции найдено приближенное значение функции в точке x_star.
Вариант 2 - если нужно объяснить метод:
Значение найдено по прямой, которая соединяет две соседние табличные точки.
Вариант 3 - если точка между близкими узлами:
Так как x_star находится между соседними узлами, линейная интерполяция дает простой приближенный результат.
Вариант 4 - если график есть:
На графике видно, что точка интерполяции лежит на отрезке между соседними узлами.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Линейная интерполяция находит значение функции между двумя известными точками.
#
# Идея: между соседними узлами функция заменяется прямой.
#
# Формула:
# y = y0 + (y1-y0)*(x-x0)/(x1-x0)
#
# Данные: x_points, y_points, x_star.
# Плюс: простой метод.
# Минус: низкая точность на сильно изогнутых функциях.
#
# Короткий ответ: значение находится по прямой между двумя соседними табличными точками.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: Лагранж, полином Лагранжа, интерполяционный многочлен.
#
# что менять
# x_points - узлы интерполяции.
# y_points - значения функции в этих узлах.
# x_star - точка, где нужно посчитать полином.
#
# куда смотреть в условии
# если дана таблица точек, просто перепиши x в x_points, а y в y_points.
# если написано найти P(1.5), то:
# x_star = 1.5
#
# что смотреть в ответе
# y* - значение полинома Лагранжа.
# Базисные L_i нужны только если преподаватель просит показать детали.
########################################################################
# ============================================================
# 4.2. ИНТЕРПОЛЯЦИЯ ЛАГРАНЖА
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
x_points = [0, 1, 2, 3]
# значения y из таблицы
y_points = [1, 2, 0, 4]
# точка, где надо найти значение
x_star = 1.5
# === дальше не трогать ===
def lagrange_value(x_points, y_points, x_star):
xs = list(map(float, x_points))
ys = list(map(float, y_points))
# определяем размеры
n = len(xs)
# считаем итоговое значение
result = 0.0
basis_values = []
# основной цикл
for i in range(n):
L = 1.0
# основной цикл
for j in range(n):
if i != j:
L *= (x_star - xs[j]) / (xs[i] - xs[j])
basis_values.append(L)
result += ys[i] * L
# возвращаем результат
return result, basis_values
y_star, basis_values = lagrange_value(x_points, y_points, x_star)
# выводим результат
print("Интерполяция Лагранжа")
# выводим результат
print("x*:", x_star)
# выводим результат
print("y*:", y_star)
# выводим результат
print("Базисные L_i(x*):", basis_values)
# график
# если график не нужен, удали этот блок
xs = np.linspace(min(x_points), max(x_points), 300)
ys = [lagrange_value(x_points, y_points, x)[0] for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим график
plt.scatter(x_points, y_points)
# строим график
plt.scatter([x_star], [y_star])
# строим график
plt.title("Полином Лагранжа")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри положение x0 относительно узлов.
# если x0 внутри таблицы, это нормальная интерполяция.
# если x0 вне таблицы, это экстраполяция и точность хуже.
# много узлов для одного полинома может дать колебания.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
Построен интерполяционный полином Лагранжа. Значение y* является приближенным значением функции в точке x_star.
Вариант 2 - если нужно написать про узлы:
Полином Лагранжа проходит через все заданные узлы интерполяции.
Вариант 3 - если точек много:
При большом числе точек полином может колебаться сильнее, но формально он проходит через все заданные точки.
Вариант 4 - если есть график:
На графике видно, как полином проходит через заданные точки.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Полином Лагранжа проходит через все заданные точки.
#
# Идея: полином строится как сумма y_i*L_i(x), где L_i - базисные полиномы.
#
# Формула в словах:
# P(x) = сумма y_i * L_i(x)
#
# Данные: x_points, y_points, x_star.
# Плюс: не нужно решать систему для коэффициентов.
# Минус: при большом числе точек полином может сильно колебаться.
#
# Короткий ответ: метод строит интерполяционный полином, который проходит через все узлы.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: кубический сплайн, натуральный сплайн, сплайн интерполяция.
#
# что менять
# x_points - x координаты точек.
# y_points - значения функции.
# x_star - точка, где нужно найти значение сплайна.
#
# куда смотреть в условии
# если даны точки (0,1), (1,2), (2,0), то:
# x_points = [0, 1, 2]
# y_points = [1, 2, 0]
#
# что смотреть в ответе
# S(x*) - значение сплайна в нужной точке.
# Коэффициенты a,b,c,d - это кусочные кубические полиномы.
# график идет отдельным блоком ниже. если он не нужен, удали блок графика.
########################################################################
# ============================================================
# 4.3. НАТУРАЛЬНЫЙ КУБИЧЕСКИЙ СПЛАЙН
# Без scipy
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция решает систему линейных уравнений методом гаусса
def gaussian_solve(A, b):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
b = np.array(b, dtype=float)
# определяем размеры
n = len(b)
# основной цикл
for k in range(n):
pivot = k
# основной цикл
for i in range(k + 1, n):
# проверяем деление на маленькое число
if abs(A[i, k]) > abs(A[pivot, k]):
pivot = i
# проверяем деление на маленькое число
if abs(A[pivot, k]) < EPS_DIV:
raise ValueError("матрица вырождена или почти вырождена")
if pivot != k:
A[[k, pivot]] = A[[pivot, k]]
b[k], b[pivot] = b[pivot], b[k]
# основной цикл
for i in range(k + 1, n):
factor = A[i, k] / A[k, k]
A[i, k] = 0.0
# основной цикл
for j in range(k + 1, n):
A[i, j] -= factor * A[k, j]
b[i] -= factor * b[k]
x = np.zeros(n)
# основной цикл
for i in range(n - 1, -1, -1):
# считаем итоговое значение
s = b[i]
# основной цикл
for j in range(i + 1, n):
s -= A[i, j] * x[j]
x[i] = s / A[i, i]
# возвращаем результат
return x
# === конец минимальной базы ===
# === менять только тут ===
x_points = [0, 1, 2, 3]
# значения y из таблицы
y_points = [1, 2, 0, 4]
# точка, где надо найти значение
x_star = 1.5
# === дальше не трогать ===
def natural_cubic_spline_coeffs(x_points, y_points):
# приводим данные к массиву
x = np.array(x_points, dtype=float)
y = np.array(y_points, dtype=float)
# определяем размеры
n = len(x) - 1
h = np.zeros(n)
# основной цикл
for i in range(n):
h[i] = x[i + 1] - x[i]
if n == 1:
c = np.zeros(2)
else:
A = np.zeros((n - 1, n - 1))
rhs = np.zeros(n - 1)
# основной цикл
for i in range(1, n):
row = i - 1
if row - 1 >= 0:
A[row, row - 1] = h[i - 1]
A[row, row] = 2 * (h[i - 1] + h[i])
if row + 1 <= n - 2:
A[row, row + 1] = h[i]
rhs[row] = 3 * ((y[i + 1] - y[i]) / h[i] - (y[i] - y[i - 1]) / h[i - 1])
c_inner = gaussian_solve(A, rhs)
c = np.zeros(n + 1)
# основной цикл
for i in range(1, n):
c[i] = c_inner[i - 1]
a = y[:-1].copy()
b = np.zeros(n)
d = np.zeros(n)
# основной цикл
for i in range(n):
b[i] = (y[i + 1] - y[i]) / h[i] - h[i] * (2*c[i] + c[i + 1]) / 3
d[i] = (c[i + 1] - c[i]) / (3 * h[i])
# возвращаем результат
return a, b, c[:-1], d
# вспомогательная функция
def spline_value(x_points, coeffs, x_star):
a, b, c, d = coeffs
# приводим данные к массиву
x = np.array(x_points, dtype=float)
# определяем размеры
n = len(x) - 1
idx = None
# основной цикл
for i in range(n):
if x[i] <= x_star <= x[i + 1]:
idx = i
break
if idx is None:
raise ValueError("x_star вне диапазона интерполяции")
dx = x_star - x[idx]
# возвращаем результат
return a[idx] + b[idx]*dx + c[idx]*dx**2 + d[idx]*dx**3, idx
coeffs = natural_cubic_spline_coeffs(x_points, y_points)
y_star, idx = spline_value(x_points, coeffs, x_star)
# выводим результат
print("Натуральный кубический сплайн")
# выводим результат
print("x*:", x_star)
# выводим результат
print("S(x*):", y_star)
# выводим результат
print("Интервал:", idx)
# выводим результат
print("Коэффициенты a,b,c,d:")
for name, arr in zip(["a", "b", "c", "d"], coeffs):
# выводим результат
print(name, arr)
# график
# если график не нужен, удали этот блок
xs = np.linspace(min(x_points), max(x_points), 300)
ys = [spline_value(x_points, coeffs, x)[0] for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим график
plt.scatter(x_points, y_points)
# строим график
plt.scatter([x_star], [y_star])
# строим график
plt.title("Кубический сплайн")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри, что x0 внутри сетки.
# сплайн хорош при большом числе узлов.
# если значения гладкие, результат обычно устойчивый.
# натуральный сплайн имеет нулевые вторые производные на концах.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
Построен натуральный кубический сплайн. Значение S(x*) является приближенным значением функции в нужной точке.
Вариант 2 - если нужно написать про гладкость:
Сплайн проходит через заданные точки и дает гладкую кусочную кривую.
Вариант 3 - если просят коэффициенты:
Коэффициенты a, b, c, d задают кубический многочлен на каждом промежутке.
Вариант 4 - если есть график:
По графику видно, что сплайн проходит через узлы и сглаживает данные.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Кубический сплайн строит гладкую кусочную интерполяцию.
#
# Идея: на каждом промежутке строится свой кубический полином, а в узлах полиномы гладко соединяются.
#
# Форма:
# S_i(x) = a + b*(x-x_i) + c*(x-x_i)^2 + d*(x-x_i)^3
#
# Натуральный сплайн: вторая производная на концах равна нулю.
# Данные: x_points, y_points, x_star.
# Плюс: гладкий график и хорошее поведение.
# Минус: нужно находить коэффициенты.
#
# Короткий ответ: сплайн - это набор кубических полиномов на отдельных интервалах.
# ====================================================================
# %%
# ## 5. Матрицы, собственные значения, QR, Шур
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: степенной метод, доминирующее собственное значение, наибольшее по модулю собственное значение.
#
# что менять
# A - матрица из условия.
# x0 - начальный вектор. Если его нет, ставь вектор из единиц нужной длины.
# tol - точность.
# max_iter - обычно не трогай.
#
# куда смотреть в условии
# если матрица 3 на 3, то A должна быть 3 на 3.
# если x0 не дан, можно поставить:
# x0 = np.array([1, 1, 1], dtype=float)
#
# что смотреть в ответе
# собственное значение - главный ответ.
# Собственный вектор - второй ответ, если его просят.
# невязка должна быть маленькой.
########################################################################
# ============================================================
# 5.1. СТЕПЕННОЙ МЕТОД
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция считает скалярное произведение
def dot(u, v):
# приводим данные к массиву
u = np.array(u, dtype=float)
# приводим данные к массиву
v = np.array(v, dtype=float)
# считаем итоговое значение
s = 0.0
# основной цикл
for i in range(len(u)):
s += float(u[i]) * float(v[i])
# возвращаем результат
return s
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция умножает матрицу на вектор
def mat_vec(A, x):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
x = np.array(x, dtype=float)
# определяем размеры
n, m = A.shape
y = np.zeros(n)
# основной цикл
for i in range(n):
# считаем итоговое значение
s = 0.0
# основной цикл
for j in range(m):
s += A[i, j] * x[j]
y[i] = s
# возвращаем результат
return y
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([
[2, 1, 0],
[1, 3, 1],
[0, 1, 2]
], dtype=float)
# начальное приближение из условия
x0 = np.array([1, 1, 1], dtype=float)
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 1000
# === дальше не трогать ===
def power_method(A, x0, tol=1e-6, max_iter=1000):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
x = np.array(x0, dtype=float)
x = x / vector_norm(x)
lambda_old = 0.0
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
y = mat_vec(A, x)
y_norm = vector_norm(y)
# проверяем деление на маленькое число
if y_norm < EPS_DIV:
raise ValueError("Получился почти нулевой вектор")
# считаем новое приближение
x_new = y / y_norm
Ax = mat_vec(A, x_new)
lambda_new = dot(Ax, x_new) / dot(x_new, x_new)
# считаем ошибку
err = abs(lambda_new - lambda_old)
# считаем ошибку
res = vector_norm(Ax - lambda_new * x_new)
history.append({
"k": k,
"lambda": round(lambda_new, 10),
"error": round(err, 10),
"residual": round(res, 10),
"x": round_vector(x_new, 8)
})
# проверяем условие остановки
if err < tol:
# возвращаем результат
return lambda_new, x_new, history
lambda_old = lambda_new
x = x_new
# возвращаем результат
return lambda_new, x_new, history
lam, vec, history = power_method(A, x0, tol, max_iter)
# выводим результат
print("Степенной метод")
# выводим результат
print("Собственное значение:", lam)
# выводим результат
print("Собственный вектор:", vec)
# выводим результат
print("Невязка ||Ax-lambda*x||:", vector_norm(mat_vec(A, vec) - lam * vec))
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# выводим результат
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри изменение lambda и невязку A*x-lambda*x.
# до 20-50 итераций - нормально.
# больше 100 - главное собственное значение плохо отделено.
# невязка меньше 1e-6 - хорошая собственная пара.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если невязка маленькая:
Степенной метод нашел доминирующее собственное значение и соответствующий собственный вектор.
Вариант 2 - если итераций мало:
Метод быстро сошелся, значит доминирующее собственное значение хорошо отделено от остальных.
Вариант 3 - если итераций много:
Сходимость была медленной. Возможно, два наибольших по модулю собственных значения близки.
Вариант 4 - если невязка большая:
Результат пока неточный. Нужно увеличить max_iter или проверить начальный вектор.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Степенной метод ищет доминирующее собственное значение матрицы.
#
# Доминирующее значение - самое большое по модулю.
# Идея: многократно умножать вектор на A и нормировать его.
#
# Формулы:
# y = A*x
# x_next = y / norm(y)
# lambda = (A*x_next, x_next) / (x_next, x_next)
#
# Данные: A, x0, eps.
# Остановка: изменение lambda меньше eps или малая невязка.
# Плюс: простой метод.
# Минус: находит только доминирующее значение и может сходиться медленно.
#
# Короткий ответ: метод выделяет направление доминирующего собственного вектора.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: со сдвигом, sigma, около sigma, обратный степенной метод.
#
# что менять
# A - матрица из условия.
# sigma - число, около которого надо искать собственное значение.
# x0 - начальный вектор. Если не дан, ставь единицы.
# tol - точность.
#
# куда смотреть в условии
# если написано: найти собственное значение около 2
# то:
# sigma = 2
#
# что смотреть в ответе
# собственное значение около sigma - главный ответ.
# невязка должна быть маленькой.
# если ошибка не падает, sigma может быть выбран плохо или матрица сложная.
########################################################################
# ============================================================
# 5.2. ОБРАТНЫЙ СТЕПЕННОЙ МЕТОД СО СДВИГОМ
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция считает скалярное произведение
def dot(u, v):
# приводим данные к массиву
u = np.array(u, dtype=float)
# приводим данные к массиву
v = np.array(v, dtype=float)
# считаем итоговое значение
s = 0.0
# основной цикл
for i in range(len(u)):
s += float(u[i]) * float(v[i])
# возвращаем результат
return s
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция умножает матрицу на вектор
def mat_vec(A, x):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
x = np.array(x, dtype=float)
# определяем размеры
n, m = A.shape
y = np.zeros(n)
# основной цикл
for i in range(n):
# считаем итоговое значение
s = 0.0
# основной цикл
for j in range(m):
s += A[i, j] * x[j]
y[i] = s
# возвращаем результат
return y
# функция создает единичную матрицу
def identity(n):
I = np.zeros((n, n))
# основной цикл
for i in range(n):
I[i, i] = 1.0
# возвращаем результат
return I
# функция решает систему линейных уравнений методом гаусса
def gaussian_solve(A, b):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
b = np.array(b, dtype=float)
# определяем размеры
n = len(b)
# основной цикл
for k in range(n):
pivot = k
# основной цикл
for i in range(k + 1, n):
# проверяем деление на маленькое число
if abs(A[i, k]) > abs(A[pivot, k]):
pivot = i
# проверяем деление на маленькое число
if abs(A[pivot, k]) < EPS_DIV:
raise ValueError("матрица вырождена или почти вырождена")
if pivot != k:
A[[k, pivot]] = A[[pivot, k]]
b[k], b[pivot] = b[pivot], b[k]
# основной цикл
for i in range(k + 1, n):
factor = A[i, k] / A[k, k]
A[i, k] = 0.0
# основной цикл
for j in range(k + 1, n):
A[i, j] -= factor * A[k, j]
b[i] -= factor * b[k]
x = np.zeros(n)
# основной цикл
for i in range(n - 1, -1, -1):
# считаем итоговое значение
s = b[i]
# основной цикл
for j in range(i + 1, n):
s -= A[i, j] * x[j]
x[i] = s / A[i, i]
# возвращаем результат
return x
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([
[2, 1, 0],
[1, 3, 1],
[0, 1, 2]
], dtype=float)
# сдвиг sigma из условия
sigma = 1.8
# начальное приближение из условия
x0 = np.array([1, 1, 1], dtype=float)
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def inverse_power_shift(A, sigma, x0, tol=1e-6, max_iter=100):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n = A.shape[0]
B = A - sigma * identity(n)
# приводим данные к массиву
x = np.array(x0, dtype=float)
x = x / vector_norm(x)
lambda_old = 0.0
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
y = gaussian_solve(B, x)
# считаем новое приближение
x_new = y / vector_norm(y)
Ax = mat_vec(A, x_new)
lambda_new = dot(Ax, x_new) / dot(x_new, x_new)
# считаем ошибку
err = abs(lambda_new - lambda_old)
# считаем ошибку
res = vector_norm(Ax - lambda_new * x_new)
history.append({
"k": k,
"lambda": round(lambda_new, 10),
"error": round(err, 10),
"residual": round(res, 10),
"x": round_vector(x_new, 8)
})
# проверяем условие остановки
if err < tol:
# возвращаем результат
return lambda_new, x_new, history
lambda_old = lambda_new
x = x_new
# возвращаем результат
return lambda_new, x_new, history
lam, vec, history = inverse_power_shift(A, sigma, x0, tol, max_iter)
# выводим результат
print("Обратный степенной метод со сдвигом")
# выводим результат
print("sigma:", sigma)
# выводим результат
print("Собственное значение около sigma:", lam)
# выводим результат
print("Собственный вектор:", vec)
# выводим результат
print("Невязка ||Ax-lambda*x||:", vector_norm(mat_vec(A, vec) - lam * vec))
# выводим результат
print("Количество итераций:", len(history))
print_history(history)
# выводим результат
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри изменение lambda и невязку A*x-lambda*x.
# до 20-50 итераций - нормально.
# больше 100 - главное собственное значение плохо отделено.
# невязка меньше 1e-6 - хорошая собственная пара.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если невязка маленькая:
Метод со сдвигом нашел собственное значение, ближайшее к sigma.
Вариант 2 - если результат близок к sigma:
Полученное собственное значение действительно находится около заданного сдвига sigma.
Вариант 3 - если результат далеко от sigma:
Результат не очень близок к sigma. Возможно, выбранный сдвиг ближе к другому собственному значению или нужно больше итераций.
Вариант 4 - если возникла ошибка решения системы:
Матрица A - sigma I может быть почти вырожденной. Это возможно, если sigma очень близко к собственному значению.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Обратный степенной метод со сдвигом ищет собственное значение около sigma.
#
# Идея: вместо умножения на A решается система с A - sigma*I.
#
# Формула:
# (A - sigma*I)*y = x
#
# Данные: A, sigma, x0, eps.
# Остановка: изменение lambda меньше eps или малая невязка.
# Плюс: можно искать значение около заданного sigma.
# Минус: на каждом шаге нужно решать систему.
#
# Короткий ответ: сдвиг sigma направляет метод к ближайшему собственному значению.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: QR разложение, A=QR, Грамм Шмидт, ортогонализация.
#
# что менять
# A - матрица из условия.
#
# куда смотреть в условии
# Просто перепиши матрицу построчно в A.
# Каждая внутренняя строка в np.array - это строка матрицы.
#
# что смотреть в ответе
# Q - ортогональная матрица.
# R - верхнетреугольная матрица.
# A - QR должно быть почти нулем.
# Q^T Q должно быть почти единичной матрицей.
########################################################################
# ============================================================
# 5.3. QR-РАЗЛОЖЕНИЕ ЧЕРЕЗ ГРАММА-ШМИДТА
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
EPS_DIV = 1e-14
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция считает скалярное произведение
def dot(u, v):
# приводим данные к массиву
u = np.array(u, dtype=float)
# приводим данные к массиву
v = np.array(v, dtype=float)
# считаем итоговое значение
s = 0.0
# основной цикл
for i in range(len(u)):
s += float(u[i]) * float(v[i])
# возвращаем результат
return s
# функция умножает две матрицы
def mat_mul(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# определяем размеры
n, m = A.shape
m2, p = B.shape
if m != m2:
raise ValueError("получился почти нулевой вектор")
C = np.zeros((n, p))
# основной цикл
for i in range(n):
# основной цикл
for j in range(p):
# считаем итоговое значение
s = 0.0
# основной цикл
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
# возвращаем результат
return C
# функция транспонирует матрицу
def transpose(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
T = np.zeros((m, n))
# основной цикл
for i in range(n):
# основной цикл
for j in range(m):
T[j, i] = A[i, j]
# возвращаем результат
return T
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([
[1, 1, 0],
[1, 0, 1],
[0, 1, 1]
], dtype=float)
# === дальше не трогать ===
def get_col(A, j):
# возвращаем результат
return np.array([A[i, j] for i in range(A.shape[0])], dtype=float)
# вспомогательная функция
def set_col(A, j, col):
# основной цикл
for i in range(A.shape[0]):
A[i, j] = col[i]
# вспомогательная функция
def qr_gram_schmidt(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
Q = np.zeros((n, m))
R = np.zeros((m, m))
# основной цикл
for j in range(m):
v = get_col(A, j)
# основной цикл
for i in range(j):
qi = get_col(Q, i)
R[i, j] = dot(qi, get_col(A, j))
v = v - R[i, j] * qi
R[j, j] = vector_norm(v)
# проверяем деление на маленькое число
if abs(R[j, j]) < EPS_DIV:
raise ValueError("Столбцы линейно зависимы")
qj = v / R[j, j]
set_col(Q, j, qj)
# возвращаем результат
return Q, R
Q, R = qr_gram_schmidt(A)
A_reconstructed = mat_mul(Q, R)
# выводим результат
print("QR-разложение")
# выводим результат
print("Q =")
# выводим результат
print(Q)
# выводим результат
print("R =")
# выводим результат
print(R)
# выводим результат
print("Q*R =")
# выводим результат
print(A_reconstructed)
# выводим результат
print("A - QR =")
# выводим результат
print(A - A_reconstructed)
# выводим результат
print("Q^T Q =")
# выводим результат
print(mat_mul(transpose(Q), Q))
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# проверь ортогональность Q и вид R.
# ошибка меньше 1e-8 - хорошо.
# R должна быть верхнетреугольной.
# если элементы ниже диагонали большие, разложение или итерации надо проверить.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если A - QR почти ноль:
QR-разложение построено правильно, потому что произведение Q*R восстанавливает матрицу A.
Вариант 2 - если Q^T Q почти единичная:
Столбцы матрицы Q ортонормированы, значит Q построена корректно.
Вариант 3 - если в R нули ниже диагонали:
Матрица R получилась верхнетреугольной, как и должно быть в QR-разложении.
Вариант 4 - если ошибка большая:
Разложение получилось неточным. Нужно проверить матрицу A или линейную зависимость столбцов.
""")
# ====================================================================
# теория по методу
# ====================================================================
# QR-разложение представляет матрицу как A = Q*R.
#
# Q - матрица с ортонормированными столбцами.
# R - верхнетреугольная матрица.
#
# Идея Грамма-Шмидта: из каждого столбца убираются проекции на уже построенные ортонормированные столбцы, потом он нормируется.
#
# Данные: A.
# Проверка: A примерно равно Q*R, а Q^T*Q примерно равно I.
# Плюс: используется в QR-алгоритме.
# Минус: классический вариант может быть неустойчив для почти зависимых столбцов.
#
# Короткий ответ: QR-разложение делит матрицу на ортогональную и верхнетреугольную части.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: QR алгоритм, спектр матрицы, собственные значения QR алгоритмом.
#
# что менять
# A - матрица из условия.
# iterations - число итераций. Если в условии не дано, оставь 50 или поставь 100.
#
# куда смотреть в условии
# если дана только матрица, меняй только A.
# если написано 20 итераций, то:
# iterations = 20
#
# что смотреть в ответе
# Приближенные собственные значения - это диагональ итоговой матрицы.
# offdiag_norm показывает, насколько матрица стала похожа на диагональную.
# Чем меньше offdiag_norm, тем лучше.
########################################################################
# ============================================================
# 5.4. QR-АЛГОРИТМ ДЛЯ СОБСТВЕННЫХ ЗНАЧЕНИЙ
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция достает диагональ матрицы
def diag_values(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
d = []
# основной цикл
for i in range(min(n, m)):
d.append(float(A[i, i]))
# возвращаем результат
return d
# функция умножает две матрицы
def mat_mul(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# определяем размеры
n, m = A.shape
m2, p = B.shape
if m != m2:
raise ValueError("получился почти нулевой вектор")
C = np.zeros((n, p))
# основной цикл
for i in range(n):
# основной цикл
for j in range(p):
# считаем итоговое значение
s = 0.0
# основной цикл
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
# возвращаем результат
return C
# === конец минимальной базы ===
# функция считает скалярное произведение
def dot(u, v):
s = 0.0
for i in range(len(u)):
s += u[i] * v[i]
# возвращаем результат
return s
# функция берет столбец матрицы
def get_col(A, j):
A = np.array(A, dtype=float)
col = np.zeros(A.shape[0])
for i in range(A.shape[0]):
col[i] = A[i, j]
# возвращаем результат
return col
# функция записывает столбец матрицы
def set_col(A, j, col):
for i in range(A.shape[0]):
A[i, j] = col[i]
def qr_gram_schmidt(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
Q = np.zeros((n, m))
R = np.zeros((m, m))
# основной цикл
for j in range(m):
v = get_col(A, j)
# основной цикл
for i in range(j):
qi = get_col(Q, i)
R[i, j] = dot(qi, get_col(A, j))
v = v - R[i, j] * qi
R[j, j] = vector_norm(v)
# проверяем деление на маленькое число
if abs(R[j, j]) < EPS_DIV:
raise ValueError("столбцы линейно зависимы")
qj = v / R[j, j]
set_col(Q, j, qj)
# возвращаем результат
return Q, R
# === менять только тут ===
A = np.array([
[2, 1, 0],
[1, 3, 1],
[0, 1, 2]
], dtype=float)
iterations = 50
# === дальше не трогать ===
def offdiag_norm(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n = A.shape[0]
# считаем итоговое значение
s = 0.0
# основной цикл
for i in range(n):
# основной цикл
for j in range(n):
if i != j:
s += A[i, j]**2
# возвращаем результат
return math.sqrt(s)
# функция выполняет qr-алгоритм
def qr_algorithm(A, iterations=50):
Ak = np.array(A, dtype=float)
# создаем историю итераций
history = []
# основной цикл
for k in range(1, iterations + 1):
Q, R = qr_gram_schmidt(Ak)
Ak = mat_mul(R, Q)
history.append({
"k": k,
"diag": round_vector(diag_values(Ak), 10),
"offdiag_norm": round(offdiag_norm(Ak), 10)
})
# возвращаем результат
return Ak, history
T, history = qr_algorithm(A, iterations)
# выводим результат
print("QR-алгоритм")
# выводим результат
print("Итоговая матрица:")
# выводим результат
print(T)
# выводим результат
print("Приближенные собственные значения - диагональ:")
# выводим результат
print(diag_values(T))
# выводим результат
print("Последние строки истории:")
print_history(history[-5:], max_rows=5)
# выводим результат
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# проверь ортогональность Q и вид R.
# ошибка меньше 1e-8 - хорошо.
# R должна быть верхнетреугольной.
# если элементы ниже диагонали большие, разложение или итерации надо проверить.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если offdiag_norm маленький:
QR-алгоритм сошелся. Диагональ итоговой матрицы можно считать приближенными собственными значениями.
Вариант 2 - если offdiag_norm уменьшается:
Внедиагональные элементы уменьшаются, значит процесс идет в сторону диагональной формы.
Вариант 3 - если offdiag_norm большой:
Алгоритму не хватило итераций. Можно увеличить iterations.
Вариант 4 - если просят спектр:
Спектр матрицы приближенно равен набору чисел на диагонали итоговой матрицы.
""")
# ====================================================================
# теория по методу
# ====================================================================
# QR-алгоритм ищет собственные значения матрицы.
#
# Идея: много раз выполнять QR-разложение и менять порядок множителей.
#
# Формулы:
# A_k = Q_k*R_k
# A_next = R_k*Q_k
#
# Данные: A, число итераций.
# Результат: собственные значения приближенно лежат на диагонали итоговой матрицы.
# Плюс: можно найти сразу несколько собственных значений.
# Минус: может требовать много итераций.
#
# Короткий ответ: QR-алгоритм постепенно приводит матрицу к почти треугольной форме.
# ====================================================================
# %%
# ============================================================
# 5.5. ПРИБЛИЖЕННОЕ РАЗЛОЖЕНИЕ ШУРА ЧЕРЕЗ QR-ИТЕРАЦИИ
# A ~ U T U^T
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция достает диагональ матрицы
def diag_values(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
d = []
# основной цикл
for i in range(min(n, m)):
d.append(float(A[i, i]))
# возвращаем результат
return d
# функция умножает две матрицы
def mat_mul(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# определяем размеры
n, m = A.shape
m2, p = B.shape
if m != m2:
raise ValueError("получился почти нулевой вектор")
C = np.zeros((n, p))
# основной цикл
for i in range(n):
# основной цикл
for j in range(p):
# считаем итоговое значение
s = 0.0
# основной цикл
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
# возвращаем результат
return C
# функция транспонирует матрицу
def transpose(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
T = np.zeros((m, n))
# основной цикл
for i in range(n):
# основной цикл
for j in range(m):
T[j, i] = A[i, j]
# возвращаем результат
return T
# функция создает единичную матрицу
def identity(n):
I = np.zeros((n, n))
# основной цикл
for i in range(n):
I[i, i] = 1.0
# возвращаем результат
return I
# === конец минимальной базы ===
# функция считает скалярное произведение
def dot(u, v):
s = 0.0
for i in range(len(u)):
s += u[i] * v[i]
# возвращаем результат
return s
# функция берет столбец матрицы
def get_col(A, j):
A = np.array(A, dtype=float)
col = np.zeros(A.shape[0])
for i in range(A.shape[0]):
col[i] = A[i, j]
# возвращаем результат
return col
# функция записывает столбец матрицы
def set_col(A, j, col):
for i in range(A.shape[0]):
A[i, j] = col[i]
def qr_gram_schmidt(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
Q = np.zeros((n, m))
R = np.zeros((m, m))
# основной цикл
for j in range(m):
v = get_col(A, j)
# основной цикл
for i in range(j):
qi = get_col(Q, i)
R[i, j] = dot(qi, get_col(A, j))
v = v - R[i, j] * qi
R[j, j] = vector_norm(v)
# проверяем деление на маленькое число
if abs(R[j, j]) < EPS_DIV:
raise ValueError("столбцы линейно зависимы")
qj = v / R[j, j]
set_col(Q, j, qj)
# возвращаем результат
return Q, R
# === менять только тут ===
A = np.array([
[2, 1, 0],
[1, 3, 1],
[0, 1, 2]
], dtype=float)
iterations = 50
# === дальше не трогать ===
# функция считает норму внедиагональных элементов
def offdiag_norm(A):
A = np.array(A, dtype=float)
n, m = A.shape
s = 0.0
for i in range(n):
for j in range(m):
if i != j:
s += A[i, j] * A[i, j]
# возвращаем результат
return math.sqrt(s)
def schur_via_qr(A, iterations=50):
Ak = np.array(A, dtype=float)
n = Ak.shape[0]
U = identity(n)
# создаем историю итераций
history = []
# основной цикл
for k in range(1, iterations + 1):
Q, R = qr_gram_schmidt(Ak)
Ak = mat_mul(R, Q)
U = mat_mul(U, Q)
history.append({
"k": k,
"diag_T": round_vector(diag_values(Ak), 10),
"offdiag_norm": round(offdiag_norm(Ak), 10)
})
T = Ak
# возвращаем результат
return U, T, history
U, T, history = schur_via_qr(A, iterations)
A_approx = mat_mul(mat_mul(U, T), transpose(U))
# выводим результат
print("Приближенное разложение Шура")
# выводим результат
print("U =")
# выводим результат
print(U)
# выводим результат
print("T =")
# выводим результат
print(T)
# выводим результат
print("U*T*U^T =")
# выводим результат
print(A_approx)
# выводим результат
print("Норма ошибки A - U*T*U^T:", vector_norm(A - A_approx))
print_history(history[-5:], max_rows=5)
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри норму ошибки A - U*T*U^T.
# если ошибка меньше 1e-6, разложение получилось хорошо.
# диагональ T связана с собственными значениями матрицы.
# если ошибка большая, надо увеличить число QR-итераций.
# ====================================================================
print("""
типовые выводы:
- построено приближенное разложение Шура A = U*T*U^T.
- матрица T почти верхнетреугольная, а U накапливает ортогональные преобразования.
- малая ошибка восстановления показывает, что разложение получилось корректным.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Разложение Шура записывает матрицу как A = U*T*U^T.
#
# U - ортогональная матрица.
# T - верхнетреугольная или почти верхнетреугольная матрица.
# Диагональ T связана с собственными значениями.
#
# Идея: QR-итерации накапливают ортогональные преобразования и приводят A к T.
#
# Данные: A, число итераций.
# Плюс: удобно для анализа собственных значений.
# Минус: простая учебная реализация дает приближение.
#
# Короткий ответ: разложение Шура приводит матрицу к верхнетреугольному виду ортогональным преобразованием.
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: круги Гершгорина, локализация собственных значений, области спектра.
#
# что менять
# A - матрица из условия.
# график идет отдельным блоком ниже. если он не нужен, удали блок графика.
#
# куда смотреть в условии
# Просто перепиши матрицу A построчно.
#
# что смотреть в ответе
# Центр круга - диагональный элемент строки.
# Радиус - сумма модулей остальных элементов строки.
# Все собственные значения лежат в объединении этих кругов.
########################################################################
# ============================================================
# 5.6. КРУГИ ГЕРШГОРИНА
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([
[4, 1, 0],
[2, 3, 1],
[0, 1, 2]
], dtype=float)
# === дальше не трогать ===
def gershgorin_circles(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n = A.shape[0]
circles = []
# основной цикл
for i in range(n):
center = A[i, i]
radius = 0.0
# основной цикл
for j in range(n):
if i != j:
radius += abs(A[i, j])
circles.append((center, radius))
# возвращаем результат
return circles
circles = gershgorin_circles(A)
# выводим результат
print("Круги Гершгорина: |lambda - center| <= radius")
for i, (center, radius) in enumerate(circles):
# выводим результат
print(f"Круг {i+1}: центр = {center}, радиус = {radius}, интервал [{center-radius}, {center+radius}]")
# график
# если график не нужен, удали этот блок
theta = np.linspace(0, 2*math.pi, 300)
# строим график
plt.figure()
# строим график
plt.axhline(0)
# строим график
plt.axvline(0)
for center, radius in circles:
xs = center + radius * np.cos(theta)
ys = radius * np.sin(theta)
# строим график
plt.plot(xs, ys)
# строим график
plt.scatter([center], [0])
# строим график
plt.title("Круги Гершгорина")
# строим график
plt.axis("equal")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри центры и радиусы кругов.
# собственные значения лежат внутри объединения кругов.
# малые непересекающиеся круги дают хорошую оценку.
# большие пересекающиеся круги дают только грубую оценку.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
Построены круги Гершгорина. Все собственные значения матрицы лежат в объединении этих кругов.
Вариант 2 - если круги не пересекаются:
Так как некоторые круги не пересекаются, можно отдельно оценивать расположение части собственных значений.
Вариант 3 - если круги сильно пересекаются:
Круги сильно пересекаются, поэтому оценка получается грубой, но все собственные значения все равно лежат внутри объединения кругов.
Вариант 4 - если есть график:
На графике показаны области, где могут находиться собственные значения.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Круги Гершгорина дают области, где находятся собственные значения.
#
# Для каждой строки строится круг:
# center = a_ii
# radius = сумма abs(a_ij), где j != i
#
# Главное утверждение: все собственные значения лежат в объединении этих кругов.
#
# Данные: A.
# Плюс: быстро дает оценку спектра.
# Минус: оценка может быть грубой.
#
# Короткий ответ: круги Гершгорина локализуют собственные значения по элементам матрицы.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: умножение матриц, наивный алгоритм, Штрассен, сравнить умножение.
#
# что менять
# A - первая матрица.
# B - вторая матрица.
#
# куда смотреть в условии
# если написано найти A*B, первая матрица идет в A, вторая в B.
# количество столбцов A должно совпасть с количеством строк B.
#
# что смотреть в ответе
# Наивное умножение - обычный результат.
# Штрассен - результат методом Штрассена.
# Разница должна быть нулевой или почти нулевой.
########################################################################
# ============================================================
# 5.7. НАИВНОЕ УМНОЖЕНИЕ МАТРИЦ И ШТРАССЕН
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
# функция умножает две матрицы
def mat_mul(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# определяем размеры
n, m = A.shape
m2, p = B.shape
if m != m2:
raise ValueError("получился почти нулевой вектор")
C = np.zeros((n, p))
# основной цикл
for i in range(n):
# основной цикл
for j in range(p):
# считаем итоговое значение
s = 0.0
# основной цикл
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
# возвращаем результат
return C
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([
[1, 2],
[3, 4]
], dtype=float)
B = np.array([
[5, 6],
[7, 8]
], dtype=float)
# === дальше не трогать ===
def next_power_of_two(n):
p = 1
# основной цикл
while p < n:
p *= 2
# возвращаем результат
return p
# вспомогательная функция
def pad_matrix(A, size):
# приводим данные к массиву
A = np.array(A, dtype=float)
P = np.zeros((size, size))
# определяем размеры
n, m = A.shape
P[:n, :m] = A
# возвращаем результат
return P
# вспомогательная функция
def strassen_square(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# определяем размеры
n = A.shape[0]
if n == 1:
# возвращаем результат
return A * B
if n <= 2:
# возвращаем результат
return mat_mul(A, B)
mid = n // 2
A11 = A[:mid, :mid]
A12 = A[:mid, mid:]
A21 = A[mid:, :mid]
A22 = A[mid:, mid:]
B11 = B[:mid, :mid]
B12 = B[:mid, mid:]
B21 = B[mid:, :mid]
B22 = B[mid:, mid:]
M1 = strassen_square(A11 + A22, B11 + B22)
M2 = strassen_square(A21 + A22, B11)
M3 = strassen_square(A11, B12 - B22)
M4 = strassen_square(A22, B21 - B11)
M5 = strassen_square(A11 + A12, B22)
M6 = strassen_square(A21 - A11, B11 + B12)
M7 = strassen_square(A12 - A22, B21 + B22)
C11 = M1 + M4 - M5 + M7
C12 = M3 + M5
C21 = M2 + M4
C22 = M1 - M2 + M3 + M6
C = np.zeros((n, n))
C[:mid, :mid] = C11
C[:mid, mid:] = C12
C[mid:, :mid] = C21
C[mid:, mid:] = C22
# возвращаем результат
return C
# функция умножает матрицы методом штрассена
def strassen(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
if A.shape[1] != B.shape[0]:
raise ValueError("Размеры матриц не подходят")
n = max(A.shape[0], A.shape[1], B.shape[0], B.shape[1])
size = next_power_of_two(n)
Ap = pad_matrix(A, size)
Bp = pad_matrix(B, size)
Cp = strassen_square(Ap, Bp)
# возвращаем результат
return Cp[:A.shape[0], :B.shape[1]]
C_naive = mat_mul(A, B)
C_strassen = strassen(A, B)
# выводим результат
print("Наивное умножение:")
# выводим результат
print(C_naive)
# выводим результат
print("Штрассен:")
# выводим результат
print(C_strassen)
# выводим результат
print("Разница:")
# выводим результат
print(C_naive - C_strassen)
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри главную ошибку, невязку, число итераций или шаг.
# если ошибка меньше tol или eps, результат хороший.
# если ошибка больше 1e-3, ответ лучше проверить.
# в выводе напиши: что нашли, насколько точно и почему метод подходит.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если разница почти ноль:
Наивный метод и метод Штрассена дали одинаковый результат. Значит, умножение выполнено корректно.
Вариант 2 - если нужно сравнить методы:
Наивный метод использует обычное умножение строк на столбцы. Метод Штрассена уменьшает число умножений блоков.
Вариант 3 - если матрицы маленькие:
Для маленьких матриц метод Штрассена может не дать выигрыша по времени, но алгоритм работает правильно.
Вариант 4 - если разница не ноль:
Результаты отличаются. Нужно проверить размеры матриц и правильность ввода A и B.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Наивное умножение матриц считает каждый элемент как строка на столбец.
#
# Формула:
# C[i,j] = сумма A[i,k]*B[k,j]
#
# Сложность наивного метода: O(n^3).
#
# Метод Штрассена делит матрицы на блоки и уменьшает число умножений блоков с 8 до 7.
#
# Данные: A, B.
# Плюс Штрассена: лучше асимптотическая сложность для больших матриц.
# Минус: сложнее и может быть невыгоден для маленьких матриц.
#
# Короткий ответ: Штрассен ускоряет матричное умножение за счет уменьшения числа блочных умножений.
# ====================================================================
# %%
# ## 6. Численное дифференцирование и ОДУ
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: численная производная, конечные разности, прямая разность, центральная разность.
#
# что менять
# f(x) - функция.
# x0 - точка, где нужна производная.
# h - шаг.
# exact_df - точная производная, если она дана или легко известна. Если не нужна, поставь None.
#
# куда смотреть в условии
# если написано найти производную sin(x) в x=1 с шагом 0.001:
# f(x) = math.sin(x)
# x0 = 1
# h = 0.001
#
# что смотреть в ответе
# Центральная разность обычно точнее прямой и обратной.
# если есть точная производная, сравни ошибки.
########################################################################
# ============================================================
# 6.1. ЧИСЛЕННОЕ ДИФФЕРЕНЦИРОВАНИЕ
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
# возвращаем результат
return math.sin(x)
# начальное приближение из условия
x0 = 1.0
# шаг из условия
h = 1e-3
# вспомогательная функция
def exact_df(x):
# возвращаем результат
return math.cos(x)
# если точной производной нет, ниже напиши: exact_df = None
# exact_df = None
# === дальше не трогать ===
forward = (f(x0 + h) - f(x0)) / h
backward = (f(x0) - f(x0 - h)) / h
central = (f(x0 + h) - f(x0 - h)) / (2*h)
# выводим результат
print("Численное дифференцирование")
# выводим результат
print("Прямая разность:", forward)
# выводим результат
print("Обратная разность:", backward)
# выводим результат
print("Центральная разность:", central)
if exact_df is not None:
exact = exact_df(x0)
# выводим результат
print("Точная производная:", exact)
# выводим результат
print("Ошибка прямой:", abs(forward - exact))
# выводим результат
print("Ошибка обратной:", abs(backward - exact))
# выводим результат
print("Ошибка центральной:", abs(central - exact))
# выводим результат
# ====================================================================
# график функции около точки дифференцирования
# если график не нужен, удали этот блок
xs = np.linspace(x0 - 5*h, x0 + 5*h, 200)
ys = [f(x) for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# отмечаем точку
plt.scatter([x0], [f(x0)])
# строим график
plt.title("функция около точки дифференцирования")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри шаг h.
# центральная разность обычно точнее односторонней.
# h=0.1, 0.01, 0.001 часто нормальные учебные варианты.
# слишком большой h дает ошибку формулы, слишком маленький - ошибку округления.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если есть точная производная:
Сравнение ошибок показывает, какой способ дал лучший результат.
Вариант 2 - если центральная разность точнее:
Центральная разность дала меньшую ошибку, потому что обычно имеет более высокий порядок точности.
Вариант 3 - если точной производной нет:
Получены три приближения производной. Обычно в качестве лучшего ответа берут центральную разность.
Вариант 4 - если ошибки большие:
Шаг h мог быть выбран слишком большим или слишком маленьким. Нужно проверить условие и значение h.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Численное дифференцирование приближенно находит производную по значениям функции.
#
# Формулы:
# forward = (f(x+h)-f(x))/h
# backward = (f(x)-f(x-h))/h
# central = (f(x+h)-f(x-h))/(2*h)
#
# Данные: f(x), x0, h.
# Плюс: не нужна аналитическая производная.
# Минус: точность зависит от h, есть ошибка округления.
#
# Короткий ответ: центральная разность обычно точнее, потому что использует значения функции с двух сторон.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: метод Эйлера, задача Коши, y'=f(t,y), начальное условие.
#
# что менять
# f(t,y) - правая часть уравнения после знака y'=.
# t0 - начальная точка из условия.
# y0 - начальное значение y(t0).
# t_end - до какой точки считать.
# h - шаг.
#
# куда смотреть в условии
# если написано y'=y-t^2+1, y(0)=0.5, h=0.2, найти на [0,2]:
# f(t,y) = y - t**2 + 1
# t0 = 0
# y0 = 0.5
# t_end = 2
# h = 0.2
#
# что смотреть в ответе
# Последнее значение y - приближенное значение в t_end.
# Таблица показывает шаги метода.
# График показывает поведение решения.
########################################################################
# ============================================================
# 6.2. МЕТОД ЭЙЛЕРА ДЛЯ ОДУ y' = f(t,y)
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(t, y):
# возвращаем результат
return y - t**2 + 1
# начальный момент времени
t0 = 0.0
# начальное значение y из условия
y0 = 0.5
# конец интервала по времени
t_end = 2.0
# шаг из условия
h = 0.2
# === дальше не трогать ===
def euler_method(f, t0, y0, t_end, h):
t_values = [t0]
y_values = [float(y0)]
# создаем историю итераций
history = []
t = t0
y = float(y0)
n = int(round((t_end - t0) / h))
# основной цикл
for k in range(1, n + 1):
# считаем новое приближение
y_new = y + h * f(t, y)
t_new = t + h
history.append({
"k": k,
"t": round(t, 8),
"y": round(y, 10),
"y_new": round(y_new, 10)
})
t, y = t_new, y_new
t_values.append(t)
y_values.append(y)
# возвращаем результат
return np.array(t_values), np.array(y_values), history
t_values, y_values, history = euler_method(f, t0, y0, t_end, h)
# выводим результат
print("Метод Эйлера")
# выводим результат
print("Последнее значение y:", y_values[-1])
print_history(history)
# график
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(t_values, y_values, marker="o")
# строим график
plt.title("Метод Эйлера")
# строим график
plt.xlabel("t")
# строим график
plt.ylabel("y")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри шаг h и поведение графика.
# h=0.1 обычно нормально для учебной гладкой задачи.
# h=1 часто грубо и может дать неустойчивость.
# ошибка Эйлера уменьшается примерно пропорционально h.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
Метод Эйлера дал приближенное решение задачи Коши. Последнее значение y является ответом в точке t_end.
Вариант 2 - если нужен вывод про точность:
Метод Эйлера простой, но имеет невысокую точность. При меньшем шаге h результат обычно становится точнее.
Вариант 3 - если есть график:
График показывает приближенное поведение решения на заданном интервале.
Вариант 4 - если сравниваешь с другим методом:
Результат Эйлера можно использовать как базовый, но метод Рунге-Кутты 4 порядка обычно дает более точное значение.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод Эйлера решает задачу Коши y'=f(t,y), y(t0)=y0.
#
# Идея: решение продолжается по касательной в начале шага.
#
# Формулы:
# y_next = y + h*f(t,y)
# t_next = t + h
#
# Данные: f(t,y), t0, y0, t_end, h.
# Порядок точности: первый.
# Плюс: самый простой метод.
# Минус: низкая точность и возможная неустойчивость при большом h.
#
# Короткий ответ: метод Эйлера строит решение пошагово по производной в текущей точке.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: предиктор корректор, улучшенный Эйлер, Эйлер с коррекцией.
#
# что менять
# f(t,y) - правая часть ОДУ.
# t0 - начальная точка.
# y0 - значение y в начальной точке.
# t_end - конец интервала.
# h - шаг.
#
# куда смотреть в условии
# если написано y(1)=2, считать до t=3, h=0.1:
# t0 = 1
# y0 = 2
# t_end = 3
# h = 0.1
#
# что смотреть в ответе
# predictor - это прогноз по Эйлеру.
# corrector - это исправленное значение.
# в ответ обычно пишут исправленное значение как итог на шаге.
########################################################################
# ============================================================
# 6.3. ПРЕДИКТОР-КОРРЕКТОР ЭЙЛЕРА
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(t, y):
# возвращаем результат
return y - t**2 + 1
# начальный момент времени
t0 = 0.0
# начальное значение y из условия
y0 = 0.5
# конец интервала по времени
t_end = 2.0
# шаг из условия
h = 0.2
# === дальше не трогать ===
def euler_predictor_corrector(f, t0, y0, t_end, h):
t_values = [t0]
y_values = [float(y0)]
# создаем историю итераций
history = []
t = t0
y = float(y0)
n = int(round((t_end - t0) / h))
# основной цикл
for k in range(1, n + 1):
y_pred = y + h * f(t, y)
y_corr = y + h / 2 * (f(t, y) + f(t + h, y_pred))
history.append({
"k": k,
"t": round(t, 8),
"y": round(y, 10),
"predictor": round(y_pred, 10),
"corrector": round(y_corr, 10)
})
t = t + h
y = y_corr
t_values.append(t)
y_values.append(y)
# возвращаем результат
return np.array(t_values), np.array(y_values), history
t_values, y_values, history = euler_predictor_corrector(f, t0, y0, t_end, h)
# выводим результат
print("Предиктор-корректор Эйлера")
# выводим результат
print("Последнее значение y:", y_values[-1])
print_history(history)
# график
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(t_values, y_values, marker="o")
# строим график
plt.title("Предиктор-корректор Эйлера")
# строим график
plt.xlabel("t")
# строим график
plt.ylabel("y")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри шаг h и поведение графика.
# h=0.1 обычно нормально для учебной гладкой задачи.
# h=1 часто грубо и может дать неустойчивость.
# ошибка Эйлера уменьшается примерно пропорционально h.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
Сначала был сделан прогноз методом Эйлера, затем значение было исправлено корректором.
Вариант 2 - если нужно указать итог:
В качестве итогового значения на каждом шаге берется corrector, а не predictor.
Вариант 3 - если сравнить с Эйлером:
Предиктор-корректор обычно точнее простого метода Эйлера, потому что учитывает наклон в начале и в конце шага.
Вариант 4 - если есть график:
График показывает численное решение после коррекции на каждом шаге.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Предиктор-корректор Эйлера улучшает метод Эйлера.
#
# Идея: сначала сделать прогноз, потом исправить его средним наклоном.
#
# Формулы:
# y_pred = y + h*f(t,y)
# y_corr = y + h/2*(f(t,y) + f(t+h,y_pred))
#
# Данные: f(t,y), t0, y0, t_end, h.
# Плюс: точнее простого Эйлера.
# Минус: нужно больше вычислений функции.
#
# Короткий ответ: метод сначала прогнозирует значение, затем корректирует его.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: Рунге-Кутта 4, метод четвертого порядка.
#
# что менять
# f(t,y) - правая часть уравнения.
# t0 - начальная точка.
# y0 - начальное значение.
# t_end - до какой точки считать.
# h - шаг.
#
# куда смотреть в условии
# если дано y'=t+y, y(0)=1, h=0.1, до t=1:
# f(t,y) = t + y
# t0 = 0
# y0 = 1
# t_end = 1
# h = 0.1
#
# что смотреть в ответе
# Последнее значение y - ответ в точке t_end.
# k1,k2,k3,k4 - промежуточные наклоны.
# метод Рунге-Кутты 4 порядка обычно точнее метода Эйлера при том же h.
########################################################################
# ============================================================
# 6.4. МЕТОД РУНГЕ-КУТТЫ 4-ГО ПОРЯДКА
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(t, y):
# возвращаем результат
return y - t**2 + 1
# начальный момент времени
t0 = 0.0
# начальное значение y из условия
y0 = 0.5
# конец интервала по времени
t_end = 2.0
# шаг из условия
h = 0.2
# === дальше не трогать ===
def rk4_method(f, t0, y0, t_end, h):
t_values = [t0]
y_values = [float(y0)]
# создаем историю итераций
history = []
t = t0
y = float(y0)
n = int(round((t_end - t0) / h))
# основной цикл
for k in range(1, n + 1):
# считаем коэффициенты метода
k1 = h * f(t, y)
# считаем коэффициенты метода
k2 = h * f(t + h/2, y + k1/2)
# считаем коэффициенты метода
k3 = h * f(t + h/2, y + k2/2)
# считаем коэффициенты метода
k4 = h * f(t + h, y + k3)
# считаем новое приближение
y_new = y + (k1 + 2*k2 + 2*k3 + k4) / 6
t_new = t + h
history.append({
"k": k,
"t": round(t, 8),
"y": round(y, 10),
"k1": round(k1, 10),
"k2": round(k2, 10),
"k3": round(k3, 10),
"k4": round(k4, 10),
"y_new": round(y_new, 10)
})
t, y = t_new, y_new
t_values.append(t)
y_values.append(y)
# возвращаем результат
return np.array(t_values), np.array(y_values), history
t_values, y_values, history = rk4_method(f, t0, y0, t_end, h)
# выводим результат
print("Метод Рунге-Кутты 4")
# выводим результат
print("Последнее значение y:", y_values[-1])
print_history(history)
# график
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(t_values, y_values, marker="o")
# строим график
plt.title("Метод Рунге-Кутты 4 порядка")
# строим график
plt.xlabel("t")
# строим график
plt.ylabel("y")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри шаг h и оценку ошибки.
# для Рунге-Кутты 4 порядка при уменьшении h в 2 раза ошибка падает примерно в 16 раз.
# если ошибка больше tol, шаг надо уменьшить.
# если ошибка меньше tol, результат можно считать достаточно точным.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
Метод Рунге-Кутты 4 порядка дал приближенное решение задачи Коши. Последнее значение y является ответом в конечной точке.
Вариант 2 - если нужно написать про k1-k4:
На каждом шаге использовались четыре промежуточных значения k1, k2, k3, k4.
Вариант 3 - если сравнивать с Эйлером:
При том же шаге метод Рунге-Кутты 4 порядка обычно точнее метода Эйлера.
Вариант 4 - если есть график:
График показывает приближенное решение, полученное методом Рунге-Кутты 4 порядка.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод Рунге-Кутты 4 порядка решает задачу Коши для ОДУ.
#
# Идея: на каждом шаге считается четыре наклона, потом они усредняются.
#
# Формулы:
# k1 = h*f(t,y)
# k2 = h*f(t+h/2, y+k1/2)
# k3 = h*f(t+h/2, y+k2/2)
# k4 = h*f(t+h, y+k3)
# y_next = y + (k1+2*k2+2*k3+k4)/6
#
# Данные: f(t,y), t0, y0, t_end, h.
# Порядок точности: четвертый.
# Плюс: высокая точность.
# Минус: 4 вычисления функции на шаг.
#
# короткий ответ: метод Рунге-Кутты 4 порядка использует четыре оценки наклона и дает высокую точность.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: система ОДУ, метод Эйлера, фазовый портрет.
# этот вариант только для системы из двух переменных x и v.
#
# что из условия куда вставлять
# пункт 1 - правую часть x' вставь в dx.
# пункт 2 - правую часть v' вставь в dv.
# пункт 3 - начальные значения вставь в x_start и v_start.
# пункт 4 - начало, конец и шаг вставь в t0, t_end, h.
########################################################################
# ============================================================
# 6.5а. система ОДУ методом Эйлера
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
# начальное значение первой переменной из условия
x_start = 1.0
# начальное значение второй переменной из условия
v_start = 0.0
# начальный момент времени
t0 = 0.0
# конец интервала по времени
t_end = 10.0
# шаг из условия
h = 0.1
# правые части системы ОДУ из условия
def F(t, x, v):
dx = v
dv = -x
# возвращаем результат
return dx, dv
# === дальше не трогать ===
# подготовка массивов
n = int((t_end - t0) / h) + 1
t_values = []
x_values = []
v_values = []
t = t0
x = x_start
v = v_start
# шаги метода эйлера
for i in range(n):
t_values.append(t)
x_values.append(x)
v_values.append(v)
dx, dv = F(t, x, v)
x = x + h * dx
v = v + h * dv
t = t + h
# вывод результата
print("система ОДУ методом Эйлера")
# выводим результат
print("x в конце:", x_values[-1])
# выводим результат
print("v в конце:", v_values[-1])
# график x(t)
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(t_values, x_values)
# строим график
plt.xlabel("t")
# строим график
plt.ylabel("x")
# строим график
plt.title("x(t), метод Эйлера")
# строим график
plt.grid(True)
# строим график
plt.show()
# фазовый портрет
# если фазовый портрет не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(x_values, v_values)
# строим график
plt.xlabel("x")
# строим график
plt.ylabel("v")
# строим график
plt.title("фазовый портрет")
# строим график
plt.grid(True)
# строим график
plt.show()
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри главное число ошибки или невязки, которое печатает блок.
# если ошибка меньше заданной точности tol или eps, результат можно считать хорошим.
# если ошибка больше 1e-3, ответ лучше проверить или уменьшить шаг.
# в выводе всегда пиши: что нашли, насколько точный ответ и почему метод подходит.
# ====================================================================
print("""
типовые выводы:
- метод эйлера считает новое состояние по наклону в текущей точке.
- для системы оду обновляются сразу обе переменные.
- при большом шаге h траектория может заметно отличаться от настоящей.
""")
# ====================================================================
# теория по методу
# ====================================================================
# метод эйлера для системы оду применяет формулу y_next = y + h*f(t,y) к каждой переменной.
# он простой, но имеет только первый порядок точности.
# для фазового портрета берут значения двух переменных и строят траекторию в плоскости.
# чем меньше шаг h, тем точнее решение, но тем больше вычислений.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: система ОДУ, метод Рунге-Кутты 4 порядка, фазовый портрет.
# этот вариант только для системы из двух переменных x и v.
#
# что из условия куда вставлять
# пункт 1 - правую часть x' вставь в dx.
# пункт 2 - правую часть v' вставь в dv.
# пункт 3 - начальные значения вставь в x_start и v_start.
# пункт 4 - начало, конец и шаг вставь в t0, t_end, h.
########################################################################
# ============================================================
# 6.5б. система ОДУ методом Рунге-Кутты 4 порядка
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
# начальное значение первой переменной из условия
x_start = 1.0
# начальное значение второй переменной из условия
v_start = 0.0
# начальный момент времени
t0 = 0.0
# конец интервала по времени
t_end = 10.0
# шаг из условия
h = 0.1
# правые части системы ОДУ из условия
def F(t, x, v):
dx = v
dv = -x
# возвращаем результат
return np.array([dx, dv], dtype=float)
# === дальше не трогать ===
# подготовка массивов
n = int((t_end - t0) / h) + 1
t_values = []
x_values = []
v_values = []
t = t0
# приводим данные к массиву
u = np.array([x_start, v_start], dtype=float)
# шаги метода Рунге-Кутты 4 порядка
for i in range(n):
t_values.append(t)
x_values.append(u[0])
v_values.append(u[1])
# считаем коэффициенты метода
k1 = F(t, u[0], u[1])
# считаем коэффициенты метода
k2 = F(t + h/2, u[0] + h*k1[0]/2, u[1] + h*k1[1]/2)
# считаем коэффициенты метода
k3 = F(t + h/2, u[0] + h*k2[0]/2, u[1] + h*k2[1]/2)
# считаем коэффициенты метода
k4 = F(t + h, u[0] + h*k3[0], u[1] + h*k3[1])
u = u + h * (k1 + 2*k2 + 2*k3 + k4) / 6
t = t + h
# вывод результата
print("система ОДУ методом Рунге-Кутты 4 порядка")
# выводим результат
print("x в конце:", x_values[-1])
# выводим результат
print("v в конце:", v_values[-1])
# график x(t)
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(t_values, x_values)
# строим график
plt.xlabel("t")
# строим график
plt.ylabel("x")
# строим график
plt.title("x(t), метод Рунге-Кутты 4 порядка")
# строим график
plt.grid(True)
# строим график
plt.show()
# фазовый портрет
# если фазовый портрет не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(x_values, v_values)
# строим график
plt.xlabel("x")
# строим график
plt.ylabel("v")
# строим график
plt.title("фазовый портрет")
# строим график
plt.grid(True)
# строим график
plt.show()
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри главное число ошибки или невязки, которое печатает блок.
# если ошибка меньше заданной точности tol или eps, результат можно считать хорошим.
# если ошибка больше 1e-3, ответ лучше проверить или уменьшить шаг.
# в выводе всегда пиши: что нашли, насколько точный ответ и почему метод подходит.
# ====================================================================
print("""
типовые выводы:
- метод рунге-кутты 4 порядка использует четыре оценки наклона на одном шаге.
- для системы оду все компоненты считаются вместе.
- обычно он точнее метода эйлера при таком же шаге.
""")
# ====================================================================
# теория по методу
# ====================================================================
# метод рунге-кутты 4 порядка считает k1, k2, k3 и k4 и берет их взвешенное среднее.
# для системы оду k является вектором, потому что меняется сразу несколько переменных.
# глобальная ошибка имеет порядок h^4, поэтому метод хорошо работает на гладких задачах.
# фазовый портрет показывает, как состояние системы движется в пространстве переменных.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: сравнить Эйлера и Рунге Кутту, сравнить методы, сравнить точность.
#
# что менять
# f(t,y) - правая часть ОДУ.
# t0 - начальная точка.
# y0 - начальное значение.
# t_end - конец интервала.
# h - шаг.
#
# куда смотреть в условии
# Данные берутся так же, как в обычной задаче Коши:
# y'=f(t,y), y(t0)=y0, шаг h, конец t_end.
#
# что смотреть в ответе
# Эйлер y_end - результат метода Эйлера.
# y_end для метода Рунге-Кутты 4 порядка - его итоговый результат.
# Разница в конце показывает, насколько методы разошлись.
# обычно метод Рунге-Кутты 4 порядка считается точнее.
########################################################################
# ============================================================
# 6.6. СРАВНЕНИЕ ЭЙЛЕРА И РУНГЕ-КУТТЫ 4
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
def euler_method(f, t0, y0, t_end, h):
t_values = [t0]
y_values = [float(y0)]
history = []
t = t0
y = float(y0)
n = int(round((t_end - t0) / h))
# основной цикл
for k in range(1, n + 1):
y_new = y + h * f(t, y)
t_new = t + h
history.append({"k": k, "t": round(t, 8), "y": round(y, 10), "y_new": round(y_new, 10)})
t, y = t_new, y_new
t_values.append(t)
y_values.append(y)
# возвращаем результат
return np.array(t_values), np.array(y_values), history
def rk4_method(f, t0, y0, t_end, h):
t_values = [t0]
y_values = [float(y0)]
history = []
t = t0
y = float(y0)
n = int(round((t_end - t0) / h))
# основной цикл
for k in range(1, n + 1):
k1 = f(t, y)
k2 = f(t + h / 2, y + h * k1 / 2)
k3 = f(t + h / 2, y + h * k2 / 2)
k4 = f(t + h, y + h * k3)
y_new = y + h * (k1 + 2*k2 + 2*k3 + k4) / 6
t_new = t + h
history.append({"k": k, "t": round(t, 8), "y": round(y, 10), "y_new": round(y_new, 10)})
t, y = t_new, y_new
t_values.append(t)
y_values.append(y)
# возвращаем результат
return np.array(t_values), np.array(y_values), history
# === менять только тут ===
def f(t, y):
# возвращаем результат
return y - t**2 + 1
# начальный момент времени
t0 = 0.0
# начальное значение y из условия
y0 = 0.5
# конец интервала по времени
t_end = 2.0
# шаг из условия
h = 0.2
# === дальше не трогать ===
t_e, y_e, _ = euler_method(f, t0, y0, t_end, h)
t_r, y_r, _ = rk4_method(f, t0, y0, t_end, h)
# выводим результат
print("Сравнение Эйлера и метода Рунге-Кутты 4 порядка")
# выводим результат
print("Эйлер y_end:", y_e[-1])
# выводим результат
print("Метод Рунге-Кутты 4 порядка y_end:", y_r[-1])
# выводим результат
print("Разница в конце:", abs(y_e[-1] - y_r[-1]))
# график
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(t_e, y_e, marker="o", label="Эйлер")
# строим график
plt.plot(t_r, y_r, marker="o", label="Рунге-Кутта 4")
# строим график
plt.title("Сравнение методов")
# строим график
plt.xlabel("t")
# строим график
plt.ylabel("y")
# строим график
plt.legend()
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри шаг h и поведение графика.
# h=0.1 обычно нормально для учебной гладкой задачи.
# h=1 часто грубо и может дать неустойчивость.
# ошибка Эйлера уменьшается примерно пропорционально h.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если значения близкие:
Методы дали близкие результаты. На данном шаге h различие между ними небольшое.
Вариант 2 - если значения сильно отличаются:
Методы дали заметно разные результаты. Это показывает, что метод Эйлера при данном шаге может быть недостаточно точным.
Вариант 3 - если нужно написать про точность:
Метод Рунге-Кутты 4 порядка обычно точнее метода Эйлера при одинаковом шаге.
Вариант 4 - если есть график:
По графику видно, как решения двух методов расходятся или совпадают на интервале.
""")
# ====================================================================
# теория по методу
# ====================================================================
# сравнение Эйлера и метода Рунге-Кутты 4 порядка показывает влияние метода на точность.
#
# Эйлер: первый порядок, одна оценка функции на шаг, простой, но менее точный.
# метод Рунге-Кутты 4 порядка: четыре оценки функции на шаг, обычно точнее.
#
# Что сравнивают:
# - конечное значение y
# - графики
# - разницу между ответами
#
# Плюс сравнения: видно, как выбор метода влияет на результат.
# Минус: без точного решения нельзя строго сказать точную ошибку, но можно оценить различие.
#
# короткий ответ: при одинаковом шаге метод Рунге-Кутты 4 порядка обычно дает более точный результат, чем Эйлер.
# ====================================================================
# %%
########################################################################
# как понять, что этот блок нужен
# ищи слова: Адамс, Адамс Башфорт, Адамс Мултон, многошаговый метод, предиктор корректор.
#
# что менять
# f(t,y) - правая часть ОДУ.
# t0 - начальная точка.
# y0 - начальное значение.
# t_end - конец интервала.
# h - шаг.
#
# куда смотреть в условии
# если написано y'=..., y(0)=..., h=..., найти до t=...
# то эти значения напрямую идут в f, t0, y0, h, t_end.
#
# что смотреть в ответе
# predictor_AB2 - прогноз методом Адамса Башфорта.
# corrector_AM2 - исправленное значение методом Адамса Мултона.
# В ответ обычно берут corrector_AM2.
########################################################################
# ============================================================
# 6.7. АДАМС-БАШФОРТ 2 + АДАМС-МУЛТОН 2
# Универсальный предиктор-корректор
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
def rk4_method(f, t0, y0, t_end, h):
t_values = [t0]
y_values = [float(y0)]
history = []
t = t0
y = float(y0)
n = int(round((t_end - t0) / h))
# основной цикл
for k in range(1, n + 1):
k1 = f(t, y)
k2 = f(t + h / 2, y + h * k1 / 2)
k3 = f(t + h / 2, y + h * k2 / 2)
k4 = f(t + h, y + h * k3)
y_new = y + h * (k1 + 2*k2 + 2*k3 + k4) / 6
t_new = t + h
history.append({"k": k, "t": round(t, 8), "y": round(y, 10), "y_new": round(y_new, 10)})
t, y = t_new, y_new
t_values.append(t)
y_values.append(y)
# возвращаем результат
return np.array(t_values), np.array(y_values), history
# === менять только тут ===
def f(t, y):
# возвращаем результат
return y - t**2 + 1
# начальный момент времени
t0 = 0.0
# начальное значение y из условия
y0 = 0.5
# конец интервала по времени
t_end = 2.0
# шаг из условия
h = 0.2
# === дальше не трогать ===
def adams_bashforth_moulton_2(f, t0, y0, t_end, h):
n = int(round((t_end - t0) / h))
t_values = [t0]
y_values = [float(y0)]
# создаем историю итераций
history = []
# первый шаг получаем методом Рунге-Кутты 4 порядка
t_tmp, y_tmp, _ = rk4_method(f, t0, y0, t0 + h, h)
t_values.append(t_tmp[-1])
y_values.append(y_tmp[-1])
# основной цикл
for k in range(1, n):
t_prev = t_values[k - 1]
y_prev = y_values[k - 1]
t_curr = t_values[k]
y_curr = y_values[k]
f_prev = f(t_prev, y_prev)
f_curr = f(t_curr, y_curr)
# прогноз: адамс-башфорт 2
y_pred = y_curr + h/2 * (3*f_curr - f_prev)
t_next = t_curr + h
# коррекция: адамс-мултон 2
y_corr = y_curr + h/2 * (f(t_next, y_pred) + f_curr)
history.append({
"k": k,
"t": round(t_curr, 8),
"y": round(y_curr, 10),
"predictor_AB2": round(y_pred, 10),
"corrector_AM2": round(y_corr, 10)
})
t_values.append(t_next)
y_values.append(y_corr)
# возвращаем результат
return np.array(t_values), np.array(y_values), history
t_values, y_values, history = adams_bashforth_moulton_2(f, t0, y0, t_end, h)
# выводим результат
print("Адамс-Башфорт 2 + Адамс-Мултон 2")
# выводим результат
print("Последнее значение y:", y_values[-1])
print_history(history)
# график
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.plot(t_values, y_values, marker="o")
# строим график
plt.title("AB2 + AM2 predictor-corrector")
# строим график
plt.xlabel("t")
# строим график
plt.ylabel("y")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри главную ошибку, невязку, число итераций или шаг.
# если ошибка меньше tol или eps, результат хороший.
# если ошибка больше 1e-3, ответ лучше проверить.
# в выводе напиши: что нашли, насколько точно и почему метод подходит.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
Метод Адамса-Башфорта использован как предиктор, а метод Адамса-Мултона как корректор.
Вариант 2 - если нужно указать итог:
В качестве итогового значения берется corrector_AM2, потому что это исправленное значение.
Вариант 3 - если predictor и corrector близки:
Прогноз и коррекция близки, значит шаг h выбран достаточно хорошо.
Вариант 4 - если predictor и corrector сильно отличаются:
Прогноз и коррекция сильно отличаются. Возможно, шаг h слишком большой.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Методы Адамса являются многошаговыми методами.
#
# Идея: новое значение считается с использованием нескольких предыдущих шагов.
#
# Адамс-Башфорт - явный метод, часто предиктор.
# Адамс-Мултон - корректор.
#
# Формулы:
# y_pred = y_n + h/2*(3*f_n - f_prev)
# y_corr = y_n + h/2*(f(t_next,y_pred) + f_n)
#
# Данные: f(t,y), t0, y0, t_end, h.
# Плюс: использует информацию с прошлых шагов.
# Минус: нужны стартовые значения.
#
# Короткий ответ: Башфорт дает прогноз, Мултон исправляет прогноз.
# ====================================================================
# %%
# ## 7. ДПФ, обратное ДПФ, БПФ
# %%
# ============================================================
# 7.1. ДИСКРЕТНОЕ ПРЕОБРАЗОВАНИЕ ФУРЬЕ - ДПФ
# ============================================================
# === минимальная база для этой ячейки ===
import math
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
N = 16
# сигнал из условия
signal = []
# основной цикл
for n in range(N):
value = math.sin(2*math.pi*2*n/N) + 0.5*math.sin(2*math.pi*4*n/N)
signal.append(value)
# === дальше не трогать ===
def dft(x):
x = list(x)
N = len(x)
X = []
# основной цикл
for k in range(N):
# считаем итоговое значение
s = 0j
# основной цикл
for n in range(N):
angle = -2 * math.pi * k * n / N
s += x[n] * complex(math.cos(angle), math.sin(angle))
X.append(s)
# возвращаем результат
return X
# функция считает обратное дискретное преобразование фурье
def idft(X):
X = list(X)
N = len(X)
x = []
# основной цикл
for n in range(N):
# считаем итоговое значение
s = 0j
# основной цикл
for k in range(N):
angle = 2 * math.pi * k * n / N
s += X[k] * complex(math.cos(angle), math.sin(angle))
x.append(s / N)
# возвращаем результат
return x
X = dft(signal)
amplitudes = [abs(z) for z in X]
# выводим результат
print("ДПФ")
# выводим результат
print("Коэффициенты X:")
for k, val in enumerate(X):
# выводим результат
print(k, val, " amplitude =", abs(val))
restored = idft(X)
max_restore_error = max(abs(restored[i].real - signal[i]) for i in range(N))
# выводим результат
print("Максимальная ошибка обратного ДПФ:", max_restore_error)
# график
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.stem(range(N), signal)
# строим график
plt.title("Исходный сигнал")
# строим график
plt.grid(True)
# строим график
plt.show()
# строим график
plt.figure()
# строим график
plt.stem(range(N), amplitudes)
# строим график
plt.title("Амплитудный спектр |X_k|")
# строим график
plt.xlabel("k")
# строим график
plt.ylabel("|X_k|")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри пики спектра и частоты.
# шаг частоты df=fs/N.
# частота Найквиста равна fs/2.
# после фильтра шумовой пик должен уменьшиться или исчезнуть.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
ДПФ перевело сигнал из временного вида в частотный. Коэффициенты X показывают частотный состав сигнала.
Вариант 2 - если есть большие пики:
Самые большие значения amplitude показывают основные частоты сигнала.
Вариант 3 - если ошибка обратного ДПФ маленькая:
Маленькая ошибка обратного ДПФ показывает, что преобразование выполнено корректно.
Вариант 4 - если есть шум:
Шум обычно проявляется как небольшие дополнительные амплитуды на разных частотах.
""")
# ====================================================================
# теория по методу
# ====================================================================
# ДПФ переводит дискретный сигнал из временной области в частотную.
#
# Идея: сигнал раскладывается на гармоники.
# Коэффициент X[k] показывает вклад частоты k.
#
# Формула:
# X[k] = сумма x[n]*exp(-j*2*pi*k*n/N)
#
# Обратное ДПФ:
# x[n] = 1/N * сумма X[k]*exp(j*2*pi*k*n/N)
#
# Данные: signal, N.
# Что смотреть: abs(X[k]) - амплитуда частоты k.
# Плюс: показывает спектр.
# Минус: прямое ДПФ требует много операций.
#
# Короткий ответ: ДПФ показывает частотный состав дискретного сигнала.
# ====================================================================
# %%
# ============================================================
# 7.2. БЫСТРОЕ ПРЕОБРАЗОВАНИЕ ФУРЬЕ - БПФ
# N должно быть степенью двойки
# ============================================================
# === минимальная база для этой ячейки ===
import math
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
N = 16
# сигнал из условия
signal = []
# основной цикл
for n in range(N):
value = math.sin(2*math.pi*2*n/N) + 0.5*math.sin(2*math.pi*4*n/N)
signal.append(value)
# === дальше не трогать ===
def is_power_of_two(n):
# возвращаем результат
return n > 0 and (n & (n - 1)) == 0
# вспомогательная функция
def fft_recursive(x):
x = list(x)
N = len(x)
if N == 1:
# возвращаем результат
return [complex(x[0])]
if not is_power_of_two(N):
raise ValueError("Для этого FFT длина сигнала должна быть степенью двойки")
even = fft_recursive(x[0::2])
odd = fft_recursive(x[1::2])
X = [0j] * N
# основной цикл
for k in range(N // 2):
angle = -2 * math.pi * k / N
w = complex(math.cos(angle), math.sin(angle))
X[k] = even[k] + w * odd[k]
X[k + N//2] = even[k] - w * odd[k]
# возвращаем результат
return X
X_fft = fft_recursive(signal)
amplitudes = [abs(z) for z in X_fft]
# выводим результат
print("БПФ / FFT")
for k, val in enumerate(X_fft):
# выводим результат
print(k, val, " amplitude =", abs(val))
# график
# если график не нужен, удали этот блок
plt.figure()
# строим график
plt.stem(range(N), amplitudes)
# строим график
plt.title("Амплитудный спектр через FFT")
# строим график
plt.xlabel("k")
# строим график
plt.ylabel("|X_k|")
# строим график
plt.grid(True)
# строим график
plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри пики спектра и частоты.
# шаг частоты df=fs/N.
# частота Найквиста равна fs/2.
# после фильтра шумовой пик должен уменьшиться или исчезнуть.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - обычный вывод:
БПФ вычисляет частотные коэффициенты сигнала быстрее, чем прямое ДПФ.
Вариант 2 - если есть пики:
Большие пики amplitude показывают главные частоты сигнала.
Вариант 3 - если нужно сравнить с ДПФ:
БПФ дает тот же смысл результата, что и ДПФ, но использует более быстрый алгоритм.
Вариант 4 - если N не степень двойки:
Для этого шаблона N должно быть степенью двойки. Нужно взять N = 8, 16, 32 и так далее.
""")
# ====================================================================
# теория по методу
# ====================================================================
# БПФ - быстрый алгоритм вычисления ДПФ.
#
# Идея: сигнал делится на четные и нечетные отсчеты, задача разбивается на меньшие задачи.
#
# Данные: signal, N. В простом варианте N должно быть степенью двойки.
# Плюс: быстрее прямого ДПФ.
# Минус: сложнее и требует подходящей длины сигнала.
#
# Короткий ответ: БПФ дает тот же спектральный смысл, что и ДПФ, но считает быстрее.
# ====================================================================
# %%
# ## 8. Короткая текстовая заготовка для ответа
# %%
########################################################################
# как понять, что этот блок нужен
# этот блок нужен, если надо быстро написать текстовую часть решения.
#
# что менять
# method_name - название метода, который использовался.
#
# куда смотреть в условии
# название метода обычно прямо написано в задании.
#
# что смотреть в ответе
# скопируй текст и подставь свои числа: корень, ошибку, число итераций.
########################################################################
# ============================================================
# 8.1. ЗАГОТОВКА ТЕКСТА ДЛЯ ОТВЕТА
# ============================================================
# === минимальная база для этой ячейки ===
# для этой ячейки отдельные функции не нужны
# === конец минимальной базы ===
# === менять только тут ===
method_name = "метод Ньютона"
# === дальше не трогать ===
print(f"""
Краткая теория:
Используется {method_name}. На каждой итерации строится новое приближение.
Итерации продолжаются, пока не выполнится критерий остановки:
ошибка < eps или невязка достаточно мала.
В коде заданы исходные данные из условия, затем выполнены итерации метода.
В результате получено приближенное решение. Также посчитаны:
- количество итераций;
- итоговая ошибка / невязка;
- при необходимости построен график.
""")
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри главную ошибку, невязку, число итераций или шаг.
# если ошибка меньше tol или eps, результат хороший.
# если ошибка больше 1e-3, ответ лучше проверить.
# в выводе напиши: что нашли, насколько точно и почему метод подходит.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
Вариант 1 - если метод сошелся:
Метод сошелся, итоговая ошибка мала. Полученное значение можно считать приближенным ответом.
Вариант 2 - если был график:
Построенный график подтверждает поведение решения и показывает найденное приближение.
Вариант 3 - если нужно кратко:
Вычисления выполнены выбранным численным методом. Получен приближенный ответ и проверена ошибка.
Вариант 4 - если точность плохая:
Полученная точность недостаточна. Нужно уменьшить шаг, увеличить число итераций или проверить исходные данные.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Это не метод, а шаблон оформления ответа.
#
# В теории обычно нужно написать:
# - для чего метод
# - идея
# - формула
# - данные
# - остановка
# - плюс
# - минус
# - смысл результата
#
# Короткий ответ: теория должна быть короткой, но обязательно с формулой и смыслом метода.
# ====================================================================
# %%
# # 9. Нестандартные случаи кода
#
# Этот раздел добавлен отдельно, чтобы не мешать основным методам.
#
# Копируй только нужную ячейку. Почти всем ячейкам нужен БЛОК 0 сверху файла.
# %%
# ============================================================
# 9.1а. численное интегрирование методом трапеций
# ============================================================
# как понять, что этот блок нужен
# ищи слова: интеграл, метод трапеций, площадь под графиком.
#
# что из условия куда вставлять
# пункт 1 - функцию под интегралом вставь в f(x).
# пункт 2 - нижний предел вставь в a.
# пункт 3 - верхний предел вставь в b.
# пункт 4 - число разбиений вставь в N.
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
# функция из условия
def f(x):
# возвращаем результат
return x**2
# левый конец отрезка или нижний предел
a = 0.0
# правый конец отрезка или верхний предел
b = 1.0
# число точек или разбиений из условия
N = 10
# === дальше не трогать ===
# расчет шага
h = (b - a) / N
# сумма по формуле трапеций
s = 0.5 * (f(a) + f(b))
# основной цикл
for i in range(1, N):
x = a + i * h
s += f(x)
# считаем итоговое значение
result = s * h
# вывод результата
print("метод трапеций")
# выводим результат
print("интеграл примерно:", result)
# график
# если график не нужен, удали этот блок
xs = np.linspace(a, b, 300)
ys = [f(x) for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим график
plt.grid(True)
# строим график
plt.show()
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри значение интеграла и шаг h.
# если при уменьшении h в 2 раза ответ почти не меняется, результат устойчивый.
# метод трапеций имеет ошибку порядка h^2.
# если функция гладкая, h=0.01 или h=0.001 обычно дает хороший учебный результат.
# ====================================================================
print("""
типовые выводы:
- интеграл получается как сумма площадей трапеций.
- чем меньше шаг h, тем точнее ответ.
- метод удобен, когда есть таблица значений или простая функция на отрезке.
""")
# ====================================================================
# теория по методу
# ====================================================================
# метод трапеций заменяет график функции на каждом маленьком отрезке прямой линией.
# площадь на одном шаге считается как площадь трапеции.
# общий интеграл получается суммой площадей по всем отрезкам.
# ошибка уменьшается при уменьшении шага, но вычислений становится больше.
# ====================================================================
# %%
# ============================================================
# 9.1б. численное интегрирование методом Симпсона
# ============================================================
# как понять, что этот блок нужен
# ищи слова: интеграл, метод Симпсона, параболы, высокая точность.
#
# что из условия куда вставлять
# пункт 1 - функцию под интегралом вставь в f(x).
# пункт 2 - нижний предел вставь в a.
# пункт 3 - верхний предел вставь в b.
# пункт 4 - число разбиений вставь в N. для Симпсона N должно быть четным.
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
# функция из условия
def f(x):
# возвращаем результат
return x**2
# левый конец отрезка или нижний предел
a = 0.0
# правый конец отрезка или верхний предел
b = 1.0
# число точек или разбиений из условия
N = 10
# === дальше не трогать ===
# проверка числа разбиений
if N % 2 != 0:
raise ValueError("для метода Симпсона N должно быть четным")
# расчет шага
h = (b - a) / N
# сумма по формуле Симпсона
s = f(a) + f(b)
# основной цикл
for i in range(1, N):
x = a + i * h
# проверяем условие метода
if i % 2 == 1:
s += 4 * f(x)
else:
s += 2 * f(x)
# считаем итоговое значение
result = s * h / 3
# вывод результата
print("метод Симпсона")
# выводим результат
print("интеграл примерно:", result)
# график
# если график не нужен, удали этот блок
xs = np.linspace(a, b, 300)
ys = [f(x) for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим график
plt.grid(True)
# строим график
plt.show()
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# проверь, что число разбиений N четное.
# метод Симпсона обычно точнее трапеций при той же сетке.
# если при уменьшении h в 2 раза ответ почти не меняется, результат устойчивый.
# ошибка порядка h^4, поэтому точность растет быстро.
# ====================================================================
print("""
типовые выводы:
- метод симпсона обычно точнее метода трапеций на гладких функциях.
- число разбиений n должно быть четным.
- если n нечетное, его надо заменить на четное.
""")
# ====================================================================
# теория по методу
# ====================================================================
# метод симпсона на паре соседних отрезков заменяет функцию параболой.
# из-за этого для гладких функций он дает более высокую точность, чем трапеции.
# главное условие - число разбиений должно быть четным.
# при уменьшении шага ошибка быстро падает, если функция без резких скачков.
# ====================================================================
# %%
# ============================================================
# 9.2. МЕТОД ГАУССА-ЗЕЙДЕЛЯ ДЛЯ A*x=b
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция умножает матрицу на вектор
def mat_vec(A, x):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
x = np.array(x, dtype=float)
# определяем размеры
n, m = A.shape
y = np.zeros(n)
# основной цикл
for i in range(n):
# считаем итоговое значение
s = 0.0
# основной цикл
for j in range(m):
s += A[i, j] * x[j]
y[i] = s
# возвращаем результат
return y
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([[10,1,1],[2,10,1],[2,2,10]], dtype=float)
# правая часть системы из условия
b = np.array([12,13,14], dtype=float)
# начальное приближение из условия
x0 = np.array([0,0,0], dtype=float)
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def gauss_seidel_linear(A, b, x0, tol=1e-6, max_iter=100):
# приводим данные к массиву
x = np.array(x0, dtype=float)
# определяем размеры
n = len(b)
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter+1):
x_old = np.array(x, dtype=float)
# основной цикл
for i in range(n):
# считаем итоговое значение
s = 0.0
# основной цикл
for j in range(n):
if j != i: s += A[i,j]*x[j]
x[i] = (b[i]-s)/A[i,i]
# считаем ошибку
err = vector_norm(x-x_old)
# считаем ошибку
res = vector_norm(mat_vec(A,x)-b)
history.append({"k":k,"x":round_vector(x, 8),"error":round(err,10),"residual":round(res,10)})
# проверяем условие остановки
if err < tol or res < tol: break
# возвращаем результат
return x, history
solution, history = gauss_seidel_linear(A,b,x0,tol,max_iter)
# выводим результат
print("Решение:", solution)
# выводим результат
print("Невязка:", vector_norm(mat_vec(A,solution)-b))
# выводим результат
print("Итераций:", len(history))
print_history(history, max_rows=20)
# выводим результат
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри невязку Ax-b и число итераций.
# до 50 итераций - нормально.
# 50-200 - медленно, но возможно.
# невязка меньше 1e-6 - хороший ответ.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. Если невязка маленькая, метод сошелся.
2. Если итераций много, это нормально для линейной сходимости.
3. Если невязка большая, проверь матрицу и условия сходимости.
""")
# ====================================================================
# теория по методу
# ====================================================================
# метод гаусса-зейделя решает систему линейных уравнений итерациями.
# новое значение переменной сразу используется при вычислении следующих переменных.
# метод обычно хорошо работает, если матрица имеет диагональное преобладание.
# остановка делается, когда изменение решения или невязка становится маленькой.
# ====================================================================
# %%
# ============================================================
# 9.3. ВТОРАЯ ПРОИЗВОДНАЯ ЦЕНТРАЛЬНОЙ РАЗНОСТЬЮ
# ============================================================
# === минимальная база для этой ячейки ===
import math
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
# возвращаем результат
return x**2 * math.exp(-x)
# начальное приближение из условия
x0 = 1.0
# шаг из условия
h = 1e-4
exact_d2f = None
# === дальше не трогать ===
d2 = (f(x0+h)-2*f(x0)+f(x0-h))/(h**2)
# выводим результат
print("f''(x0) примерно:", d2)
if exact_d2f is not None:
exact = exact_d2f(x0)
# выводим результат
print("Точное значение:", exact)
# выводим результат
print("Ошибка:", abs(d2-exact))
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри шаг h.
# центральная разность обычно точнее односторонней.
# h=0.1, 0.01, 0.001 часто нормальные учебные варианты.
# слишком большой h дает ошибку формулы, слишком маленький - ошибку округления.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. Центральная формула дала приближение второй производной.
2. Точность зависит от h.
3. Слишком маленький h может усилить ошибку округления.
""")
# ====================================================================
# теория по методу
# ====================================================================
# вторая производная численно считается через значения функции в точках x-h, x и x+h.
# центральная формула симметричная, поэтому она обычно точнее односторонних формул.
# если h слишком большой, ошибка аппроксимации заметная.
# если h слишком маленький, может усилиться ошибка округления.
# ====================================================================
# %%
# ============================================================
# 9.4. МЕТОД ВРАЩЕНИЙ ЯКОБИ
# Только для симметричной матрицы
# ============================================================
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция считает максимум модулей
def max_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
best = 0.0
if len(x.shape) == 1:
for value in x:
cur = abs(float(value))
if cur > best:
best = cur
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
cur = abs(float(x[i, j]))
if cur > best:
best = cur
# возвращаем результат
return best
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция достает диагональ матрицы
def diag_values(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
d = []
# основной цикл
for i in range(min(n, m)):
d.append(float(A[i, i]))
# возвращаем результат
return d
# функция умножает две матрицы
def mat_mul(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# определяем размеры
n, m = A.shape
m2, p = B.shape
if m != m2:
raise ValueError("получился почти нулевой вектор")
C = np.zeros((n, p))
# основной цикл
for i in range(n):
# основной цикл
for j in range(p):
# считаем итоговое значение
s = 0.0
# основной цикл
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
# возвращаем результат
return C
# функция транспонирует матрицу
def transpose(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
T = np.zeros((m, n))
# основной цикл
for i in range(n):
# основной цикл
for j in range(m):
T[j, i] = A[i, j]
# возвращаем результат
return T
# функция создает единичную матрицу
def identity(n):
I = np.zeros((n, n))
# основной цикл
for i in range(n):
I[i, i] = 1.0
# возвращаем результат
return I
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([[4,1,1],[1,3,1],[1,1,2]], dtype=float)
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def is_symmetric(A, tol=1e-10):
# возвращаем результат
return max_norm(A-transpose(A)) < tol
# вспомогательная функция
def max_offdiag(A):
# определяем размеры
n = A.shape[0]
best, p, q = 0.0, 0, 1
# основной цикл
for i in range(n):
# основной цикл
for j in range(i+1,n):
# проверяем деление на маленькое число
if abs(A[i,j]) > best:
best, p, q = abs(A[i,j]), i, j
# возвращаем результат
return best, p, q
# вспомогательная функция
def jacobi_eigen_symmetric(A, tol=1e-6, max_iter=100):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n = A.shape[0]
V = identity(n)
# создаем историю итераций
history = []
if not is_symmetric(A): raise ValueError("Матрица несимметрична")
# основной цикл
for k in range(1,max_iter+1):
off,p,q = max_offdiag(A)
history.append({"k":k,"max_offdiag":round(off,10),"diag":round_vector(diag_values(A), 8)})
# проверяем условие остановки
if off < tol: break
angle = math.pi/4 if abs(A[p,p]-A[q,q]) < EPS_DIV else 0.5*math.atan(2*A[p,q]/(A[p,p]-A[q,q]))
c,s = math.cos(angle), math.sin(angle)
J = identity(n)
J[p,p],J[q,q],J[p,q],J[q,p] = c,c,-s,s
A = mat_mul(transpose(J), mat_mul(A,J))
V = mat_mul(V,J)
# возвращаем результат
return np.array(diag_values(A)), V, A, history
vals, vecs, D, history = jacobi_eigen_symmetric(A,tol,max_iter)
# выводим результат
print("Собственные значения:", vals)
# выводим результат
print("Собственные векторы:")
# выводим результат
print(vecs)
print_history(history, max_rows=20)
# выводим результат
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри главную ошибку, невязку, число итераций или шаг.
# если ошибка меньше tol или eps, результат хороший.
# если ошибка больше 1e-3, ответ лучше проверить.
# в выводе напиши: что нашли, насколько точно и почему метод подходит.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. Если max_offdiag маленький, метод сошелся.
2. Диагональ итоговой матрицы дает собственные значения.
3. Для несимметричной матрицы этот метод не подходит.
""")
# ====================================================================
# теория по методу
# ====================================================================
# метод вращений якоби используется для собственных значений симметрической матрицы.
# на каждом шаге выбирается внедиагональный элемент и зануляется поворотом.
# после многих поворотов матрица становится почти диагональной.
# элементы на диагонали в конце являются приближениями собственных значений.
# ====================================================================
# %%
# ============================================================
# 9.5. ФИЛЬТРАЦИЯ СИГНАЛА ЧЕРЕЗ ДПФ
# ============================================================
# === минимальная база для этой ячейки ===
import math
import matplotlib.pyplot as plt
# === конец минимальной базы ===
def dft(x):
x = list(x)
N = len(x)
X = []
# основной цикл
for k in range(N):
s = 0j
# основной цикл
for n in range(N):
angle = -2 * math.pi * k * n / N
s += x[n] * complex(math.cos(angle), math.sin(angle))
X.append(s)
# возвращаем результат
return X
# функция считает обратное дискретное преобразование фурье
def idft(X):
X = list(X)
N = len(X)
x = []
# основной цикл
for n in range(N):
s = 0j
# основной цикл
for k in range(N):
angle = 2 * math.pi * k * n / N
s += X[k] * complex(math.cos(angle), math.sin(angle))
x.append(s / N)
# возвращаем результат
return x
# === менять только тут ===
N = 32
# сигнал из условия
signal = []
# основной цикл
for n in range(N):
clean = math.sin(2*math.pi*3*n/N)
noise = 0.25*math.sin(2*math.pi*11*n/N)
signal.append(clean + noise)
threshold_ratio = 0.3
# === дальше не трогать ===
X = dft(signal)
amps = [abs(z) for z in X]
threshold = threshold_ratio*max(amps)
X_filtered = [z if abs(z) >= threshold else 0j for z in X]
restored = [z.real for z in idft(X_filtered)]
# выводим результат
print("Порог:", threshold)
for k, amp in enumerate(amps): print(k, amp)
# график
# если график не нужен, удали этот блок
plt.figure(); plt.plot(range(N), signal, marker="o", label="исходный"); plt.plot(range(N), restored, marker="o", label="после фильтра"); plt.legend(); plt.grid(True); plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри пики спектра и частоты.
# шаг частоты df=fs/N.
# частота Найквиста равна fs/2.
# после фильтра шумовой пик должен уменьшиться или исчезнуть.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. ДПФ показало частотный состав сигнала.
2. Слабые коэффициенты можно обнулить как шум.
3. Если порог слишком большой, можно удалить полезные частоты.
""")
# ====================================================================
# теория по методу
# ====================================================================
# дискретное преобразование фурье переводит сигнал из времени в частоты.
# шум часто виден как большие пики на отдельных частотах.
# после удаления выбранных гармоник делается обратное преобразование.
# так можно получить сигнал, где мешающие частоты ослаблены или удалены.
# ====================================================================
# %%
# ============================================================
# 9.6. ЧАСТОТНАЯ СЕТКА И НАЙКВИСТ
# ============================================================
# === минимальная база для этой ячейки ===
# для этой ячейки отдельные функции не нужны
# === конец минимальной базы ===
# === менять только тут ===
# число точек или разбиений из условия
N = 32
# частота дискретизации из условия
fs = 32.0
# === дальше не трогать ===
df = fs/N
nyquist = fs/2
# выводим результат
print("df:", df)
# выводим результат
print("Частота Найквиста:", nyquist)
# основной цикл
for k in range(N):
freq = k*df if k <= N//2 else (k-N)*df
# выводим результат
print("k =", k, "freq =", freq)
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри пики спектра и частоты.
# шаг частоты df=fs/N.
# частота Найквиста равна fs/2.
# после фильтра шумовой пик должен уменьшиться или исчезнуть.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. Частотное разрешение равно fs/N.
2. Максимальная различимая частота равна fs/2.
3. Частоты выше fs/2 могут дать алиасинг.
""")
# ====================================================================
# теория по методу
# ====================================================================
# частотная сетка показывает, каким частотам соответствуют номера гармоник.
# шаг по частоте равен fs/n, где fs - частота дискретизации, n - длина фрагмента.
# частота найквиста равна fs/2.
# частоты выше найквиста корректно восстановить нельзя, они дают наложение спектров.
# ====================================================================
# %%
# ============================================================
# 9.7. АППРОКСИМАЦИЯ МЕТОДОМ НАИМЕНЬШИХ КВАДРАТОВ
# y примерно равно a*x + b
# ============================================================
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция решает систему линейных уравнений методом гаусса
def gaussian_solve(A, b):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
b = np.array(b, dtype=float)
# определяем размеры
n = len(b)
# основной цикл
for k in range(n):
pivot = k
# основной цикл
for i in range(k + 1, n):
# проверяем деление на маленькое число
if abs(A[i, k]) > abs(A[pivot, k]):
pivot = i
# проверяем деление на маленькое число
if abs(A[pivot, k]) < EPS_DIV:
raise ValueError("матрица вырождена или почти вырождена")
if pivot != k:
A[[k, pivot]] = A[[pivot, k]]
b[k], b[pivot] = b[pivot], b[k]
# основной цикл
for i in range(k + 1, n):
factor = A[i, k] / A[k, k]
A[i, k] = 0.0
# основной цикл
for j in range(k + 1, n):
A[i, j] -= factor * A[k, j]
b[i] -= factor * b[k]
x = np.zeros(n)
# основной цикл
for i in range(n - 1, -1, -1):
# считаем итоговое значение
s = b[i]
# основной цикл
for j in range(i + 1, n):
s -= A[i, j] * x[j]
x[i] = s / A[i, i]
# возвращаем результат
return x
# === конец минимальной базы ===
# === менять только тут ===
x_points = [0,1,2,3,4]
# значения y из таблицы
y_points = [1.1,2.0,2.9,4.2,5.1]
# === дальше не трогать ===
def least_squares_line(x_points, y_points):
# приводим данные к массиву
x = np.array(x_points, dtype=float)
y = np.array(y_points, dtype=float)
# определяем размеры
n = len(x)
sx, sy = sum(x), sum(y)
sxx, sxy = sum(x*x), sum(x*y)
A_norm = np.array([[sxx,sx],[sx,n]], dtype=float)
b_norm = np.array([sxy,sy], dtype=float)
# возвращаем результат
return gaussian_solve(A_norm, b_norm)
a_coef, b_coef = least_squares_line(x_points, y_points)
sse = sum((y-(a_coef*x+b_coef))**2 for x,y in zip(x_points,y_points))
# выводим результат
print("a:", a_coef)
# выводим результат
print("b:", b_coef)
# выводим результат
print("Сумма квадратов ошибок:", sse)
# график
# если график не нужен, удали этот блок
xs = np.linspace(min(x_points), max(x_points), 200)
ys = [a_coef*x+b_coef for x in xs]
# строим график
plt.figure(); plt.scatter(x_points, y_points); plt.plot(xs, ys); plt.grid(True); plt.show()
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри коэффициенты и остатки.
# малые остатки без явного рисунка - модель подходит.
# большие остатки или закономерный рисунок - нужна другая модель.
# в выводе напиши смысл коэффициентов.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. МНК построил прямую, приближающую данные.
2. Аппроксимация не обязана проходить через все точки.
3. Маленькая сумма квадратов ошибок означает хорошее приближение.
""")
# ====================================================================
# теория по методу
# ====================================================================
# метод наименьших квадратов подбирает параметры так, чтобы сумма квадратов ошибок была минимальной.
# для прямой y=a*x+b получаются два нормальных уравнения.
# коэффициенты a и b показывают наклон и свободный член прямой.
# метод полезен, когда данные неточные и не лежат ровно на одной линии.
# ====================================================================
# %%
# ============================================================
# 9.8. ОЦЕНКА ЧИСЛА ИТЕРАЦИЙ БИСЕКЦИИ
# ============================================================
# === минимальная база для этой ячейки ===
import math
# === конец минимальной базы ===
# === менять только тут ===
a = 1.0
# правый конец отрезка или верхний предел
b = 2.0
# требуемая точность из условия
eps = 1e-6
# === дальше не трогать ===
needed = math.ceil(math.log(abs(b-a)/eps, 2))
digits = int(-math.log10(eps))
# выводим результат
print("Нужно итераций не меньше:", needed)
# выводим результат
print("Примерно верных десятичных цифр:", digits)
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри главную ошибку, невязку, число итераций или шаг.
# если ошибка меньше tol или eps, результат хороший.
# если ошибка больше 1e-3, ответ лучше проверить.
# в выводе напиши: что нашли, насколько точно и почему метод подходит.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. Число итераций бисекции зависит только от длины отрезка и eps.
2. Чтобы уменьшить число итераций, нужно сузить начальный отрезок.
3. Если eps = 10^(-k), ожидают около k верных десятичных цифр.
""")
# ====================================================================
# теория по методу
# ====================================================================
# в методе бисекции длина отрезка уменьшается в два раза на каждой итерации.
# поэтому число итераций можно заранее оценить через log2((b-a)/eps).
# метод надежный, если на концах отрезка функция имеет разные знаки.
# минус метода - он сходится медленнее Ньютона.
# ====================================================================
# %%
import numpy as np
import matplotlib.pyplot as plt
# ============================================================
# 9.9. СРАВНЕНИЕ НЕСКОЛЬКИХ МЕТОДОВ ДЛЯ КОРНЯ
# Бисекция, Ньютон, секущие
# ============================================================
# как понять, что этот блок нужен
# ищи слова: сравнить методы, корень уравнения, Ньютон, бисекция, секущие.
#
# что менять
# f(x) - функция из уравнения f(x)=0.
# df(x) - производная для Ньютона.
# a, b - отрезок для бисекции и две начальные точки для секущих.
# x0 - начальное приближение для Ньютона.
# === минимальная база для этой ячейки ===
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
# возвращаем результат
return x**3 - x - 2
# производная функции из условия
def df(x):
# возвращаем результат
return 3*x**2 - 1
# левый конец отрезка или нижний предел
a = 1.0
# правый конец отрезка или верхний предел
b = 2.0
# начальное приближение из условия
x0 = 1.5
# точность из условия
tol = 1e-6
# максимум итераций, обычно можно не менять
max_iter = 50
# === дальше не трогать ===
def bisection_local(f, a, b, tol, max_iter):
fa = f(a)
fb = f(b)
if fa * fb > 0:
# возвращаем результат
return None, 0, "нет смены знака"
# основной цикл
for k in range(1, max_iter + 1):
c = (a + b) / 2
fc = f(c)
# проверяем деление на маленькое число
if abs(fc) < tol or abs(b - a) / 2 < tol:
# возвращаем результат
return c, k, "ok"
if fa * fc <= 0:
b = c
fb = fc
else:
a = c
fa = fc
# возвращаем результат
return c, max_iter, "max_iter"
# функция ньютона для сравнения методов
def newton_local(f, df, x0, tol, max_iter):
x = x0
# основной цикл
for k in range(1, max_iter + 1):
dfx = df(x)
# проверяем деление на маленькое число
if abs(dfx) < EPS_DIV:
# возвращаем результат
return x, k, "производная почти 0"
# считаем новое приближение
x_new = x - f(x) / dfx
# проверяем деление на маленькое число
if abs(x_new - x) < tol or abs(f(x_new)) < tol:
# возвращаем результат
return x_new, k, "ok"
x = x_new
# возвращаем результат
return x, max_iter, "max_iter"
# функция секущих для сравнения методов
def secant_local(f, x0, x1, tol, max_iter):
f0 = f(x0)
f1 = f(x1)
# основной цикл
for k in range(1, max_iter + 1):
# проверяем деление на маленькое число
if abs(f1 - f0) < EPS_DIV:
# возвращаем результат
return x1, k, "деление на малую разность"
x2 = x1 - f1 * (x1 - x0) / (f1 - f0)
# проверяем деление на маленькое число
if abs(x2 - x1) < tol or abs(f(x2)) < tol:
# возвращаем результат
return x2, k, "ok"
x0, x1 = x1, x2
f0, f1 = f1, f(x1)
# возвращаем результат
return x1, max_iter, "max_iter"
results = []
for name, ans in [
("бисекция", bisection_local(f, a, b, tol, max_iter)),
("ньютон", newton_local(f, df, x0, tol, max_iter)),
("секущие", secant_local(f, a, b, tol, max_iter))
]:
root, iters, status = ans
# считаем ошибку
residual = None if root is None else abs(f(root))
results.append({"method": name, "root": root, "iters": iters, "residual": residual, "status": status})
print_history(results)
# выводим результат
# ====================================================================
# график числа итераций у разных методов
# если график не нужен, удали этот блок
method_names = []
iters_plot = []
for row in results:
method_names.append(row["method"])
iters_plot.append(row["iters"])
# строим график
plt.figure()
# строим график
plt.bar(method_names, iters_plot)
# строим график
plt.ylabel("итерации")
# строим график
plt.title("сравнение числа итераций")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри главную ошибку, невязку, число итераций или шаг.
# если ошибка меньше tol или eps, результат хороший.
# если ошибка больше 1e-3, ответ лучше проверить.
# в выводе напиши: что нашли, насколько точно и почему метод подходит.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. Если все методы дали близкие корни, ответ устойчивый.
2. Бисекция надежнее, но обычно требует больше итераций.
3. Ньютон часто быстрее, но зависит от x0 и производной.
4. Секущие не требуют производной, но могут быть менее устойчивыми.
""")
# ====================================================================
# теория по методу
# ====================================================================
# бисекция самая надежная, потому что держит корень внутри отрезка.
# метод ньютона быстрее, но зависит от начальной точки и производной.
# метод секущих не требует производную, но может вести себя хуже при плохих начальных точках.
# поэтому в ответе удобно сравнить число итераций и близость f(x) к нулю.
# ====================================================================
# %%
import numpy as np
import matplotlib.pyplot as plt
# ============================================================
# 9.10. АДАПТИВНЫЙ ШАГ ДЛЯ ОДУ
# Контроль ошибки через один большой шаг и два маленьких
# ============================================================
# как понять, что этот блок нужен
# ищи слова: адаптивный шаг, подобрать шаг, контроль ошибки, ОДУ.
#
# что менять
# f(t,y) - правая часть ОДУ.
# t0, y0 - начальное условие.
# t_end - конец отрезка.
# h - начальный шаг.
# tol - допустимая ошибка на шаге.
# === минимальная база для этой ячейки ===
EPS_DIV = 1e-14
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# === конец минимальной базы ===
# === менять только тут ===
def f(t, y):
# возвращаем результат
return -y + t
# начальный момент времени
t0 = 0.0
# начальное значение y из условия
y0 = 0.0
# конец интервала по времени
t_end = 1.0
# шаг из условия
h = 0.2
# точность из условия
tol = 1e-5
# === дальше не трогать ===
def rk4_one_step(f, t, y, h):
# считаем коэффициенты метода
k1 = f(t, y)
# считаем коэффициенты метода
k2 = f(t + h/2, y + h*k1/2)
# считаем коэффициенты метода
k3 = f(t + h/2, y + h*k2/2)
# считаем коэффициенты метода
k4 = f(t + h, y + h*k3)
# возвращаем результат
return y + h*(k1 + 2*k2 + 2*k3 + k4)/6
t = t0
y = y0
# создаем историю итераций
history = []
# основной цикл
while t < t_end - EPS_DIV:
if t + h > t_end:
h = t_end - t
y_big = rk4_one_step(f, t, y, h)
y_half = rk4_one_step(f, t, y, h/2)
y_two = rk4_one_step(f, t + h/2, y_half, h/2)
# считаем ошибку
err = abs(y_two - y_big)
# проверяем условие остановки
if err <= tol or h < 1e-10:
t = t + h
y = y_two
history.append({"t": round(t, 8), "y": round(y, 10), "h": round(h, 8), "err": round(err, 12)})
# проверяем условие остановки
if err < tol / 10:
h = h * 2
else:
h = h / 2
# выводим результат
print("Решение в конце:", y)
# выводим результат
print("Число принятых шагов:", len(history))
print_history(history, max_rows=30)
# выводим результат
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри шаг h и оценку ошибки.
# для Рунге-Кутты 4 порядка при уменьшении h в 2 раза ошибка падает примерно в 16 раз.
# если ошибка больше tol, шаг надо уменьшить.
# если ошибка меньше tol, результат можно считать достаточно точным.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. Адаптивный шаг уменьшает h там, где ошибка велика.
2. Если шагов много, решение требует более мелкой сетки.
3. Если ошибка маленькая, шаг можно увеличивать.
4. Метод удобен, когда поведение решения меняется на разных участках.
""")
# ====================================================================
# теория по методу
# ====================================================================
# адаптивный шаг нужен, чтобы менять шаг по времени во время решения оду.
# обычно сравнивают один большой шаг и два маленьких шага.
# если разница большая, шаг уменьшают.
# если разница маленькая, шаг можно увеличить и сэкономить вычисления.
# ====================================================================
# %%
# ============================================================
# 9.11. PAGERANK КАК СТЕПЕННОЙ МЕТОД
# ============================================================
# как понять, что этот блок нужен
# ищи слова: PageRank, граф, ссылки, важность страниц, степенной метод.
#
# что менять
# M - матрица переходов. Столбец j показывает, куда можно перейти из страницы j.
# параметр damping - вероятность перехода по ссылке, обычно 0.85.
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
# функция печатает таблицу итераций
def print_history(history, headers=None, max_rows=20):
if not history:
# выводим результат
print("история пустая")
return
rows = history[:max_rows]
if isinstance(rows[0], dict):
headers = list(rows[0].keys())
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(row[h]) for h in headers))
else:
if headers is not None:
# выводим результат
print(" | ".join(headers))
# выводим результат
print("-" * 80)
for row in rows:
# выводим результат
print(" | ".join(str(x) for x in row))
if len(history) > max_rows:
# выводим результат
print(f"... показано {max_rows} строк из {len(history)}")
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция округляет вектор для красивого вывода
def round_vector(x, digits=8):
# возвращаем результат
return [round(float(value), digits) for value in x]
# функция умножает матрицу на вектор
def mat_vec(A, x):
# приводим данные к массиву
A = np.array(A, dtype=float)
# приводим данные к массиву
x = np.array(x, dtype=float)
# определяем размеры
n, m = A.shape
y = np.zeros(n)
# основной цикл
for i in range(n):
# считаем итоговое значение
s = 0.0
# основной цикл
for j in range(m):
s += A[i, j] * x[j]
y[i] = s
# возвращаем результат
return y
# === конец минимальной базы ===
# === менять только тут ===
M = np.array([
[0, 0.5, 1/3],
[0.5, 0, 1/3],
[0.5, 0.5, 1/3]
], dtype=float)
# коэффициент затухания, если дан в условии
damping = 0.85
# точность из условия
tol = 1e-8
# максимум итераций, обычно можно не менять
max_iter = 100
# === дальше не трогать ===
def pagerank_power(M, damping=0.85, tol=1e-8, max_iter=100):
M = np.array(M, dtype=float)
# определяем размеры
n, m = M.shape
if n != m:
raise ValueError("Матрица должна быть квадратной")
r = np.zeros(n)
# основной цикл
for i in range(n):
r[i] = 1.0 / n
# создаем историю итераций
history = []
# основной цикл
for k in range(1, max_iter + 1):
Mr = mat_vec(M, r)
r_new = np.zeros(n)
# основной цикл
for i in range(n):
r_new[i] = damping * Mr[i] + (1 - damping) / n
# считаем ошибку
err = vector_norm(r_new - r)
history.append({"k": k, "rank": round_vector(r_new, 8), "error": round(err, 12)})
# проверяем условие остановки
if err < tol:
# возвращаем результат
return r_new, history
r = r_new
# возвращаем результат
return r, history
ranks, history = pagerank_power(M, damping, tol, max_iter)
# выводим результат
print("PageRank:", ranks)
# выводим результат
print("Сумма рангов:", sum(ranks))
# выводим результат
print("Итераций:", len(history))
print_history(history, max_rows=20)
# выводим результат
# ====================================================================
# график невязки по итерациям
# если график не нужен, удали этот блок
ks_plot = []
residuals_plot = []
for row in history:
if "k" in row and "residual" in row:
ks_plot.append(row["k"])
residuals_plot.append(row["residual"])
if residuals_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, residuals_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel("невязка")
# строим график
plt.title("невязка по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# график контрольной величины по итерациям
# если график не нужен, удали этот блок
ks_plot = []
metric_plot = []
metric_name = ""
for candidate in ["error", "residual", "offdiag_norm", "max_offdiag", "diff"]:
if history and candidate in history[0]:
metric_name = candidate
break
if metric_name:
for row in history:
if "k" in row and metric_name in row:
ks_plot.append(row["k"])
metric_plot.append(row[metric_name])
if metric_plot:
# строим график
plt.figure()
# строим график
plt.plot(ks_plot, metric_plot, marker="o")
# строим график
plt.xlabel("итерация")
# строим график
plt.ylabel(metric_name)
# строим график
plt.title("контрольная величина по итерациям")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри изменение lambda и невязку A*x-lambda*x.
# до 20-50 итераций - нормально.
# больше 100 - главное собственное значение плохо отделено.
# невязка меньше 1e-6 - хорошая собственная пара.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. PageRank можно считать степенным методом для матрицы переходов.
2. Чем больше компонент ранга, тем важнее соответствующая страница.
3. damping защищает от тупиков и делает процесс устойчивее.
""")
# ====================================================================
# теория по методу
# ====================================================================
# pagerank можно понимать как степенной метод для матрицы переходов.
# на каждой итерации считается новый вектор важности страниц.
# коэффициент damping добавляет вероятность случайного перехода.
# в конце получается устойчивое распределение весов страниц.
# ====================================================================
# %%
# ============================================================
# 9.12. QR-РАЗЛОЖЕНИЕ ЧЕРЕЗ ОТРАЖЕНИЯ ХАУСХОЛДЕРА
# ============================================================
# как понять, что этот блок нужен
# ищи слова: Хаусхолдер, отражения, QR более устойчиво, ортогонализация.
#
# что менять
# A - матрица из условия.
# === минимальная база для этой ячейки ===
import math
import numpy as np
EPS_DIV = 1e-14
# функция считает евклидову норму вектора
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# функция считает максимум модулей
def max_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
best = 0.0
if len(x.shape) == 1:
for value in x:
cur = abs(float(value))
if cur > best:
best = cur
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
cur = abs(float(x[i, j]))
if cur > best:
best = cur
# возвращаем результат
return best
# функция умножает две матрицы
def mat_mul(A, B):
# приводим данные к массиву
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
# определяем размеры
n, m = A.shape
m2, p = B.shape
if m != m2:
raise ValueError("получился почти нулевой вектор")
C = np.zeros((n, p))
# основной цикл
for i in range(n):
# основной цикл
for j in range(p):
# считаем итоговое значение
s = 0.0
# основной цикл
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
# возвращаем результат
return C
# функция транспонирует матрицу
def transpose(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
T = np.zeros((m, n))
# основной цикл
for i in range(n):
# основной цикл
for j in range(m):
T[j, i] = A[i, j]
# возвращаем результат
return T
# функция создает единичную матрицу
def identity(n):
I = np.zeros((n, n))
# основной цикл
for i in range(n):
I[i, i] = 1.0
# возвращаем результат
return I
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([
[12, -51, 4],
[6, 167, -68],
[-4, 24, -41]
], dtype=float)
# === дальше не трогать ===
def householder_qr(A):
# приводим данные к массиву
A = np.array(A, dtype=float)
# определяем размеры
n, m = A.shape
R = np.array(A, dtype=float)
Q = identity(n)
steps = min(n - 1, m)
# основной цикл
for k in range(steps):
# строим вектор x из k-го столбца ниже диагонали
x = []
# основной цикл
for i in range(k, n):
x.append(R[i, k])
norm_x = vector_norm(x)
# проверяем деление на маленькое число
if norm_x < EPS_DIV:
continue
sign = 1.0 if x[0] >= 0 else -1.0
v = []
# основной цикл
for i in range(len(x)):
v.append(x[i])
v[0] += sign * norm_x
norm_v = vector_norm(v)
# проверяем деление на маленькое число
if norm_v < EPS_DIV:
continue
# основной цикл
for i in range(len(v)):
v[i] = v[i] / norm_v
H = identity(n)
# основной цикл
for i in range(k, n):
# основной цикл
for j in range(k, n):
H[i, j] -= 2 * v[i-k] * v[j-k]
R = mat_mul(H, R)
Q = mat_mul(Q, H)
# возвращаем результат
return Q, R
Q, R = householder_qr(A)
# выводим результат
print("Q:")
# выводим результат
print(Q)
# выводим результат
print("R:")
# выводим результат
print(R)
# выводим результат
print("Q*R:")
# выводим результат
print(mat_mul(Q, R))
# выводим результат
print("Ошибка A - Q*R:", max_norm(A - mat_mul(Q, R)))
# выводим результат
print("Ошибка ортогональности Q^T*Q - I:", max_norm(mat_mul(transpose(Q), Q) - identity(Q.shape[0])))
# выводим результат
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# проверь ортогональность Q и вид R.
# ошибка меньше 1e-8 - хорошо.
# R должна быть верхнетреугольной.
# если элементы ниже диагонали большие, разложение или итерации надо проверить.
# ====================================================================
print("""
ТИПОВЫЕ ВЫВОДЫ
1. QR через Хаусхолдера дает A = Q*R.
2. Q должна быть ортогональной, то есть Q^T*Q примерно I.
3. Метод Хаусхолдера обычно устойчивее классического Грамма-Шмидта.
""")
# ====================================================================
# теория по методу
# ====================================================================
# qr-разложение представляет матрицу как произведение q и r.
# q - ортогональная матрица, r - верхнетреугольная матрица.
# отражения хаусхолдера зануляют элементы под диагональю устойчивым способом.
# qr-разложение используют для решения систем, мНК и собственных значений.
# ====================================================================
# %%
# # 10. редкие случаи и запасные блоки
#
# эти блоки нужны не всегда. они добавлены на случай, если в билете попадется редкая тема из лекций или теории.
#
# как пользоваться:
# - сначала ищи тему в таблице ключевых слов
# - копируй только нужную ячейку
# - меняй только блок `менять только тут`
# - если график или проверка не нужны, удали этот кусок
# %%
# ============================================================
# 10.1. SVD через A^T A для маленькой матрицы
# ============================================================
# как понять, что этот блок нужен
# ищи слова: SVD, сингулярные числа, A^T A, U Sigma V^T.
# этот блок дает сингулярные числа через собственные значения A^T A.
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
# функция транспонирует матрицу
def transpose(A):
A = np.array(A, dtype=float)
n, m = A.shape
T = np.zeros((m, n))
for i in range(n):
for j in range(m):
T[j, i] = A[i, j]
return T
# функция умножает две матрицы
def mat_mul(A, B):
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
n, m = A.shape
m2, p = B.shape
C = np.zeros((n, p))
for i in range(n):
for j in range(p):
s = 0.0
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
return C
# функция считает собственные значения симметрической матрицы 2 на 2
def eigenvalues_2x2_symmetric(B):
a = B[0, 0]
b = B[0, 1]
d = B[1, 1]
trace = a + d
disc = math.sqrt((a - d)**2 + 4*b*b)
lambda1 = (trace + disc) / 2
lambda2 = (trace - disc) / 2
# возвращаем результат
return [float(lambda1), float(lambda2)]
# === конец минимальной базы ===
# === менять только тут ===
# матрица из условия
A = np.array([
[3, 1],
[1, 3],
[0, 1]
], dtype=float)
# этот вариант рассчитан на матрицу A с двумя столбцами
# для такой матрицы A^T A имеет размер 2 на 2
# === дальше не трогать ===
# строим A^T A
B = mat_mul(transpose(A), A)
print("A^T A:")
print(B)
# считаем собственные значения A^T A
lambda_values = eigenvalues_2x2_symmetric(B)
print("собственные значения A^T A:", lambda_values)
# считаем сингулярные числа
sigmas = []
for lam in lambda_values:
if lam < 0 and abs(lam) < 1e-10:
lam = 0.0
sigmas.append(float(math.sqrt(lam)))
print("сингулярные числа:", sigmas)
print("""
вывод:
сингулярные числа равны корням из собственных значений матрицы A^T A.
если нужны полные матрицы U и V, то V берется из собственных векторов A^T A, а U можно найти как A*V/sigma.
""")
# ====================================================================
# график сингулярных чисел
# если график не нужен, удали этот блок
positions = []
for i in range(len(sigmas)):
positions.append(i + 1)
# строим график
plt.figure()
# строим график
plt.bar(positions, sigmas)
# строим график
plt.xlabel("номер")
# строим график
plt.ylabel("сингулярное число")
# строим график
plt.title("сингулярные числа")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри сингулярные числа.
# большие сингулярные числа отвечают главным направлениям матрицы.
# если одно число намного больше остальных, матрицу можно хорошо приблизить низким рангом.
# нулевые или очень маленькие сингулярные числа показывают зависимость столбцов или строк.
# ====================================================================
print("""
типовые выводы:
сингулярные числа найдены через собственные значения матрицы A^T A. это подходит для маленькой матрицы, если надо показать связь SVD с задачей на собственные значения.
""")
# ====================================================================
# теория по методу
# ====================================================================
# SVD записывается как A = U*Sigma*V^T.
# Sigma содержит сингулярные числа.
# Сингулярные числа равны sqrt(lambda_i), где lambda_i - собственные значения A^T A.
# PCA и сжатие изображений часто используют первые самые большие сингулярные числа.
# ====================================================================
# %%
# ============================================================
# 10.2. PCA для маленькой таблицы данных
# ============================================================
# как понять, что этот блок нужен
# ищи слова: PCA, главные компоненты, ковариационная матрица.
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# функция умножает две матрицы
def mat_mul(A, B):
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
n, m = A.shape
m2, p = B.shape
C = np.zeros((n, p))
for i in range(n):
for j in range(p):
s = 0.0
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
return C
# функция транспонирует матрицу
def transpose(A):
A = np.array(A, dtype=float)
n, m = A.shape
T = np.zeros((m, n))
for i in range(n):
for j in range(m):
T[j, i] = A[i, j]
return T
# функция считает собственные значения симметрической матрицы 2 на 2
def eigenvalues_2x2_symmetric(B):
a = B[0, 0]
b = B[0, 1]
d = B[1, 1]
trace = a + d
disc = (a - d)**2 + 4*b*b
disc = disc**0.5
lambda1 = (trace + disc) / 2
lambda2 = (trace - disc) / 2
# возвращаем результат
return [float(lambda1), float(lambda2)]
# === конец минимальной базы ===
# === менять только тут ===
# строки - объекты, столбцы - признаки
X = np.array([
[2.5, 2.4],
[0.5, 0.7],
[2.2, 2.9],
[1.9, 2.2]
], dtype=float)
# === дальше не трогать ===
# центрируем данные
n, m = X.shape
means = np.zeros(m)
for j in range(m):
s = 0.0
for i in range(n):
s += X[i, j]
means[j] = s / n
X_centered = np.zeros((n, m))
for i in range(n):
for j in range(m):
X_centered[i, j] = X[i, j] - means[j]
# считаем ковариационную матрицу
C = mat_mul(transpose(X_centered), X_centered)
for i in range(m):
for j in range(m):
C[i, j] = C[i, j] / (n - 1)
print("средние:", means)
print("центрированные данные:")
print(X_centered)
print("ковариационная матрица:")
print(C)
# считаем собственные значения и доли дисперсии
lambda_values = eigenvalues_2x2_symmetric(C)
total_lambda = lambda_values[0] + lambda_values[1]
explained = []
for value in lambda_values:
explained.append(float(value / total_lambda))
print("собственные значения ковариационной матрицы:", lambda_values)
print("доли дисперсии:", explained)
# ====================================================================
# график долей дисперсии
# если график не нужен, удали этот блок
if "explained" in globals():
positions = []
for i in range(len(explained)):
positions.append(i + 1)
# строим график
plt.figure()
# строим график
plt.bar(positions, explained)
# строим график
plt.xlabel("компонента")
# строим график
plt.ylabel("доля дисперсии")
# строим график
plt.title("доли дисперсии PCA")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# смотри доли дисперсии главных компонент.
# если первая компонента объясняет больше 70-80%, данные почти одномерные.
# если первые две компоненты дают больше 90%, двумерное представление хорошее.
# если доли распределены равномерно, сильного сжатия без потерь не получится.
# ====================================================================
print("""
типовые выводы:
данные центрированы, затем построена ковариационная матрица. собственные значения показывают дисперсию по главным направлениям, а доли дисперсии помогают понять, сколько информации дает каждая компонента.
""")
# ====================================================================
# теория по методу
# ====================================================================
# PCA ищет направления максимальной дисперсии данных.
# Сначала из каждого признака вычитают среднее.
# Потом строят ковариационную матрицу.
# Собственные векторы ковариационной матрицы - главные компоненты.
# Большие собственные значения показывают самые важные направления.
# ====================================================================
# %%
# ============================================================
# 10.3. CSR для разреженной матрицы
# ============================================================
# как понять, что этот блок нужен
# ищи слова: разреженная матрица, CSR, compressed sparse row, умножить на вектор.
# === минимальная база для этой ячейки ===
import numpy as np
# === конец минимальной базы ===
# === менять только тут ===
# плотная матрица из условия
A = np.array([
[10, 0, 0, 2],
[0, 3, 0, 0],
[4, 0, 5, 0]
], dtype=float)
# вектор из условия
x = np.array([1, 2, 3, 4], dtype=float)
# === дальше не трогать ===
# переводим плотную матрицу в CSR
data = []
indices = []
indptr = [0]
n, m = A.shape
for i in range(n):
for j in range(m):
if A[i, j] != 0:
data.append(float(A[i, j]))
indices.append(j)
indptr.append(len(data))
# умножаем CSR на вектор
y = np.zeros(n)
for i in range(n):
s = 0.0
for k in range(indptr[i], indptr[i+1]):
j = indices[k]
s += data[k] * x[j]
y[i] = s
print("data:", data)
print("indices:", indices)
print("indptr:", indptr)
print("A*x:", y)
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри число ненулевых элементов nnz.
# если nnz намного меньше n^2, разреженное хранение выгодно.
# CSR удобен для умножения матрицы на вектор и прохода по строкам.
# если матрица плотная, CSR смысла почти не имеет.
# ====================================================================
print("""
типовые выводы:
матрица записана в формате CSR. хранятся только ненулевые элементы, поэтому такой формат экономит память для разреженных матриц.
""")
# ====================================================================
# теория по методу
# ====================================================================
# CSR хранит три массива: data, indices, indptr.
# data - ненулевые значения.
# indices - номера столбцов этих значений.
# indptr - указатели на начало каждой строки.
# Память O(nnz), где nnz - число ненулевых элементов.
# ====================================================================
# %%
# ============================================================
# 10.4. COO формат разреженной матрицы
# ============================================================
# как понять, что этот блок нужен
# ищи слова: COO, координатный формат, строки столбцы значения.
# === минимальная база для этой ячейки ===
import numpy as np
# === конец минимальной базы ===
# === менять только тут ===
# размеры матрицы
n = 3
m = 4
# ненулевые элементы: строка, столбец, значение
rows = [0, 0, 1, 2]
cols = [0, 3, 1, 2]
values = [10, 2, 3, 5]
# === дальше не трогать ===
# восстанавливаем плотную матрицу
A = np.zeros((n, m))
for k in range(len(values)):
A[rows[k], cols[k]] = values[k]
print("матрица из COO:")
print(A)
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри списки строк, столбцов и значений.
# число элементов в трех списках должно совпадать.
# COO удобно собирать по отдельным ненулевым элементам.
# для быстрых вычислений COO часто переводят в CSR.
# ====================================================================
print("""
типовые выводы:
формат COO хранит каждый ненулевой элемент как тройку: строка, столбец, значение.
""")
# ====================================================================
# теория по методу
# ====================================================================
# COO - координатный формат разреженной матрицы.
# Он удобен для создания матрицы, потому что можно просто хранить списки rows, cols, values.
# Для быстрых вычислений часто переводят COO в CSR.
# ====================================================================
# %%
# ============================================================
# 10.5. Характеристический многочлен и корни
# ============================================================
# как понять, что этот блок нужен
# ищи слова: характеристическое уравнение, характеристический многочлен, np.poly, np.roots.
# np.poly и np.roots разрешены только для корней характеристического уравнения.
# === минимальная база для этой ячейки ===
import numpy as np
# === конец минимальной базы ===
# === менять только тут ===
# матрица из условия
A = np.array([
[2, 1],
[1, 2]
], dtype=float)
# === дальше не трогать ===
# считаем коэффициенты характеристического многочлена
coeffs = np.poly(A)
roots = np.roots(coeffs)
print("коэффициенты характеристического многочлена:", coeffs)
print("корни, то есть собственные значения:", roots)
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри коэффициенты многочлена и корни.
# для матрицы 2 на 2 или 3 на 3 такой способ нормальный.
# для больших матриц характеристический многочлен численно неустойчив.
# корни многочлена являются собственными значениями матрицы.
# ====================================================================
print("""
типовые выводы:
коэффициенты характеристического многочлена найдены, а его корни являются собственными значениями матрицы.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Собственные значения матрицы являются корнями det(A-lambda*I)=0.
# В условиях экзамена np.poly и np.roots разрешены только для характеристического уравнения.
# Для больших матриц такой способ хуже QR-алгоритма, но для маленьких матриц подходит.
# ====================================================================
# %%
# ============================================================
# 10.6. Проверка нормальности матрицы
# ============================================================
# как понять, что этот блок нужен
# ищи слова: нормальная матрица, A A^T = A^T A, Шур.
# === минимальная база для этой ячейки ===
import numpy as np
# функция транспонирует матрицу
def transpose(A):
A = np.array(A, dtype=float)
n, m = A.shape
T = np.zeros((m, n))
for i in range(n):
for j in range(m):
T[j, i] = A[i, j]
return T
# функция умножает матрицы
def mat_mul(A, B):
A = np.array(A, dtype=float)
B = np.array(B, dtype=float)
n, m = A.shape
m2, p = B.shape
C = np.zeros((n, p))
for i in range(n):
for j in range(p):
s = 0.0
for k in range(m):
s += A[i, k] * B[k, j]
C[i, j] = s
return C
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([
[1, 2],
[2, 1]
], dtype=float)
tol = 1e-8
# === дальше не трогать ===
AT = transpose(A)
AAT = mat_mul(A, AT)
ATA = mat_mul(AT, A)
max_diff = 0.0
n, m = AAT.shape
for i in range(n):
for j in range(m):
cur = abs(AAT[i, j] - ATA[i, j])
if cur > max_diff:
max_diff = cur
print("max |A A^T - A^T A| =", max_diff)
if max_diff < tol:
print("матрица нормальная")
else:
print("матрица не нормальная")
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# сравни A*A^T и A^T*A.
# если разница меньше 1e-8, матрицу можно считать нормальной.
# если разница заметная, матрица не нормальная.
# для нормальной матрицы разложение Шура имеет диагональный вид в комплексном случае.
# ====================================================================
print("""
типовые выводы:
если max |A A^T - A^T A| мал, матрица нормальная. для нормальной матрицы разложение Шура дает диагональную T.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Матрица нормальная, если A*A^T = A^T*A.
# Для нормальной матрицы в разложении Шура A = Q*T*Q^T матрица T диагональная.
# Для общей матрицы T обычно только верхнетреугольная.
# ====================================================================
# %%
# ============================================================
# 10.7. Проверка верхне-гессенберговой формы
# ============================================================
# как понять, что этот блок нужен
# ищи слова: верхне-гессенбергова форма, QR-алгоритм.
# === минимальная база для этой ячейки ===
import numpy as np
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[0, 9, 1, 2],
[0, 0, 3, 4]
], dtype=float)
tol = 1e-10
# === дальше не трогать ===
ok = True
n, m = A.shape
for i in range(n):
for j in range(m):
if i > j + 1 and abs(A[i, j]) > tol:
ok = False
print("нарушение в позиции", i, j, "значение", A[i, j])
if ok:
print("матрица верхне-гессенбергова")
else:
print("матрица не верхне-гессенбергова")
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри элементы ниже первой поддиагонали.
# если они равны нулю или меньше 1e-10, форма верхне-гессенбергова.
# такая форма удобна для QR-алгоритма.
# если есть большие элементы ниже поддиагонали, форма не выполнена.
# ====================================================================
print("""
типовые выводы:
проверены элементы ниже первой поддиагонали. если они нулевые, матрица имеет верхне-гессенбергову форму.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Верхне-гессенбергова матрица имеет нули ниже первой поддиагонали.
# Такая форма ускоряет QR-алгоритм.
# На ней QR-шаг дешевле, чем на полной матрице.
# ====================================================================
# %%
# ============================================================
# 10.8. Отношение Релея
# ============================================================
# как понять, что этот блок нужен
# ищи слова: отношение Релея, оценка собственного значения.
# === минимальная база для этой ячейки ===
import numpy as np
# функция умножает матрицу на вектор
def mat_vec(A, x):
A = np.array(A, dtype=float)
x = np.array(x, dtype=float)
n, m = A.shape
y = np.zeros(n)
for i in range(n):
s = 0.0
for j in range(m):
s += A[i, j] * x[j]
y[i] = s
return y
# функция считает скалярное произведение
def dot(u, v):
s = 0.0
for i in range(len(u)):
s += u[i] * v[i]
return s
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([[2, 1], [1, 2]], dtype=float)
x = np.array([1, 1], dtype=float)
# === дальше не трогать ===
Ax = mat_vec(A, x)
rayleigh = dot(Ax, x) / dot(x, x)
print("отношение Релея:", rayleigh)
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри значение отношения Релея и невязку.
# если вектор близок к собственному, отношение Релея близко к собственному значению.
# если невязка маленькая, оценка хорошая.
# если вектор случайный, значение является только грубой оценкой.
# ====================================================================
print("""
типовые выводы:
отношение Релея дает оценку собственного значения по текущему вектору.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Отношение Релея: lambda = (A*x, x)/(x, x).
# Его используют в степенном методе для уточнения собственного значения.
# Если x близок к собственному вектору, оценка lambda получается хорошей.
# ====================================================================
# %%
# ============================================================
# 10.9. Невязка для СЛАУ Ax=b
# ============================================================
# как понять, что этот блок нужен
# ищи слова: проверить решение, невязка, Ax-b.
# === минимальная база для этой ячейки ===
import math
import numpy as np
# функция умножает матрицу на вектор
def mat_vec(A, x):
A = np.array(A, dtype=float)
x = np.array(x, dtype=float)
n, m = A.shape
y = np.zeros(n)
for i in range(n):
s = 0.0
for j in range(m):
s += A[i, j] * x[j]
y[i] = s
return y
# функция считает норму
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([[2, 1], [1, 3]], dtype=float)
b = np.array([1, 2], dtype=float)
x = np.array([0.2, 0.6], dtype=float)
# === дальше не трогать ===
r = mat_vec(A, x) - b
print("невязка r=Ax-b:", r)
print("норма невязки:", vector_norm(r))
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри норму r=Ax-b.
# меньше 1e-6 - хороший ответ для учебной точности.
# от 1e-6 до 1e-3 - средняя точность.
# больше 1e-3 - решение лучше проверить или уточнить.
# ====================================================================
print("""
типовые выводы:
невязка Ax-b показывает, насколько найденный вектор подходит к системе. чем меньше норма невязки, тем лучше решение.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Невязка для СЛАУ равна r = A*x - b.
# Если r близка к нулю, решение хорошо удовлетворяет системе.
# Невязка не всегда равна реальной ошибке, но это главная проверка результата.
# ====================================================================
# %%
# ============================================================
# 10.10. Невязка собственного значения
# ============================================================
# как понять, что этот блок нужен
# ищи слова: проверить собственный вектор, Ax-lambda*x.
# === минимальная база для этой ячейки ===
import math
import numpy as np
# функция умножает матрицу на вектор
def mat_vec(A, x):
A = np.array(A, dtype=float)
x = np.array(x, dtype=float)
n, m = A.shape
y = np.zeros(n)
for i in range(n):
s = 0.0
for j in range(m):
s += A[i, j] * x[j]
y[i] = s
return y
# функция считает норму
def vector_norm(x):
# приводим данные к массиву
x = np.array(x, dtype=float)
# считаем итоговое значение
s = 0.0
if len(x.shape) == 1:
for value in x:
s += float(value) * float(value)
else:
for i in range(x.shape[0]):
for j in range(x.shape[1]):
s += float(x[i, j]) * float(x[i, j])
# возвращаем результат
return math.sqrt(s)
# === конец минимальной базы ===
# === менять только тут ===
A = np.array([[2, 1], [1, 2]], dtype=float)
lam = 3.0
x = np.array([1, 1], dtype=float)
# === дальше не трогать ===
r = mat_vec(A, x) - lam * x
print("невязка Ax-lambda*x:", r)
print("норма невязки:", vector_norm(r))
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри норму A*x-lambda*x.
# меньше 1e-6 - собственная пара хорошая.
# от 1e-6 до 1e-3 - приближенно нормально.
# больше 1e-3 - lambda или x подобраны плохо.
# ====================================================================
print("""
типовые выводы:
невязка Ax-lambda*x показывает, насколько хорошо пара lambda и x является собственной парой.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Для собственного значения и вектора должно быть A*x = lambda*x.
# Поэтому невязка r = A*x - lambda*x должна быть близка к нулю.
# Это удобная проверка степенного метода и QR-алгоритма.
# ====================================================================
# %%
# ============================================================
# 10.11. Метод золотого сечения для минимума
# ============================================================
# как понять, что этот блок нужен
# ищи слова: золотое сечение, минимум на отрезке.
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
return (x - 2)**2 + 1
a = 0.0
b = 5.0
tol = 1e-5
# === дальше не трогать ===
phi = (1 + math.sqrt(5)) / 2
x1 = b - (b - a) / phi
x2 = a + (b - a) / phi
while abs(b - a) > tol:
if f(x1) > f(x2):
a = x1
x1 = x2
x2 = a + (b - a) / phi
else:
b = x2
x2 = x1
x1 = b - (b - a) / phi
x_min = (a + b) / 2
print("точка минимума:", x_min)
print("значение минимума:", f(x_min))
# график
# если график не нужен, удали этот блок
xs = np.linspace(a, b, 200)
ys = [f(x) for x in xs]
plt.figure()
plt.plot(xs, ys)
plt.scatter([x_min], [f(x_min)])
plt.grid(True)
plt.show()
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри длину итогового отрезка.
# если длина меньше tol, минимум найден с нужной точностью.
# обычно 20-40 итераций для eps около 1e-6 - нормально.
# если функция не унимодальная на отрезке, метод может найти локальный минимум.
# ====================================================================
print("""
типовые выводы:
метод золотого сечения нашел приближенную точку минимума на отрезке. метод не использует производную.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод золотого сечения ищет минимум функции на отрезке.
# Он похож на дихотомию, но выбирает точки так, чтобы часть вычислений переиспользовать.
# Метод подходит для унимодальной функции на отрезке.
# ====================================================================
# %%
# ============================================================
# 10.12. Метод прямоугольников
# ============================================================
# как понять, что этот блок нужен
# ищи слова: левые прямоугольники, правые прямоугольники, средние прямоугольники.
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
def f(x):
return x**2
a = 0.0
b = 1.0
N = 10
# === дальше не трогать ===
h = (b - a) / N
left = 0.0
right = 0.0
middle = 0.0
for i in range(N):
left += f(a + i*h) * h
right += f(a + (i+1)*h) * h
middle += f(a + (i+0.5)*h) * h
print("левые прямоугольники:", left)
print("правые прямоугольники:", right)
print("средние прямоугольники:", middle)
# ====================================================================
# график функции для метода прямоугольников
# если график не нужен, удали этот блок
xs = np.linspace(a, b, 300)
ys = [f(x) for x in xs]
# строим график
plt.figure()
# строим график
plt.plot(xs, ys)
# строим график
plt.title("график функции для интегрирования")
# строим график
plt.grid(True)
# строим график
plt.show()
# как писать вывод по этому блоку
# ====================================================================
# сравни левый, правый и средний прямоугольники, если они есть.
# чем меньше h, тем точнее интеграл.
# метод средних прямоугольников обычно точнее левого и правого.
# если ответы сильно отличаются, сетка слишком грубая.
# ====================================================================
print("""
типовые выводы:
интеграл посчитан тремя вариантами прямоугольников. средние прямоугольники обычно точнее левых и правых.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод прямоугольников заменяет площадь под графиком суммой площадей прямоугольников.
# Левые берут значение функции в левом конце, правые - в правом, средние - в середине.
# При уменьшении шага точность обычно растет.
# ====================================================================
# %%
# ============================================================
# 10.13. Правило Рунге для оценки ошибки
# ============================================================
# как понять, что этот блок нужен
# ищи слова: правило Рунге, оценка ошибки, шаг h и h/2.
# === минимальная база для этой ячейки ===
import math
# === конец минимальной базы ===
# === менять только тут ===
# два результата одного метода с шагами h и h/2
y_h = 1.234
y_h2 = 1.236
p_order = 2
# === дальше не трогать ===
error_est = abs(y_h2 - y_h) / (2**p_order - 1)
y_better = y_h2 + (y_h2 - y_h) / (2**p_order - 1)
print("оценка ошибки:", error_est)
print("уточненное значение:", y_better)
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри оценку ошибки по двум шагам.
# если ошибка меньше tol, шаг подходит.
# если ошибка больше tol, надо уменьшить h.
# для метода порядка p ошибка оценивается как |y_h2-y_h|/(2^p-1).
# ====================================================================
print("""
типовые выводы:
по двум расчетам с шагами h и h/2 получена оценка ошибки. если ошибка большая, шаг надо уменьшить.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Правило Рунге оценивает ошибку по двум расчетам одного метода.
# Если порядок метода p, то error примерно равна |y_h2 - y_h|/(2^p - 1).
# Уточненное значение можно получить добавлением поправки к расчету с h/2.
# ====================================================================
# %%
# ============================================================
# 10.14. Неявный Эйлер для y' = lambda*y
# ============================================================
# как понять, что этот блок нужен
# ищи слова: неявный Эйлер, жесткая задача, устойчивость.
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
# === конец минимальной базы ===
# === менять только тут ===
lambda_value = -10.0
y0 = 1.0
t0 = 0.0
t_end = 2.0
h = 0.1
# === дальше не трогать ===
t_values = []
y_explicit = []
y_implicit = []
t = t0
ye = y0
yi = y0
while t <= t_end + 1e-12:
t_values.append(t)
y_explicit.append(ye)
y_implicit.append(yi)
ye = ye + h * lambda_value * ye
yi = yi / (1 - h * lambda_value)
t = t + h
print("явный Эйлер в конце:", y_explicit[-1])
print("неявный Эйлер в конце:", y_implicit[-1])
# график
# если график не нужен, удали этот блок
plt.figure()
plt.plot(t_values, y_explicit, label="явный")
plt.plot(t_values, y_implicit, label="неявный")
plt.grid(True)
plt.legend()
plt.show()
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри устойчивость при отрицательном lambda.
# неявный Эйлер устойчивее явного на жестких задачах.
# если h большой, решение может быть грубым, но не должно взрываться при lambda<0.
# для y'=lambda*y формула имеет вид y_next=y/(1-h*lambda).
# ====================================================================
print("""
типовые выводы:
неявный Эйлер обычно устойчивее явного, особенно для жестких задач и отрицательных больших lambda.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Явный Эйлер считает новое значение напрямую по старому.
# Неявный Эйлер использует правую часть в новой точке.
# Для y'=lambda*y формула неявного Эйлера: y_next = y/(1-h*lambda).
# Неявные методы часто устойчивее на жестких задачах.
# ====================================================================
# %%
# ============================================================
# 10.15. Низкочастотный фильтр через ДПФ
# ============================================================
# как понять, что этот блок нужен
# ищи слова: фильтр, убрать высокие частоты, шум, f_cut.
# === минимальная база для этой ячейки ===
import math
import numpy as np
import matplotlib.pyplot as plt
# функция считает ДПФ
def dft(x):
N = len(x)
X = []
for k in range(N):
s = 0j
for n in range(N):
angle = -2 * math.pi * k * n / N
s += x[n] * complex(math.cos(angle), math.sin(angle))
X.append(s)
return X
# функция считает обратное ДПФ
def idft(X):
N = len(X)
x = []
for n in range(N):
s = 0j
for k in range(N):
angle = 2 * math.pi * k * n / N
s += X[k] * complex(math.cos(angle), math.sin(angle))
x.append((s / N).real)
return x
# === конец минимальной базы ===
# === менять только тут ===
N = 32
fs = 32.0
f_cut = 4.0
signal = []
for n in range(N):
t = n / fs
signal.append(math.sin(2*math.pi*2*t) + 0.4*math.sin(2*math.pi*10*t))
# === дальше не трогать ===
X = dft(signal)
df = fs / N
X_filtered = []
for k in range(N):
freq = k*df if k <= N/2 else (k-N)*df
if abs(freq) <= f_cut:
X_filtered.append(X[k])
else:
X_filtered.append(0j)
restored = idft(X_filtered)
print("частотное разрешение:", df)
print("порог фильтра:", f_cut)
# график
# если график не нужен, удали этот блок
plt.figure()
plt.plot(signal, label="исходный")
plt.plot(restored, label="после фильтра")
plt.grid(True)
plt.legend()
plt.show()
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри, какие частоты удалены, и сравни график до и после.
# если шум был узкой гармоникой, после удаления пик должен исчезнуть почти полностью.
# если шум широкий, он только уменьшается, полностью убрать его трудно.
# слишком жесткий фильтр может испортить полезный сигнал.
# ====================================================================
print("""
типовые выводы:
после ДПФ высокие частоты выше порога обнулены, затем сигнал восстановлен обратным ДПФ. так можно убрать высокочастотный шум.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Фильтрация через ДПФ делается в частотной области.
# Сначала считаем спектр, потом обнуляем ненужные частоты.
# Низкочастотный фильтр оставляет медленные компоненты и убирает быстрый шум.
# ====================================================================
# %%
# ============================================================
# 10.16. МНК параболой y = a*x^2 + b*x + c
# ============================================================
# как понять, что этот блок нужен
# ищи слова: метод наименьших квадратов, парабола, квадратичная аппроксимация.
# === минимальная база для этой ячейки ===
import numpy as np
import matplotlib.pyplot as plt
EPS_DIV = 1e-14
# функция решает СЛАУ методом Гаусса
def gaussian_solve(A, b):
A = np.array(A, dtype=float)
b = np.array(b, dtype=float)
n = len(b)
for k in range(n):
pivot = k
for i in range(k + 1, n):
if abs(A[i, k]) > abs(A[pivot, k]):
pivot = i
if abs(A[pivot, k]) < EPS_DIV:
raise ValueError("матрица вырождена")
if pivot != k:
A[[k, pivot]] = A[[pivot, k]]
b[k], b[pivot] = b[pivot], b[k]
for i in range(k + 1, n):
factor = A[i, k] / A[k, k]
A[i, k] = 0.0
for j in range(k + 1, n):
A[i, j] -= factor * A[k, j]
b[i] -= factor * b[k]
x = np.zeros(n)
for i in range(n - 1, -1, -1):
s = b[i]
for j in range(i + 1, n):
s -= A[i, j] * x[j]
x[i] = s / A[i, i]
return x
# === конец минимальной базы ===
# === менять только тут ===
x_points = [0, 1, 2, 3]
y_points = [1, 2.2, 5.1, 10.2]
# === дальше не трогать ===
n = len(x_points)
sx = sum(x_points)
sx2 = sum(x*x for x in x_points)
sx3 = sum(x*x*x for x in x_points)
sx4 = sum(x*x*x*x for x in x_points)
sy = sum(y_points)
sxy = sum(x_points[i]*y_points[i] for i in range(n))
sx2y = sum(x_points[i]*x_points[i]*y_points[i] for i in range(n))
A = np.array([[sx4, sx3, sx2], [sx3, sx2, sx], [sx2, sx, n]], dtype=float)
b = np.array([sx2y, sxy, sy], dtype=float)
coef = gaussian_solve(A, b)
a, b_coef, c = coef[0], coef[1], coef[2]
print("a:", a)
print("b:", b_coef)
print("c:", c)
# график
# если график не нужен, удали этот блок
xs = np.linspace(min(x_points), max(x_points), 200)
ys = [a*x*x + b_coef*x + c for x in xs]
plt.figure()
plt.scatter(x_points, y_points)
plt.plot(xs, ys)
plt.grid(True)
plt.show()
# ====================================================================
# как писать вывод по этому блоку
# ====================================================================
# смотри коэффициенты и остатки.
# если остатки маленькие и без явного рисунка, модель подходит нормально.
# если остатки большие или меняют знак по схеме, нужна другая модель.
# для прямой коэффициент при x показывает скорость роста или убывания.
# ====================================================================
print("""
типовые выводы:
по точкам построена квадратичная аппроксимация y=a*x^2+b*x+c методом наименьших квадратов.
""")
# ====================================================================
# теория по методу
# ====================================================================
# Метод наименьших квадратов выбирает параметры так, чтобы сумма квадратов ошибок была минимальна.
# Для параболы неизвестны a, b, c.
# Они находятся из нормальных уравнений.
# ====================================================================
# %%
# # 11. короткая теория по редким темам
#
# ## ieee 754
# число с плавающей точкой хранится как знак, порядок и мантисса. не все десятичные числа представимы точно, поэтому появляются ошибки округления. машинный эпсилон показывает уровень относительной ошибки округления.
#
# overflow - результат слишком большой и превращается в бесконечность. underflow - результат слишком маленький и может округлиться к нулю или стать денормализованным числом.
#
# ## потеря значимости
# потеря значимости возникает при вычитании близких чисел. старшие разряды сокращаются, а оставшиеся младшие уже содержат ошибки округления.
#
# ## обусловленность
# если задача плохо обусловлена, маленькая ошибка во входных данных дает большую ошибку в ответе. в методе ньютона для системы это часто связано с плохо обусловленным якобианом.
#
# ## спектральный радиус и отношение релея
# спектральный радиус - это max |lambda_i|. степенной метод обычно находит собственное значение с наибольшим модулем. отношение релея равно (Ax,x)/(x,x) и дает оценку собственного значения по вектору x.
#
# ## гессенбергова форма
# верхне-гессенбергова матрица имеет нули ниже первой поддиагонали. она нужна для ускорения QR-алгоритма: один QR-шаг становится дешевле.
#
# ## нормальная матрица и шур
# матрица нормальная, если A A^T = A^T A. разложение Шура имеет вид A = Q T Q^T. для нормальной матрицы T получается диагональной, а для общей - верхнетреугольной.
#
# ## svd и pca
# SVD: A = U Sigma V^T. диагональ Sigma содержит сингулярные числа. сингулярные числа равны корням из собственных значений A^T A. PCA ищет направления максимальной дисперсии данных. если данные центрированы, главные компоненты связаны с собственными векторами ковариационной матрицы.
#
# ## разреженные матрицы
# плотная матрица хранит все элементы. разреженная хранит только ненулевые. COO хранит строки, столбцы и значения. CSR хранит data, indices, indptr. память становится O(nnz) вместо O(n^2), где nnz - число ненулевых элементов.
#
# ## архитектура памяти
# на практике скорость зависит не только от числа операций, но и от работы с памятью. кэш быстрее оперативной памяти. если алгоритм создает много временных матриц или плохо переиспользует данные, он может быть медленнее даже при лучшей асимптотике. поэтому Штрассен на маленьких матрицах может проигрывать обычному умножению.
#
# ## локальная и глобальная ошибка
# локальная ошибка - ошибка одного шага, если предыдущее значение точное. глобальная ошибка - накопленная ошибка на всем интервале. у метода Рунге-Кутты 4 порядка локальная ошибка порядка h^5, глобальная - порядка h^4.
#
# ## согласованность, устойчивость, сходимость
# согласованность значит, что схема при h -> 0 приближает исходное уравнение. устойчивость значит, что ошибки не растут бесконтрольно. сходимость значит, что численное решение стремится к точному. для корректных линейных задач согласованность плюс устойчивость дают сходимость.
#
# ## строгая и слабая устойчивость
# строгая устойчивость значит, что ошибки затухают. слабая устойчивость значит, что ошибки хотя бы не растут. для жестких задач обычно нужны более устойчивые, часто неявные методы.
#
# ## константа липшица
# для простых итераций x=phi(x) важно, чтобы |phi'(x)| < 1 около решения. чем ближе это число к 1, тем медленнее сходимость. если оно больше 1, итерации могут расходиться.
#
# ## дпф, бпф и найквист
# ДПФ переводит сигнал из временной области в частотную. БПФ делает то же самое быстрее: примерно O(N log N) вместо O(N^2). частотное разрешение df = fs/N. максимальная частота без наложения спектров равна fs/2, это частота Найквиста.
# %%
# # 12. полный текст теории из отдельного файла
#
# ниже полностью вставлен файл `numerical_methods_exam_theory_pack_v8_extended.txt`, чтобы вся теория была прямо в конце ноутбука.
# %%
# ## теория, часть 1
#
# ЧИСЛЕННЫЕ МЕТОДЫ - РАСШИРЕННАЯ ТЕОРИЯ V8
#
# Как пользоваться:
# 1. Сначала найди тему по названию метода.
# 2. Для короткого ответа бери: определение, идею, формулу, условия, плюс и минус.
# 3. Для длинного ответа добавь сравнение с соседним методом и устойчивость/погрешность.
# 4. В конце можно написать: результат приближенный, точность проверяется по ошибке или невязке.
#
# Карта теории к нашим блокам:
#
# 1.1 Кэхэн - смотри IEEE 754, потеря значимости, накопление ошибок округления.
# 2.1 Бисекция - теорема о промежуточном значении, смена знака, линейная сходимость.
# 2.2 Ньютон - касательная, производная, квадратичная сходимость, зависимость от x0.
# 2.3 Модифицированный Ньютон - фиксированная производная, дешевле шаг, медленнее сходимость.
# 2.4 Секущие - замена производной разностным отношением, быстрее бисекции, менее надежно.
# 2.5 Простые итерации - неподвижная точка, сжимающее отображение, константа Липшица.
# 2.6 Дихотомия - одномерная минимизация, сужение отрезка, сравнение двух точек.
# 3.1 Ньютон для системы - вектор F, Якобиан, линейная система для поправки.
# 3.2 Итерации для системы - x=g(x), сжатие в многомерном случае, невязка.
# 4.1 Линейная интерполяция - локальная интерполяция прямой между узлами.
# 4.2 Лагранж - базисные полиномы, точное прохождение через узлы, феномен Рунге.
# 4.3 Сплайн - кусочные кубические полиномы, гладкая сшивка, натуральные условия.
# 5.1 Степенной метод - собственные значения, спектральный радиус, спектральный зазор.
# 5.2 Сдвиг - обратный степенной метод, sigma, отношение Релея, решение систем.
# 5.3 QR - ортогональность Q, верхнетреугольная R, Грамм-Шмидт.
# 5.4 QR-алгоритм - последовательные QR-шаги, спектр, сходимость к форме Шура.
# 5.5 Шур - A=U*T*U^T, верхнетреугольная форма, связь с собственными значениями.
# 5.6 Гершгорин - круги, центры, радиусы, локализация собственных значений.
# 5.7 Штрассен - O(n^3), O(n^2.807), память, кэш, 7 умножений вместо 8.
# 6.1 Дифференцирование - прямая, обратная, центральная разность, ошибка усечения и округления.
# 6.2 Эйлер - задача Коши, первый порядок, локальная и глобальная ошибка.
# 6.3 Предиктор-корректор - прогноз и исправление, метод Хойна, порядок 2.
# 6.4 метод Рунге-Кутты 4 порядка - четыре наклона, порядок 4, сравнение с Эйлером.
# 6.5 Система ОДУ - векторная форма, фазовый портрет, осциллятор/маятник.
# 6.6 Сравнение Эйлера и метод Рунге-Кутты 4 порядка - точность, стоимость шага, устойчивость.
# 6.7 Адамс - многошаговые методы, предиктор Башфорта, корректор Мултона.
# 7.1 ДПФ - временная и частотная области, амплитудный спектр, шум.
# 7.2 БПФ - разбиение на четные и нечетные отсчеты, O(N log N).
#
# Нестандартные темы, которые добавлены запасом:
# - численное интегрирование: прямоугольники, трапеции, Симпсон
# - Гаусс-Зейдель
# - метод вращений Якоби
# - вторая производная
# - адаптивный шаг
# - фильтрация ДПФ
# - Найквист и алиасинг
# - аппроксимация МНК
# - PageRank
# - Хаусхолдер и гессенбергова форма
# - SVD, PCA, разреженные матрицы, жесткие ОДУ
#
#
# ЧИСЛЕННЫЕ МЕТОДЫ - ТЕОРИЯ ДЛЯ ЭКЗАМЕНА
#
# Как отвечать на теорию
#
# Пиши по одной схеме:
# 1. Метод нужен для такой-то задачи.
# 2. Идея метода такая-то.
# 3. Основная формула такая-то.
# 4. Для метода нужны такие данные.
# 5. Остановка идет по ошибке, невязке или числу итераций.
# 6. Плюс метода.
# 7. Минус метода.
# 8. Итог является приближенным.
#
# Общая теория
#
# Численные методы применяют, когда точное решение трудно получить или его нет.
# Численный метод сводит задачу к последовательности арифметических действий.
# Ответ обычно является приближенным.
#
# Причины погрешности:
# 1. Неточные исходные данные.
# 2. Неточная математическая модель.
# 3. Приближенный метод.
# 4. Округление чисел в компьютере.
# 5. Накопление ошибки при большом числе операций.
#
# Виды погрешности:
# - неустранимая погрешность - связана с исходными данными и моделью
# - погрешность метода - связана с заменой точной задачи приближенной
# - вычислительная погрешность - связана с округлением в компьютере
#
# Абсолютная погрешность:
# abs_error = abs(exact - approx)
#
# Относительная погрешность:
# rel_error = abs_error / abs(exact)
#
# Если точного ответа нет, проверяют:
# - невязку
# - разность соседних итераций
# - уменьшение ошибки при уменьшении шага
# - сравнение разных методов
#
# Невязка показывает, насколько хорошо найденный ответ удовлетворяет исходной задаче.
# Для f(x)=0 невязка равна abs(f(x)).
# Для A*x=b невязка равна norm(A*x-b).
# Для собственного значения невязка равна norm(A*x-lambda*x).
#
# Сходимость означает, что приближения становятся ближе к решению.
# Устойчивость означает, что маленькие ошибки не начинают быстро расти.
# Порядок точности показывает, как быстро падает ошибка при уменьшении шага.
#
#
# 1.1. АЛГОРИТМ КЭХЭНА ДЛЯ СУММИРОВАНИЯ
# =====================================
#
# Метод Кэхэна нужен для более точного суммирования чисел.
#
# Проблема: при сложении большого и маленького числа маленькая часть может потеряться из-за округления.
# Идея: хранить не только сумму, но и поправку c.
#
# Формулы:
# y = x_i - c
# t = sum + y
# c = (t - sum) - y
# sum = t
#
# Данные: список чисел.
# Остановка: метод просто проходит по списку один раз.
# Плюс: уменьшает накопление ошибки.
# Минус: не убирает ошибку полностью.
#
# Короткий ответ: алгоритм Кэхэна уменьшает ошибку округления при суммировании за счет хранения поправки.
#
#
# 2.1. МЕТОД БИСЕКЦИИ
# ===================
#
# Метод бисекции решает уравнение f(x)=0 на отрезке [a,b].
#
# Главное условие: f(a)*f(b) < 0. Тогда на отрезке есть смена знака.
# Идея: делить отрезок пополам и оставлять половину, где есть смена знака.
#
# Формула:
# c = (a + b) / 2
#
# Данные: f(x), a, b, eps.
# Остановка: abs(f(c)) < eps или abs(b-a) < eps.
# Плюс: надежный метод, не нужна производная.
# Минус: сходится медленно.
#
# Короткий ответ: метод последовательно сужает отрезок, внутри которого находится корень.
#
#
# 2.2. МЕТОД НЬЮТОНА ДЛЯ ОДНОГО УРАВНЕНИЯ
# =======================================
#
# Метод Ньютона решает уравнение f(x)=0.
#
# Идея: в текущей точке строится касательная, а ее пересечение с осью Ox дает новое приближение.
#
# Формула:
# x_next = x - f(x) / f_prime(x)
#
# Данные: f(x), f_prime(x), x0, eps.
# Остановка: abs(x_next-x) < eps или abs(f(x_next)) < eps.
# Плюс: часто сходится очень быстро.
# Минус: нужна производная, метод зависит от x0, нельзя делить на почти нулевую производную.
#
# Короткий ответ: метод Ньютона заменяет функцию касательной и уточняет корень по формуле x_next.
#
#
# 2.3. МОДИФИЦИРОВАННЫЙ МЕТОД НЬЮТОНА
# ===================================
#
# Модифицированный метод Ньютона похож на обычный Ньютон.
#
# Отличие: производная считается один раз в начальной точке x0.
#
# Формула:
# x_next = x - f(x) / f_prime(x0)
#
# Данные: f(x), f_prime(x), x0, eps.
# Остановка: abs(x_next-x) < eps или abs(f(x_next)) < eps.
# Плюс: меньше вычислений производной.
# Минус: обычно медленнее обычного Ньютона.
#
# Короткий ответ: производная фиксируется в начальной точке, поэтому метод проще, но может сходиться медленнее.
#
#
# 2.4. МЕТОД СЕКУЩИХ
# ==================
#
# Метод секущих решает уравнение f(x)=0 без явной производной.
#
# Идея: производная заменяется наклоном секущей через две последние точки.
#
# Формула:
# x_next = x_curr - f(x_curr)*(x_curr-x_prev)/(f(x_curr)-f(x_prev))
#
# Данные: f(x), x0, x1, eps.
# Остановка: abs(x_next-x_curr) < eps или abs(f(x_next)) < eps.
# Плюс: не нужна производная.
# Минус: нужны две стартовые точки, метод может не сойтись.
#
# Короткий ответ: метод секущих использует две последние точки, чтобы приблизить производную.
#
#
# 2.5. МЕТОД ПРОСТЫХ ИТЕРАЦИЙ x = phi(x)
# ======================================
#
# Метод простых итераций решает уравнение, записанное как x = phi(x).
#
# Идея: много раз считать x_next = phi(x), пока значения не перестанут сильно меняться.
#
# Формула:
# x_next = phi(x)
#
# Данные: phi(x), x0, eps.
# Остановка: abs(x_next-x) < eps.
# Условие хорошей сходимости: abs(phi_prime(x)) < 1 около решения.
# Плюс: простая схема.
# Минус: нужно правильно выбрать phi, иначе метод не сойдется.
#
# Короткий ответ: метод ищет неподвижную точку функции phi.
#
#
# 2.6. МИНИМИЗАЦИЯ МЕТОДОМ ДИХОТОМИИ
# ==================================
#
# Метод дихотомии ищет минимум функции на отрезке.
#
# Идея: около середины отрезка берутся две близкие точки. По значениям функции выбирается половина, где может быть минимум.
#
# Данные: f(x), a, b, eps, delta.
# Остановка: abs(b-a) < eps.
# Плюс: не нужна производная.
# Минус: нужен отрезок и метод может быть медленным.
#
# Короткий ответ: метод постепенно сужает отрезок, в котором находится точка минимума.
#
#
# 3.1. МЕТОД НЬЮТОНА ДЛЯ СИСТЕМЫ
# ==============================
#
# Метод Ньютона для системы решает F(x)=0, где x - вектор.
#
# Идея: нелинейная система заменяется линейной системой с матрицей Якоби.
#
# Формулы:
# J(x_k)*delta = -F(x_k)
# x_next = x_k + delta
#
# Данные: F(x), J(x), x0, eps.
# J(x) - матрица частных производных.
# Остановка: norm(delta) < eps или norm(F(x_next)) < eps.
# Плюс: быстро сходится около решения.
# Минус: нужен Якобиан и решение линейной системы.
#
# Короткий ответ: метод на каждом шаге решает линейную систему с матрицей Якоби.
#
#
# 3.2. ПРОСТЫЕ ИТЕРАЦИИ ДЛЯ СИСТЕМЫ x = g(x)
# ==========================================
#
# Метод простых итераций для системы решает систему вида x = g(x).
#
# Идея: строится последовательность векторов x_next = g(x).
#
# Формула:
# x_next = g(x)
#
# Данные: g(x), x0, eps, исходная F(x) для проверки.
# Остановка: norm(x_next-x) < eps.
# Плюс: не нужен Якобиан.
# Минус: метод может не сойтись, если g выбрана плохо.
#
# Короткий ответ: метод повторяет векторную формулу g до стабилизации решения.
#
#
# 4.1. ЛИНЕЙНАЯ ИНТЕРПОЛЯЦИЯ
# ==========================
#
# Линейная интерполяция находит значение функции между двумя известными точками.
#
# Идея: между соседними узлами функция заменяется прямой.
#
# Формула:
# y = y0 + (y1-y0)*(x-x0)/(x1-x0)
#
# Данные: x_points, y_points, x_star.
# Плюс: простой метод.
# Минус: низкая точность на сильно изогнутых функциях.
#
# Короткий ответ: значение находится по прямой между двумя соседними табличными точками.
#
#
# 4.2. ИНТЕРПОЛЯЦИЯ ЛАГРАНЖА
# ==========================
#
# Полином Лагранжа проходит через все заданные точки.
#
# Идея: полином строится как сумма y_i*L_i(x), где L_i - базисные полиномы.
#
# Формула в словах:
# P(x) = сумма y_i * L_i(x)
#
# Данные: x_points, y_points, x_star.
# Плюс: не нужно решать систему для коэффициентов.
# Минус: при большом числе точек полином может сильно колебаться.
#
# Короткий ответ: метод строит интерполяционный полином, который проходит через все узлы.
#
#
# 4.3. НАТУРАЛЬНЫЙ КУБИЧЕСКИЙ СПЛАЙН
# ==================================
#
# Кубический сплайн строит гладкую кусочную интерполяцию.
#
# Идея: на каждом промежутке строится свой кубический полином, а в узлах полиномы гладко соединяются.
#
# Форма:
# S_i(x) = a + b*(x-x_i) + c*(x-x_i)^2 + d*(x-x_i)^3
#
# Натуральный сплайн: вторая производная на концах равна нулю.
# Данные: x_points, y_points, x_star.
# Плюс: гладкий график и хорошее поведение.
# Минус: нужно находить коэффициенты.
#
# Короткий ответ: сплайн - это набор кубических полиномов на отдельных интервалах.
#
#
# 5.1. СТЕПЕННОЙ МЕТОД
# ====================
#
# Степенной метод ищет доминирующее собственное значение матрицы.
#
# Доминирующее значение - самое большое по модулю.
# Идея: многократно умножать вектор на A и нормировать его.
#
# Формулы:
# y = A*x
# x_next = y / norm(y)
# lambda = (A*x_next, x_next) / (x_next, x_next)
#
# Данные: A, x0, eps.
# Остановка: изменение lambda меньше eps или малая невязка.
# Плюс: простой метод.
# Минус: находит только доминирующее значение и может сходиться медленно.
#
# Короткий ответ: метод выделяет направление доминирующего собственного вектора.
#
#
# 5.2. ОБРАТНЫЙ СТЕПЕННОЙ МЕТОД СО СДВИГОМ
# ========================================
#
# Обратный степенной метод со сдвигом ищет собственное значение около sigma.
#
# Идея: вместо умножения на A решается система с A - sigma*I.
#
# Формула:
# (A - sigma*I)*y = x
#
# Данные: A, sigma, x0, eps.
# Остановка: изменение lambda меньше eps или малая невязка.
# Плюс: можно искать значение около заданного sigma.
# Минус: на каждом шаге нужно решать систему.
#
# Короткий ответ: сдвиг sigma направляет метод к ближайшему собственному значению.
#
#
# 5.3. QR-РАЗЛОЖЕНИЕ ЧЕРЕЗ ГРАММА-ШМИДТА
# ======================================
#
# QR-разложение представляет матрицу как A = Q*R.
#
# Q - матрица с ортонормированными столбцами.
# R - верхнетреугольная матрица.
#
# Идея Грамма-Шмидта: из каждого столбца убираются проекции на уже построенные ортонормированные столбцы, потом он нормируется.
#
# Данные: A.
# Проверка: A примерно равно Q*R, а Q^T*Q примерно равно I.
# Плюс: используется в QR-алгоритме.
# Минус: классический вариант может быть неустойчив для почти зависимых столбцов.
#
# Короткий ответ: QR-разложение делит матрицу на ортогональную и верхнетреугольную части.
#
#
# 5.4. QR-АЛГОРИТМ ДЛЯ СОБСТВЕННЫХ ЗНАЧЕНИЙ
# =========================================
#
# QR-алгоритм ищет собственные значения матрицы.
#
# Идея: много раз выполнять QR-разложение и менять порядок множителей.
#
# Формулы:
# A_k = Q_k*R_k
# A_next = R_k*Q_k
#
# Данные: A, число итераций.
# Результат: собственные значения приближенно лежат на диагонали итоговой матрицы.
# Плюс: можно найти сразу несколько собственных значений.
# Минус: может требовать много итераций.
#
# Короткий ответ: QR-алгоритм постепенно приводит матрицу к почти треугольной форме.
#
#
# 5.5. ПРИБЛИЖЕННОЕ РАЗЛОЖЕНИЕ ШУРА ЧЕРЕЗ QR-ИТЕРАЦИИ
# ===================================================
#
# Разложение Шура записывает матрицу как A = U*T*U^T.
#
# U - ортогональная матрица.
# T - верхнетреугольная или почти верхнетреугольная матрица.
# Диагональ T связана с собственными значениями.
#
# Идея: QR-итерации накапливают ортогональные преобразования и приводят A к T.
#
# Данные: A, число итераций.
# Плюс: удобно для анализа собственных значений.
# Минус: простая учебная реализация дает приближение.
#
# Короткий ответ: разложение Шура приводит матрицу к верхнетреугольному виду ортогональным преобразованием.
#
#
# 5.6. КРУГИ ГЕРШГОРИНА
# =====================
#
# Круги Гершгорина дают области, где находятся собственные значения.
#
# Для каждой строки строится круг:
# center = a_ii
# radius = сумма abs(a_ij), где j != i
#
# Главное утверждение: все собственные значения лежат в объединении этих кругов.
#
# Данные: A.
# Плюс: быстро дает оценку спектра.
# Минус: оценка может быть грубой.
#
# Короткий ответ: круги Гершгорина локализуют собственные значения по элементам матрицы.
#
#
# 5.7. НАИВНОЕ УМНОЖЕНИЕ МАТРИЦ И ШТРАССЕН
# ========================================
#
# Наивное умножение матриц считает каждый элемент как строка на столбец.
#
# Формула:
# C[i,j] = сумма A[i,k]*B[k,j]
#
# Сложность наивного метода: O(n^3).
#
# Метод Штрассена делит матрицы на блоки и уменьшает число умножений блоков с 8 до 7.
#
# Данные: A, B.
# Плюс Штрассена: лучше асимптотическая сложность для больших матриц.
# Минус: сложнее и может быть невыгоден для маленьких матриц.
#
# Короткий ответ: Штрассен ускоряет матричное умножение за счет уменьшения числа блочных умножений.
#
#
# 6.1. ЧИСЛЕННОЕ ДИФФЕРЕНЦИРОВАНИЕ
# ================================
#
# Численное дифференцирование приближенно находит производную по значениям функции.
#
# Формулы:
# forward = (f(x+h)-f(x))/h
# backward = (f(x)-f(x-h))/h
# central = (f(x+h)-f(x-h))/(2*h)
#
# Данные: f(x), x0, h.
# Плюс: не нужна аналитическая производная.
# Минус: точность зависит от h, есть ошибка округления.
#
# Короткий ответ: центральная разность обычно точнее, потому что использует значения функции с двух сторон.
#
#
# 6.2. МЕТОД ЭЙЛЕРА ДЛЯ ОДУ y' = f(t,y)
# =====================================
#
# Метод Эйлера решает задачу Коши y'=f(t,y), y(t0)=y0.
#
# Идея: решение продолжается по касательной в начале шага.
#
# Формулы:
# y_next = y + h*f(t,y)
# t_next = t + h
#
# Данные: f(t,y), t0, y0, t_end, h.
# Порядок точности: первый.
# Плюс: самый простой метод.
# Минус: низкая точность и возможная неустойчивость при большом h.
#
# Короткий ответ: метод Эйлера строит решение пошагово по производной в текущей точке.
#
#
# 6.3. ПРЕДИКТОР-КОРРЕКТОР ЭЙЛЕРА
# ===============================
#
# Предиктор-корректор Эйлера улучшает метод Эйлера.
#
# Идея: сначала сделать прогноз, потом исправить его средним наклоном.
#
# Формулы:
# y_pred = y + h*f(t,y)
# y_corr = y + h/2*(f(t,y) + f(t+h,y_pred))
#
# Данные: f(t,y), t0, y0, t_end, h.
# Плюс: точнее простого Эйлера.
# Минус: нужно больше вычислений функции.
#
# Короткий ответ: метод сначала прогнозирует значение, затем корректирует его.
#
#
# 6.4. МЕТОД РУНГЕ-КУТТЫ 4-ГО ПОРЯДКА
# ===================================
#
# Метод Рунге-Кутты 4 порядка решает задачу Коши для ОДУ.
#
# Идея: на каждом шаге считается четыре наклона, потом они усредняются.
#
# Формулы:
# k1 = h*f(t,y)
# k2 = h*f(t+h/2, y+k1/2)
# k3 = h*f(t+h/2, y+k2/2)
# k4 = h*f(t+h, y+k3)
# y_next = y + (k1+2*k2+2*k3+k4)/6
#
# Данные: f(t,y), t0, y0, t_end, h.
# Порядок точности: четвертый.
# Плюс: высокая точность.
# Минус: 4 вычисления функции на шаг.
#
# Короткий ответ: метод Рунге-Кутты 4 порядка использует четыре оценки наклона и дает высокую точность.
#
#
# 6.5. СИСТЕМА ОДУ: ЭЙЛЕР ИЛИ метод Рунге-Кутты 4 порядка
# ===============================
#
# Система ОДУ содержит несколько неизвестных функций.
#
# Идея: все неизвестные собираются в вектор u, а правая часть записывается как F(t,u).
#
# Для Эйлера:
# u_next = u + h*F(t,u)
#
# Для метод Рунге-Кутты 4 порядка формулы такие же, но k1, k2, k3, k4 являются векторами.
#
# Данные: F(t,u), u0, t0, t_end, h, method.
# Плюс: можно решать уравнения высокого порядка через систему первого порядка.
# Минус: нужно правильно переписать систему.
#
# Короткий ответ: систему ОДУ решают так же, как одно уравнение, но вместо числа используется вектор.
#
#
# 6.6. СРАВНЕНИЕ ЭЙЛЕРА И метод Рунге-Кутты 4 порядка
# ===========================
#
# Сравнение Эйлера и метод Рунге-Кутты 4 порядка показывает влияние метода на точность.
#
# Эйлер: первый порядок, одна оценка функции на шаг, простой, но менее точный.
# метод Рунге-Кутты 4 порядка: четвертый порядок, четыре оценки функции на шаг, обычно точнее.
#
# Что сравнивают:
# - конечное значение y
# - графики
# - разницу между ответами
#
# Плюс сравнения: видно, как выбор метода влияет на результат.
# Минус: без точного решения нельзя строго сказать точную ошибку, но можно оценить различие.
#
# Короткий ответ: при одинаковом шаге метод Рунге-Кутты 4 порядка обычно дает более точный результат, чем Эйлер.
#
#
# 6.7. АДАМС-БАШФОРТ 2 + АДАМС-МУЛТОН 2
# =====================================
#
# Методы Адамса являются многошаговыми методами.
#
# Идея: новое значение считается с использованием нескольких предыдущих шагов.
#
# Адамс-Башфорт - явный метод, часто предиктор.
# Адамс-Мултон - корректор.
#
# Формулы:
# y_pred = y_n + h/2*(3*f_n - f_prev)
# y_corr = y_n + h/2*(f(t_next,y_pred) + f_n)
#
# Данные: f(t,y), t0, y0, t_end, h.
# Плюс: использует информацию с п
# %%
# ## теория, часть 2
#
# рошлых шагов.
# Минус: нужны стартовые значения.
#
# Короткий ответ: Башфорт дает прогноз, Мултон исправляет прогноз.
#
#
# 7.1. ДИСКРЕТНОЕ ПРЕОБРАЗОВАНИЕ ФУРЬЕ - ДПФ
# ==========================================
#
# ДПФ переводит дискретный сигнал из временной области в частотную.
#
# Идея: сигнал раскладывается на гармоники.
# Коэффициент X[k] показывает вклад частоты k.
#
# Формула:
# X[k] = сумма x[n]*exp(-j*2*pi*k*n/N)
#
# Обратное ДПФ:
# x[n] = 1/N * сумма X[k]*exp(j*2*pi*k*n/N)
#
# Данные: signal, N.
# Что смотреть: abs(X[k]) - амплитуда частоты k.
# Плюс: показывает спектр.
# Минус: прямое ДПФ требует много операций.
#
# Короткий ответ: ДПФ показывает частотный состав дискретного сигнала.
#
#
# 7.2. БЫСТРОЕ ПРЕОБРАЗОВАНИЕ ФУРЬЕ - БПФ / FFT
# =============================================
#
# БПФ - быстрый алгоритм вычисления ДПФ.
#
# Идея: сигнал делится на четные и нечетные отсчеты, задача разбивается на меньшие задачи.
#
# Данные: signal, N. В простом варианте N должно быть степенью двойки.
# Плюс: быстрее прямого ДПФ.
# Минус: сложнее и требует подходящей длины сигнала.
#
# Короткий ответ: БПФ дает тот же спектральный смысл, что и ДПФ, но считает быстрее.
#
#
# 8.1. ЗАГОТОВКА ТЕКСТА ДЛЯ ОТВЕТА
# ================================
#
# Это не метод, а шаблон оформления ответа.
#
# В теории обычно нужно написать:
# - для чего метод
# - идея
# - формула
# - данные
# - остановка
# - плюс
# - минус
# - смысл результата
#
# Короткий ответ: теория должна быть короткой, но обязательно с формулой и смыслом метода.
# ЧИСЛЕННОЕ ИНТЕГРИРОВАНИЕ
#
# Численное интегрирование нужно, чтобы приближенно найти определенный интеграл.
# Геометрическая идея: интеграл - это площадь под графиком на отрезке [a,b].
#
# Шаг:
# h = (b-a)/N
#
# Метод прямоугольников заменяет площадь суммой прямоугольников.
# Метод трапеций заменяет площадь суммой трапеций.
# Метод Симпсона использует параболическое приближение.
#
# Плюс: можно считать интегралы без первообразной.
# Минус: ответ приближенный и зависит от шага.
#
# АНАЛИТИЧЕСКИЕ, ГРАФИЧЕСКИЕ И ЧИСЛЕННЫЕ МЕТОДЫ
#
# Аналитический метод дает формулу или точное решение.
# Плюс: точность и теоретический анализ.
# Минус: не всегда возможно.
#
# Графический метод дает приближенный ответ по графику.
# Плюс: наглядность.
# Минус: низкая точность.
#
# Численный метод дает число, таблицу или график через алгоритм.
# Плюс: можно решать задачи без точной формулы.
# Минус: есть погрешность и затраты времени.
#
#
# ДОПОЛНИТЕЛЬНЫЕ РАЗВЕРНУТЫЕ ОПРЕДЕЛЕНИЯ
#
# IEEE 754 и машинная точность
#
# Число с плавающей точкой хранится как знак, мантисса и порядок. В double используется 64 бита: 1 бит знака, 11 бит порядка и 52 бита мантиссы. Из-за конечной мантиссы большинство вещественных чисел хранится приближенно. Машинный eps показывает уровень относительной ошибки округления. Для double он примерно равен 2.22e-16.
#
# Overflow - это переполнение, когда результат слишком большой и становится inf. Underflow - это исчезновение порядка, когда результат слишком маленький и может стать нулем или денормализованным числом. В численных методах это важно, например, в степенном методе, где вектор надо нормировать.
#
# Потеря значимости
#
# Потеря значимости возникает при вычитании близких чисел. Старшие разряды сокращаются, и относительная ошибка результата резко растет. Это важно в численном дифференцировании, методе секущих и вычислении невязок. Поэтому шаг h нельзя брать бесконечно маленьким: ошибка формулы уменьшается, но ошибка округления растет.
#
# Сжимающее отображение
#
# Отображение g называется сжимающим, если существует число L<1, такое что abs(g(x)-g(y)) <= L*abs(x-y). В методе простых итераций это означает, что точки сближаются. Если g сжимающее, то существует единственная неподвижная точка, и итерации x_next=g(x) сходятся к ней. Для гладкой функции часто проверяют max abs(g_prime(x)) < 1.
#
# Локальная и глобальная ошибка ОДУ
#
# Локальная ошибка - ошибка одного шага, если предыдущее значение было точным. Глобальная ошибка - ошибка, накопленная к концу всего интервала. У метод Рунге-Кутты 4 порядка локальная ошибка порядка h^5, а глобальная порядка h^4. У Эйлера глобальная ошибка порядка h.
#
# Явные и неявные методы
#
# Явный метод напрямую выражает новое значение через старые данные. Примеры: явный Эйлер, метод Рунге-Кутты 4 порядка, Адамс-Башфорт. Неявный метод содержит новое значение внутри правой части, поэтому на каждом шаге надо решать уравнение. Примеры: неявный Эйлер, Адамс-Мултон. Неявные методы обычно устойчивее и полезны для жестких задач.
#
# Жесткие системы ОДУ
#
# Жесткая система имеет процессы с сильно разными масштабами времени. Явные методы для таких задач требуют очень маленького шага ради устойчивости. Поэтому для жестких задач часто применяют неявные методы.
#
# Интерполяция, экстраполяция, аппроксимация
#
# Интерполяция ищет значения внутри диапазона заданных узлов и строит функцию, проходящую через точки. Экстраполяция ищет значения вне диапазона узлов и обычно менее надежна. Аппроксимация не обязана проходить через все точки, а подбирает функцию, которая в среднем хорошо описывает данные. Для зашумленных данных аппроксимация часто лучше интерполяции.
#
# SVD и PCA
#
# SVD представляет матрицу как A=U*S*V^T. Сингулярные числа показывают важность компонент. В изображениях первые сингулярные компоненты сохраняют главные детали, а малые компоненты часто отвечают за шум. PCA ищет направления максимальной дисперсии данных и связан с собственными значениями ковариационной матрицы. На практике PCA часто считают через SVD.
#
# Разреженные матрицы
#
# Разреженная матрица содержит много нулей. Вместо хранения всех элементов хранят только ненулевые элементы и их позиции. Форматы: COO, CSR, CSC. Это экономит память и ускоряет умножение матрицы на вектор, если ненулевых элементов мало.
#
# Гессенбергова форма
#
# Верхне-гессенбергова матрица почти верхнетреугольная: ненулевые элементы могут быть на диагонали, выше диагонали и на первой поддиагонали. Ее используют перед QR-алгоритмом, чтобы ускорить итерации. Получают ее ортогональными преобразованиями, например отражениями Хаусхолдера.
#
# Фазовый портрет и хаос
#
# Фазовый портрет показывает траекторию системы в координатах переменных, например координата-скорость. Он помогает понять устойчивость, циклы и характер движения. Детерминированный хаос означает, что система описывается точными уравнениями, но очень чувствительна к начальным условиям.
#
# Частота дискретизации и Найквист
#
# Частота дискретизации fs - это число отсчетов в секунду. Максимальная частота, которую можно различить без искажения, равна fs/2. Это частота Найквиста. Если в сигнале есть частоты выше fs/2, возникает алиасинг: высокие частоты выглядят как ложные низкие.
#
#
# БАНК ВОПРОСОВ ИЗ ЗАГРУЖЕННОГО ФАЙЛА
#
# # 1. Решение нелинейных уравнений
#
# **В: Как метод бисекции гарантирует сходимость для непрерывных функций? Как это связано с теоремой о промежуточном значении? Сравните метод бисекции с методом секущих по скорости сходимости и устойчивости к ошибкам округления в арифметике с плавающей точкой.**
#
#
# Метод бисекции работает с отрезком [a, b], на концах которого функция имеет разные знаки (f(a) * f(b) < 0). По теореме о промежуточном значении (теорема Больцано-Коши): если f непрерывна на [a, b] и принимает на концах значения разных знаков, то на отрезке гарантированно есть точка, где f = 0. Метод делит отрезок пополам, оставляя ту половину, где сохраняется смена знака, поэтому корень всегда остается внутри и сходимость гарантирована.
#
# Сравнение с методом секущих: бисекция сходится линейно (длина отрезка сокращается вдвое за итерацию, q = 0.5), но абсолютно надежна. Метод секущих сходится быстрее (сверхлинейно, порядок около 1.618), но не гарантирует сходимость и может расходиться при неудачных начальных точках.
#
# Устойчивость к ошибкам округления: бисекция очень устойчива - она лишь сравнивает знаки и не усиливает погрешности; ошибка вычисления f(m) порядка eps_machine * |f(m)| пренебрежимо мала по сравнению с tol. Метод секущих вычисляет разности близких значений функции, что при сходимости приводит к потере значимости и большей чувствительности к округлению.
#
# ---
#
# **В: Какую стратегию в методе бисекции можно использовать для минимизации числа итераций? В чем разница между верными в строгом и широком смысле цифрами числа, как это связано с округлением и значащими цифрами?**
#
#
# Минимизация числа итераций: число шагов оценивается как n >= log2((b-a)/eps),
# то есть зависит только от длины начального отрезка. Поэтому стратегия -
# максимально сузить начальный отрезок, используя информацию о функции
# (анализ производной, монотонность, график), прежде чем запускать
# бисекцию.
#
# Верные цифры: в строгом (узком) смысле верными считаются разряды, в
# которых погрешность не превышает 0.5 единицы последнего разряда; в
# широком смысле - разряды, где погрешность не превышает единицу
# последнего разряда. Это напрямую связано с правилами округления:
# корректно округленное число имеет все цифры верными в строгом смысле.
# При tol = 10^(-5) гарантировано примерно 5 верных значащих цифр.
#
# ---
#
# ### В: Когда модифицированный метод Ньютона предпочтителен по сравнению с обычным методом Ньютона? Как ошибки в вычислении производной влияют на сходимость?
#
#
# Модифицированный метод Ньютона фиксирует производную (или якобиан) один раз в начальной точке x0 и переиспользует ее на всех итерациях:
# x_{n+1} = x_n - f(x_n)/f'(x_n).
# Он предпочтителен, когда вычисление производной дорого (например, большой якобиан для системы), когда производную трудно получить аналитически или когда решается много похожих задач.
#
# Цена - скорость: обычный метод Ньютона сходится квадратично, а модифицированный лишь линейно (множитель q = |1 - f'(x0)/f'(x*)|). Ошибки в вычислении производной не разрушают сходимость, пока приближение f'(x0) сохраняет правильный знак и разумную величину: они лишь замедляют ее (увеличивают q). Однако грубо неверная производная (особенно с неправильным знаком) может привести к расходимости.
#
# ---
#
# В: Как выбор начального приближения влияет на сходимость?
# Сравните метод Ньютона с методом бисекции по скорости сходимости и требованиям к функции. Приведите пример функции, где метод Ньютона может не сойтись.
#
#
# Метод Ньютона сходится только локально: при хорошем начальном приближении вблизи корня - квадратично, но при плохом старте может расходиться или уходить к другому корню. В задаче Ван-дер-Ваальса удачный старт - объем идеального газа V0 = RT/P.
#
# Сравнение с бисекцией. Скорость: Ньютон квадратичен, бисекция линейна (q = 0.5). Требования к функции: бисекции достаточно непрерывности и смены знака на концах; Ньютону нужна дифференцируемость и вычислимая производная f', а также невырожденность f'(x*) != 0. Надежность: бисекция сходится всегда, Ньютон - нет.
#
# Пример несходимости: для f(x) = x^(1/3) производная в корне бесконечна, и итерации Ньютона расходятся (удаляются от 0). Также метод зацикливается или уходит в бесконечность, если f'(x) ~ 0 рядом со стартовой точкой (горизонтальная касательная).
#
# ---
#
# ## 2. Системы нелинейных уравнений
#
# В: Каковы условия сходимости метода Ньютона для систем нелинейных уравнений? Сравните метод Ньютона для систем с методом Гаусса-Зейделя по вычислительной сложности и устойчивости к ошибкам округления.
#
#
# Условия сходимости метода Ньютона для систем: якобиан J(x*) в корне невырожден (det J != 0), функции достаточно гладкие, а начальное приближение лежит в окрестности корня. При этих условиях сходимость квадратичная.
#
# Вычислительная сложность: на каждой итерации Ньютон требует вычисления якобиана и решения линейной системы - O(n^3). Метод Гаусса-Зейделя (итерации вида x_{n+1} = g(x_n)) стоит O(n^2) на итерацию и не требует якобиана, но сходится лишь линейно и нуждается в диагональном преобладании или малой константе Липшица.
#
# Устойчивость к ошибкам округления: когда |det J| -> 0 (якобиан плохо обусловлен), решение приращения dx становится неточным и сходимость Ньютона ухудшается. Гаусс-Зейдель в этом смысле мягче, но компенсирует это медленной сходимостью.
#
# ---
#
# В: Сравните метод Гаусса-Зейделя с методом Ньютона по скорости сходимости и устойчивости к ошибкам округления. Как эти понятия связаны с ошибками вычислений с плавающей точкой?
#
#
# Скорость: метод Ньютона - квадратичная сходимость (число верных знаков примерно удваивается за итерацию); Гаусс-Зейдель - линейная сходимость (ошибка убывает в постоянное число раз). Ньютон делает меньше итераций, но каждая дороже.
#
# Устойчивость: устойчивость обоих методов падает, когда задача плохо обусловлена. Для Ньютона критичен плохо обусловленный якобиан. В арифметике с плавающей точкой (IEEE 754) это проявляется так: вычитание близких чисел при формировании невязки F(x) и решении системы вызывает потерю значимости, а накопление ошибок округления за итерации ограничивает достижимую точность - нет смысла задавать tol меньше уровня машинной погрешности.
#
# ---
#
# # 3. Собственные значения и степенной метод
#
# **В: Что такое спектральный радиус матрицы и как он связан с собственными значениями? Как зазор между собственными значениями влияет на сходимость? Как круги Гершгорина помогают оценить собственные значения?**
#
#
# Спектральный радиус rho(A) = max |lambda_i| - наибольший модуль среди всех собственных значений матрицы. Именно его находит степенной метод.
#
# Зазор и сходимость: степенной метод сходится со скоростью, пропорциональной |lambda_2/lambda_1|^k. Чем больше зазор между наибольшим и вторым по модулю собственными значениями, тем быстрее сходимость; при малом зазоре она очень медленная.
#
# Круги Гершгорина: каждое собственное значение лежит хотя бы в одном круге D_i = { z : |z - a_ii| <= R_i }, где R_i - сумма модулей внедиагональных элементов i-й строки. Круги дают быструю локализацию спектра без точного вычисления собственных значений и помогают оценить, например, диапазон rho(A).
#
# ---
#
# В: Как зазор между собственными значениями влияет на скорость сходимости? Как ошибки округления в арифметике с плавающей точкой (стандарт IEEE754) могут повлиять на точность вычислений? Как можно было бы минимизировать их влияние?
#
#
# Зазор и сходимость: скорость степенного метода (и QR со сдвигами) определяется отношением соседних собственных значений. Малый зазор -> медленная сходимость. Степенной метод со сдвигом заменяет A на (A - muI): подбор mu увеличивает относительный зазор у нужного собственного значения и ускоряет сходимость к нему.
#
# Ошибки округления (IEEE 754): число double занимает 64 бита (1 знак, 11 порядок, 52 мантисса), машинный эпсилон eps_machine ~ 2.22 * 10^(-16). При малом зазоре ошибки округления могут имитировать ложную сходимость или мешать различить близкие собственные значения.
#
# Минимизация влияния: нормировать вектор на каждой итерации (предотвращает переполнение/исчезновение порядка), использовать отношение Релея для оценки lambda, применять сдвиги и не задавать допуск ниже уровня машинной точности.
#
# ---
#
# В: Напишите функцию, которая находит собственные векторы методом вращений. Проверьте сходимость результата с точностью xi = 0.001. (теоретический аспект: применимость метода)
#
#
# Метод вращений Якоби последовательно обнуляет внедиагональные элементы ортогональными поворотами и применяется к симметричным матрицам, для которых он гарантированно сходится к диагональной форме, давая собственные значения на диагонали и собственные векторы в произведении поворотов.
#
# Если матрица несимметрична, классический метод Якоби неприменим. В этом случае собственные значения находят через характеристический многочлен (для малых матриц) либо степенным методом с дефляцией: находят наибольшее собственное значение и вектор, затем "вычитают" найденную компоненту A - lambda * v * v^T и повторяют. Контроль сходимости - по малости изменения вектора/значения между итерациями (< xi).
#
# ---
#
# # 4. SVD и анализ данных
#
# **В: Как вычисление SVD используется для анализа изображений? Как это связано с собственными значениями? Объясните связь с вычислением собственных значений матрицы ковариации.**
#
#
# Сингулярное разложение представляет матрицу как A = U * Sigma * V^T, где U и V ортогональны, а Sigma - диагональ неотрицательных сингулярных чисел sigma_1 >= sigma_2 >= ... . При анализе изображений матрицу пикселей приближают суммой нескольких первых сингулярных компонент (низкоранговое приближение): это сжимает изображение, сохраняя главные детали и отбрасывая мелкие, отвечающие малым sigma.
#
# Связь с собственными значениями: сингулярные числа A - это корни из собственных значений матрицы A^T A (или A A^T), а правые/левые сингулярные векторы - собственные векторы этих матриц.
#
# Связь с ковариацией (PCA): если данные центрированы, матрица ковариации пропорциональна A^T A. Ее собственные векторы - главные компоненты (направления максимальной дисперсии), а собственные значения - величины дисперсии вдоль них. Поэтому SVD матрицы данных напрямую дает результат метода главных компонент.
#
# ---
#
# # 5. Умножение матриц: алгоритм Штрассена
#
# **В:** Как алгоритм Штрассена оптимизирует умножение матриц? Как это влияет на асимптотическую сложность в нотации big-O? Объясните, как архитектура памяти влияет на производительность матричных операций.
#
#
# Наивное умножение матриц n*n требует n3 скалярных умножений - сложность O(n3). Штрассен разбивает каждую матрицу на 4 блока и вычисляет произведение через 7 рекурсивных умножений блоков вместо 8 (ценой большего числа сложений).
#
# Влияние на сложность: рекуррента дает O(n^(log_2 7)) ~ O(n^(2.807)), что
# асимптотически лучше O(n^3). Для n = 2^k это 7^k умножений вместо 8^k; экономия
# (7/8)^k растет с размером (для n = 16 - около 41 %).
#
# Архитектура памяти: реальная производительность зависит не только от
# числа операций. Наивное блочное умножение кэш-дружелюбно
# (последовательный доступ к данным). Штрассен создает много временных
# матриц (суммы/разности блоков), что увеличивает обращения к памяти и
# кэш-промахи. Поэтому на малых матрицах он проигрывает, а выигрыш
# проявляется только при больших n (примерно n >= 100).
#
#
# В: Какова точность центральной разности для аппроксимации второй производной? Как ошибка зависит от шага h? Сравните центральную разность с прямой разностью по точности и вычислительным затратам.
#
#
# Вторая производная аппроксимируется формулой f''(x) ~ (f(x+h) - 2f(x) + f(x-h))/h^2 с погрешностью O(h^2): уменьшение h в 10 раз снижает ошибку усечения примерно в 100
# %%
# ## теория, часть 3
#
# раз. Прямая (односторонняя) аппроксимация второй производной имеет лишь порядок O(h).
#
# Затраты: центральная формула использует три значения функции (x-h, x, x+h) и симметрична, за счет чего члены нечетного порядка в разложении Тейлора сокращаются - отсюда более высокий порядок точности. При очень малом h (h < sqrteps_machine ~ 10^(-8)) деление на h^2 усиливает ошибку округления; оптимум для второй производной примерно h ~ eps_machine^(1/4) ~ 10^(-4).
#
# ---
#
# # 7. Интерполяция
#
# **В: Объясните, как многочлен Лагранжа обеспечивает точное прохождение через заданные точки. Как степень полинома влияет на точность интерполяции для зашумленных данных?**
#
#
# Полином Лагранжа строится из базисных многочленов L_i(x), каждый из которых равен 1 в своем узле x_i и 0 во всех остальных узлах (свойство L_i(x_j) = delta_ij). Поэтому сумма sum y_i * L_i(x) в каждом узле x_i дает ровно y_i - полином проходит точно через все заданные точки. По n точкам строится единственный полином степени n-1.
#
# Влияние степени на зашумленных данных: высокая степень (много узлов) приводит к феномену Рунге - сильным осцилляциям полинома, особенно у концов отрезка. Если данные содержат шум, интерполяция, проходящая точно через все точки, повторяет и усиливает шум. Для зашумленных данных лучше использовать сглаживающие методы (метод наименьших квадратов) или интерполяцию сплайнами невысокой степени, а не полином высокой степени.
#
# ---
#
# # 8. Решение обыкновенных дифференциальных уравнений
#
# **В: Что такое локальная и глобальная ошибки в численных методах для ОДУ? Как порядок точности метода влияет на эти ошибки? Объясните понятие устойчивости численных методов для ОДУ.**
#
#
# Локальная ошибка (ошибка усечения на шаге) - погрешность за один шаг при условии, что предыдущее значение точное. Глобальная ошибка - накопленная погрешность к концу отрезка интегрирования. Обычно глобальная ошибка на один порядок ниже локальной: у метод Рунге-Кутты 4 порядка локальная O(h^5), глобальная O(h^4).
#
# Порядок точности p означает, что глобальная ошибка ведет себя как O(h^p):
# чем выше порядок, тем быстрее падает ошибка при уменьшении шага. Метод
# Эйлера имеет порядок 1, Хойн (предиктор-корректор) - 2, метод Рунге-Кутты 4 порядка - 4.
# Устойчивость: метод устойчив, если ошибки (округления и начальные
# возмущения) не нарастают неограниченно при интегрировании. Для тестового
# уравнения y' = lambday у каждого явного метода есть область устойчивости по h:
# например, явный Эйлер устойчив при |1 + hlambda| <= 1, метод Рунге-Кутты 4 порядка - при |hlambda| <= 2.785. Для
# жестких задач явные методы требуют слишком малого шага, и выгоднее
# неявные методы.
#
# ---
#
# В: Как геометрическая интерпретация метода Рунге-Кутта 4-го порядка помогает понять его работу? Как локальная ошибка усечения влияет на глобальную ошибку? Сравните метод предиктора-корректора Эйлера с методом Рунге-Кутты по точности и вычислительным затратам.
#
#
# Геометрическая интерпретация метод Рунге-Кутты 4 порядка: коэффициенты k1...k4 - это наклоны решения в начале шага (k1), дважды в середине (k2, k3) и в конце (k4).
# Итоговое приращение - взвешенное среднее этих наклонов с весами (1/6, 2/6, 2/6, 1/6), где средние отдан больший вес. Такое усреднение "угадывает" кривизну решения на шаге и дает высокую точность.
#
# Связь локальной и глобальной ошибок: за один шаг метод Рунге-Кутты 4 порядка вносит локальную ошибку O(h^5); при прохождении около 1/h шагов эти ошибки накапливаются, давая глобальную ошибку O(h^4).
#
# Сравнение: предиктор-корректор Эйлера (Хойн) использует 2 вычисления f на шаг и имеет глобальный порядок O(h^2); метод Рунге-Кутты 4 порядка использует 4 вычисления f на шаг и порядок O(h^4). метод Рунге-Кутты 4 порядка вдвое дороже по числу вызовов f, но при той же точности позволяет брать гораздо больший шаг, поэтому обычно эффективнее для гладких задач.
#
# ---
#
# В: Сравните метод Адамса-Мултона с методом Рунге-Кутты 4-го порядка (порядок сходимости, вычислительная сложность на шаг, устойчивость, применимость к жестким системам ОДУ).
# Объясните, в каких сценариях метод Адамса-Мултона предпочтительнее, а в каких - метод Рунге-Кутты.
#
# Порядок: оба метода можно сделать 4-го порядка (О(h^4)). Вычислительная сложность: метод Рунге-Кутты 4 порядка требует 4 вычисления правой части f на каждом шаге; многошаговый Адамс-Мултон (в схеме РЕСЕ) - обычно лишь 1-2, переиспользуя значения f с предыдущих шагов, что экономично при дорогой f.
# Устойчивость и жесткость: неявный метод Адамса-Мултона имеет лучшие свойства устойчивости и пригоден для жестких систем, тогда как явный метод Рунге-Кутты 4 порядка на жестких задачах требует чрезмерно малого шага.
#
# Особенности: Адамс-Мултон - многошаговый, ему нужна стартовая инициализация первых точек (например, через метод Рунге-Кутты 4 порядка) и сложнее менять шаг/порядок. метод Рунге-Кутты 4 порядка самостартующий и удобен для адаптивного шага.
#
# Когда что выбирать: Адамс-Мултон - при дорогом вычислении f и для жестких систем (экономит вызовы f, устойчив); метод Рунге-Кутты 4 порядка - для нежестких задач, при адаптивном шаге и когда важна простота реализации.
#
# ---
#
# В: Как фазовые портреты помогают в анализе систем дифференциальных уравнений? Может ли адаптивный шаг улучшить точность решения ОДУ? Как это связано с устойчивостью и ошибками округления?
#
#
# Фазовые портреты: строятся в координатах фазовых переменных (например, (y, y') или (y_1, y_2)) и показывают траектории системы без явного решения во времени. По ним видно тип особых точек (узел, фокус, седло, центр), устойчивость равновесий, наличие предельных циклов - то есть качественное поведение системы.
#
# Адаптивный шаг: оценивает локальную ошибку и уменьшает h там, где
# решение быстро меняется (жесткие участки), и увеличивает там, где оно
# гладкое. Это улучшает точность при той же вычислительной стоимости и
# помогает удерживаться в области устойчивости метода.
#
# Связь с устойчивостью и округлением: слишком крупный шаг может вывести
# метод за границу устойчивости (ошибки нарастают), а слишком мелкий -
# увеличить число шагов и накопление ошибок округления. Адаптивный шаг
# ищет баланс между ошибкой усечения и ошибкой округления.
#
# ---
#
# В: Как ДПФ помогает выделить частотные компоненты сигнала?
# Как наличие шума влияет на спектр? Как можно уменьшить влияние шума? Объясните, как быстрое преобразование Фурье (БПФ) уменьшает вычислительную сложность по сравнению с обычным ДПФ. В чем принцип работы БПФ?
#
#
# Выделение частот: ДПФ переводит сигнал из временной области в частотную по формуле X[k] = sum x[n] * e^(-2pi i kn/N). Каждый коэффициент X[k] показывает вклад гармоники с частотой k; пики амплитудного спектра соответствуют доминирующим частотам сигнала.
#
# Влияние шума: случайный шум распределяется по всему спектру, поднимая "фон" на всех частотах. Полезные компоненты остаются заметными пиками над этим фоном, но слабые гармоники могут в нем потеряться.
#
# Уменьшение шума: фильтрация в частотной области - обнуление/ослабление коэффициентов вне интересующих частот (порог по амплитуде, низкочастотный фильтр), затем обратное преобразование. Также помогает усреднение нескольких реализаций и увеличение длины выборки N.
#
# БПФ: прямое ДПФ через матрицу стоит O(N^2). Быстрое преобразование Фурье (алгоритм Кули-Тьюки) рекурсивно разбивает ДПФ длины N на два ДПФ длины N/2 (по четным и нечетным отсчетам) и комбинирует их за O(N), что в сумме дает O(N log N). Принцип - "разделяй и властвуй" с переиспользованием общих множителей-поворотов.
#
# ---
#
# В: Как частота дискретизации влияет на разрешение частотного спектра в ДПФ? Как шум может повлиять на точность выделения частот? Объясните, как быстрое преобразование Фурье (БПФ) уменьшает вычислительную сложность по сравнению с ДПФ. Как это связано с разбиением сигнала?
#
#
# Частота дискретизации и разрешение: число отсчетов N и частота дискретизации определяют частотное разрешение Deltaf = f_s / N (или 1/N в нормированных единицах). Чем больше точек на интервале, тем мельче шаг по частоте и тем лучше разделяются близкие частоты; при малом N соседние компоненты сливаются. Частота дискретизации также ограничивает максимальную различимую частоту (критерий Найквиста).
#
# Влияние шума: шум поднимает уровень фона спектра, снижая контраст пиков; слабые частоты становятся труднее отличить от шумового фона, что снижает точность их выделения.
#
# БПФ и разбиение сигнала: прямое ДПФ - O(N^2). БПФ рекурсивно делит сигнал на четные и нечетные отсчеты, вычисляет два ДПФ половинной длины и объединяет их за линейное время. Это "разбиение сигнала" и переиспользование промежуточных результатов снижает сложность до O(N log N).
#
# ---
#
# ### В: Как ДПФ преобразует сигнал в частотную область? Почему это важно для анализа периодических сигналов?
#
#
# ДПФ раскладывает дискретный сигнал по базису комплексных экспонент (гармоник) разных частот:
# X[k] = Sigma x[n] * e^(-2pi i k n / N)
# Коэффициент X[k] - это амплитуда и фаза гармоники с частотой, соответствующей индексу k. Так сигнал, заданный значениями во времени, представляется набором частотных компонент.
#
# Важность для периодических сигналов: периодический сигнал состоит из суммы гармоник, и ДПФ прямо выявляет их - позволяет определить доминирующие частоты, амплитуды и периоды компонент, обнаружить скрытую периодичность (в том числе сезонность во временных рядах) и выполнить фильтрацию шума в частотной области.
#
# ---
#
# Сравнение с QR-разложением. Применимость: QR раскладывает матрицу как произведение ортогональной Q и треугольной R и существует для любой матрицы (это разовая факторизация); разложение Шура раскрывает спектр и для общей матрицы получается только итерационно. Сложность: одно QR-разложение стоит O(n^3); разложение Шура вычисляют QR-алгоритмом - итерационным процессом из многих QR-шагов (с предварительным приведением к гессенберговой форме), что в сумме дороже.
#
# Приложения: вычисление собственных значений (QR-алгоритм опирается на форму Шура), решение матричных уравнений (например, уравнения Сильвестра и Ляпунова), вычисление функций от матриц и анализ устойчивости динамических систем.
#
# ---
#
# # 1. Числа с плавающей точкой и стандарт IEEE 754
#
# **В: Как устроено представление чисел с плавающей точкой (стандарт IEEE 754) и какие ошибки представления при этом возникают?**
#
# Число с плавающей точкой хранится в виде знака * мантисса * 2^(порядок). По стандарту IEEE 754 одинарная точность (float) занимает 32 бита (1 знак, 8 порядок, 23 мантисса), двойная (double) - 64 бита (1 знак, 11 порядок, 52 мантисса). Мантисса нормализована (старший бит подразумевается единичным).
#
# Ошибки представления: множество представимых чисел конечно и неравномерно (плотность убывает с ростом величины). Большинство вещественных чисел (даже простые десятичные дроби вроде 0.1) не представимы точно и округляются к ближайшему представимому.
# Относительная ошибка такого округления ограничена машинным эпсилоном: для double eps_machine ~ 2.22 * 10^(-16). Стандарт также определяет специальные значения: +/-0, +/-infinity и NaN.
#
# ---
#
# # B: Что такое переполнение (overflow) и потеря порядка (underflow)?
#
# Overflow (переполнение) возникает, когда результат по модулю превышает максимально представимое число (для double порядка 1.8*1030⁸): результат становится +/-infinity. Underflow (исчезновение порядка) - когда результат по модулю меньше наименьшего нормализованного числа (около 2.2*10⁻30⁸): он представляется денормализованным числом со сниженной точностью или округляется до нуля.
#
# Практическое следствие: промежуточные вычисления нужно масштабировать (например, нормировать векторы в степенном методе, использовать логарифмы при перемножении многих малых вероятностей), чтобы избежать выхода за диапазон.
#
# ---
#
# В: Что такое накопление ошибок округления и потеря значимости?
# Как суммирование по Кахану помогает с этим бороться?
#
# Накопление ошибок округления: каждая операция вносит малую погрешность;
# при большом числе операций (длинные суммы, итерации) эти погрешности
# складываются и могут существенно исказить результат.
#
# Потеря значимости (катастрофическое сокращение): при вычитании двух
# близких чисел старшие совпадающие разряды сокращаются, и в результате
# остаются в основном ошибочные младшие разряды - относительная
# погрешность резко возрастает. Поэтому опасны формулы вида f(x+h) - f(x)
# при малом h.
#
# Суммирование по Кахану: при последовательном сложении хранится
# дополнительная переменная-компенсация с, в которую накапливается
# потерянная при округлении часть каждой суммы и которая возвращается на следующем шаге. В результате итоговая ошибка почти не зависит от числа слагаемых (вместо роста ~ O(n) * eps она остается ~ O(1) * eps), что важно при суммировании длинных рядов и скалярных произведений.
# # В: Что такое интерполяция сплайнами и метод кубической сплайн-интерполяции?
#
# Сплайн - кусочно-полиномиальная функция: на каждом отрезке между узлами свой полином невысокой степени, а в узлах полиномы "сшиваются" с условиями гладкости. Это сочетает достоинства локальной интерполяции (нет осцилляций Рунге) с гладкостью.
#
# Кубический сплайн: на каждом отрезке полином 3-й степени; в узлах совпадают значения, первые и вторые производные, что дает непрерывную кривизну (гладкую кривую без изломов). Коэффициенты находятся из системы уравнений (обычно трехдиагональной, решаемой за O(n)) с краевыми условиями (например, естественный сплайн: вторые производные на концах равны нулю). Кубические сплайны широко используются для гладкой интерполяции и в компьютерной графике.
#
# ---
#
# # 6. Линейная алгебра: операции, QR, Хаусхолдер, гессенберг
#
# **В: Каковы основные операции вычислительной линейной алгебры и их сложность?**
#
# Базовые операции и их типичная сложность для плотных матриц n * n:
# скалярное произведение и сложение векторов - O(n); умножение матрицы на вектор - O(n^2); умножение матриц - O(n^3) наивно (или O(n^(2.807)) по Штрассену); решение системы методом Гаусса и LU-разложение - O(n^3);
# QR-разложение - O(n^3). На этих операциях строятся методы решения систем, наименьших квадратов и поиска собственных значений.
#
# ---
#
# ### В: Что такое отражения Хаусхолдера и как они используются?
#
# Отражение Хаусхолдера - ортогональное преобразование H = I - 2 * v * v^T / (v^T v),
# которое зеркально отражает векторы относительно гиперплоскости с
# нормалью v. Подбором v можно одним отражением обнулить все элементы
# вектора ниже выбранного.
#
# Применение: построение QR-разложения (последовательное обнуление
# поддиагональных элементов столбцов) и приведение матрицы к
# верхне-гессенберговой форме. Поскольку преобразования ортогональны, они
# численно устойчивы (не увеличивают нормы и ошибки).
#
# ---
#
# В: Что такое верхне-гессенбергова форма и как к ней приводят матрицу?
#
# Верхне-гессенбергова матрица - почти треугольная: ненулевые элементы только на и выше первой поддиагонали (один поддиагональный ряд).
# Произвольную матрицу приводят к этой форме ортогональными отражениями Хаусхолдера (преобразованием подобия, сохраняющим собственные значения).
#
# Зачем: гессенбергова форма - подготовительный шаг QR-алгоритма; на ней каждая итерация QR стоит O(n^2) вместо O(n^3). Для симметричной матрицы гессенбергова форма становится трехдиагональной.
#
# ---
#
# ### B: Как работает QR-алгоритм, какова его сходимость и сложность?
#
# QR-алгоритм ищет собственные значения: на каждом шаге раскладывается A_k = Q_k * R_k и формирует A_{k+1} = R_k * Q_k (преобразование подобия).
# Последовательность сходится к (квази)треугольной форме Шура, на диагонали которой - собственные значения.
#
# Сходимость и сложность: скорость зависит от зазоров между собственными значениями; ее ускоряют сдвигами (QR со сдвигами). С предварительным приведением к гессенберговой форме одна итерация стоит O(n^2), а всего метод обычно дешев на практике. Это стандартный алгоритм вычисления.
#
# ---
#
# # 7. SVD, PCA и PageRank
#
# **В: Что такое метод главных компонент (PCA) и как он связан с поиском сингулярных значений?**
#
# PCA ищет ортогональные направления (главные компоненты), вдоль которых дисперсия центрированных данных максимальна. Эти направления - собственные векторы ковариационной матрицы, а величина дисперсии вдоль них - собственные значения.
#
# Связь с SVD: для центрированной матрицы данных X главные компоненты совпадают с правыми сингулярными векторами X, а сингулярные числа задают разброс вдоль них (sigma_1^2 пропорциональны дисперсиям). Поэтому PCA на практике вычисляют через SVD - это устойчившее, чем явно строить ковариацию. Прикладные аспекты: понижение размерности, сжатие, шумоподавление, визуализация многомерных данных.
#
# ---
#
# В: В чем состоит задача Google PageRank и как она сводится к собственному вектору?
#
# PageRank оценивает "важность" веб-страниц по структуре ссылок. Модель - случайный серфер, который с некоторой вероятностью переходит по ссылкам, а с малой вероятностью (демпфирование) прыгает на случайную страницу. Это задает стохастическую матрицу переходов.
#
# Ранги страниц - это стационарное распределение цепи Маркова, то есть собственный вектор матрицы переходов, отвечающий наибольшему собственному значению lambda = 1. Его находят степенным методом, который эффективен для огромных разреженных матриц веба (умножение разреженной матрицы на вектор дешево).
#
# ---
#
# # 8. Плотные и разреженные матрицы
#
# **В: Чем плотные матрицы отличаются от разреженных и какие есть способы хранения разреженных матриц?**
#
# Плотная матрица хранит все n^2 элементов. Разреженная содержит в основном нули, и хранить их явно расточительно - запоминают только ненулевые элементы и их позиции.
#
# Форматы хранения: COO (coordinate) - тройки (строка, столбец, значение); CSR (compressed sparse row) - массив значений, их столбцовых индексов и указателей на начала строк; CSC - то же по столбцам; DIA/LIL и другие для специфичных структур. Выигрыш: память O(nnz) вместо O(n^2), а умножение разреженной матрицы на вектор - O(nnz) вместо O(n^2), где nnz - число ненулевых элементов. В курсе для этого используется модуль scipy.sparse.
#
# ---
#
# # 9. Численное дифференцирование и интегрирование
#
# **В: Чем различаются методы прямой, обратной и центральной разности?**
#
# Прямая (правая) разность:
# f'(x) ~ (f(x+h) - f(x))/h
# - использует точку справа, порядок точности O(h). Обратная (левая) разность:
# f'(x) ~ (f(x) - f(x-h))/h
# - использует точку слева, тоже O(h). Центральная разность:
# f'(x) ~ (f(x+h) - f(x-h))/(2h)
# - симметрична, порядок O(h^2).
#
# Центральная точнее при том же h (симметрия сокращает члены нечетного порядка в разложении Тейлора), но требует значений по
# %%
# ## теория, часть 4
#
# обе стороны от точки. Прямая/обратная незаменимы у границ области, где сосед есть только с одной стороны.
#
# ---
#
# В: Что такое сетка дифференцирования, ошибка сокращения (усечения) и ошибка округления? Как они определяют оптимальный шаг?
#
# Сетка дифференцирования - набор дискретных точек (узлов) с шагом h, в которых вычисляется функция для приближения производных. Качество аппроксимации зависит от h.
#
# Ошибка усечения (сокращения) - погрешность самой разностной формулы; она убывает при уменьшении h (например, как O(h) или O(h^2)). Ошибка округления, наоборот, растет при малом h, потому что в формулах делится разность близких чисел на малое h. Суммарная ошибка имеет минимум при некотором оптимальном h: для центральной разности первой производной h ~ eps_machine^(1/2), для второй - около eps_machine^(1/4).
#
# ---
#
# В: В чем состоит правило Симпсона для численного интегрирования и какова его точность?
#
# Правило Симпсона приближает интеграл, заменяя подынтегральную функцию на каждом сдвоенном отрезке параболой (по трем точкам). Формула:
# ∫ f(x) dx ~ (h/3) * (f_0 + 4f_1 + 2f_2 + 4f_3 + ... + f_n),
# где коэффициенты чередуются 4 и 2.
# Точность: правило Симпсона имеет погрешность O(h^4) и точно интегрирует многочлены до 3-й степени включительно - это заметно точнее метода трапеций (O(h^2)) при той же сетке. Требует четного числа интервалов.
#
# ---
#
# # 10. Типы и свойства методов для ОДУ
#
# **В: Какие бывают типы ОДУ и постановок задач для них?**
#
# ОДУ классифицируют по порядку (высший порядок производной), по линейности (линейные/нелинейные), по числу уравнений (одно уравнение или система). По типу условий различают задачу Коши (начальные условия в одной точке) и краевую задачу (условия на концах отрезка). Отдельно выделяют жесткие системы, где сосуществуют процессы с очень разными масштабами времени.
#
# ---
#
# В: В чем разница между явными и неявными методами? Что такое многошаговые методы?
#
# Явный метод выражает новое значение y_{n+1} непосредственно через уже известные величины (например, явный Эйлер, метод Рунге-Кутты 4 порядка) - прост в реализации, но имеет ограниченную область устойчивости. Неявный метод задает y_{n+1} уравнением, где оно входит и в правую часть (например, неявный Эйлер, методы Адамса-Мултона), поэтому на каждом шаге решается уравнение, зато устойчивость гораздо лучше - это важно для жестких задач.
#
# Одношаговые методы используют только предыдущую точку (Эйлер, метод Рунге-Кутты 4 порядка). Многошаговые (Адамса-Башфорта - явные, Адамса-Мултона - неявные) используют несколько предыдущих точек, переиспользуя ранее вычисленные значения правой части. Это экономит вызовы f, но требует стартовой инициализации первых точек (например, методом Рунге-Кутты).
#
# ---
#
# ### В: Зачем нужен адаптивный шаг при решении ОДУ?
#
# Адаптивный шаг автоматически подбирает h по оценке локальной ошибки: уменьшает его на участках быстрого изменения решения и увеличивает там, где решение гладкое. Это обеспечивает заданную точность при минимальном числе шагов и помогает удерживать метод в области устойчивости. Оценку ошибки получают, например, сравнением шагов разного порядка (вложенные методы Рунге-Кутты-Фельберга).
#
# ---
#
# # 11. Согласованность, устойчивость, сходимость
#
# **В: Что такое согласованность, устойчивость и сходимость численного метода и как они связаны?**
#
# Согласованность (consistency): разностная схема при h -> 0 аппроксимирует исходное дифференциальное уравнение, то есть локальная ошибка усечения стремится к нулю. Устойчивость (stability): малые возмущения (ошибки округления, начальные ошибки) не нарастают неограниченно в ходе вычислений. Сходимость (convergence): численное решение при h -> 0 стремится к точному.
#
# Связь выражает теорема Лакса (для корректно поставленных линейных задач): согласованность + устойчивость <-> сходимость. То есть одной аппроксимации недостаточно - без устойчивости метод может расходиться, даже будучи согласованным.
#
# ---
#
# ### В: Чем строгая устойчивость отличается от нестрогой (слабой)?
#
# Для тестового уравнения y' = lambday метод порождает множитель роста на шаг.
# Строгая (абсолютная) устойчивость означает, что в заданной области параметра h * lambda ошибки строго затухают (множитель по модулю < 1) - это нужно, например, для жестких задач. Нестрогая (слабая) устойчивость допускает, что возмущения не нарастают, но и не затухают (множитель по модулю = 1), например осцилляции с постоянной амплитудой: метод не "взрывается", но и не подавляет ошибки. Различение важно при выборе шага и метода для конкретной задачи.
#
# ---
#
# # 12. Детерминированный хаос и динамические системы
#
# ## В: Что такое детерминированный хаос, бифуркация и странные аттракторы?
#
# Детерминированный хаос - поведение полностью детерминированной нелинейной системы, при котором траектории крайне чувствительны к начальным условиям: сколь угодно близкие старты со временем расходятся экспоненциально. Поэтому долгосрочный прогноз практически невозможен, хотя уравнения не содержат случайности.
#
# Бифуркация - качественное изменение поведения системы при плавном изменении параметра (например, удвоение периода, появление новых устойчивых состояний). Каскад бифуркаций удвоения периода - типичный путь к хаосу (логистическое отображение).
#
# Странный аттрактор - множество в фазовом пространстве, к которому притягиваются траектории хаотической системы; оно имеет сложную (фрактальную) структуру и не сводится к точке или циклу (классический пример - аттрактор Лоренца). Численное моделирование таких систем особенно чувствительно к ошибкам округления.
#
# ---
#
# В: Что такое фильтрация сигналов и как преобразование Фурье применяют для анализа сезонности во временных рядах?
#
# Фильтрация - подавление нежелательных частотных компонент. В частотной области это делается просто: вычисляют спектр (ДПФ/БПФ), ослабляют или обнуляют ненужные частоты (низкочастотный фильтр убирает высокочастотный шум, высокочастотный - медленный тренд, полосовой - оставляет нужный диапазон), затем выполняют обратное преобразование.
# Анализ сезонности: временной ряд (продажи, температура и т. п.) раскладывают по частотам. Пики амплитудного спектра на определенных частотах соответствуют периодическим (сезонным) компонентам: например, пик на частоте 1/12 для месячных данных указывает на годовую сезонность.
# Так преобразование Фурье выявляет скрытые периодичности и их периоды.
#
# ---
#
# # 13. Сигналы: характеристики и спектры
#
# **В: Дайте определения основных характеристик сигнала: амплитуда, период, частота, длина волны, частота дискретизации, герц, угловая частота, фаза.**
#
# Амплитуда - максимальное отклонение сигнала от среднего значения.
# Период T - время одного полного колебания. Частота f = 1/T - число колебаний в секунду, измеряется в герцах (Гц). Угловая частота omega = 2pif - скорость изменения фазы в радианах в секунду. Фаза - сдвиг колебания во времени (аргумент синуса/косинуса), задает "положение" волны в начальный момент.
#
# Длина волны lambda - пространственный период (расстояние между соседними одинаковыми фазами), связана со скоростью распространения:
# lambda = v/f.
# Частота дискретизации f_s - число отсчетов в секунду при оцифровке сигнала; по теореме Котельникова-Найквиста она должна быть не меньше удвоенной максимальной частоты сигнала, иначе возникает наложение (алиасинг).
#
# ---
#
# В: Чем амплитудный спектр отличается от частотного (фазового) спектра?
#
# Преобразование Фурье дает для каждой частоты комплексный коэффициент.
# Амплитудный спектр - зависимость модуля этого коэффициента от частоты:
# он показывает, насколько сильно представлена каждая частота (высота пиков = вклад гармоник).
# Фазовый (частотный) спектр - зависимость аргумента (фазы) коэффициента от частоты: он описывает временной сдвиг каждой гармоники.
#
# Для анализа "какие частоты есть в сигнале" обычно используют амплитудный спектр; фазовый важен при восстановлении сигнала и анализе его формы.
# В: Как алгоритм Штрассена уменьшает количество умножений по сравнению с наивным алгоритмом? Как это влияет на асимптотическую сложность? Опишите, как архитектура памяти влияет на производительность алгоритма Штрассена. В каких приложениях он используется?
#
#
# Сокращение умножений: на каждом уровне рекурсии Штрассен заменяет 8 блочных умножений на 7. Для матриц размера 2^k * 2^k это дает 7^k скалярных умножений против 8^k = n^3 у наивного метода. Асимптотика улучшается до O(n^(2.807)).
#
# Архитектура памяти: множество промежуточных матриц повышает потребление памяти и число кэш-промахов, поэтому на практике Штрассена применяют рекурсивно лишь до некоторого порога размера, а ниже переключаются на наивное (кэш-эффективное) умножение.
#
# Приложения: умножение очень больших плотных матриц в научных вычислениях и линейной алгебре, где асимптотический выигрыш перевешивает накладные расходы; идея "меньше умножений за счет сложений" лежит в основе и более поздних быстрых алгоритмов.
#
# ---
#
# В: Объясните, как архитектура памяти влияет на производительность алгоритмов умножения матриц. Как суммирование по Кахану может улучшить точность?
#
#
# Архитектура памяти: процессор работает с данными через иерархию кэшей.
# Алгоритмы, которые обращаются к памяти последовательно и переиспользуют загруженные блоки (блочное умножение), работают быстро; алгоритмы с разрозненным доступом и множеством временных массивов (как Штрассен) страдают от кэш-промахов, и реальная скорость может быть хуже, чем предсказывает число операций.
#
# Суммирование по Кахану: при сложении многих чисел с плавающей точкой младшие биты теряются на каждом шаге, и ошибка накапливается. Алгоритм Кахана хранит отдельную переменную-компенсацию, в которую собирает потерянную при округлении часть и добавляет ее на следующем шаге. Это резко снижает накопление ошибки округления (погрешность почти не зависит от числа слагаемых), что важно при суммировании скалярных произведений в матричных операциях.
#
# ---
#
# В: Какова точность центральной разности для аппроксимации первой производной? Как ошибка зависит от шага h? Сравните прямую разность с центральной разностью по точности и вычислительным затратам.
#
#
# Центральная разность f'(x) ~ (f(x+h) - f(x-h))/(2h) имеет погрешность аппроксимации O(h2): при уменьшении h в 10 раз ошибка усечения падает примерно в 100 раз. Прямая разность f'(x) ~ (f(x+h) - f(x))/h имеет погрешность O(h): уменьшение h в 10 раз снижает ошибку лишь в 10 раз.
#
# Сравнение: по точности центральная разность на порядок лучше при том же h. По вычислительным затратам она требует двух вычислений функции (в x+h и x-h), прямая - тоже двух (в x+h и x), так что центральная разность дает выигрыш в точности практически бесплатно. При слишком малом h обе формулы начинают страдать от ошибки округления (вычитание близких чисел), поэтому существует оптимальный шаг.
#
# ---
#
# # 2. Погрешности: абсолютная, относительная, накопление
#
# **В: Что такое абсолютная и относительная погрешности, и как они связаны со значащими цифрами?**
#
# Абсолютная погрешность - модуль разности точного значения x и приближенного \hat{x}:
# Delta = |x - \hat{x}|.
# Относительная погрешность - отношение delta = Delta / |x| (часто в процентах), оно показывает точность независимо от масштаба величины.
#
# Связь со значащими цифрами: число верных значащих цифр приближенно определяется относительной погрешностью - если delta ~ 10^(-k), то верны примерно k значащих цифр. Значащие цифры - это все цифры записи, начиная с первой ненулевой; именно относительная погрешность (а не абсолютная) отражает их количество.
#
# ---
#
# # В: Как погрешности распространяются и накапливаются при вычислениях?
#
# При сложении/вычитании складываются абсолютные погрешности слагаемых; при умножении/делении складываются относительные погрешности множителей. Поэтому вычитание близких чисел опасно (большая относительная погрешность результата при малых абсолютных у входов).
#
# В длинной цепочке операций погрешности накапливаются. Различают неустранимую погрешность (из-за неточных входных данных), погрешность метода (например, обрыв ряда, ошибка усечения) и вычислительную погрешность (округление). Цель численного метода - чтобы суммарная ошибка оставалась в пределах требуемой точности и не нарастала катастрофически.
#
# ---
#
# # 3. Сложность алгоритмов и профилирование в Python
#
# ## В: Что такое сложность алгоритма и нотация big-O?
#
# Сложность алгоритма описывает, как растут затраты (время или память) с увеличением размера входа n. Нотация big-O дает верхнюю асимптотическую оценку, отбрасывая константы и младшие члены: O(1) - постоянная, O(log n) - логарифмическая, O(n) - линейная, O(n log n), O(n2), O(n3), O(2ⁿ) - экспоненциальная.
#
# Примеры из курса: наивное умножение матриц - O(n3), Штрассен - O(n^(2.807)), бисекция - O(log((b-a)/eps)) итераций, ДПФ - O(N2), БПФ - O(N log N). Big-O позволяет сравнивать алгоритмы независимо от конкретной машины.
#
# ---
#
# # В: Как профилируют код в Python и зачем это нужно?
#
# Профилирование измеряет, сколько времени и ресурсов тратит каждая часть программы, чтобы найти узкие места. В Python для этого используют модули cProfile и profile (детальная статистика по функциям), timeit (точный замер коротких фрагментов), а также построчные профилировщики и анализаторы памяти.
#
# Смысл: асимптотика big-O описывает рост, но реальная скорость зависит от констант, работы с памятью и накладных расходов интерпретатора.
# Профилирование показывает, какую именно функцию стоит оптимизировать (например, векторизовать через NumPy), вместо преждевременной оптимизации наград.
#
# ---
#
# # 4. Сжимающие отображения и теория сходимости
#
# **В: Что такое сжимающее отображение и что утверждает теория сжимающих отображений?**
#
# Отображение g сжимающее на множестве, если существует константа 0 <= L < 1 такая, что |g(x) - g(y)| <= L * |x - y| для всех x, y из этого множества. То есть оно сближает любые две точки.
#
# Принцип сжимающих отображений (теорема Банаха о неподвижной точке): сжимающее отображение полного множества в себя имеет единственную неподвижную точку x* = g(x*), и итерации x_{n+1} = g(x_n) сходятся к ней из любого начального приближения. На этом основан метод простых итераций для решения уравнений вида x = g(x).
#
# ---
#
# В: Что такое константа Липшица и как она задает критерии и скорость сходимости?
#
# Константа Липшица L - наименьшая константа в условии |g(x) - g(y)| <= L * |x - y|. Для гладкой g на отрезке L = max|g'(x)|. Критерий сходимости простых итераций: L < 1 (отображение сжимающее).
#
# Скорость сходимости линейная: ошибка убывает примерно как e_n ~ L^n * e_0, то есть в L раз за итерацию. Чем меньше L, тем быстрее. Можно заранее оценить число итераций для нужной точности. Если L >= 1, сходимость не гарантирована. Критериями остановки служат малость приращения |x_{n+1} - x_n| или невязки.
#
# ---
#
# # 5. Виды интерполяции и сплайны
#
# **В: Чем отличаются интерполяция, экстраполяция и аппроксимация?**
#
# Интерполяция - построение функции, проходящей точно через заданные узлы, и оценка значений внутри диапазона узлов. Экстраполяция - оценка значений вне диапазона узлов (менее надежна, ошибка быстро растет).
# Аппроксимация - приближение данных функцией, которая не обязана проходить через точки точно (например, методом наименьших квадратов); она предпочтительна для зашумленных данных.
#
# ---
#
# В: В чем разница между глобальной и локальной интерполяцией?
# Приведите примеры (ступенчатая, линейная, квадратичная).
#
# Глобальная интерполяция строит один полином по всем узлам сразу (например, полином Лагранжа). Недостаток - при большом числе узлов высокая степень дает осцилляции (феномен Рунге). Локальная интерполяция использует на каждом участке лишь несколько соседних узлов.
#
# Примеры локальной: ступенчатая (значение ближайшего узла, разрывная); линейная (соединение соседних узлов отрезками, непрерывная, но с изломами); квадратичная (парабола по трем соседним узлам, более гладкая).
# Чем выше локальная степень, тем глаже результат.
#
# # Дополнительные темы для нестандартных случаев
#
# ## Численное интегрирование
# Численное интегрирование нужно, когда надо приближенно найти площадь под графиком или значение определенного интеграла. Основная идея - заменить криволинейную площадь суммой простых фигур.
#
# Метод левых прямоугольников берет значение функции в левой точке каждого маленького отрезка. Метод правых прямоугольников берет правую точку. Метод средних прямоугольников берет середину. Метод трапеций заменяет график на отрезки прямых. Метод Симпсона использует параболы и обычно точнее, но требует четное число промежутков.
#
# Что писать в ответе: при уменьшении шага h точность обычно растет, но вычислений становится больше. Ошибка метода зависит от гладкости функции и выбранной формулы.
#
# ## Метод Гаусса-Зейделя
# Метод Гаусса-Зейделя решает систему линейных уравнений A*x=b итерационно. На каждом шаге новые значения неизвестных сразу используются для вычисления следующих неизвестных.
#
# Метод хорошо работает для диагонально преобладающих матриц и некоторых положительно определенных матриц. Критерий остановки - маленькое изменение x или маленькая невязка ||A*x-b||.
#
# Плюс: простой и не требует прямого разложения матрицы. Минус: может не сойтись, если матрица не подходит.
#
# ## Метод вращений Якоби
# Метод вращений Якоби ищет собственные значения симметричной матрицы. Идея - последовательно занулять внедиагональные элементы ортогональными поворотами. Когда внедиагональные элементы стали малыми, диагональ матрицы дает собственные значения.
#
# Метод применяется только к симметричным матрицам. Плюс - хорошая численная устойчивость. Минус - много итераций для больших матриц.
#
# ## Вторая производная центральной разностью
# Вторая производная может быть приближенно найдена формулой
# f_second(x) = (f(x+h) - 2*f(x) + f(x-h)) / h^2.
#
# Если h слишком большой, растет ошибка аппроксимации. Если h слишком маленький, растет ошибка округления. Поэтому h надо выбирать разумно.
#
# ## Адаптивный шаг для ОДУ
# Адаптивный шаг нужен, когда решение ОДУ на разных участках меняется с разной скоростью. Идея - сравнить один большой шаг с двумя маленькими. Если разница большая, шаг уменьшают. Если разница маленькая, шаг можно увеличить.
#
# Плюс: метод тратит больше шагов только там, где это нужно. Минус: код сложнее, чем у метода с постоянным шагом.
#
# ## Фильтрация сигнала через ДПФ
# ДПФ переводит сиг
# %%
# ## теория, часть 5
#
# нал из временной области в частотную. Если шум проявляется как отдельные пики в спектре, можно обнулить соответствующие гармоники и затем применить обратное ДПФ.
#
# Если шум широкополосный, полностью убрать его трудно: сильная фильтрация может испортить полезный сигнал. В ответе важно написать, как были найдены шумовые частоты: по большим пикам амплитудного спектра или по сравнению спектров до и после.
#
# ## Частотная сетка, Найквист и алиасинг
# Если сигнал имеет N отсчетов и частоту дискретизации fs, то шаг частотной сетки равен df = fs/N. Частота Найквиста равна fs/2. Частоты выше fs/2 не различаются корректно и могут перейти в ложные низкие частоты. Это называется алиасинг.
#
# Чем больше N при фиксированной fs, тем лучше частотное разрешение.
#
# ## Метод наименьших квадратов
# Метод наименьших квадратов строит приближающую функцию так, чтобы сумма квадратов ошибок была минимальной. Для прямой y = a*x + b параметры a и b находятся из нормальных уравнений.
#
# Метод не обязан проходить через все точки. Он нужен для аппроксимации данных с шумом. Плюс - устойчив к небольшим ошибкам измерений. Минус - выбросы могут сильно влиять на результат.
#
# ## PageRank как степенной метод
# PageRank ищет стационарный вектор важности страниц. Его можно понимать как степенной метод для матрицы переходов. На каждом шаге новый ранг получается умножением матрицы переходов на текущий вектор рангов.
#
# Коэффициент damping обычно берут около 0.85. Он учитывает случайный переход на любую страницу и делает метод устойчивее.
#
# ## QR через отражения Хаусхолдера
# QR-разложение через отражения Хаусхолдера представляет матрицу как A = Q*R, где Q ортогональная, а R верхнетреугольная. Идея - занулять элементы ниже диагонали отражениями.
#
# Этот способ обычно устойчивее классического Грамма-Шмидта, потому что лучше сохраняет ортогональность Q при ошибках округления.
#
# # Редкие темы, которые могут попасться на экзамене
#
# ## SVD
# SVD - сингулярное разложение матрицы. Оно записывается как A = U * Sigma * V^T. Матрицы U и V ортогональные, а Sigma содержит сингулярные числа. Сингулярные числа показывают важность направлений в данных. Малые сингулярные числа часто можно отбросить, если нужна низкоранговая аппроксимация или сжатие.
#
# Связь с собственными значениями: сингулярные числа равны корням из собственных значений матрицы A^T A. То есть сначала можно рассмотреть B = A^T A, найти ее собственные значения lambda_i, а потом взять sigma_i = sqrt(lambda_i).
#
# ## PCA
# PCA - метод главных компонент. Он ищет новые оси, вдоль которых данные имеют максимальную дисперсию. Сначала данные центрируют, то есть из каждого признака вычитают среднее. Потом строят ковариационную матрицу и ищут ее собственные значения и собственные векторы.
#
# Собственные векторы ковариационной матрицы - это главные компоненты. Большие собственные значения показывают направления, где данных больше всего разбросано. PCA используют для сжатия, визуализации, удаления шума и уменьшения размерности.
#
# ## Разреженные матрицы
# Плотная матрица хранит все элементы, даже нули. Разреженная матрица содержит много нулей, поэтому хранить их явно невыгодно. Вместо этого хранят только ненулевые элементы.
#
# COO хранит три списка: номера строк, номера столбцов и значения. CSR хранит data, indices и indptr. data - ненулевые значения, indices - номера столбцов, indptr - где начинается каждая строка. Память становится O(nnz) вместо O(n^2), где nnz - число ненулевых элементов.
#
# ## IEEE 754
# В формате с плавающей точкой число хранится как знак, порядок и мантисса. Из-за конечной длины мантиссы не все числа можно представить точно. Поэтому появляются ошибки округления.
#
# Машинный эпсилон - это примерный уровень относительной ошибки округления. Для double он около 2.22e-16.
#
# Overflow - переполнение, когда число слишком большое и превращается в бесконечность. Underflow - потеря порядка, когда число слишком маленькое и может округлиться к нулю.
#
# ## Потеря значимости
# Потеря значимости возникает при вычитании близких чисел. Старшие разряды сокращаются, и в ответе остаются младшие разряды, где уже есть ошибка округления. Поэтому выражения с вычитанием близких чисел часто численно неустойчивы.
#
# ## Обусловленность
# Обусловленность показывает, насколько ошибка во входных данных влияет на ошибку в ответе. Если задача плохо обусловлена, маленькое изменение исходных данных может сильно изменить результат.
#
# Для системы Ньютона проблема часто возникает, когда якобиан почти вырожден. Тогда поправка считается неточно, и метод может плохо сходиться.
#
# ## Спектральный радиус
# Спектральный радиус матрицы - это максимум модулей ее собственных значений: rho(A) = max |lambda_i|. Степенной метод обычно ищет собственное значение с наибольшим модулем.
#
# Скорость степенного метода зависит от зазора между первым и вторым по модулю собственными значениями. Чем больше зазор, тем быстрее сходимость.
#
# ## Отношение Релея
# Отношение Релея считается по формуле lambda = (A*x, x)/(x, x). Оно дает оценку собственного значения по текущему вектору x. В степенном методе его часто используют для уточнения lambda.
#
# ## Круги Гершгорина
# Круги Гершгорина дают области, где могут лежать собственные значения. Центр круга - диагональный элемент a_ii. Радиус - сумма модулей остальных элементов строки. Все собственные значения лежат в объединении этих кругов.
#
# ## Верхне-гессенбергова форма
# Верхне-гессенбергова матрица почти треугольная: элементы ниже первой поддиагонали равны нулю. Такая форма нужна для ускорения QR-алгоритма. На гессенберговой матрице один QR-шаг дешевле, чем на полной матрице.
#
# ## Нормальная матрица и разложение Шура
# Матрица называется нормальной, если A*A^T = A^T*A. Разложение Шура имеет вид A = Q*T*Q^T, где Q ортогональная, а T верхнетреугольная. Для нормальной матрицы T получается диагональной. Поэтому нормальные матрицы хорошо диагонализуются ортогональными преобразованиями.
#
# ## Архитектура памяти и Штрассен
# На практике скорость зависит не только от числа операций, но и от работы с памятью. Кэш быстрее оперативной памяти. Если алгоритм часто создает временные матрицы и плохо переиспользует данные, он может работать медленно.
#
# Алгоритм Штрассена уменьшает число умножений, но создает много дополнительных сумм и временных блоков. Поэтому на маленьких матрицах он может быть хуже обычного алгоритма.
#
# ## Локальная и глобальная ошибка
# Локальная ошибка - ошибка одного шага, если предыдущее значение считается точным. Глобальная ошибка - ошибка, накопленная на всем интервале.
#
# Для метода Рунге-Кутты 4 порядка локальная ошибка имеет порядок h^5, а глобальная - h^4.
#
# ## Согласованность, устойчивость и сходимость
# Согласованность значит, что разностная схема при h -> 0 приближает исходное уравнение. Устойчивость значит, что ошибки не растут бесконтрольно. Сходимость значит, что численное решение стремится к точному.
#
# Для корректных линейных задач важная идея такая: согласованность плюс устойчивость дают сходимость.
#
# ## Строгая и слабая устойчивость
# Строгая устойчивость значит, что ошибки затухают. Слабая устойчивость значит, что ошибки хотя бы не растут. Для жестких задач обычно нужны устойчивые методы, часто неявные.
#
# ## Жесткие задачи
# Жесткая задача - это ОДУ, где есть быстрые и медленные процессы одновременно. Явный Эйлер для таких задач требует очень маленький шаг, иначе решение может стать неустойчивым. Неявные методы обычно устойчивее.
#
# ## Константа Липшица
# Для простых итераций x = phi(x) важно, чтобы около решения было |phi'(x)| < 1. Тогда phi является сжимающим отображением, и итерации сходятся. Если это число близко к 1, сходимость медленная. Если больше 1, итерации могут расходиться.
#
# ## ДПФ, БПФ, Найквист
# ДПФ переводит сигнал из временной области в частотную. БПФ считает то же самое быстрее: O(N log N) вместо O(N^2).
#
# Частотное разрешение df = fs/N. Частота Найквиста равна fs/2. Частоты выше fs/2 могут исказиться и дать aliasing.
#
# ## Фильтрация сигнала
# Чтобы убрать шум, можно перейти в спектр через ДПФ, обнулить ненужные частоты и выполнить обратное ДПФ. Высокочастотный шум убирают низкочастотным фильтром. Если нужно оставить только нужный диапазон, используют полосовой фильтр.
#
# ## Феномен Рунге
# Феномен Рунге - это сильные колебания интерполяционного полинома высокой степени, особенно на краях отрезка. Поэтому многочлен Лагранжа высокой степени может давать плохой результат. Сплайны обычно устойчивее, потому что используют кусочные полиномы невысокой степени.
#
# ## Метод золотого сечения
# Метод золотого сечения ищет минимум функции на отрезке. Он похож на дихотомию, но выбирает точки так, чтобы часть вычислений можно было переиспользовать. Метод не требует производной.
#
# ## Правило Рунге
# Правило Рунге оценивает ошибку по двум расчетам: с шагом h и с шагом h/2. Если порядок метода p, то ошибка примерно равна |y_{h/2} - y_h|/(2^p - 1). Это помогает понять, достаточно ли маленький шаг.