import pyperclip


def p1():
    answer = f"""
def bisection_method(f, a, b, tolerance=1e-6, max_iterations=100):
    '''
    Реализация метода половинного деления для нахождения корня функции.

    Аргументы:
    f: функция, для которой ищется корень.
    a, b: концы отрезка, на котором ищется корень.
    tolerance: желаемая точность.
    max_iterations: максимальное количество итераций.

    Возвращает:
    Приблизительное значение корня.
    '''
    
    # Проверка начального условия: знаки функции на концах отрезка должны быть разными
    if f(a) * f(b) >= 0:
        print("Метод половинного деления не может быть применен: f(a) и f(b) должны иметь разные знаки.")
        return None

    for i in range(max_iterations):
        c = (a + b) / 2.0  # Вычисление середины отрезка

        # Если длина отрезка стала меньше заданной точности, возвращаем результат
        if (b - a) / 2.0 < tolerance:
            return c

        # Выбор нового отрезка
        if f(a) * f(c) < 0:
            b = c
        else:
            a = c
            
    # Если максимальное число итераций достигнуто, а корень не найден
    print("Достигнуто максимальное количество итераций.")
    return c

# Определим функцию, для которой будем искать корень
# Например, f(x) = x^3 - x - 2
def example_function(x):
    return x**3 - x - 2

# Начальный отрезок [a, b]
a = 1.0
b = 2.0

# Проверим значения функции на концах отрезка
print(f"f(a) = example_function(a)")
print(f"f(b) = example_function(b)")
print("-" * 20)

# Вызов метода
root = bisection_method(example_function, a, b)

"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p1t():
    answer = f"""
## 1. Метод половинного деления (бисекции)

### Теоретическое описание:
Метод половинного деления — это простейший и самый надежный численный метод для нахождения корня нелинейного уравнения f(x) = 0.

**Основная идея:**
Метод основан на теореме о промежуточных значениях (теореме Больцано-Коши). Если непрерывная функция f(x) на отрезке [a, b] принимает на концах значения разных знаков (т.е. f(a) * f(b) < 0), то на этом отрезке существует как минимум один корень.

**Алгоритм:**
1.  **Начальный шаг:** Выбирается отрезок [a, b], на концах которого функция имеет разные знаки.
2.  **Итерация:** Отрезок делится пополам, вычисляется середина c = (a + b) / 2.
3.  **Проверка:**
    -   Если |f(c)| меньше заданной точности (tol), то c — искомый корень.
    -   Если знаки f(a) и f(c) совпадают, то корень находится на отрезке [c, b]. Новый отрезок для поиска становится [c, b].
    -   Если знаки f(b) и f(c) совпадают, то корень находится на отрезке [a, c]. Новый отрезок для поиска становится [a, c].
4.  **Повторение:** Процесс повторяется, сужая отрезок поиска вдвое на каждом шаге, до тех пор, пока не будет достигнута требуемая точность.

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

**Недостатки:**
-   Относительно медленная скорость сходимости (линейная).
-   Неприменим для отыскания корней четного порядка (например, в точке минимума/максимума, касающейся оси), когда график функции не пересекает ось.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p2():
    answer = f"""
import numpy as np

def f_iteration(g, x0, n_iterations=100, tol=1e-5):
    '''
    Реализация метода простой итерации для уравнения x = g(x)

    Параметры:
        g (function): Функция, для которой ищется неподвижная точка.
        x0 (float): Начальное приближение.
        n_iterations (int): Максимальное количество итераций.
        tol (float): Точность для остановки.
    '''
    x = x0
    for n in range(n_iterations):
        x_new = g(x)
        if abs(x_new - x) < tol or g(x) < tol:
            return x_new
        x = x_new
    return x

# Пример использования:
# Решим уравнение x = cos(x), что эквивалентно f(x) = x - cos(x) = 0.
# Здесь φ(x) = cos(x). Проверим условие сходимости: |φ'(x)| = |-sin(x)| <= 1.
# Условие выполнено.
# g = lambda x: np.cos(x)
# root = f_iteration(g, x0=0.5)
# print(root)
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p2t():
    answer = f"""
## 2. Методы функциональной итерации (метод простых итераций)

### Теоретическое описание:
Этот метод является одним из самых фундаментальных для решения нелинейных уравнений. Он заключается в преобразовании исходного уравнения f(x) = 0 в эквивалентную форму x = φ(x).

**Основная идея:**
Если найдена такая функция φ(x), то корень исходного уравнения является её неподвижной точкой (т.е. точкой, где y = x пересекается с y = φ(x)). Решение ищется с помощью итерационного процесса:
`x_(k+1) = φ(x_k)`

**Условие сходимости:**
Метод сходится к единственному корню на отрезке [a, b], если выполнено **условие сжимающего отображения**:
`|φ'(x)| < 1` для всех x из отрезка [a, b].
Чем меньше значение |φ'(x)|, тем быстрее сходимость. Если |φ'(x)| > 1, метод расходится.

**Алгоритм:**
1.  Преобразовать уравнение f(x) = 0 к виду x = φ(x). Это можно сделать разными способами, например, `x = x - α*f(x)`, где α — параметр, который подбирается для выполнения условия сходимости.
2.  Выбрать начальное приближение `x0`.
3.  Выполнять итерации `x_(k+1) = φ(x_k)` до тех пор, пока `|x_(k+1) - x_k|` не станет меньше заданной точности.

**Преимущества:**
-   Концептуально прост и является основой для многих других методов.

**Недостатки:**
-   Сходимость не гарантирована и сильно зависит от выбора функции φ(x).
-   Скорость сходимости, как правило, линейная и может быть очень медленной.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p3():
    answer = f"""
def chord_method(f, x0, x1, tol=1e-5, n_iterations=100):
    '''
    Реализация метода хорд (секущих).

    Параметры:
        f (function): Функция, корень которой ищется.
        x0, x1 (float): Два начальных приближения.
        tol (float): Точность.
    '''
    for n in range(n_iterations):
        fx0 = f(x0)
        fx1 = f(x1)

        if abs(fx1 - fx0) < 1e-12: # Избегание деления на ноль
            break

        x_new = x1 - fx1 * (x1 - x0) / (fx1 - fx0)

        if abs(x_new - x1) < tol:
            return x_new

        x0 = x1
        x1 = x_new

    return x1

# Пример использования:
# Найдём корень уравнения x^2 - 2 = 0
# f = lambda x: x**2 - 2
# root = chord_method(f, 1, 2, tol=1e-7)
# print(root)
# print(root)"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p3t():
    answer = f"""
## 3. Метод хорд (метод секущих)

### Теоретическое описание:
Метод хорд является усовершенствованием метода простых итераций и альтернативой методу Ньютона. Он не требует вычисления производной, что является его ключевым преимуществом.

**Основная идея:**
Вместо касательной, как в методе Ньютона, через две последние точки `(x_k-1, f(x_k-1))` и `(x_k, f(x_k))` проводится *секущая* (хорда). Следующее приближение `x_k+1` — это точка пересечения этой секущей с осью Ox.

Производная `f'(x_k)` из метода Ньютона аппроксимируется конечной разностью:
`f'(x_k) ≈ (f(x_k) - f(x_k-1)) / (x_k - x_k-1)`

Подставив это в формулу Ньютона, получаем итерационную формулу метода хорд:
`x_k+1 = x_k - f(x_k) * (x_k - x_k-1) / (f(x_k) - f(x_k-1))`

**Алгоритм:**
1.  Выбрать два начальных приближения `x0` и `x1`.
2.  Выполнять итерации по приведенной выше формуле.
3.  Остановить процесс, когда `|x_k+1 - x_k|` станет меньше заданной точности.

**Преимущества:**
-   Не требует вычисления аналитической производной.
-   Скорость сходимости (суперлинейная, ~1.618) выше, чем у метода простых итераций и метода половинного деления.

**Недостатки:**
-   Требует два начальных приближения.
-   Сходимость медленнее, чем у метода Ньютона (который имеет квадратичную сходимость).
-   Как и метод Ньютона, может расходиться, если начальные точки выбраны неудачно.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p4():
    answer = f"""
def newton_method(f, df, x0, tolerance=1e-7, max_iterations=100):
    '''
    Реализация метода Ньютона.

    Параметры:
        f (function): Функция, корень которой ищется.
        df (function): Производная функции f.
        x0 (float): Начальное приближение.
        tolerance (float): Точность.
    '''
    for i in range(max_iterations):
        fx = f(x0)
        if abs(fx) < tolerance:
            return x0

        dfx = df(x0)
        if dfx == 0:
            break # Деление на ноль

        x0 = x0 - fx / dfx

    return x0

# Пример использования:
# Найдём корень уравнения x**2 - 2 = 0
# f = lambda x: x**2 - 2
# f_prime = lambda x: 2*x
# root = newton_method(f, f_prime, 1.5, tolerance=1e-7)
# print(root)
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p4t():
    answer = f"""
## 4. Метод Ньютона (метод касательных)

### Теоретическое описание:
Метод Ньютона — один из самых мощных и быстросходящихся итерационных методов для нахождения корня f(x) = 0.

**Основная идея:**
Метод основан на замене функции `f(x)` её линейной аппроксимацией — касательной, построенной в точке текущего приближения `x_k`. Следующее приближение `x_k+1` находится как точка пересечения этой касательной с осью Ox.

Формула выводится из разложения функции в ряд Тейлора в окрестности `x_k`:
`f(x) ≈ f(x_k) + f'(x_k) * (x - x_k)`

Полагая `x = x_k+1` и `f(x_k+1) = 0`, получаем итерационную формулу:
`x_k+1 = x_k - f(x_k) / f'(x_k)`

**Алгоритм:**
1.  Выбрать начальное приближение `x0`.
2.  Найти аналитическое выражение для производной `f'(x)`.
3.  Выполнять итерации по формуле Ньютона до достижения необходимой точности.

**Преимущества:**
-   Очень быстрая (квадратичная) скорость сходимости вблизи корня. Это означает, что количество верных значащих цифр примерно удваивается на каждой итерации.

**Недостатки:**
-   Требует вычисления аналитической производной, что не всегда возможно или удобно.
-   Сходимость сильно зависит от выбора начального приближения `x0`. При неудачном выборе метод может расходиться.
-   Проблемы возникают, если производная `f'(x_k)` близка к нулю (касательная почти горизонтальна).
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p5():
    answer = f"""
def modified_newton_method(f, df, x0, tol=1e-5, n_iterations=100):
    '''
    Реализация модифицированного метода Ньютона.

    Параметры:
        f (function): Функция, корень которой ищется.
        df (function): Производная функции f.
        x0 (float): Начальное приближение.
        tol (float): Точность.
    '''
    df_x0 = df(x0) # Вычисляем производную только один раз
    if df_x0 == 0:
        raise ValueError("Производная в начальной точке равна нулю.")

    for i in range(n_iterations):
        fx0 = f(x0)
        if abs(fx0) < tol:
            return x0

        x_new = x0 - fx0 / df_x0
        x0 = x_new

    return x0

# Пример использования:
# f = lambda x: x**2 - 2
# f_prime = lambda x: 2*x
# root = modified_newton_method(f, f_prime, 1.5, tol=1e-7)
# print(root)"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p5t():
    answer = f"""
## 5. Модифицированный метод Ньютона

### Теоретическое описание:
Это вариация классического метода Ньютона, предназначенная для ситуаций, когда вычисление производной `f'(x)` на каждой итерации является вычислительно затратным.

**Основная идея:**
Вместо того чтобы пересчитывать значение производной `f'(x_k)` на каждом шаге `k`, мы вычисляем её только один раз в начальной точке `x0` и используем это значение на всех последующих итерациях.

Итерационная формула принимает вид:
`x_k+1 = x_k - f(x_k) / f'(x0)`

**Геометрическая интерпретация:**
Касательные, которые мы проводим на каждом шаге, параллельны друг другу, так как их наклон определяется постоянной величиной `f'(x0)`.

**Преимущества:**
-   Не требует вычисления производной на каждой итерации, что экономит вычислительные ресурсы, если `f'(x)` — сложная функция.

**Недостатки:**
-   Скорость сходимости снижается с квадратичной (как у Ньютона) до линейной.
-   Сходимость по-прежнему зависит от выбора начальной точки `x0`.
-   Если производная сильно меняется на интервале поиска, метод может сходиться очень медленно или вовсе расходиться.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p6t():
    answer = f"""
## 6. Метод дихотомии для поиска экстремума

### Теоретическое описание:
Метод дихотомии (не путать с методом половинного деления для поиска корней) используется для нахождения минимума или максимума *унимодальной* функции на заданном отрезке [a, b]. Унимодальная функция — это функция, имеющая на отрезке только один локальный экстремум.

**Основная идея:**
На каждом шаге мы сужаем интервал, содержащий экстремум. Для этого внутри текущего интервала [a, b] выбираются две точки `x1` и `x2`, расположенные симметрично относительно его середины на небольшом расстоянии `ε`.

`x1 = (a + b) / 2 - ε`
`x2 = (a + b) / 2 + ε`

Далее сравниваются значения функции в этих точках, `f(x1)` и `f(x2)`.

**Алгоритм (для поиска минимума):**
1.  **Начальный шаг:** Задается начальный интервал [a, b], где предполагается наличие минимума, и малая величина `ε`.
2.  **Итерация:** Вычисляются `x1` и `x2` по формулам выше.
3.  **Проверка:**
    -   Если `f(x1) < f(x2)`, то минимум находится левее `x2`. Новый интервал поиска: `[a, x2]`.
    -   Если `f(x1) > f(x2)`, то минимум находится правее `x1`. Новый интервал поиска: `[x1, b]`.
    -   Если `f(x1) = f(x2)`, интервал можно сузить до `[x1, x2]`.
4.  **Повторение:** Процесс повторяется, пока длина интервала `(b - a)` не станет меньше заданной точности.

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

**Недостатки:**
-   Медленная сходимость.
-   Требует, чтобы функция была унимодальной на исследуемом интервале.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p6():
    answer = f"""
def dichotomy_method(f, a, b, tol=1e-5, epsilon=1e-7):
    '''
    Реализация метода дихотомии для поиска минимума.

    Параметры:
        f (function): Унимодальная функция, минимум которой ищется.
        a, b (float): Границы начального интервала.
        tol (float): Точность для остановки (длина интервала).
        epsilon (float): Малое смещение для тестовых точек.
    '''
    while (b - a) > tol:
        mid = (a + b) / 2
        x1 = mid - epsilon
        x2 = mid + epsilon

        if f(x1) < f(x2):
            b = x2
        else:
            a = x1

    return (a + b) / 2

# Пример использования:
# Найдем минимум функции f(x) = (x-2)^2 на отрезке [0, 5]
# f = lambda x: (x - 2)**2
# min_point = dichotomy_method(f, 0, 5)
# print(min_point)"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p7():
    answer = f"""
import numpy as np

def fixed_point_system(g_sys, x0_vec, tol=1e-9, max_iter=100):
    \"\"\"
    Решает систему уравнений X = G(X) методом простой итерации.
    Сходимость зависит от свойств отображения G (например, является ли оно сжимающим).

    Аргументы:
        g_sys: Вектор-функция G(X).
        x0_vec: Вектор начального приближения.
        tol: Допуск для остановки.
        max_iter: Максимальное число итераций.
    \"\"\"
    x = np.array(x0_vec, dtype=float)
    for i in range(max_iter):
        x_next = np.array(g_sys(x), dtype=float)

        # Условие сходимости по максимальному абсолютному изменению компоненты
        # Это эквивалентно проверке по норме L-infinity.
        if np.max(np.abs(x_next - x)) < tol:
            return x_next

        x = x_next

    print("Достигнуто максимальное число итераций.")
    return x

# ----- Пример вызова -----
# Решаем систему, преобразованную к виду x = G(x):
# x1 = (cos(x2) + 2) / 10
# x2 = (sin(x1) + 2) / 10
def g_system_example(x):
    x1 = (np.cos(x[1]) + 2) / 10.0
    x2 = (np.sin(x[0]) + 2) / 10.0
    return [x1, x2]

initial_guess = [0.0, 0.0]
solution = fixed_point_system(g_system_example, initial_guess)
print("\\n--- 7. Метод функциональной итерации для систем ---")
print(f"Найденное решение: x1 = solution[0]:.7f, x2 = solution[1]:.7f")
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p7t():
    answer = f"""
## 7. Метод функциональной итерации для решения систем нелинейных уравнений

### Теоретическое описание:
Метод функциональной итерации (или метод простых итераций) — это итерационный метод решения систем нелинейных уравнений вида `X = G(X)`, где `X` и `G` — векторы.

**Основная идея:**
Система уравнений `F(X) = 0` преобразуется к эквивалентному виду `X = G(X)`, где `G` — некоторая итерирующая функция. Затем последовательно вычисляются приближения:
`X_k+1 = G(X_k)`

**Условия сходимости:**
1. Функция `G` должна быть сжимающей в некоторой окрестности решения
2. Достаточное условие сходимости: норма матрицы Якоби `||J_G(X)|| < 1` в окрестности решения

**Алгоритм:**
1. Преобразовать исходную систему `F(X) = 0` к виду `X = G(X)`
2. Выбрать начальное приближение `X0`
3. На каждой итерации вычислять новое приближение: `X_k+1 = G(X_k)`
4. Повторять до достижения заданной точности
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p8t():
    answer = f"""
## 8. Метод Гаусса-Зейделя для систем нелинейных уравнений

### Теоретическое описание:
Метод Гаусса-Зейделя (или метод Зейделя) — это модификация метода простых итераций для СНУ, которая часто обеспечивает более быструю сходимость.

**Основная идея:**
Главное отличие от метода простых итераций заключается в том, что при вычислении `(k+1)`-го приближения для переменной `x_i` используются *уже вычисленные* на `(k+1)`-й итерации значения других переменных (`x_1, ..., x_i-1`).

Итерационный процесс для `i`-й компоненты вектора `X`:
`x_i^(k+1) = g_i(x_1^(k+1), ..., x_i-1^(k+1), x_i^(k), ..., x_n^(k))`

**Условие сходимости:**
Условия сходимости аналогичны методу простых итераций и связаны со сжимающим отображением. На практике метод Зейделя часто сходится быстрее, если система обладает свойством диагонального преобладания.

**Алгоритм:**
1.  Преобразовать систему к виду `X = G(X)`.
2.  Выбрать начальное векторное приближение `X0`.
3.  На `(k+1)`-й итерации последовательно вычислять `x_i^(k+1)`, сразу используя новые значения в последующих вычислениях на этом же шаге.
4.  Процесс останавливается, когда норма разности между итерациями становится достаточно малой.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p8():
    answer = f"""
def gauss_seidel(A, b, x0=None, tol=1e-9, max_iter=200):
    \"\"\"
    Решает систему линейных алгебраических уравнений Ax = b методом Гаусса-Зейделя.
    Это итерационный метод. На каждой итерации при вычислении i-й компоненты
    решения используются уже обновленные компоненты 1, ..., i-1.

    Аргументы:
        A: Матрица коэффициентов.
        b: Вектор правых частей.
        x0: Начальное приближение.
        tol: Желаемая точность.
        max_iter: Максимальное число итераций.
    \"\"\"
    n = len(b)
    A = np.array(A, dtype=float)
    b = np.array(b, dtype=float)
    x = np.zeros(n, dtype=float) if x0 is None else np.array(x0, dtype=float)

    for it_count in range(max_iter):
        x_old = np.copy(x)
        for i in range(n):
            s1 = np.dot(A[i, :i], x[:i])
            s2 = np.dot(A[i, i + 1:], x_old[i + 1:])
            x[i] = (b[i] - s1 - s2) / A[i, i]

        # Проверка на сходимость по максимальному изменению компоненты
        if np.max(np.abs(x - x_old)) < tol:
            return x

    print("Достигнуто максимальное число итераций.")
    return x

# ----- Пример вызова -----
# Решаем систему:
# 10*x1 - 1*x2 + 2*x3 = 6
# -1*x1 + 11*x2 - 1*x3 = 25
# 2*x1 - 1*x2 + 10*x3 = -11
A_matrix = [[10, -1, 2], [-1, 11, -1], [2, -1, 10]]
b_vector = [6, 25, -11]
# Точное решение: [1, 2, -1]

solution = gauss_seidel(A_matrix, b_vector)
print("\\n--- 8. Метод Гаусса–Зейделя ---")
print(f"Найденное решение: solution")
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p9():
    answer = f"""
def solve_2x2_system(A, b):
    \"\"\"
    Решает линейную систему Ax=b для случая 2x2, используя явную формулу.
    \"\"\"
    # Определитель матрицы A
    det = A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0]

    if abs(det) < 1e-12:
        raise ValueError("Матрица вырождена (определитель близок к нулю).")

    # Формула для x = A_inv * b
    x0 = (A[1, 1] * b[0] - A[0, 1] * b[1]) / det
    x1 = (-A[1, 0] * b[0] + A[0, 0] * b[1]) / det

    return np.array([x0, x1])

def newton_system(f_sys, j_sys, x0, tol=1e-9, max_iter=50):
    \"\"\"
    Решает систему нелинейных уравнений F(X)=0 методом Ньютона.

    Аргументы:
        f_sys: Вектор-функция F(X).
        j_sys: Функция, возвращающая матрицу Якоби J(X).
        x0: Вектор начального приближения.
    \"\"\"
    x = np.array(x0, dtype=float)
    for i in range(max_iter):
        fx = f_sys(x)
        if np.max(np.abs(fx)) < tol:
            return x

        J = j_sys(x)
        # Решаем систему J * delta_x = -fx с помощью явной формулы для 2x2
        delta_x = solve_2x2_system(J, -fx)
        x += delta_x

    print("Достигнуто максимальное число итераций.")
    return x

# ----- Пример вызова -----
# Решаем систему:
# x1^2 + x2^2 - 4 = 0
# x1*x2 - 1 = 0
def f_system_example(x):
    x1, x2 = x[0], x[1]
    return np.array([x1**2 + x2**2 - 4, x1 * x2 - 1])

def j_system_example(x):
    x1, x2 = x[0], x[1]
    return np.array([[2*x1, 2*x2], [x2, x1]])

initial_guess = [2.0, 0.0]
solution = newton_system(f_system_example, j_system_example, initial_guess)
print("\\n--- 9. Метод Ньютона для систем (без linalg.solve) ---")
print(f"Найденное решение: solution") 
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p9t():
    answer = f"""
## 9. Метод Ньютона для систем нелинейных уравнений

### Теоретическое описание:
Это обобщение метода Ньютона для систем нелинейных уравнений `F(X) = 0`, где `X` и `F` — векторы.

**Основная идея:**
На каждом шаге система нелинейных уравнений `F(X) = 0` линеаризуется в окрестности текущего приближения `X_k` с помощью матрицы Якоби `J(X_k)`. Матрица Якоби — это матрица из частных производных.

Получается система линейных алгебраических уравнений (СЛАУ) для нахождения поправки `ΔX_k`:
`J(X_k) * ΔX_k = -F(X_k)`

**Алгоритм:**
1.  Выбрать начальное приближение `X0`.
2.  На каждой итерации `k`:
    a. Вычислить вектор `F(X_k)` и матрицу Якоби `J(X_k)`.
    b. Решить СЛАУ для нахождения вектора поправки `ΔX_k`.
    c. Обновить приближение: `X_k+1 = X_k + ΔX_k`.
3.  Повторять до достижения необходимой точности.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p10():
    answer = f"""
def solve_2x2_system(A, b):
    \"\"\"
    Решает линейную систему Ax=b для случая 2x2, используя явную формулу.
    Это замена для np.linalg.solve для 2x2 матриц.
    \"\"\"
    # Определитель матрицы A
    det = A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0]

    if abs(det) < 1e-12:
        raise ValueError("Матрица вырождена (определитель близок к нулю).")

    # Формула для x = A_inv * b
    x0 = (A[1, 1] * b[0] - A[0, 1] * b[1]) / det
    x1 = (-A[1, 0] * b[0] + A[0, 0] * b[1]) / det

    return np.array([x0, x1])

def modified_newton_system(f_sys, j_sys, x0, tol=1e-9, max_iter=100):
    \"\"\"
    Модифицированный метод Ньютона для систем.
    Матрица Якоби вычисляется только один раз в начальной точке.
    \"\"\"
    x = np.array(x0, dtype=float)
    # Якобиан вычисляется один раз
    J0 = j_sys(x0)

    for i in range(max_iter):
        fx = f_sys(x)
        if np.max(np.abs(fx)) < tol:
            return x

        # Используем постоянную матрицу Якоби
        # Решаем систему J0 * delta_x = -fx с помощью явной формулы
        delta_x = solve_2x2_system(J0, -fx)
        x += delta_x

    print("Достигнуто максимальное число итераций.")
    return x

# ----- Пример вызова -----
# Используем ту же систему, что и для обычного метода Ньютона
def f_system_example(x):
    x1, x2 = x[0], x[1]
    return np.array([x1**2 + x2**2 - 4, x1 * x2 - 1])

def j_system_example(x):
    x1, x2 = x[0], x[1]
    return np.array([[2*x1, 2*x2], [x2, x1]])

initial_guess = [2.0, 0.0]
solution = modified_newton_system(f_system_example, j_system_example, initial_guess)
print("\\n--- 10. Модифицированный метод Ньютона для систем (без linalg.solve) ---")
print(f"Найденное решение: solution")
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p10t():
    answer = f"""
## 10. Модифицированный метод Ньютона для систем нелинейных уравнений

### Теоретическое описание:
Это вариация классического метода Ньютона для систем, предназначенная для снижения вычислительной сложности.

**Основная идея:**
Матрица Якоби `J`, как и производная в одномерном случае, вычисляется не на каждой итерации, а только один раз в начальной точке `X0`. Это значительно упрощает вычисления, так как сложная операция решения СЛАУ (например, методом Гаусса) выполняется для одной и той же матрицы на всех шагах.

Итерационный процесс включает решение СЛАУ:
`J(X0) * ΔX_k = -F(X_k)`
И обновление приближения:
`X_k+1 = X_k + ΔX_k`

**Преимущества:**
-   Значительно снижает вычислительные затраты на каждой итерации.

**Недостатки:**
-   Скорость сходимости падает с квадратичной до линейной.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p11t():
    answer = f"""
## 11. Линейная интерполяция

### Теоретическое описание:
Линейная интерполяция — это простейший вид полиномиальной интерполяции, при котором для нахождения промежуточного значения используется линейный полином (прямая линия).

**Основная идея:**
Если известны значения функции в двух точках `(x_i, y_i)` и `(x_i+1, y_i+1)`, то для любой точки `x` между `x_i` и `x_i+1` значение `y` можно найти, проведя прямую через эти две точки.

Формула выводится из уравнения прямой, проходящей через две точки:
`(y - y_i) / (x - x_i) = (y_i+1 - y_i) / (x_i+1 - x_i)`

Отсюда, формула для интерполированного значения `y`:
`y = y_i + (y_i+1 - y_i) * (x - x_i) / (x_i+1 - x_i)`

**Геометрическая интерпретация:**
График исходной функции на отрезке `[x_i, x_i+1]` заменяется отрезком прямой линии (хордой).

**Преимущества:**
-   Простота реализации и низкая вычислительная сложность.

**Недостатки:**
-   Низкая точность, особенно для сильно нелинейных функций.
-   Производная интерполирующей функции является кусочно-постоянной, что означает наличие "изломов" в узловых точках.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p11():
    answer = f"""
def linear_interpolation(x_values, y_values, x_new):
    '''
    Линейная интерполяция.

    Параметры:
        x_values, y_values (list or np.array): Координаты известных точек.
        x_new (float): Точка, в которой нужно найти значение.
    '''
    # Поиск нужного интервала
    for i in range(len(x_values) - 1):
        if x_values[i] <= x_new <= x_values[i+1]:
            # Формула линейной интерполяции
            y_new = y_values[i] + (y_values[i+1] - y_values[i]) * \\
                    (x_new - x_values[i]) / (x_values[i+1] - x_values[i])
            return y_new
    return None # Если x_new вне диапазона

# Пример использования:
# x = [0, 1, 2]
# y = [1, 3, 2]
# y_hat = linear_interpolation(x, y, 1.5)
# print(f'Интерполированное значение y при x = 1.5: y_hat')
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p12():
    answer = f"""
def lagrange_polynomial_python(x_values, y_values, x_new):
    '''
    Вычисление значения интерполяционного многочлена Лагранжа.
    '''
    n = len(x_values)
    result = 0.0

    for i in range(n):
        # Вычисляем i-й базисный полином l_i(x_new)
        term = y_values[i]
        for j in range(n):
            if i != j:
                term = term * (x_new - x_values[j]) / (x_values[i] - x_values[j])
        result += term

    return result

# Пример использования:
# x = [0, 1, 2]
# y = [1, 3, 2]
# y_hat = lagrange_polynomial_python(x, y, 1.5)
# print(f'Интерполированное значение y при x = 1.5: y_hat')"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p12t():
    answer = f"""
## 12. Интерполяционный многочлен Лагранжа

### Теоретическое описание:
Многочлен Лагранжа — это метод глобальной полиномиальной интерполяции. Для `n+1` точки он строит единственный полином степени не выше `n`, который проходит через все эти точки.

**Основная идея:**
Многочлен `L(x)` строится как сумма базисных полиномов Лагранжа `l_i(x)`, каждый из которых умножается на соответствующее значение `y_i`.
`L(x) = sum_i=0 to n y_i * l_i(x)`
Каждый базисный полином `l_i(x)` обладает свойством `l_i(x_j) = δ_ij` (символ Кронекера).

**Недостатки:**
-   Высокая вычислительная сложность.
-   **Феномен Рунге:** При интерполяции на равномерной сетке по краям интервала могут возникать сильные осцилляции.
-   При добавлении нового узла весь полином нужно пересчитывать заново.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p13():
    answer = f"""
def _solve_tridiagonal_system(a, b, c, d):
    \"\"\"
    Решает СЛАУ с трехдиагональной матрицей методом прогонки.
    a - поддиагональ, b - главная диагональ, c - наддиагональ, d - правая часть.
    \"\"\"
    n = len(d)
    c_p = [0] * n
    d_p = [0] * n
    x = [0] * n

    # Прямой ход
    c_p[0] = c[0] / b[0]
    d_p[0] = d[0] / b[0]
    for i in range(1, n):
        m = 1.0 / (b[i] - a[i] * c_p[i-1])
        c_p[i] = c[i] * m
        d_p[i] = (d[i] - a[i] * d_p[i-1]) * m

    # Обратный ход
    x[n-1] = d_p[n-1]
    for i in range(n-2, -1, -1):
        x[i] = d_p[i] - c_p[i] * x[i+1]

    return x

def cubic_spline_coeffs_python(x, y):
    \"\"\"
    Вычисляет коэффициенты для естественного кубического сплайна.
    \"\"\"
    n = len(x) - 1
    h = [x[i+1] - x[i] for i in range(n)]

    # Составляем СЛАУ для вторых производных (c_i)
    # Матрица будет трехдиагональной
    A_diag = [2 * (h[i] + h[i+1]) for i in range(n-1)]
    A_upper = h[1:-1]
    A_lower = h[1:-1]

    B = [6 * ((y[i+2] - y[i+1]) / h[i+1] - (y[i+1] - y[i]) / h[i]) for i in range(n-1)]

    # Решаем систему для c_1, ..., c_n-1
    # c_0 и c_n равны 0 для естественного сплайна
    c_internal = _solve_tridiagonal_system([0]+A_lower, A_diag, A_upper+[0], B)
    c_coeffs = [0] + c_internal + [0]

    a_coeffs = list(y)
    b_coeffs = [(y[i+1] - y[i]) / h[i] - h[i] * (c_coeffs[i+1] + 2*c_coeffs[i]) / 3 for i in range(n)]
    d_coeffs = [(c_coeffs[i+1] - c_coeffs[i]) / (3 * h[i]) for i in range(n)]

    return a_coeffs, b_coeffs, c_coeffs, d_coeffs

def cubic_spline_python(x_values, y_values, x_new):
    '''
    Вычисляет значение кубического сплайна в точке x_new.
    '''
    a, b, c, d = cubic_spline_coeffs_python(x_values, y_values)

    # Поиск интервала
    for i in range(len(x_values) - 1):
        if x_values[i] <= x_new <= x_values[i+1]:
            dx = x_new - x_values[i]
            return a[i] + b[i] * dx + c[i] * dx**2 + d[i] * dx**3

    return None

# Пример использования:
# x = [0, 1, 2, 3]
# y = [0, 1, 0, 1]
# y_hat = cubic_spline_python(x, y, 1.5)
# print(f'Интерполированное значение y при x = 1.5: y_hat')
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p13t():
    answer = f"""
## 13. Кубическая сплайн-интерполяция

### Теоретическое описание:
Сплайн-интерполяция — это метод кусочно-полиномиальной интерполяции. Кубический сплайн наиболее распространен. На каждом отрезке `[x_i, x_i+1]` функция аппроксимируется своим кубическим полиномом `S_i(x)`.

**Основная идея:**
Коэффициенты полиномов подбираются так, чтобы в узловых точках (`x_i`) были непрерывны значения, а также первая и вторая производные. Это обеспечивает гладкость.

`S_i(x) = a_i + b_i(x-x_i) + c_i(x-x_i)^2 + d_i(x-x_i)^3`

Нахождение коэффициентов `a, b, c, d` сводится к решению системы линейных уравнений с трехдиагональной матрицей.

**Преимущества:**
-   Обеспечивает гладкую интерполяцию без осцилляций.
-   Высокая точность.

**Недостатки:**
-   Сложная реализация "с нуля".
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p14():
    answer = f"""
def naive_matrix_multiplication_python(A, B):
    '''
    Наивное перемножение матриц A и B.
    Матрицы представлены как списки списков.
    '''
    m = len(A)
    n = len(A[0])
    p = len(B[0])

    if n != len(B):
        raise ValueError("Несовместимые размеры матриц.")

    # Инициализация результирующей матрицы нулями
    C = [[0 for _ in range(p)] for _ in range(m)]

    for i in range(m):
        for j in range(p):
            for k in range(n):
                C[i][j] += A[i][k] * B[k][j]

    return C

# Пример использования:
# A = [[1, 2], [3, 4]]
# B = [[5, 6], [7, 8]]
# C = naive_matrix_multiplication_python(A, B)
# print("Результат перемножения:")
# print(C)
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p14t():
    answer = f"""
## 14. Наивный алгоритм перемножения матриц

### Теоретическое описание:
Это базовый алгоритм перемножения двух матриц A (размера m x n) и B (размера n x p), основанный непосредственно на определении матричного умножения.

**Основная идея:**
Каждый элемент `C_ij` результирующей матрицы `C` (размера m x p) вычисляется как скалярное произведение `i`-й строки матрицы `A` на `j`-й столбец матрицы `B`.

Формула для элемента `C_ij`:
`C_ij = sum_k=0 to n-1 A_ik * B_kj`

**Алгоритм:**
Для вычисления результирующей матрицы `C` используются три вложенных цикла.

**Сложность:** O(n³) для квадратных матриц.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p15():
    answer = f"""
def strassen_multiplication(A, B):
    \"\"\"
    Перемножение матриц A и B алгоритмом Штрассена.
    Предполагается, что матрицы квадратные и их размер - степень двойки.
    \"\"\"
    n = A.shape[0]

    # Базовый случай рекурсии
    if n <= 2:
        # Используем наивный метод для маленьких матриц
        m, n_a = A.shape
        n_b, p = B.shape
        C = np.zeros((m, p))
        for i in range(m):
            for j in range(p):
                for k in range(n_a):
                    C[i, j] += A[i, k] * B[k, j]
        return C

    # Разделение матриц на подблоки
    mid = n // 2
    A11, A12 = A[:mid, :mid], A[:mid, mid:]
    A21, A22 = A[mid:, :mid], A[mid:, mid:]
    B11, B12 = B[:mid, :mid], B[:mid, mid:]
    B21, B22 = B[mid:, :mid], B[mid:, mid:]

    # 7 рекурсивных вызовов по формулам Штрассена
    P1 = strassen_multiplication(A11 + A22, B11 + B22)
    P2 = strassen_multiplication(A21 + A22, B11)
    P3 = strassen_multiplication(A11, B12 - B22)
    P4 = strassen_multiplication(A22, B21 - B11)
    P5 = strassen_multiplication(A11 + A12, B22)
    P6 = strassen_multiplication(A21 - A11, B11 + B12)
    P7 = strassen_multiplication(A12 - A22, B21 + B22)

    # Комбинирование результатов
    C11 = P1 + P4 - P5 + P7
    C12 = P3 + P5
    C21 = P2 + P4
    C22 = P1 - P2 + P3 + P6

    # Сборка итоговой матрицы
    C = np.vstack((np.hstack((C11, C12)), np.hstack((C21, C22))))

    return C

# Пример использования:
# n_size = 16 # Размер должен быть степенью двойки
# A = np.random.rand(n_size, n_size)
# B = np.random.rand(n_size, n_size)
# C_strassen = strassen_multiplication(A, B)
# print("Результат (Штрассен):")
# print(C_strassen)
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p15t():
    answer = f"""
## 15. Алгоритм Штрассена для перемножения матриц

### Теоретическое описание:
Алгоритм Штрассена — это рекурсивный алгоритм для быстрого перемножения матриц, который асимптотически быстрее наивного алгоритма.

**Основная идея:**
Алгоритм основан на методе "разделяй и властвуй". Две исходные квадратные матрицы `A` и `B` размера n x n (где n - степень двойки) разбиваются на четыре подматрицы размера (n/2) x (n/2) каждая:
`A = [[A11, A12], [A21, A22]]`
`B = [[B11, B12], [B21, B22]]`

Вместо 8 рекурсивных умножений (как в наивном блочном методе) Штрассен показал, что можно обойтись всего 7 умножениями и дополнительными операциями сложения/вычитания матриц. Вычисляются 7 промежуточных матриц `P1, ..., P7`, из которых затем комбинируются блоки результирующей матрицы `C`.

**Сложность:**
-   **Временная сложность:** O(n^log₂(7)) ≈ O(n^2.807). Это лучше, чем O(n³) у наивного алгоритма.
-   На практике алгоритм становится эффективнее наивного только для матриц достаточно большого размера из-за накладных расходов на рекурсию и сложение/вычитание.

**Недостатки:**
-   Сложнее в реализации.
-   Может быть менее численно устойчивым из-за большего числа сложений и вычитаний.
-   Наиболее простая реализация требует, чтобы размер матриц был степенью двойки.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p16():
    answer = f"""
def characteristic_poly_2x2(A):
    \"\"\"
    Находит собственные значения матрицы 2x2 через корни характ. многочлена.
    \"\"\"
    if A.shape != (2, 2):
        raise ValueError("Матрица должна быть размера 2x2")

    # Характеристический многочлен: λ^2 - tr(A)λ + det(A) = 0
    # где tr(A) - след матрицы (сумма диагональных элементов)

    tr_A = A[0, 0] + A[1, 1]
    det_A = A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0]

    # Решение квадратного уравнения: a=1, b=-tr_A, c=det_A
    discriminant = tr_A**2 - 4 * det_A

    if discriminant >= 0:
        lambda1 = (tr_A + discriminant**0.5) / 2
        lambda2 = (tr_A - discriminant**0.5) / 2
    else:
        # Комплексные корни
        lambda1 = (tr_A + np.sqrt(discriminant)) / 2
        lambda2 = (tr_A - np.sqrt(discriminant)) / 2

    return lambda1, lambda2

# Пример использования:
# A = np.array([[4, 1], [2, 3]])
# eigenvalues = characteristic_poly_2x2(A)
# print(f'Собственные значения: eigenvalues')
# print(f'Проверка с помощью numpy: np.linalg.eigvals(A)')
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p16t():
    answer = f"""
## 16. Вычисление собственных значений через характеристический многочлен

### Теоретическое описание:
Это классический аналитический метод нахождения собственных значений квадратной матрицы `A`. Собственные значения `λ` являются корнями **характеристического уравнения**:
`det(A - λI) = 0`
где `I` — единичная матрица, а `det` — определитель.

**Основная идея:**
1.  **Построение многочлена:** Выражение `det(A - λI)` представляет собой многочлен степени `n` относительно `λ`, который называется характеристическим многочленом.
2.  **Нахождение корней:** Собственные значения `λ_1, ..., λ_n` являются корнями этого многочлена.

**Алгоритм:**
1.  Сформировать матрицу `A - λI`.
2.  Вычислить её определитель, получив явное выражение для характеристического многочлена `p(λ)`.
3.  Найти корни многочлена `p(λ) = 0`.

**Преимущества:**
-   Дает точное аналитическое решение для малых матриц (2x2, 3x3).
-   Фундаментальный метод, лежащий в основе теории собственных значений.

**Недостатки:**
-   **Вычислительная сложность:** Нахождение коэффициентов многочлена через определитель очень затратно (O(n!)).
-   **Численная неустойчивость:** Корни многочлена очень чувствительны к малейшим изменениям его коэффициентов. Небольшие ошибки округления при вычислении коэффициентов могут привести к огромным ошибкам в найденных корнях (собственных значениях).
-   Из-за этих недостатков на практике этот метод почти не используется для матриц размером больше 4x4. Вместо него применяются итерационные методы (например, QR-алгоритм).
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p17():
    answer = f"""
def power_iteration_python(A, n_iter=100, tol=1e-6):
    \"\"\"
    Степенной метод для нахождения доминантного собственного значения и вектора.
    A - список списков.
    \"\"\"
    n = len(A)
    # Случайный начальный вектор
    x = [1.0] * n

    lambda_old = 0.0

    for _ in range(n_iter):
        # Умножение матрицы на вектор
        Ax = [0.0] * n
        for i in range(n):
            for j in range(n):
                Ax[i] += A[i][j] * x[j]

        # Нахождение нормы (для нормализации)
        norm_ax = 0
        for val in Ax:
            norm_ax += val**2
        norm_ax = norm_ax**0.5

        # Нормализация
        x_new = [val / norm_ax for val in Ax]

        # Оценка собственного значения (используя норму)
        lambda_new = norm_ax

        # Проверка сходимости
        if abs(lambda_new - lambda_old) < tol:
            break

        x = x_new
        lambda_old = lambda_new

    return lambda_new, x

# Пример использования:
# A_list = [[4, 1], [2, 3]]
# eigenvalue, eigenvector = power_iteration_python(A_list)
# print(f"Доминантное собственное значение: eigenvalue")
# print(f"Соответствующий собственный вектор: eigenvector")

"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p17t():
    answer = f"""
## 17. Степенной метод

### Теоретическое описание:
Степенной метод — это итерационный алгоритм для нахождения собственного значения с наибольшим модулем (доминантного собственного значения) и соответствующего ему собственного вектора.

**Основная идея:**
Метод основан на многократном умножении матрицы `A` на некоторый начальный вектор `x0`.
`x_k+1 = A * x_k`
При большом количестве итераций `k`, вектор `A^k * x0` будет стремиться к направлению собственного вектора, соответствующего доминантному собственному значению `λ1`.

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

**Алгоритм:**
1.  Выбрать произвольный начальный вектор `x0` (обычно с единичной нормой).
2.  На каждой итерации `k`:
    a. Вычислить `y_k+1 = A * x_k`.
    b. Нормализовать вектор: `x_k+1 = y_k+1 / ||y_k+1||`.
    c. Оценить собственное значение: `λ_1 ≈ (x_k^T * A * x_k) / (x_k^T * x_k)` (отношение Рэлея) или проще `λ_1 ≈ ||y_k+1||`.
3.  Повторять, пока изменения в векторе `x_k` или значении `λ_1` не станут пренебрежимо малы.

**Преимущества:**
-   Простота реализации.
-   Эффективен для больших разреженных матриц, так как требует только операции умножения матрицы на вектор.

**Недостатки:**
-   Находит только доминантное собственное значение.
-   Скорость сходимости зависит от отношения `|λ2/λ1|`, где `λ2` — следующее по величине собственное значение. Если `|λ1| ≈ |λ2|`, сходимость очень медленная.
-   Не сходится, если доминантных собственных значений несколько (например, комплексная пара).

"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p18():
    answer = f"""
def _solve_gauss_python(A, b):
    # (Код этой функции представлен в p10 для модифицированного метода Ньютона)
    # ...
    n = len(b)
    # Создаем расширенную матрицу
    M = [row[:] + [b_i] for row, b_i in zip(A, b)]

    # Прямой ход
    for i in range(n):
        pivot_row = i
        for k in range(i + 1, n):
            if abs(M[k][i]) > abs(M[pivot_row][i]):
                pivot_row = k
        M[i], M[pivot_row] = M[pivot_row], M[i]
        pivot = M[i][i]
        for j in range(i, n + 1):
            M[i][j] /= pivot
        for k in range(i + 1, n):
            factor = M[k][i]
            for j in range(i, n + 1):
                M[k][j] -= factor * M[i][j]

    # Обратный ход
    x = [0] * n
    for i in range(n - 1, -1, -1):
        x[i] = M[i][n]
        for j in range(i + 1, n):
            x[i] -= M[i][j] * x[j]
    return x

def inverse_iteration_python(A, sigma, n_iter=100, tol=1e-6):
    \"\"\"
    Степенной метод со сдвигом (обратная итерация) на чистом Python.
    A - список списков, sigma - сдвиг.
    \"\"\"
    n = len(A)
    # Создаем матрицу A - sigma*I
    A_s = [[(A[i][j] - sigma) if i == j else A[i][j] for j in range(n)] for i in range(n)]

    # Случайный начальный вектор
    x = [1.0] * n
    lambda_eigen = 0

    for _ in range(n_iter):
        # Решаем СЛАУ (A - σI) * y = x
        y = _solve_gauss_python(A_s, x)

        # Находим норму
        norm_y = 0
        for val in y:
            norm_y += val**2
        norm_y = norm_y**0.5

        # Нормализация
        x = [val / norm_y for val in y]

        # Оценка собственного значения
        mu = norm_y
        lambda_new = 1.0/mu + sigma

        if abs(lambda_new - lambda_eigen) < tol:
            break
        lambda_eigen = lambda_new

    return lambda_eigen, x

# Пример использования:
# A_list = [[4, 1], [2, 3]]
# # Ищем собственное значение, близкое к 2.8 (точное значение 2.0)
# eigenvalue, eigenvector = inverse_iteration_python(A_list, sigma=2.8)
# print(f"Собственное значение, близкое к 2.8: eigenvalue")


"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p18t():
    answer = f"""
## 18. Степенной метод со сдвигами (обратная итерация)

### Теоретическое описание:
Степенной метод со сдвигами, или обратная итерация, — это модификация степенного метода, позволяющая находить собственное значение, ближайшее к заданному числу `σ` (сдвигу).

**Основная идея:**
Если `λ_i` — собственные значения матрицы `A`, то `(λ_i - σ)` — собственные значения матрицы `(A - σI)`. Соответственно, `1 / (λ_i - σ)` являются собственными значениями обратной матрицы `(A - σI)⁻¹`.

Если выбрать сдвиг `σ` очень близко к искомому собственному значению `λ_j`, то `(λ_j - σ)` будет очень малым числом. Тогда `1 / (λ_j - σ)` станет очень большим — доминантным собственным значением для матрицы `(A - σI)⁻¹`.

Таким образом, применяя степенной метод к матрице `(A - σI)⁻¹`, мы находим её доминантное собственное значение `μ`, а искомое собственное значение `λ_j` матрицы `A` равно:
`λ_j = 1/μ + σ`

**Алгоритм:**
1.  Выбрать начальный вектор `x0` и сдвиг `σ`, близкий к искомому `λ_j`.
2.  На каждой итерации `k`:
    a. Решить СЛАУ `(A - σI) * y_k + 1 = x_k`. (Это эквивалентно умножению на `(A - σI)⁻¹`, но численно гораздо устойчивее).
    b. Нормализовать вектор: `x_k + 1 = y_k+1 / ||y_k+1||`.
3.  Повторять до сходимости.

**Преимущества:**
-   Позволяет находить не только доминантное, но и любое другое собственное значение, если известно его примерное положение.
-   Очень быстрая сходимость, если сдвиг `σ` выбран удачно.

**Недостатки:**
-   Требует решения СЛАУ на каждом шаге, что является вычислительно дорогой операцией.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p19t():
    answer = f"""
## 19. Метод вращений (метод Якоби)

### Теоретическое описание:
Метод вращений Якоби — это итерационный алгоритм для нахождения всех собственных значений и собственных векторов **симметричной** матрицы.

**Основная идея:**
Метод основан на последовательном "занулении" внедиагональных элементов матрицы с помощью преобразований подобия с ортогональными матрицами. В качестве таких матриц используются **матрицы вращения Гивенса**.

На каждой итерации находится самый большой по модулю внедиагональный элемент `a_ij`. Затем строится матрица вращения `R(i, j, θ)` так, чтобы после преобразования `A_k+1 = R^T * A_k * R`, элемент на позиции `(i, j)` (и `(j, i)`) стал равен нулю.

Этот процесс повторяется. Так как сумма квадратов внедиагональных элементов на каждом шаге уменьшается, матрица `A_k` итерационно стремится к диагональной.

**Алгоритм:**
1.  Начать с исходной симметричной матрицы `A_0 = A`.
2.  На итерации `k`:
    a. Найти внедиагональный элемент `a_ij` с максимальным абсолютным значением.
    b. Вычислить угол поворота `θ` по формуле, которая занулит этот элемент.
    c. Построить матрицу вращения `R_k`.
    d. Выполнить преобразование: `A_k+1 = R_k^T * A_k * R_k`.
3.  Повторять до тех пор, пока все внедиагональные элементы не станут достаточно малы.
4.  Собственные значения будут находиться на диагонали итоговой матрицы, а собственные векторы — в столбцах произведения всех матриц вращения `R_1 * R_2 * ...`.

**Преимущества:**
-   Надежный и простой для понимания метод для симметричных матриц.
-   Находит сразу все собственные значения и векторы.

**Недостатки:**
-   Применим только к симметричным матрицам.
-   Медленнее, чем QR-алгоритм для больших матриц.


"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p19():
    answer = f"""
def jacobi_rotation_method(A, tol=1e-9, max_iter=100):
    \"\"\"
    Находит все собственные значения и векторы симметричной матрицы A
    методом вращений Якоби.
    \"\"\"
    n = A.shape[0]
    A_k = np.copy(A)
    V = np.identity(n) # Матрица собственных векторов

    for _ in range(max_iter):
        # Находим максимальный внедиагональный элемент
        max_val = 0
        p, q = 0, 1
        for i in range(n):
            for j in range(i + 1, n):
                if abs(A_k[i, j]) > max_val:
                    max_val = abs(A_k[i, j])
                    p, q = i, j

        if max_val < tol:
            break

        # Вычисляем угол вращения
        app = A_k[p, p]
        aqq = A_k[q, q]
        apq = A_k[p, q]

        if app == aqq:
            theta = np.pi / 4
        else:
            theta = 0.5 * math.atan(2 * apq / (app - aqq))

        c = math.cos(theta)
        s = math.sin(theta)

        # Создаем матрицу вращения
        R = np.identity(n)
        R[p, p], R[q, q] = c, c
        R[p, q], R[q, p] = -s, s

        # Обновляем матрицу A и матрицу собственных векторов
        A_k = R.T @ A_k @ R
        V = V @ R

    eigenvalues = np.diag(A_k)
    eigenvectors = V

    return eigenvalues, eigenvectors

# Пример использования:
# A = np.array([[4, 2, 1], [2, 5, 3], [1, 3, 6]], dtype=float)
# eigvals, eigvecs = jacobi_rotation_method(A)
# print("Собственные значения:", eigvals)
# print("Собственные векторы:\\n", eigvecs)
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p20t():
    answer = f"""
## 20. QR-алгоритм

### Теоретическое описание:
QR-алгоритм — это один из самых эффективных и широко используемых итерационных методов для вычисления всех собственных значений (и векторов) матрицы.

**Основная идея:**
Алгоритм генерирует последовательность матриц A_k, которые подобны исходной матрице A и сходятся к верхнетреугольной (для общих матриц) или диагональной (для симметричных) форме.

**Базовый QR-алгоритм:**
1.  **Начальный шаг:** `A_0 = A`.
2.  **Итерация k:**
    a. Выполнить QR-разложение матрицы `A_k`: `A_k = Q_k * R_k`, где `Q_k` — ортогональная матрица, а `R_k` — верхнетреугольная.
    b. Вычислить следующую матрицу: `A_k+1 = R_k * Q_k`.

Поскольку `R_k = Q_k^T * A_k`, то `A_k+1 = Q_k^T * A_k * Q_k`, что является преобразованием подобия. Следовательно, все матрицы `A_k` имеют те же собственные значения, что и `A`. В пределе `A_k` сходится к **форме Шура** (верхнетреугольной), и собственные значения оказываются на её диагонали.

**Преимущества:**
-   Численно устойчив.
-   Сходится для широкого класса матриц.

**Недостатки:**
-   Базовая версия сходится медленно. На практике всегда используются улучшения: **сдвиги** для ускорения сходимости и предварительное **приведение к форме Хессенберга** для удешевления QR-разложения.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p20():
    answer = f"""
import numpy as np

# Часть 1: QR-разложение через отражения Хаусхолдера
#
def householder_reflection(a):
    '''
    Вычисляет вектор Хаусхолдера для вектора a.
    '''
    v = a.copy()
    v[0] += np.sign(a[0]) * np.linalg.norm(a)
    v /= np.linalg.norm(v)
    return v

def qr_decomposition(A):
    '''
    Выполняет QR-разложение матрицы A с использованием отражений Хаусхолдера.
    '''
    m, n = A.shape
    Q = np.identity(m)
    R = A.copy()

    for i in range(n - (m == n)):
        H = np.identity(m)
        # Применяем отражение к подматрице
        v = householder_reflection(R[i:, i])
        H[i:, i:] -= 2.0 * np.outer(v, v)
        R = H @ R
        Q = Q @ H.T
    return Q, R


# Часть 2: QR-алгоритм для поиска собственных значений
# Эта версия алгоритма включает итерации со сдвигом для ускорения сходимости.
#
def qr_algorithm_simple(A, max_iter=1000, tol=1e-9):
    '''
    Простой QR-алгоритм для поиска собственных значений.
    
    Аргументы:
    A: Квадратная матрица.
    max_iter: Максимальное количество итераций.
    tol: Точность для остановки (не используется в этой простой версии).
    
    Возвращает:
    Массив собственных значений.
    '''
    Ak = np.copy(A)

    for _ in range(max_iter):
        Q, R = qr_decomposition(Ak)
        Ak = R @ Q
    
    # Собственные значения - это диагональные элементы матрицы Ak
    eigenvalues = np.diag(Ak)
    return eigenvalues

# --- Пример использования ---

# Создадим матрицу A (ту же, что и в прошлый раз)
A = np.array([[6.0, 2.0, 1.0],
              [2.0, 3.0, 1.0],
              [1.0, 1.0, 1.0]])

# Найдем собственные значения с помощью QR-алгоритма из вашего файла
estimated_eigenvalues = qr_algorithm_simple(A, max_iter=50)


print("Исходная матрица A:")
print(A)
print("\nСобственные значения, найденные с помощью QR-алгоритма:")
# Сортируем для удобства сравнения
print(np.sort(estimated_eigenvalues))

# --- Проверка с помощью встроенной функции NumPy ---
correct_eigenvalues = np.linalg.eigvals(A)
print("\nСобственные значения, найденные с помощью numpy.linalg.eigvals (для проверки):")
print(np.sort(correct_eigenvalues))
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p21t():
    answer = f"""
## 21. Разложение Шура и Теорема Шура

### Теоретическое описание:
Разложение (или декомпозиция) Шура утверждает, что любую квадратную матрицу `A` с комплексными элементами можно представить в виде:
`A = U * T * U*`
где:
-   `U` — унитарная матрица (`U* * U = I`, где `U*` — эрмитово-сопряженная матрица). Для вещественных матриц `U` — ортогональная матрица (`U^T * U = I`).
-   `T` — верхнетреугольная матрица, называемая **формой Шура** матрицы `A`.

**Теорема Шура:**
Для любой квадратной матрицы `A` размера n x n существует унитарная матрица `U`, такая что `T = U* * A * U` является верхнетреугольной.

**Ключевые свойства:**
1.  **Собственные значения:** Собственные значения матрицы `A` совпадают с диагональными элементами её формы Шура `T`. Это следует из того, что `A` и `T` подобны.
2.  **Численная устойчивость:** Преобразования с унитарными/ортогональными матрицами численно устойчивы.
3.  **Связь с QR-алгоритмом:** QR-алгоритм — это итерационный способ построения разложения Шура. В ходе алгоритма матрица `A_k` сходится к форме Шура `T`, а произведение всех ортогональных матриц `Q_k` сходится к матрице `U`.

`T = lim_k->∞ A_k`
`U = lim_k->∞ (Q_0 * Q_1 * ... * Q_k)`

**Практическая реализация:**
Нахождение разложения Шура эквивалентно выполнению полного QR-алгоритма до сходимости.


"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p21():
    answer = f"""
import numpy as np

def gram_schmidt_qr(A):
    '''
    Вспомогательная функция: выполняет QR-разложение матрицы A
    с использованием процесса Грама-Шмидта.
    '''
    m, n = A.shape
    Q = np.zeros((m, n))
    R = np.zeros((n, n))

    for j in range(n):
        v = A[:, j]
        for i in range(j):
            R[i, j] = np.dot(Q[:, i].T, A[:, j])
            v = v - R[i, j] * Q[:, i]
            
        norm_v = np.sqrt(np.sum(v**2))
        R[j, j] = norm_v
        
        if R[j, j] > 1e-10: # Избегаем деления на ноль для линейно зависимых столбцов
             Q[:, j] = v / R[j, j]
        
    return Q, R

def schur_decomposition_via_qr(A, num_iterations=100):
    '''
    Вычисляет разложение Шура для матрицы A, используя QR-алгоритм.

    Аргументы:
    A: Квадратная numpy-матрица.
    num_iterations: Количество итераций.

    Возвращает:
    T: Верхняя треугольная матрица (матрица Шура).
    U: Ортогональная матрица.
    '''
    # Создаем копию, чтобы не изменять исходную матрицу
    Ak = np.copy(A)
    
    # Инициализируем U как единичную матрицу
    # U будет накапливать произведение всех матриц Qk
    U = np.identity(A.shape[0])

    for _ in range(num_iterations):
        # Выполняем QR-разложение для текущей матрицы Ak
        Qk, Rk = gram_schmidt_qr(Ak)
        
        # Обновляем Ak для следующей итерации
        Ak = Rk @ Qk
        
        # Накапливаем ортогональные преобразования
        U = U @ Qk
        
    # После достаточного числа итераций Ak сходится к T
    T = Ak
    
    return T, U

# --- Пример использования ---

# Возьмем ту же матрицу, что и в примере с scipy
A = np.array([
    [ 0.0,  5.0, -2.0],
    [-1.0,  6.0, -2.0],
    [-1.0,  1.0,  3.0]
])

# Выполняем разложение Шура с помощью нашего QR-алгоритма
T, U = schur_decomposition_via_qr(A, num_iterations=50)

print("Исходная матрица A:")
print(A)

print("\n----------------------------------")
print("Верхняя треугольная матрица T (полученная QR-алгоритмом):")
print(np.round(T, 4))

print("\n----------------------------------")
print("Ортогональная матрица U (полученная QR-алгоритмом):")
print(np.round(U, 4))

# --- Проверка результатов ---

# 1. Проверим, что A ≈ U * T * U^T
A_reconstructed = U @ T @ U.T
print("\n----------------------------------")
print("\nВосстановленная матрица A (U @ T @ U.T):")
print(np.round(A_reconstructed, 4))
print(f"\nРазложение корректно: np.allclose(A, A_reconstructed)")

# 2. Собственные значения на диагонали T
eigenvalues_from_T = np.diag(T)
print("\nСобственные значения (диагональ T):")
print(np.round(np.sort(eigenvalues_from_T), 4))
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p22():
    answer = f"""
import numpy as np

def gram_schmidt_qr(A):
    '''
    Выполняет QR-разложение матрицы A с использованием процесса Грама-Шмидта.
    Не использует специфические функции, кроме базовых операций numpy.

    Аргументы:
    A: numpy-матрица (m x n), которую нужно разложить.

    Возвращает:
    Q: Ортогональная матрица (m x n).
    R: Верхняя треугольная матрица (n x n).
    '''
    # Получаем размеры исходной матрицы
    m, n = A.shape
    
    # Инициализируем пустые матрицы Q и R
    Q = np.zeros((m, n))
    R = np.zeros((n, n))

    # Итерируемся по столбцам матрицы A
    for j in range(n):
        # Текущий столбец для ортогонализации
        v = A[:, j] 
        
        # Вычитаем проекции на предыдущие ортогональные векторы
        for i in range(j):
            # R[i, j] = q_i^T * a_j
            R[i, j] = np.dot(Q[:, i].T, A[:, j])
            # v = v - (q_i^T * a_j) * q_i
            v = v - R[i, j] * Q[:, i]
            
        # Вычисляем норму (длину) нового ортогонального вектора.
        # Это будет диагональный элемент в R.
        norm_v = np.sqrt(np.sum(v**2))
        R[j, j] = norm_v
        
        # Нормализуем столбец Q
        Q[:, j] = v / norm_v
        
    return Q, R

# --- Пример использования ---
# Возьмем матрицу из примера на стр. 317 вашего документа
A = np.array([[4.0, 1.0, 2.0],
              [1.0, 3.0, 0.0],
              [2.0, 0.0, 5.0]])

# Выполняем QR-разложение
Q, R = gram_schmidt_qr(A)

print("Исходная матрица A:")
print(A)

print("\n--- Результаты разложения ---")

print("\nОртогональная матрица Q:")
print(np.round(Q, 4)) # Округляем для наглядности

print("\nВерхняя треугольная матрица R:")
print(np.round(R, 4))

# --- Проверка результатов ---

print("\n--- Проверка ---")
# 1. Восстанавливаем A, перемножая Q и R. Результат должен быть равен A.
A_reconstructed = np.dot(Q, R)
print("\nВосстановленная матрица A (Q * R):")
print(np.round(A_reconstructed, 4))


# 2. Проверяем ортогональность Q. Произведение Q^T * Q должно быть единичной матрицей.
identity_matrix = np.dot(Q.T, Q)
print("\nПроверка ортогональности Q (Q.T * Q):")
print(np.round(identity_matrix, 4)) 
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p22t():
    answer = f"""
Теория QR-разложения
QR-разложение (или QR-факторизация) — это представление матрицы A в виде произведения двух других матриц: ортогональной матрицы Q и верхней треугольной матрицы R.

A=QR

Составляющие разложения
Ортогональная матрица (Q):

Ее столбцы представляют собой ортонормированные векторы, что означает, что они взаимно перпендикулярны и имеют длину, равную единице.
Ключевое свойство такой матрицы заключается в том, что ее транспонированная версия является и ее обратной матрицей: Q 
T
 Q=I.
С геометрической точки зрения, умножение на матрицу Q соответствует повороту или отражению, при котором сохраняются длины векторов и углы между ними.
Верхняя треугольная матрица (R):

Это матрица, в которой все элементы, расположенные ниже главной диагонали, равны нулю.
Метод вычисления: Процесс Грама-Шмидта
Одним из классических способов нахождения матриц Q и R является процесс ортогонализации Грама-Шмидта.

Концептуально, процесс состоит из следующих шагов:

Инициализация: Берется первый столбец матрицы A. Он нормализуется (приводится к единичной длине) и становится первым столбцом матрицы Q.
Ортогонализация: Берется следующий столбец матрицы A и из него вычитается его проекция на все уже найденные столбцы матрицы Q. Это делает новый вектор перпендикулярным ко всем предыдущим.
Нормализация: Полученный ортогональный вектор нормализуется, и результат становится следующим столбцом матрицы Q.
Повторение: Шаги 2 и 3 повторяются для всех оставшихся столбцов матрицы A.
Матрица R затем может быть вычислена по формуле R=Q 
T
 A.

Применение
Как указано в вашем курсе, основное применение QR-разложения — это его использование в качестве фундаментального шага в QR-алгоритме для поиска собственных значений и собственных векторов матрицы
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p23t():
    answer = f"""
## 23. Метод Эйлера (явный)

### Теоретическое описание:
Это простейший численный метод для решения обыкновенных дифференциальных уравнений (ОДУ) с начальным условием (задачи Коши).
`y' = f(x, y)`, `y(x0) = y0`

**Основная идея:**
На каждом шаге `h` мы аппроксимируем решение, двигаясь по касательной к графику функции в текущей точке. Наклон касательной равен значению производной `f(x, y)`.

Итерационная формула:
`y_n + 1 = y_n + h * f(x_n, y_n)`

**Геометрическая интерпретация:**
Решение аппроксимируется ломаной линией, где каждый сегмент является отрезком касательной к истинному решению.

**Преимущества:**
-   Крайне прост в реализации.

**Недостатки:**
-   **Низкая точность:** Метод имеет первый порядок точности, ошибка на каждом шаге пропорциональна `h^2`, а глобальная ошибка — `h`.
-   **Условная устойчивость:** Для многих задач (например, `y' = -ky`) метод устойчив только при достаточно малом шаге `h`. При большом `h` численное решение может неограниченно расти, даже если аналитическое затухает.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p23():
    answer = f"""
def euler_method_python(f, y0, t0, t_end, h):
    '''
    Явный метод Эйлера для решения ОДУ y' = f(t, y).
    '''
    t_values = [t0]
    y_values = [y0]

    t = t0
    y = y0

    while t < t_end:
        y = y + h * f(t, y)
        t = t + h
        t_values.append(t)
        y_values.append(y)

    return t_values, y_values

# Пример использования:
# Решим y' = -y, y(0) = 1
# f = lambda t, y: -y
#
# t_vals, y_vals = euler_method_python(f, 1.0, 0, 5, 0.1)
#
# import matplotlib.pyplot as plt
# plt.plot(t_vals, y_vals, 'b-o', label='Метод Эйлера')
# plt.plot(t_vals, [math.exp(-t) for t in t_vals], 'r--', label='Точное решение')
# plt.legend()
# plt.show()
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p24():
    answer = f"""
def euler_predictor_corrector(f, y0, t0, t_end, h):
    \"\"\"
    Метод предиктора-корректора Эйлера (метод Хойна).
    \"\"\"
    t_values = [t0]
    y_values = [y0]

    t = t0
    y = y0

    while t < t_end:
        # Предиктор
        f_n = f(t, y)
        y_star = y + h * f_n

        # Корректор
        f_n_plus_1_star = f(t + h, y_star)
        y = y + (h / 2.0) * (f_n + f_n_plus_1_star)

        t = t + h
        t_values.append(t)
        y_values.append(y)

    return t_values, y_values

# Пример использования:
# Решим y' = -y, y(0) = 1
# f = lambda t, y: -y
#
# t_vals_pc, y_vals_pc = euler_predictor_corrector(f, 1.0, 0, 5, 0.1)
#
# import matplotlib.pyplot as plt
# plt.plot(t_vals_pc, y_vals_pc, 'b-o', label='Предиктор-корректор')
# plt.plot(t_vals_pc, [math.exp(-t) for t in t_vals_pc], 'r--', label='Точное решение')
# plt.legend()
# plt.show()
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p24t():
    answer = f"""
## 24. Метод предиктора-корректора (улучшенный метод Эйлера, метод Хойна)

### Теоретическое описание:
Это метод второго порядка точности, который улучшает простой метод Эйлера за счет двухэтапного вычисления на каждом шаге.

**Основная идея:**
1.  **Предиктор (прогноз):** Сначала делается "грубый" шаг методом Эйлера для оценки значения в следующей точке `y*_n+1`.
    `y*_n+1 = y_n + h * f(x_n, y_n)`
2.  **Корректор (уточнение):** Затем вычисляется средний наклон на отрезке `[x_n, x_n+1]` как среднее арифметическое наклонов в начальной точке (`f_n`) и в "предсказанной" конечной точке (`f*_n+1`). Это среднее значение используется для окончательного шага.
    `y_n+1 = y_n + (h/2) * (f(x_n, y_n) + f(x_n+1, y*_n+1))`

**Преимущества:**
-   **Выше точность:** Второй порядок точности (глобальная ошибка O(h²)), что значительно точнее простого метода Эйлера.
-   **Лучше устойчивость:** Область устойчивости метода больше, чем у явного метода Эйлера.

**Недостатки:**
-   Требует двух вычислений функции `f` на каждом шаге, что вдвое затратнее метода Эйлера.

"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p25t():
    answer = f"""
## 25. Метод Рунге-Кутты 4-го порядка (РК4)

### Теоретическое описание:
Это один из самых популярных и широко используемых численных методов для решения обыкновенных дифференциальных уравнений (ОДУ) вида `y' = f(x, y)` с начальным условием `y(x0) = y0`. Он относится к классу одношаговых явных методов.

**Основная идея:**
Вместо одного вычисления наклона (как в методе Эйлера), РК4 использует четыре "пробных" вычисления наклона внутри каждого шага `h` для получения более точной аппроксимации. Эти наклоны затем взвешиваются и усредняются.

**Алгоритм:**
Для перехода от точки `(x_n, y_n)` к `(x_n+1, y_n+1)` вычисляются четыре коэффициента:
`k1 = f(x_n, y_n)`
`k2 = f(x_n + h/2, y_n + h*k1/2)`
`k3 = f(x_n + h/2, y_n + h*k2/2)`
`k4 = f(x_n + h, y_n + h*k3)`

Затем вычисляется следующее значение `y_n+1`:
`y_n+1 = y_n + (h/6) * (k1 + 2*k2 + 2*k3 + k4)`

**Преимущества:**
-   Высокая точность (четвертый порядок), что позволяет использовать больший шаг `h` по сравнению с методами низшего порядка при той же точности.
-   Численная устойчивость для широкого круга задач.

**Недостатки:**
-   Требует четыре вычисления правой части `f(x, y)` на каждом шаге, что может быть затратно, если функция `f` сложная.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p25():
    answer = f"""
def runge_kutta_4(f, y0, t_span, h):
    '''
    Метод Рунге-Кутты 4-го порядка для решения ОДУ y' = f(t, y).

    Параметры:
        f (function): Функция правой части ОДУ, f(t, y).
        y0 (float): Начальное условие.
        t_span (tuple): Интервал времени (t_start, t_end).
        h (float): Шаг интегрирования.
    '''
    t_values = [t_span[0]]
    y_values = [y0]

    t = t_span[0]
    y = y0

    while t < t_span[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 = y + (h/6) * (k1 + 2*k2 + 2*k3 + k4)
        t = t + h

        t_values.append(t)
        y_values.append(y)

    return t_values, y_values

# Пример использования:
# Решим y' = y, y(0) = 1. Точное решение y(t) = e^t.
# f = lambda t, y: y
# t, y = runge_kutta_4(f, 1.0, (0, 1), 0.1)
# print(f"Приближенное значение в t=1: y[-1]")
# print(f"Точное значение в t=1: 2.71828")

"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p26t():
    answer = f"""
## 26. Методы Адамса-Башфорта

### Теоретическое описание:
Это семейство явных многошаговых методов для решения ОДУ. В отличие от одношаговых методов (как Рунге-Кутта), они используют информацию из нескольких предыдущих точек для вычисления следующей.

**Основная идея:**
Метод основан на аппроксимации функции `f(x,y)` в интегральной форме уравнения `y_n+1 = y_n + ∫[x_n, x_n+1] f(x, y(x)) dx` с помощью интерполяционного полинома, построенного по уже известным точкам `(x_n-k, f_n-k)`.

**Формулы (для равноотстоящих узлов с шагом h):**
- **2-шаговый:** `y_n+1 = y_n + h/2 * (3*f_n - f_n-1)`
- **3-шаговый:** `y_n+1 = y_n + h/12 * (23*f_n - 16*f_n-1 + 5*f_n-2)`
- **4-шаговый:** `y_n+1 = y_n + h/24 * (55*f_n - 59*f_n-1 + 37*f_n-2 - 9*f_n-3)`
где `f_k = f(x_k, y_k)`.

**Проблема "старта":**
Для k-шагового метода необходимо знать `k` начальных значений `y_0, y_1, ..., y_k-1`. Поскольку дано только `y_0`, остальные "стартовые" значения обычно вычисляются другим методом, например, Рунге-Кутты.

**Преимущества:**
-   Вычислительно эффективны: требуется только одно вычисление `f(x, y)` на шаге.

**Недостатки:**
-   Проблема "старта".
-   Сложнее изменять шаг интегрирования в процессе вычислений.
-   Меньшая область устойчивости по сравнению с неявными методами.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p26():
    answer = f"""
def adams_bashforth_4(f, y0, t_span, h):
    '''
    4-шаговый метод Адамса-Башфорта.
    '''
    num_steps = int((t_span[1] - t_span[0]) / h)
    t = [t_span[0] + i*h for i in range(num_steps + 1)]
    y = [0.0] * (num_steps + 1)
    y[0] = y0

    # "Старт" с помощью РК4 для первых 3-х шагов
    for i in range(3):
        k1 = f(t[i], y[i])
        k2 = f(t[i] + h/2, y[i] + h*k1/2)
        k3 = f(t[i] + h/2, y[i] + h*k2/2)
        k4 = f(t[i] + h, y[i] + h*k3)
        y[i+1] = y[i] + (h/6) * (k1 + 2*k2 + 2*k3 + k4)

    # Основной цикл Адамса-Башфорта
    for i in range(3, num_steps):
        f_im3 = f(t[i-3], y[i-3])
        f_im2 = f(t[i-2], y[i-2])
        f_im1 = f(t[i-1], y[i-1])
        f_i   = f(t[i], y[i])

        y[i+1] = y[i] + (h/24) * (55*f_i - 59*f_im1 + 37*f_im2 - 9*f_im3)

    return t, y

# Пример использования:
# f = lambda t, y: y
# t, y = adams_bashforth_4(f, 1.0, (0, 1), 0.01)
# print(f"Приближенное значение в t=1: y[-1]")

"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p27t():
    answer = f"""
## 27. Методы Адамса-Мултона

### Теоретическое описание:
Это семейство **неявных** многошаговых методов. Они более устойчивы, чем явные методы Адамса-Башфорта, но требуют решения уравнения на каждом шаге.

**Основная идея:**
Как и в методах Башфорта, используется аппроксимация интеграла `∫f(x,y)dx`. Однако интерполяционный полином строится по точкам, включающим и будущую (еще неизвестную) точку `(x_n+1, f_n+1)`. Это делает метод неявным.

**Формулы (для равноотстоящих узлов с шагом h):**
- **2-шаговый (метод трапеций):** `y_n+1 = y_n + h/2 * (f_n+1 + f_n)`
- **3-шаговый:** `y_n+1 = y_n + h/12 * (5*f_n+1 + 8*f_n - f_n-1)`
- **4-шаговый:** `y_n+1 = y_n + h/24 * (9*f_n+1 + 19*f_n - 5*f_n-1 + f_n-2)`

**Методы "Предиктор-Корректор":**
Поскольку методы неявные, для нахождения `y_n+1` (которое входит в `f_n+1`) нужно решать уравнение. На практике это делают с помощью схемы "предиктор-корректор":
1.  **Предиктор:** Явным методом (например, Адамса-Башфорта) вычисляется предварительное значение `y*_n+1`.
2.  **Корректор:** Это значение `y*_n+1` подставляется в правую часть формулы Адамса-Мултона для уточнения (коррекции) значения `y_n+1`.
`y_n+1 = y_n + h/24 * (9*f(x_n+1, y*_n+1) + 19*f_n - 5*f_n-1 + f_n-2)`

**Преимущества:**
-   Более высокая точность и лучшая устойчивость по сравнению с явными аналогами того же порядка.

"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p27():
    answer = f"""
def adams_predictor_corrector_4(f, y0, t_span, h):
    '''
    4-шаговый метод Адамса (Башфорт - предиктор, Мултон - корректор).
    '''
    num_steps = int((t_span[1] - t_span[0]) / h)
    t = [t_span[0] + i*h for i in range(num_steps + 1)]
    y = [0.0] * (num_steps + 1)
    y[0] = y0

    # Старт с помощью РК4
    for i in range(3):
        k1 = f(t[i], y[i])
        k2 = f(t[i] + h/2, y[i] + h*k1/2)
        k3 = f(t[i] + h/2, y[i] + h*k2/2)
        k4 = f(t[i] + h, y[i] + h*k3)
        y[i+1] = y[i] + (h/6) * (k1 + 2*k2 + 2*k3 + k4)

    for i in range(3, num_steps):
        f_im3 = f(t[i-3], y[i-3])
        f_im2 = f(t[i-2], y[i-2])
        f_im1 = f(t[i-1], y[i-1])
        f_i   = f(t[i], y[i])

        # Предиктор (Адамс-Башфорт 4)
        y_pred = y[i] + (h/24) * (55*f_i - 59*f_im1 + 37*f_im2 - 9*f_im3)

        # Корректор (Адамс-Мултон 3 - требует 3 точки)
        f_pred = f(t[i+1], y_pred)
        y[i+1] = y[i] + (h/24) * (9*f_pred + 19*f_i - 5*f_im1 + f_im2)

    return t, y

# Пример использования:
# f = lambda t, y: y
# t, y = adams_predictor_corrector_4(f, 1.0, (0, 1), 0.01)
# print(f"Приближенное значение в t=1: y[-1]")

"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p28t():
    answer = f"""
## 28. Дискретное преобразование Фурье (ДПФ) и обратное ДПФ

### Теоретическое описание:
ДПФ является аналогом непрерывного преобразования Фурье для дискретных сигналов (последовательностей чисел). Оно преобразует сигнал из временной/пространственной области в частотную.

**Основная идея:**
ДПФ разлагает конечную последовательность из `N` отсчетов `x[n]` на `N` комплексных экспонент (синусоид и косинусоид) с различными частотами.

**Прямое ДПФ:**
Вычисляет `N` комплексных коэффициентов `X[k]`, которые представляют амплитуду и фазу каждой частотной компоненты:
`X[k] = sum_n=0 to N-1 x[n] * exp(-j * 2 * pi * k * n / N)`
где `k = 0, 1, ..., N-1`, а `j` — мнимая единица.

**Обратное ДПФ (ОДПФ):**
Позволяет восстановить исходный сигнал из его частотного представления:
`x[n] = (1/N) * sum_k=0 to N-1 X[k] * exp(j * 2 * pi * k * n / N)`

**Свойства:**
-   **Периодичность:** Спектр, полученный с помощью ДПФ, является периодическим с периодом N.
-   **Линейность, сдвиг, свёртка:** Обладает свойствами, аналогичными непрерывному преобразованию, но в дискретной форме (например, круговая свёртка вместо линейной).

**Недостатки:**
-   Прямое вычисление "в лоб" требует O(N²) комплексных умножений и сложений, что очень медленно для больших `N`.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p28():
    answer = f"""
import numpy as np
import matplotlib.pyplot as plt
import math # Ваши функции используют библиотеку math

# --- Ваши функции ДПФ и ОДПФ ---
def dft(x):
    '''
    Дискретное преобразование Фурье.
    '''
    N = len(x)
    X = np.zeros(N, dtype=np.complex128) # Используем numpy массив для удобства
    for k in range(N):
        sum_val = 0
        for n in range(N):
            angle = -2 * np.pi * k * n / N
            # Важно: math.exp не работает с комплексными числами, нужна cmath или numpy
            # Используем np.exp, он универсален
            sum_val += x[n] * np.exp(1j * angle)
        X[k] = sum_val
    return X

def idft(X):
    '''
    Обратное дискретное преобразование Фурье.
    '''
    N = len(X)
    x = np.zeros(N, dtype=np.complex128) # Используем numpy массив
    for n in range(N):
        sum_val = 0
        for k in range(N):
            angle = 2 * np.pi * k * n / N
            sum_val += X[k] * np.exp(1j * angle)
        x[n] = sum_val / N
    return x

# --- РЕШЕНИЕ ЗАДАЧИ ---

# 1. Задаем параметры сигнала
duration = 2.0  # Длительность сигнала в секундах (чтобы увидеть несколько периодов)
sampling_rate = 100  # Частота дискретизации в Гц (должна быть > 2 * f_max = 2 * 3 = 6 Гц)
n_samples = int(duration * sampling_rate) # Количество точек
t = np.linspace(0, duration, n_samples, endpoint=False) # Временная шкала

# 2. Создаем исходный сигнал по формуле f(t) = sin(2πt) + cos(6πt)
# f1 = 1 Гц, f2 = 3 Гц
f1 = 1
f2 = 3
original_signal = np.sin(2 * np.pi * f1 * t) + np.cos(2 * np.pi * f2 * t)

# 3. Выполняем дискретное преобразование Фурье
dft_coeffs = dft(original_signal)

# 4. Фильтрация: удаляем компоненты с частотой выше 5 Гц
# Сначала создаем шкалу частот, соответствующую коэффициентам ДПФ
freqs = np.fft.fftfreq(n_samples, d=1/sampling_rate)

# Копируем коэффициенты, чтобы не изменять оригинал
filtered_coeffs = dft_coeffs.copy()

# Находим индексы частот, которые нужно обнулить (> 5 Гц)
# abs() нужен, так как freqs содержит и отрицательные частоты
indices_to_remove = np.where(np.abs(freqs) > 5)

# Обнуляем эти компоненты
filtered_coeffs[indices_to_remove] = 0

# 5. Выполняем обратное преобразование Фурье для получения восстановленного сигнала
reconstructed_signal_complex = idft(filtered_coeffs)
# Берем только действительную часть, т.к. мнимая должна быть близка к нулю
reconstructed_signal = reconstructed_signal_complex.real

# --- Графики ---
plt.figure(figsize=(15, 10))

# График 1: Исходный и восстановленный сигналы
plt.subplot(2, 1, 1)
plt.plot(t, original_signal, label='Исходный сигнал (1 Гц + 3 Гц)')
plt.plot(t, reconstructed_signal, '--', label='Восстановленный сигнал', linewidth=2)
plt.title('Сравнение сигналов')
plt.xlabel('Время (t), с')
plt.ylabel('Амплитуда')
plt.grid(True, alpha=0.3)
plt.legend()

# График 2: Спектр (используем plt.stem)
plt.subplot(2, 1, 2)
positive_freq_indices = np.where((freqs >= 0) & (freqs <= 10))  # Ограничим диапазон

# Настройки для стебельчатой диаграммы
markerline, stemlines, baseline = plt.stem(
    freqs[positive_freq_indices],
    np.abs(dft_coeffs[positive_freq_indices]),
    linefmt='b-',
    markerfmt='bo',
    basefmt=' ',
    label='Исходный спектр'
)
plt.setp(stemlines, 'linewidth', 0.7)
plt.setp(markerline, 'markersize', 4)

# Фильтрованный спектр (красные точки)
markerline, stemlines, baseline = plt.stem(
    freqs[positive_freq_indices],
    np.abs(filtered_coeffs[positive_freq_indices]),
    linefmt='r-',
    markerfmt='ro',
    basefmt=' ',
    label='Фильтрованный спектр'
)
plt.setp(stemlines, 'linewidth', 0.7, 'alpha', 0.5)
plt.setp(markerline, 'markersize', 4)

plt.axvline(x=5, color='green', linestyle='--', label='Частота среза (5 Гц)')
plt.title('Спектр сигнала (stem-график)')
plt.xlabel('Частота (f), Гц')
plt.ylabel('Амплитуда |X(f)|')
plt.grid(True, alpha=0.3)
plt.legend()
plt.xlim(0, 10)

plt.tight_layout()
plt.show()
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p29t():
    answer = f"""
## 29. Быстрое преобразование Фурье (БПФ)

### Теоретическое описание:
Быстрое преобразование Фурье — это не новое преобразование, а набор высокоэффективных алгоритмов для вычисления ДПФ. Самый известный из них — алгоритм Кули-Тьюки.

**Основная идея (алгоритм Кули-Тьюки с прореживанием по времени):**
1.  **Разделяй и властвуй:** Если длина входного сигнала `N` является степенью двойки, его можно разделить на два подсигнала: один из четных отсчетов, другой — из нечетных.
2.  **Рекурсия:** ДПФ вычисляется рекурсивно для каждого из этих подсигналов (длиной N/2).
3.  **Объединение:** Результаты двух ДПФ меньшего размера объединяются с помощью умножения на так называемые "поворотные множители" (`exp(-j*2*pi*k/N)`) для получения итогового ДПФ.

Формула объединения:
`X[k] = E[k] + exp(-j*2*pi*k/N) * O[k]`
`X[k + N/2] = E[k] - exp(-j*2*pi*k/N) * O[k]`
где `E[k]` и `O[k]` — ДПФ четных и нечетных отсчетов соответственно.

**Сложность:**
-   **Временная сложность:** O(N log N). Это значительно быстрее, чем O(N²) у прямого ДПФ, и делает возможным спектральный анализ больших массивов данных.

**Применение:**
-   Цифровая обработка сигналов (аудио, видео, изображения).
-   Анализ временных рядов в финансах и науке.
-   Решение дифференциальных уравнений в частных производных.
"""
    pyperclip.copy(answer)
    pyperclip.paste()


def p29():
    answer = f"""
import numpy as np
import matplotlib.pyplot as plt

# --- Ваши функции БПФ и ОБПФ ---
def fft(x):
    '''
    Рекурсивная реализация БПФ (алгоритм Кули-Тьюки).
    Длина входного списка должна быть степенью двойки.
    '''
    N = len(x)
    if N <= 1:
        return x
    even = fft(x[0::2])
    odd =  fft(x[1::2])
    T = [np.exp(-2j*np.pi*k/N) * odd[k] for k in range(N//2)]
    X = np.zeros(N, dtype=np.complex128)
    for k in range(N//2):
        X[k] = even[k] + T[k]
        X[k+N//2] = even[k] - T[k]
    return X

def ifft(X):
    '''
    Обратное БПФ на основе прямого БПФ.
    '''
    N = len(X)
    X_conj = np.conjugate(X)
    x_conj = fft(X_conj)
    x = np.conjugate(x_conj) / N
    return x

# --- 1. ПОДГОТОВКА ДАННЫХ (из нашей задачи) ---
# Параметры сигнала
t_start = 0.0
t_end = 4.0
h = 0.05
x_initial = np.arange(t_start, t_end, h)
n_samples_initial = len(x_initial)
signal_initial = np.exp(-x_initial**2) + np.arctan(x_initial)

# Дополнение нулями для БПФ
N_fft = 2**int(np.ceil(np.log2(n_samples_initial)))
signal_padded = np.zeros(N_fft)
signal_padded[:n_samples_initial] = signal_initial

# Прямое БПФ и получение спектра
fft_coeffs = fft(signal_padded)
sampling_rate = 1 / h
freqs = np.fft.fftfreq(N_fft, d=h)

# Фильтрация в частотной области
cutoff_frequency = 5.0
filtered_coeffs = fft_coeffs.copy()
indices_to_zero = np.where(np.abs(freqs) > cutoff_frequency)
filtered_coeffs[indices_to_zero] = 0

# Обратное БПФ для получения восстановленного сигнала
signal_filtered_padded = ifft(filtered_coeffs)
signal_filtered = np.real(signal_filtered_padded)


# --- 2. ПОСТРОЕНИЕ ГРАФИКОВ (в стиле вашего примера) ---

plt.style.use('seaborn-v0_8-whitegrid')
plt.figure(figsize=(15, 10))

# --- График 1: Исходный и восстановленный сигналы ---
plt.subplot(2, 1, 1)
plt.plot(x_initial, signal_initial, label='Исходный сигнал f(x) = e^(-x²) + atan(x)')
# Используем ту же длину, что и у исходного сигнала для корректного сравнения
plt.plot(x_initial, signal_filtered[:n_samples_initial], '--', label='Восстановленный (отфильтрованный) сигнал', linewidth=2)
plt.title('Сравнение исходного и восстановленного сигналов')
plt.xlabel('x')
plt.ylabel('Амплитуда f(x)')
plt.grid(True, alpha=0.5)
plt.legend()

# --- График 2: Спектр (используем plt.stem, совмещенный) ---
plt.subplot(2, 1, 2)

# Ограничим диапазон частот для наглядности, как в примере
display_freq_limit = 10
positive_freq_indices = np.where((freqs >= 0) & (freqs <= display_freq_limit))

# 1. Строим ИСХОДНЫЙ спектр (синий)
markerline, stemlines, baseline = plt.stem(
    freqs[positive_freq_indices],
    np.abs(fft_coeffs[positive_freq_indices]),
    linefmt='b-',      # Синяя линия "стебля"
    markerfmt='bo',    # Синий круглый маркер
    basefmt=' ',       # Не рисовать базовую линию
    label='Исходный спектр'
)
plt.setp(stemlines, 'linewidth', 1)
plt.setp(markerline, 'markersize', 5)

# 2. Строим на тех же осях ОТФИЛЬТРОВАННЫЙ спектр (красный)
markerline, stemlines, baseline = plt.stem(
    freqs[positive_freq_indices],
    np.abs(filtered_coeffs[positive_freq_indices]),
    linefmt='r-',      # Красная линия "стебля"
    markerfmt='ro',    # Красный круглый маркер
    basefmt=' ',       # Не рисовать базовую линию
    label='Фильтрованный спектр'
)
plt.setp(stemlines, 'linewidth', 1.5, 'alpha', 0.7) # Чуть толще и полупрозрачнее
plt.setp(markerline, 'markersize', 5)

# Добавляем вертикальную линию среза
plt.axvline(x=cutoff_frequency, color='green', linestyle='--', label=f'Частота среза (cutoff_frequency Гц)')

plt.title('Спектр сигнала (stem-график)')
plt.xlabel('Частота (f), Гц')
plt.ylabel('Амплитуда |F(f)|')
plt.grid(True, alpha=0.5)
plt.legend()
plt.xlim(0, display_freq_limit)
plt.ylim(bottom=0) # Начинаем ось Y с нуля

plt.tight_layout()
plt.show()
"""
    pyperclip.copy(answer)
    pyperclip.paste()
