questions = {1: {'theory': '\nПри умножении матрицы $A$ размером $n \\times m$ на вектор $x$ размером $m$, результат — вектор $y$ размером $n$, вычисляемый по формуле:\n\n$$\ny_i = \\sum_{j=1}^{m} A_{ij} x_j\n$$\n\nАлгоритм выполняет $O(n \\cdot m)$ операций.\n\n#### Умножение двух матриц\n\nДля матриц $A$ ($n \\times k$) и $B$ ($k \\times m$) результирующая матрица $C$ ($n \\times m$) вычисляется как:\n\n$$\nC_{ij} = \\sum_{s=1}^{k} A_{is} B_{sj}\n$$\n\nНаивный алгоритм имеет три вложенных цикла и сложность $O(n \\cdot k \\cdot m)$.\n                    ',
              'code': '\n# Рукописная реализация перемножения двух матриц\ndef matmul(a, b):\n    n = a.shape[0]\n    k = a.shape[1]\n    m = b.shape[1]\n    c = np.zeros((n, m))\n    for i in range(n):\n        for j in range(m):\n            for s in range(k):\n                c[i, j] += a[i, s] * b[s, j]\n    return c\n                    '},
             2: {'theory': '\n1. Регистры процессора. Скорость доступа — порядка 1 такта, объём — несколько сотен или тысяч байт. 5\n2. Кэш процессора L1. Скорость доступа — порядка несколько тактов, объём — десятки килобайт. 5\n3. Кэш процессора L2. От 2 до 10 раз медленнее L1, объём — от 0,5 МБ. 5\n4. Кэш процессора L3. Около сотни тактов, объём — от нескольких мегабайт до сотен. 5\n5. ОЗУ. От сотен до тысяч тактов, объём — от нескольких гигабайт до нескольких терабайт. 5\n6. Дисковое хранилище. Миллионы тактов, объём — до нескольких сотен терабайт. 5\n7. Третичная память. До нескольких секунд или минут, объём практически неограничен.  \n\nКэш — промежуточный буфер с быстрым доступом, содержащий информацию, которая может быть запрошена с наибольшей вероятностью. Доступ к данным в кэше осуществляется быстрее, чем выборка исходных данных из более медленной памяти или удалённого источника, однако её объём существенно ограничен по сравнению с хранилищем исходных данных.\n\nLRU (Least Recently Used) — алгоритм замещения кэш-строк, при котором новые данные вытесняют самые старые.\n\nПромахи в обращении к кэшу — это случаи, когда в кэше нет запрашиваемых данных. Промахи по чтению задерживают исполнение, поскольку они требуют запроса данных в более медленной основной памяти. Промахи по записи могут не давать задержку, поскольку записываемые данные сразу могут быть сохранены в кэше, а запись их в основную память можно произвести в фоновом режиме.\n                    ',
              'code': 'from collections import OrderedDict\n\nclass CacheLRU:\n    def __init__(self, capacity):\n        self.cache = OrderedDict()\n        self.capacity = capacity\n        self.hits = 0\n        self.misses = 0\n\n    def get(self, key):\n        if key in self.cache:\n            self.cache.move_to_end(key)  # Обновить порядок использования\n            self.hits += 1\n            return self.cache[key]\n        else:\n            self.misses += 1\n            return None\n\n    def put(self, key, value):\n        if key in self.cache:\n            self.cache.move_to_end(key)  # Обновить порядок использования\n        elif len(self.cache) >= self.capacity:\n            self.cache.popitem(last=False)  # Удалить самый старый элемент\n        self.cache[key] = value\n\n    def stats(self):\n        return {"hits": self.hits, "misses": self.misses}\n\n# Пример использования\ndef simulate_cache_operations():\n    cache = CacheLRU(3)  # Создаем кэш с ёмкостью 3\n    operations = [("GET", 1), ("PUT", (1, 100)), ("GET", 1), ("GET", 2),\n                  ("PUT", (2, 200)), ("PUT", (3, 300)), ("GET", 1),\n                  ("PUT", (4, 400)), ("GET", 2)]\n\n    for op in operations:\n        if op[0] == "GET":\n            print(f"GET {op[1]}: {cache.get(op[1])}")\n        elif op[0] == "PUT":\n            key, value = op[1]\n            cache.put(key, value)\n            print(f"PUT {key} = {value}")\n\n    print("Статистика:", cache.stats())\n\nsimulate_cache_operations()\n                    '},
             3: {'theory': '\nЕсли при помощи классического наивного подхода мы можем вычислить произведение двух матриц 2х2 используя 8 умножений и 4 сложения, то при помощи алгоритма Штрассена можно его вычислить при помощи 7 умножений и 18 сложений\nc11 = f1 + f4  - f5 + f7\nc12 = f3 + f5\nc21 = f2 + f4\nc22 = f1 - f2 + f3 + f6\n\nf1 = (a11 + a22)(b11 + b22)\nf2 = (a21 + a22)*b11\nf3 = a11*(b12 - b22)\nf4 = a22*(b21 - b11)\nf5 = (a11+ a12)*b22\nf6 = (a21 - a11)*(b11 + b12)\nf7 = (a12 - a22)*(b21 + b22)\n\n\n\nСхема алгоритма:\n\nРазбиваем исходные матрицы a и b размера n x n на 4 блока n/2 x n/2\nВычисляем произведения по приведенным выше формулам рекурсивно\n\n\n                    ',
              'code': '\ndef strassen(A, B):\n    n = len(A)\n\n    if n <= 2:\n        return np.dot(A, B)\n\n    # Разделим матрицы на подматрицы\n    mid = n // 2\n    A11 = A[:mid, :mid]\n    A12 = A[:mid, mid:]\n    A21 = A[mid:, :mid]\n    A22 = A[mid:, mid:]\n    B11 = B[:mid, :mid]\n    B12 = B[:mid, mid:]\n    B21 = B[mid:, :mid]\n    B22 = B[mid:, mid:]\n\n    # Рекурсивное умножение\n    P1 = strassen(A11, B12 - B22)\n    P2 = strassen(A11 + A12, B22)\n    P3 = strassen(A21 + A22, B11)\n    P4 = strassen(A22, B21 - B11)\n    P5 = strassen(A11 + A22, B11 + B22)\n    P6 = strassen(A12 - A22, B21 + B22)\n    P7 = strassen(A11 - A21, B11 + B12)\n\n    # Соединим результаты в матрицу С\n    C11 = P5 + P4 - P2 + P6\n    C12 = P1 + P2\n    C21 = P3 + P4\n    C22 = P5 + P1 - P3 - P7\n\n    C = np.vstack((np.hstack((C11, C12)), np.hstack((C21, C22))))\n\n    return C\n                    '},
             4: {'theory': '\nПусть A — действительная числовая квадратная матрица размеров (n × n).\nНенулевой вектор $X = (x_1, ..., x_n)^T$ размеров (n × 1), удовлетворяющий\nусловию:  \n$AX = \\lambda X => (A - \\lambda E)X = 0$  \nСледовательно система имеет ненулевое решение для вектора X при условии $|A - \\lambda E| = 0$\nЭто равенство называется **характерестическим уровнением**\n\nЗадача состоит в ранжировании веб-страницы: какие из\nних являются важными, а какие нет.\nВ интернете страницы ссылаются друг на друга.\nPageRank определяется рекурсивно. Обозначим за pi\nважность i-ой страницы. Тогда определим эту важность\nкак усреднённую важность всех страниц, которые\nссылаются на данную страницу. Это определение\nприводит к следующей линейной системе:  \n$p_i = \\sum_{j \\in N(i)}\\frac{p_j}{L(j)}$  \nгде L(j)– число исходящих ссылок с j-ой страницы, N(i)\n– число соседей i-ой страницы. Это может быть\nзаписано следующим образом:  \n$p = Gp;  G_{ij} = \\frac{1}{L(j)}$\n\nто есть мы уже знаем, что у матрицы G есть\nсобственное значение равное 1. Заметим, что G–\nлевостохастичная матрица, то есть сумма в каждом\nстолбце равна 1.\n                    ',
              'code': '\ndef power_iteration(A, num_simulations=100, tol=1e-10):\n    """\n    Нахождение наибольшего собственного значения и соответствующего вектора методом степенной итерации.\n    """\n    n, _ = A.shape\n    b_k = np.random.rand(n)\n    for _ in range(num_simulations):\n        # Умножение матрицы на вектор\n        b_k1 = np.dot(A, b_k)\n        # Нормализация вектора\n        b_k1_norm = np.linalg.norm(b_k1)\n        b_k1 = b_k1 / b_k1_norm\n        # Проверка сходимости\n        if np.linalg.norm(b_k - b_k1) < tol:\n            break\n        b_k = b_k1\n    eigenvalue = np.dot(b_k.T, np.dot(A, b_k)) / np.dot(b_k.T, b_k)\n    eigenvector = b_k\n    return eigenvalue, eigenvector\n\ndef find_all_eigenvalues_and_vectors(A, tol=1e-10):\n    """\n    Нахождение всех собственных значений и собственных векторов квадратной матрицы.\n    """\n    n = A.shape[0]\n    eigenvalues = []\n    eigenvectors = []\n    B = A.copy()\n\n    for _ in range(n):\n        # Найти наибольшее собственное значение и вектор\n        eigenvalue, eigenvector = power_iteration(B)\n        eigenvalues.append(eigenvalue)\n        eigenvectors.append(eigenvector)\n        \n        # Вычитать найденное собственное значение/вектор из матрицы\n        eigenvector = eigenvector.reshape(-1, 1)\n        B = B - eigenvalue * (eigenvector @ eigenvector.T)\n    \n    return np.array(eigenvalues), np.array(eigenvectors).T\n\n# Пример использования\nA = np.array([[2, 1],\n              [1, 2]])\n\neigenvalues, eigenvectors = find_all_eigenvalues_and_vectors(A)\nprint("Eigenvalues:")\nprint(eigenvalues)\nprint("\nEigenvectors:")\nprint(eigenvectors)\n'},
             5: {'theory': '\nЕсли мы приведем матрицу А к верхнетреугольному виду T с помощью унитарной матрицы U: $U^*AU = T$, то мы решим задачу. Умножая слева и справа на $U \\, и \\, U^*$ получим следующее разложение: $A = UTU^*$ - **Разложение (форма) Шура**\n\n* Использование унитарных матриц приводит к устойчивым алгоритмам, таким образом собственные значения вычисляются очень точно\n\n* Разложение Шура показывает, почему нам нужны матричные разложения: они представляют матрицу в виде произведения трёх матриц подходящей структуры\n\n**Теорема Шура**  \n\nКаждая матрица $A \\in C^{n\\times n}$ может быть представлена в виде формы Шура.\n\n1. Каждая матрица имеет как минимум один ненулевой собственный вектор (для корня характеристического многочлена матрица $(A - \\lambda I)$ вырождена и имеет нетривиальное ядро). Пусть:  \n        $Av_1 = \\lambda_1 v_1, \\,||v_1||_2 = 1$\n       \n2. Пусть $U_1 = [v_1, v_2, ..., v_n]$, где $v_2, ..., v_n$ любые векторы ортогональные $v_1$. Тогда $U_1^*AU_1 = \\begin{bmatrix}\n\\lambda_1 & * \\\\\n0 & A_2\n\\end{bmatrix}$, где $A_2$ матрица размера $(n-1)*(n-1)$&. Она же называется блочнотреугольной формой. Теперь мы можем проделать аналогичную процедуру для матрицы $A_2$ и так далее.\n\n* QR-алгоритм *\nQR алгоритм использует QR разложение для вычисления разложения Шура\n\nРассмотрим выражение: $ A = QTQ^* => QT = AQ $ (T - верхнетреугольная)\n\nЗапишим следующий итерационный процесс:\n$Q_{k+1}R_{k+1} = AQ_k, \\\\\nQ_{k+1}^*A = R_{k+1}Q_k^*, \\\\\nВведем \\, новую \\, матрицу \\\\\nA_k = Q_k^*AQ_{k} = Q_k^*Q_{k+1}R_{k+1} = \\widehat{Q}_kR_{k+1} \\\\$  \n\nТогда для аппроксимация для $A_{k+1}$ имеет вид  \n\n$A_{k+1} = Q_{k+1}^*AQ_{k+1} = (Q_{k+1}^*A = R_{k+1}Q_k^*) = R_{k+1}\\widehat{Q}_k\n$\n\nФинальные формулы обычно записывают в QRRQ - форме\n1. Инициализируем $A_0 = A$\n2. Вычислим QR разложение матрицы $A_k: A_k = Q_kR_k$.\n3. Обновим аппроксимацию $A_{k+1} = R_kQ_k$\n\nПродолжаем итерации пока A_k не станет достаточно треугольной (например, норма подматрицы под главной дианональю не станет достаточно мала)\n                    ',
              'code': '\n# Отражение Хаусхолдера:\ndef householder(x):\n  e1 = np.zeros_like(x)\n  e1[0] = 1\n\n  # вычисление alpha\n  alpha = np.sign(x[0])*np.sqrt(np.sum(x**2))\n\n  # матрица отражения u\n  u = x - alpha*e1\n  # нормализация u\n  norm_u = np.sum(u**2)\n\n  if norm_u > 1e-10:\n    u = u/np.sqrt(norm_u)\n  else:\n    u = np.zeros_like(u)\n\n\n  return u\n\n# QR-разложение\ndef qr_dec(A):\n  n, m = A.shape\n\n  # единичная матрица Q\n  Q = np.eye(n)\n  R = A.copy()\n\n  # итариция разложения\n  for i in range(min(n, m)):\n    # подмножество x:\n    x = R[i:, i]\n    # вычисление u - отражение для x:\n    u = householder(x)\n\n    # нахождение матрицы Хаусхолдера:\n    H = np.eye(n)\n    H[i:, i:] -= 2*np.outer(u, u)\n\n    # обновление R и Q:\n    R = H@R\n    Q = Q@H.T\n\n  return Q, R\n\n# реализация QR алгоритма:\ndef qr(A, iter=100):\n  for i in range(iter):\n    # Разложение:\n    Q, R = qr_dec(A)\n    # Перемножение матриц:\n    A = R@Q\n\n  return A\n\n# реализация неявного QR алгоритма:\ndef implict_qr(A, iter=100):\n  n = A.shape[0]\n  for i in range(iter):\n    # сдвиг\n    shift = A[n-1, n-1]\n    Q, R = qr_dec(A - shift*np.eye(n))\n    A = R@Q + shift*np.eye(n)\n\n  return A\n                    '},
             6: {'theory': '\nСтепенной метод (Power method):\n\nБерём некоторый случайный вектор b и начинаем действовать на него оператором А (умножая), при этом нормируя: <br>\n$b_{i+1} = A * b_i$ / ||$A$||\n\nИ так повторяем, пока изменение вектора не будет меньше заданного значения eps. <br>\nКогда достигнем этого условия, считаем, что мы нашли собственный вектор, соответствующий наибольшему собственному значению\n\n* Степенной метод дает оценку для максимального по модую собственного числа или спектрального радиуса матрицы\n* Одна итерация требует одного умножения матрицы на вектор. Если умножить вектор на матрицу за O(n) (например, она разреженная), тогда степенной метод можно использовать для больших n\n* Сходимость может быть медленной\n* Для грубой оценки максимального по модулю собственного значения и соответствующего вектора достаточно небольшого числа итераций\n* Вектор решения лежит в Крыловском подпространстве $\\{x_0, Ax_0, …, A^kx_0\\}$ и имеет вид $μA^kx_0$, где μ– нормировочная постоянная.\n                    ',
              'code': '\nA = np.array([[2, 1], [1, 2]]) #Линейный оператор\nx = np.array([[1, 2]]).T #Исходный вектор\ntol = 1e-6 #Порог точности\nmax_iter = 100\n\nlam_prev = 0\n\nfor i in range(max_iter):\n    x = A @ x / np.linalg.norm(A @ x)\n\n    lam = (x.T @ A @ x) / (x.T @ x)\n\n    if np.abs(lam - lam_prev) < tol:\n        break\n\n    lam_prev = lam\n\nprint(lam)\nprint(x)\n                    '},
             7: {'theory': '\nЕсть теорема, которая часто помогает локализовать собственные значения.\nОна называется теоремой Гершгорина.\nТеорема утверждает, что все собственные значения λi, i= 1, 2, ..., n находятся внутри объединения кругов Гершгорина $C_i$, где $C_i$–окружность на комплексной плоскости с центром в $a_{ii}$ и радиусом: $\\sum_{j≠i} |a_{ij}|$  \nБолее того, если круги не\nпересекаются, то они содержат\nпо одному собственному\nзначению внутри каждого\nкруга.\n\n\nСначала покажем, что если матрица $A$ обладает строгим диагональным преобладанием, то есть\n\n$$\n|a_{ii}| > \\sum_{j \neq i} |a_{ij}|,\n$$\n\nтогда такая матрица невырождена.\n\nРазделим диагональную и недиагональную части и получим\n\n$$\nA = D + S = D(I + D^{-1}S),\n$$\n\nгде $\\|D^{-1}S\\|_1 < 1$. Поэтому, в силу теоремы о ряде Неймана, матрица $I + D^{-1}S$ обратима и, следовательно, $A$ также обратима.\n\n                    ',
              'code': '\n# Круги Гершгорина\n\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nn = 3\nfig, ax = plt.subplots(1, 1)\na = np.array([[5, 1, 1], [1, 0, 0.5], [2, 0, 10]])\n\na += 2*np.random.randn(n, n)\n\nxg = np.diag(a).real\nyg = np.diag(a).imag\nrg = np.zeros(n)\nev = np.linalg.eigvals(a)\n\nfor i in range(n):\n    rg[i] = np.sum(np.abs(a[i, :])) - np.abs(a[i, i])\n    crc = plt.Circle((xg[i], yg[i]), radius = rg[i], fill = False)\n    ax.add_patch(crc)\nplt.scatter(ev.real, ev.imag, color = \'r\', label = "Eigenvalues")\nplt.axis(\'equal\')\n                    '},
             8: {'theory': '\nЕсли мы приведем матрицу А к верхнетреугольному виду T с помощью унитарной матрицы U: $U^*AU = T$, то мы решим задачу. Умножая слева и справа на $U \\, и \\, U^*$ получим следующее разложение: $A = UTU^*$ - **Разложение (форма) Шура**\n\n* Использование унитарных матриц приводит к устойчивым алгоритмам, таким образом собственные значения вычисляются очень точно\n\n* Разложение Шура показывает, почему нам нужны матричные разложения: они представляют матрицу в виде произведения трёх матриц подходящей структуры\n\n**Теорема Шура**  \n\nКаждая матрица $A \\in C^{n\\times n}$ может быть представлена в виде формы Шура.\n\n1. Каждая матрица имеет как минимум один ненулевой собственный вектор (для корня характеристического многочлена матрица $(A - \\lambda I)$ вырождена и имеет нетривиальное ядро). Пусть:  \n      $Av_1 = \\lambda_1 v_1, \\,||v_1||_2 = 1$\n       \n2. Пусть $U_1 = [v_1, v_2, ..., v_n]$, где $v_2, ..., v_n$ любые векторы ортогональные $v_1$. Тогда $U_1^*AU_1 = \\begin{bmatrix}\n\\lambda_1 & * \\\\\n0 & A_2\n\\end{bmatrix}$, где $A_2$ матрица размера $(n-1)*(n-1)$&. Она же называется блочнотреугольной формой. Теперь мы можем проделать аналогичную процедуру для матрицы $A_2$ и так далее.\n                    ',
              'code': '\n\nimport numpy as np\n\ndef gram_schmidt(A):\n    """Ортогонализация Грама-Шмидта."""\n    n = A.shape[0]\n    Q = np.zeros_like(A, dtype=np.complex128)  # Изменение типа на complex128\n    for i in range(n):\n        v = A[:, i]\n        for j in range(i):\n            v -= np.dot(Q[:, j], v) * Q[:, j]\n        Q[:, i] = v / np.linalg.norm(v)\n    return Q\n\ndef schur_decomposition(A, max_iter=1000, tol=1e-10):\n    """\n    Разложение Шура методом QR-алгоритма.\n    A: исходная матрица\n    max_iter: максимальное количество итераций\n    tol: точность для остановки\n    """\n    A = np.array(A, dtype=np.complex128)  # Приведение матрицы к complex128\n    n = A.shape[0]\n    Q_total = np.eye(n, dtype=np.complex128)  # Начальная унитарная матрица\n    A_k = A.copy()  # Текущая матрица A\n\n    for _ in range(max_iter):\n        # QR-разложение с помощью ортогонализации Грама-Шмидта\n        Q_k = gram_schmidt(A_k)\n        R_k = Q_k.T.conj() @ A_k\n\n        # Обновление матрицы\n        A_k = R_k @ Q_k\n        Q_total = Q_total @ Q_k\n\n        # Проверка на сходимость (проверяем поддиагональные элементы)\n        if np.allclose(A_k - np.diag(np.diag(A_k)), 0, atol=tol):\n            break\n\n    return A_k, Q_total\n\n# Пример\nA = np.array([[4, 2],\n              [1, 3]], dtype=np.float64)\n\nT, Q = schur_decomposition(A)\n\nprint("Матрица T (верхнетреугольная):")\nprint(T)\n\nprint("\nУнитарная матрица Q:")\nprint(Q)\n\nprint("\nПроверка A = Q T Q*:")\nprint(Q @ T @ Q.T.conj())\n\n'},
             9: {'theory': '\n1. Нормальная матрица $$AA^*=A^*A,$$ где A* - сопряжённая матрица\n\n  Эрмитовы и унитарные матрицы являются нормальными\n\n\n2. Эрмитова (самосопряженная) матрица\n$$A^T = \\overline{A},$$\n$\\overline{A}$ - комплексно сопряжённая матрица\n\n  Для Эрмитовых матриц собственные\nзначения всегда действительны\n\n\n\n3. Квадратная матрица A называется унитарно диагонализируемой, если существует унитарная матрица U, такая что $$U^*AU = D,$$\nD - диагональная матрица\n\n  Любая нормальная матрица – унитарно диагонализуема\n\n4. Матрица 𝐴 имеет верхне-гессенбергову форму, если $𝑎_{𝑖𝑗}$ = 0, при $𝑖 ≥ 𝑗 + 2$ $$H = \\begin{bmatrix}\n* & * & * & * & * \\\\\n* & * & * & * & * \\\\\n0 & * & * & * & * \\\\\n0 & 0 & * & * & * \\\\\n0 & 0 & 0 & * & *\n\\end{bmatrix}$$\nС помощью отражений Хаусхолдера можно привести любую матрицу к верхнегессенберговой форме:\n$U^*AU=H$\n\n  Если матрица 𝐴 симметричная (эрмитова), то $A=A^*$\n,\nтогда $H=H^*$ и верхне-гессенбергова форма оказывается\nтрёхдиагональной матрицей.\n\\begin{bmatrix}\na_{11} & b_1 & 0 & \\cdots & 0 \\\\\nc_2 & a_{22} & b_2 & \\ddots & \\vdots \\\\\n0 & c_3 & a_{33} & \\ddots & 0 \\\\\n\\vdots & \\ddots & \\ddots & \\ddots & b_{n-1} \\\\\n0 & \\cdots & 0 & c_n & a_{nn}\n\\end{bmatrix}\n                    ',
              'code': 'Тут никакого кода не придумаешь'},
             10: {'theory': '\nДля динамических систем с матрицей A, спектр может много\nсообщить о поведении системы (например, о её устойчивости).\n\nОднако для не нормальных матриц, спектр может быть неустойчивым\nотносительно малых возмущений матрицы.\n\nДля измерения подобных возмущений было разработана концепция\nпсевдоспектра.\n\n**Теорема**: A – нормальная матрица, тогда и только тогда, когда A =\nUΛU*\n, где U унитарна и Λ диагональна.\nЛюбая нормальная матрица – унитарно диагонализуема. Это\nозначает, что она может быть приведена к диагональному виду с\nпомощью унитарной матрицы U. Другими словами, каждая\nнормальная матрица имеет ортогональный базис из собственных\nвекторов.\n\nРассмотрим объединение всех возможных собственных значений для\nвсевозможных возмущений матрицы A.\n\n$Λ_{\\epsilon}(A) = \\{\\lambda \\in C: \\exists E, x ≠ 0: (A+E)x = \\lambda x, ||E||_2 \\leq \\epsilon\\}$\n\nДля малых E и нормальных A это круги вокруг собственных значений, для не\nнормальных матриц, структура может сильно отличаться.\n\n### Спектр матрицы\n\n**Спектр матрицы** — это множество собственных значений (или спектральных значений) квадратной матрицы $A$.  \n\nЕсли $\\lambda$ — собственное значение матрицы $A$, то существует ненулевой вектор $v$ (собственный вектор), для которого выполняется уравнение:  \n\n$A v = \\lambda v$\n\nТаким образом, спектр матрицы $A$ определяется как множество всех собственных значений:  \n\n$\\text{Sp}(A) = \\{ \\lambda \\in \\mathbb{C} \\mid \\det(A - \\lambda I) = 0 \\}$\n\nгде $I$ — единичная матрица той же размерности, что и $A$, а $\\det$ — определитель.\n\n---\n\n### Псевдоспектр матрицы\n\n**Псевдоспектр матрицы** $A$ — это обобщение спектра, которое учитывает численную устойчивость и небольшие возмущения в матрице.  \n\nДля комплексного числа $z$, псевдоспектр $\\varepsilon$-уровня определяется как множество таких $z$, что:  \n\n$z \\in \\Lambda_\\varepsilon(A) \\quad \\text{если} \\quad \\| (A - zI)^{-1} \\| \\geq \\frac{1}{\\varepsilon}$\n\nили, эквивалентно,  \n\n$\\Lambda_\\varepsilon(A) = \\{ z \\in \\mathbb{C} \\mid \\sigma_{\\text{min}}(A - zI) \\leq \\varepsilon \\}$\n\nгде $\\sigma_{\\text{min}}(A - zI)$ — наименьшее сингулярное значение матрицы $A - zI$.\n\n---\n\n### Интерпретация\n\n- **Спектр** — это точки, где оператор $A - zI$ становится вырожденным (необратимым).  \n- **Псевдоспектр** включает в себя точки, где оператор близок к необратимому, что важно в приложениях, где матрица подвержена шумам или численным погрешностям.\n\n                    ',
              'code': '\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef compute_spectrum(A):\n    """Вычисляет собственные значения (спектр) матрицы A методом степени."""\n    n = A.shape[0]\n    eigenvalues = []\n\n    for _ in range(n):\n        v = np.random.rand(n)\n        for _ in range(50):  # Итерации степенного метода\n            v = np.dot(A, v)\n            v /= np.linalg.norm(v)\n        eigenvalue = np.dot(v, np.dot(A, v)) / np.dot(v, v)\n        eigenvalues.append(eigenvalue)\n\n        # Дефляция\n        A = A - eigenvalue * np.outer(v, v)\n\n    return np.array(eigenvalues)\n\ndef compute_pseudospectrum(A, epsilon=1e-3, grid_size=500, extent=5):\n    """Вычисляет псевдоспектр матрицы A."""\n    x = np.linspace(-extent, extent, grid_size)\n    y = np.linspace(-extent, extent, grid_size)\n    X, Y = np.meshgrid(x, y)\n    Z = X + 1j * Y  # Комплексная сетка\n    pseudospectrum = np.zeros_like(Z, dtype=float)\n\n    for i in range(grid_size):\n        for j in range(grid_size):\n            z = Z[i, j]\n            M = A - z * np.eye(A.shape[0])\n            try:\n                inv_norm = 1 / np.linalg.norm(np.linalg.solve(M, np.eye(A.shape[0])))\n            except np.linalg.LinAlgError:\n                inv_norm = 0  # Если матрица необратима\n            pseudospectrum[i, j] = inv_norm\n\n    return X, Y, pseudospectrum\n\ndef plot_spectrum_and_pseudospectrum(A, epsilon=1e-3, grid_size=500, extent=5):\n    """Строит график спектра и псевдоспектра матрицы A."""\n    eigenvalues = compute_spectrum(A)\n    X, Y, pseudospectrum = compute_pseudospectrum(A, epsilon, grid_size, extent)\n\n    # График\n    plt.figure(figsize=(10, 8))\n    plt.contourf(X, Y, -np.log10(pseudospectrum), levels=50, cmap=\'viridis\')  # Логарифмическая шкала\n    plt.colorbar(label=\'-log10(1 / ||inv(M)||)\')\n    plt.scatter(eigenvalues.real, eigenvalues.imag, color=\'red\', marker=\'o\', label=\'Eigenvalues\')\n    plt.xlabel(\'Real part\')\n    plt.ylabel(\'Imaginary part\')\n    plt.title(\'Spectrum and Pseudospectrum\')\n    plt.legend()\n    plt.grid(True)\n    plt.show()\n\n# Пример использования\nA = np.array([[1, 2], [3, 4]])\nplot_spectrum_and_pseudospectrum(A, epsilon=1e-3, grid_size=500, extent=5)\n                    '},
             11: {'theory': '\n### Неявный QR алгоритм (со сдвигами)\n\n#### Основная идея\n- QR алгоритм используется для вычисления собственных значений и векторов матрицы.\n- Ускорение достигается за счёт использования **сдвигов** и сохранения структурных свойств матрицы (например, верхне-гессенберговой формы).\n\n#### Шаги алгоритма\n1. Преобразуем матрицу $A$ к **верхне-гессенберговой форме** с помощью отражений Хаусхолдера:\n   - Сложность приведения: $O(n^3)$.\n   - Для симметричных матриц форма становится **трёхдиагональной**.\n\n2. Итерационный процесс:\n   $$\n   A_k = Q_k R_k, \\quad A_{k+1} = R_k Q_k\n   $$\n   - Использование сдвига $\\lambda$: $A_k - \\lambda I$ ускоряет сходимость.\n\n3. Если $A$ симметрична (эрмитова):\n   - Верхне-гессенбергова форма превращается в трёхдиагональную.\n   - Итерации QR алгоритма сохраняют эту структуру, что сокращает сложность до $O(n)$ на шаг.\n\n4. **Неявный QR-шаг**:\n   - Вычисление $A_{k+1}$ напрямую, без явного вычисления матрицы $Q_k$.\n   - Основан на теореме: первый столбец матрицы $Q$ определяет все остальные её столбцы.\n   - Используем уравнение:\n     $$\n     A Q = Q H\n     $$\n\n#### Преимущества сдвигов\n- Ускоряют сходимость, выбирая $\\lambda$ близким к собственным значениям (например, элемент в правом нижнем углу $A_k$).\n- Итерации становятся более стабильными и эффективными.\n\n                    ',
              'code': '\ndef householder_reflection(x):\n  """\n  Функция, позволяющая применить к матрице отражение Хаусхолдера\n\n  x - матрица для вычислений\n  """\n\n  # создадим единичный вектор (или матрицу) того же размера, что и x\n  e1 = np.zeros_like(x)\n  e1[0] = 1\n\n  # вычисляем alpha, используя знак первого элемента x и его норму\n  alpha = np.sign(x[0])*np.sqrt(np.sum(x**2))\n\n  # вычисляем вектор(матрицу) отражения u\n  u = x - alpha*e1\n  # нормализуем u\n  u_norm_squared = np.sum(u**2)\n\n  # введём условие на соответствие некой малой величине,\n  # чтобы избежать деления на 0\n  if u_norm_squared > 1e-20:\n    u = u/np.sqrt(u_norm_squared)\n  else:\n    u = np.zeros_like(u)\n\n  return u\n\ndef qr_decomposition(A):\n  """\n  Функция, выполняющая QR-разложение матрицы\n\n  A - матрица для разложения\n  """\n\n  # получаем размерность матрицы A\n  n, m = A.shape\n  # создаем единичную матрицу Q\n  Q = np.eye(n)\n  # создаем R как копию A\n  R = A.copy()\n\n  # итерируемся по столбцам A\n  for i in range(min(n, m)):\n    # получаем подстолбец x, начиная с i-ой строки\n    x = R[i:, i]\n    # вычисляем вектор отражения для x\n    u = householder_reflection(x)\n\n    # матрица Хаусхолдера\n    H = np.eye(n)\n    H[i:, i:] -= 2*np.outer(u, u)\n\n    # обновляем R и Q\n    R = H@R\n    Q = Q@H.T\n\n  return Q, R\n\ndef qr_algorithm(A, iter=100):\n  """\n  Функция, выполняющая стандартный QR-алгоритм\n\n  A - матрица\n  iter - допустимое количество итераций\n\n  """\n  # выполняем допустимое количество итераций\n  for _ in range(iter):\n    # разглагаем A на Q и R\n    Q, R = qr_decomposition(A)\n    # обновляем A\n    A = R@Q\n\n  return A\n\ndef implict_qr_algorithm(A, iter=100):\n  """\n  Функция, выполняющая неявный QR-алгоритм со сдвигами\n\n  A - матрица\n  iter - допустимое количество итераций\n  """\n\n  n = A.shape[0]\n\n  for _ in range(iter):\n    # вычисляем сдвиг (последний элемент диагонали)\n    shift = A[n-1, n-1]\n    Q, R = qr_decomposition(A - shift*np.eye(n))\n    A = R@Q + shift*np.eye(n)\n\n  return A\n                    '},
             12: {'theory': '\nАлгоритм "разделяй и властвуй" является одним из базовых методов по ускорению алгоритмов. Примером тому служит переход от квадратичной сложности пузырьковой сортировки или сортировки вставками к сложности $O (nlogn)$ при сортировке слиянием.\nРешение задачи с помощью данного подхода обладает следующими тремя свойствами:\n1. Разделить входные данные на меньшие подмножества.\n2. Решить подзадачи рекурсивно.\n3. Объединить решения подзадач в решение исходной задачи.\n                    ',
              'code': '\nimport math\n\ndef normalize(vector):\n    """Нормализация вектора (приведение к единичной длине)."""\n    norm = math.sqrt(sum(x**2 for x in vector))\n    return [x / norm for x in vector]\n\ndef solve(diagonal, off_diagonal):\n    """Вычисление собственных значений и векторов для матрицы."""\n    trace = diagonal[0] + diagonal[1]  # След матрицы\n    determinant = diagonal[0] * diagonal[1] - off_diagonal[0]**2  # Определитель\n    discriminant = trace**2 - 4 * determinant\n    \n    # Собственные значения\n    eigenvalue1 = (trace + math.sqrt(discriminant)) / 2\n    eigenvalue2 = (trace - math.sqrt(discriminant)) / 2\n    \n    # Собственные векторы\n    eigenvector1 = normalize([1, (eigenvalue1 - diagonal[0]) / off_diagonal[0]])\n    eigenvector2 = normalize([1, (eigenvalue2 - diagonal[0]) / off_diagonal[0]])\n    \n    return [eigenvalue1, eigenvalue2], [eigenvector1, eigenvector2]\n\ndef compute_eigen_tridiagonal(diagonal, off_diagonal):\n    """Вычисление собственных значений и векторов симметричной трёхдиагональной матрицы методом "разделяй и властвуй"."""\n    n = len(diagonal)\n\n    # Базовые случаи\n    if n == 1:\n        return [diagonal[0]], [[1.0]]  # Для матрицы 1x1\n    elif n == 2:\n        return solve(diagonal, off_diagonal)\n\n    # Разделение матрицы\n    mid = n // 2\n    d1 = diagonal[:mid]  # Диагональ первой подматрицы\n    e1 = off_diagonal[:mid - 1] if mid > 1 else []  # Поддиагональ первой подматрицы\n    d2 = diagonal[mid:]  # Диагональ второй подматрицы\n    e2 = off_diagonal[mid - 1:] if mid < n else []  # Поддиагональ второй подматрицы\n    coupling_element = off_diagonal[mid - 1] if mid > 1 else 0  # Связывающий элемент между подматрицами\n\n    # Рекурсивные вызовы для подматриц\n    eigenvalues1, eigenvectors1 = compute_eigen_tridiagonal(d1, e1)\n    eigenvalues2, eigenvectors2 = compute_eigen_tridiagonal(d2, e2)\n\n    # Слияние результатов\n    eigenvalues = eigenvalues1 + eigenvalues2\n\n    # Обработка связи через центральный элемент\n    combined_eigenvectors = []\n\n    for vec in eigenvectors1:\n        combined_vec = vec + [0] * len(eigenvectors2)  # Расширение векторов первой подматрицы\n        combined_eigenvectors.append(combined_vec)\n\n    for vec in eigenvectors2:\n        combined_vec = [0] * len(eigenvectors1) + vec  # Расширение векторов второй подматрицы\n        combined_eigenvectors.append(combined_vec)\n\n    return eigenvalues, combined_eigenvectors\n\n# Пример использования\ndiagonal = [2.0, 3.0, 4.0, 5.0, 6.0]  \noff_diagonal = [-1.0, -1.0, -1.0, -1.0]  \n\neigenvalues, eigenvectors = compute_eigen_tridiagonal(diagonal, off_diagonal)\n\nprint("Собственные значения:")\nprint(eigenvalues)\n\nprint("\nСобственные векторы:")\nfor vec in eigenvectors:\n    print(vec)\n'},
             13: {'theory': '\n**Разреженная матрица** — матрица с преимущественно нулевыми элементами.\n\\begin{equation*}\nA = \\left(\n\\begin{array}{cccc}\n1 & 0 & 0\\\\\n0 & 0 & 0 \\\\\n0 & 9 & 0\n\\end{array}\n\\right)\n\\end{equation*}\n\nФорматы храниения разреженных матриц:\n1. COO (Coordinate list) - хранится список из элементов вида (строка, столбец, значение). \\\n`matrix = [(0, 0, 1), (2, 1, 9)]`\n2. LIL (List of Lists) - строится как список строк, где строка — это список узлов вида (столбец, значение). \\\n`matrix = [[(0, 1)], [], [(1, 9)]]`\n3. CSR (compressed sparse row) - представляем исходную матрицу $M^{n \\cdot m}$, cодержащую $N_{NZ}$ ненулевых значений в виде трёх массивов:\n - **массив значений** - массив размера , в котором хранятся ненулевые значения, взятые подряд из первой непустой строки, затем идут значения из следующей непустой строки и т. д. \\\n - **массив индексов столбцов** - массив размера хранит номера столбцов соответствующих элементов из массива значений. \\\n - **массив индексации строк** - массив размера $n + 1$, для индекса $i$ хранит количество ненулевых элементов в строках с первой до $i - 1$ строки включительно, стоит отметить что последний элемент массива индексации строк совпадает с $N_{NZ}$, а первый всегда равен 0.\n\n```\n  value_matrix = [1, 9]\n  column_index_matrix = [0, 1]\n  row_index_matrix = [0, 1, 1, 2]\n```\n\n4. CSC - только строки и столбцы меняются ролями — значения храним по столбцам, по второму массиву можем определить строку, после подсчётов с третьим массивом — узнаём столбцы.\n5. Блочный вариант BSR. Матрица предварительно разбивается на блоки одинакового размера $n \\cdot n$. Также формируется 3 массива:\n - **Значения ненулевых блоков** - cодержит ненулевые блоки, хранящиеся в построчном порядке. \\\n - **Индексы начала строк блоков** - указывает, где начинается каждая строка в массиве блоков. \\\n - **Индексы столбцов ненулевых блоков** - указывает, в каких столбцах находятся ненулевые блоки.\n\n ---\n **Методы для решения больших разреженных систем**\n\n\n1. **LU-разложение**\n\n**Общий вид матриц**:\n\n$\nA = \\begin{bmatrix}\na_{11} & a_{12} & a_{13} & \\cdots & a_{1n} \\\\\na_{21} & a_{22} & a_{23} & \\cdots & a_{2n} \\\\\na_{31} & a_{32} & a_{33} & \\cdots & a_{3n} \\\\\n\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\\na_{n1} & a_{n2} & a_{n3} & \\cdots & a_{nn}\n\\end{bmatrix},\n\\quad\nL = \\begin{bmatrix}\n1 & 0 & 0 & \\cdots & 0 \\\\\nl_{21} & 1 & 0 & \\cdots & 0 \\\\\nl_{31} & l_{32} & 1 & \\cdots & 0 \\\\\n\\vdots & \\vdots & \\vdots & \\ddots & 0 \\\\\nl_{n1} & l_{n2} & l_{n3} & \\cdots & 1\n\\end{bmatrix},\n\\quad\nU = \\begin{bmatrix}\nu_{11} & u_{12} & u_{13} & \\cdots & u_{1n} \\\\\n0 & u_{22} & u_{23} & \\cdots & u_{2n} \\\\\n0 & 0 & u_{33} & \\cdots & u_{3n} \\\\\n\\vdots & \\vdots & \\vdots & \\ddots & u_{nn} \\\\\n0 & 0 & 0 & \\cdots & u_{nn}\n\\end{bmatrix}.\n$\n\n**Элементы матриц**:\n\nДля $( i \\geq j)$ (нижнетреугольные элементы L):\n$l_{ij} = \\frac{1}{u_{jj}} \\left( a_{ij} - \\sum_{k=1}^{j-1} l_{ik} u_{kj} \\right).$\n\nДля $(i \\leq j)$ (верхнетреугольные элементы  U):\n$u_{ij} = a_{ij} - \\sum_{k=1}^{i-1} l_{ik} u_{kj}.$\n\n**Итеративное вычисление**:\n\nLU-разложение вычисляется по шагам:  \n- На k-м шаге элементы $u_{kj}$ для $( j \\geq k )$ вычисляются как:\n$u_{kj} = a_{kj} - \\sum_{m=1}^{k-1} l_{km} u_{mj}.$\n- Элементы $l_{ik}$ для $i > k$ вычисляются как:\n$l_{ik} = \\frac{1}{u_{kk}} \\left( a_{ik} - \\sum_{m=1}^{k-1} l_{im} u_{mk} \\right).$\n                    ',
              'code': '\nimport numpy as np\nfrom scipy.sparse import coo_matrix, lil_matrix, csr_matrix\nfrom scipy.sparse.linalg import splu\n\ndense_matrix = np.array([[1, 0, 0],\n                         [0, 0, 0],\n                         [0, 9, 0]])\n\n# 1. Формат CSR\ncsr = csr_matrix(dense_matrix)\nprint("Матрица в формате CSR:")\nprint(csr)\n\n# 2. Формат LIL\nlil = lil_matrix(dense_matrix)\nprint("\nМатрица в формате LIL:")\nprint(lil)\n\n# 3. Формат COO\ncoo = coo_matrix(dense_matrix)\nprint("\nМатрица в формате COO:")\nprint(coo)\n\ndef lu_decomposition(A):\n    n = A.shape[0]\n\n    L = np.zeros((n, n))\n    U = np.zeros((n, n))\n\n    for i in range(n):\n        U[i, i:] = A[i, i:] - np.dot(L[i, :i], U[:i, i:])\n\n        L[i:, i] = (A[i:, i] - np.dot(L[i:, :i], U[:i, i])) / U[i, i]\n\n        L[i, i] = 1.0\n\n    return L, U\n\nlu = splu(A)\n\nL = lu.L\nU = lu.U\n                    '},
             14: {'theory': '\nОбыкновенные дифференциальные уравнения (ОДУ) - это уравнения, содержащие одну или несколько производных от искомой функции\n\n**F\n(х, у, уʹ, уʹʹ, уʹʹʹ, ……, у^(n)) = 0**\n\nгде\n**x** независимая переменная, **у = у (х)** искомая функция.\n\nНаивысший порядок производной n входящей в предыдущее уравнение, называют порядком дифференциального уравнения\n\n**Определение задачи Коши**\n\nРассмотрим систему ОДУ первого порядка, записанную в виде\n **уʹ(x) = f(x, y(x))**\n\nРешение: любая функция y(x), которая удовлетворяет уравнению. Решением ОДУ на интервале (a,b) называется функция **у = ф(х)**, которая при ее подстановке в исходное уравнение обращает его в тождество на (a,b)\nРешение ОДУ в неявном виде **Ф(х, у) = 0** называется интегралом ОДУ\nСуществует множество возможных решений Для одного уникального решения\nнеобходимо указать независимые условия (для системы размером 𝑛)\nНапример, когда 𝑛 условий заданы для одной точки **$у(0) = у_0$** называется задачей Коши\n\n*Постановка задачи:* **Ly = f**\n*   **L** - дифференциальный оператор\n*   **у** - точное решение\n*   **f** - правая часть\n                    ',
              'code': 'Тут никакого кода не придумаешь'},
             15: {'theory': '\n- *Локальные погрешности* – погрешности, образовавшиеся на каждом шаге,\n- *Глобальная (накопленная) погрешность* – погрешность, образовавшаяся за несколько шагов.\n\nПорядок глобальной погрешности относительно шага интегрирования на единицу ниже, чем порядок локальной погрешности. Таким образом, глобальная погрешность метода Эйлера имеет порядок p = 1: $g_1$ = C ∙ h, где C– некоторая постоянная.\n\nПорядок численного метода для решения ОДУ определяется порядком его глобальной погрешности. Он может быть также определен, как количество вычислений значения производной f(x, y) искомой функции на каждом шаге. В соответствии с этим метод Эйлера является методом первого порядка.\n\nРассмотрим решение обыкновенного дифференциального уравнения первого порядка:\n\n$\\frac{dx}{dy}$ = $\\frac{y}{(cos(x))^2}$\n\nметодом Эйлера на отрезке [0, 1] с шагом h = 0.1.\n\nНачальные условия: $x_0$ = 0; $y_0$ = 2.7183.\nПостроим таблицу значений переменной $y_i$ при соответствующих значениях переменной $x_i$.\n\n1. $y_1 = y_0 + h \\cdot \\frac{y_0}{(\\cos(x_0))^2} = 2.7183 + 0.1 \\cdot \\frac{2.7183}{(\\cos(0))^2} = 2.9901$\n\n2. $y_2 = y_1 + h \\cdot \\frac{y_1}{(\\cos(x_1))^2} = 2.9901 + 0.1 \\cdot \\frac{2.9901}{(\\cos(0.1))^2} = 3.2922$\n\n3. $y_3 = y_2 + h \\cdot \\frac{y_2}{(\\cos(x_2))^2} = 3.2922 + 0.1 \\cdot \\frac{3.2922}{(\\cos(0.2))^2} = 3.6349$\n\n                    ',
              'code': '\nimport math\ndef func(x, y):\n  return y / math.cos(x) ** 2\n\ndef eiler(func, x0, xf, y0, h):\n  count = int((xf - x0) / h) + 1\n  y = [y0]\n  x = x0\n  for i in range (1, count):\n    y.append(y[i-1] + h * func(x, y[i-1]))\n    x += h\n  return y\n\nprint(eiler(func, 0, 1, 2.7183, 0.1))\n                    '},
             16: {'theory': "\nПринцип метода центральной разности  \nМетод центральной разности — это способ численного приближения производной функции. Идея основана на том, что вместо прямой (продвинутой) или обратной разности для оценки производной берётся средняя точка между двумя соседними значениями функции. Это даёт более высокую точность по сравнению с методами вперёд или назад, так как ошибка аппроксимации имеет порядок $O(h^2)$, где $h$ — шаг сетки.\n\nРассмотрим функцию $f(x)$. Для оценки первой производной в точке $x_0$ используем формулу:\n$f'(x_0) \\approx \\frac{f(x_0 + h) - f(x_0 - h)}{2h}.$\n\nДля второй производной (при условии достаточной гладкости $f$) формула будет:\n$f''(x_0) \\approx \\frac{f(x_0 + h) - 2f(x_0) + f(x_0 - h)}{h^2}.$\n\nЧем меньше шаг $h$, тем точнее получается результат, однако при этом растёт влияние ошибок округления и вычислительные затраты (если речь идёт о задаче с большим числом узлов).\n\nПример вычисления первой производной  \nПусть дана функция $f(x) = \\sin(x)$. Мы хотим найти значение производной $f'(x)$ при $x_0 = 2$ с шагом $h = 0.01$ методом центральной разности. Аналитическая производная для $\\sin(x)$ — это $\\cos(x)$. Значит, точное значение в точке $x_0 = 2$ равно $\\cos(2)$.\n                    ",
              'code': '\nimport numpy as np\ndef central_difference_first_derivative(f, x_0, x_end, h, N = 1000):\n    """\n    f: функция f(x), исходная функция\n    x_0: начальное значение x\n    x_end: конечное значение x\n    N: количество шагов\n    """\n    x = np.linspace(x_0, x_end, N)\n    return (f(x + h) - f(x - h)) / (2 * h)\n\ndef f(x):\n    return x**3*np.cos(x) # Исходная функция\nh = 0.1\nx_0 = 0\nx_end = 10\nN = 100\nx = np.linspace(x_0, x_end, N)\ny_true = 3*x**2 * np.cos(x) - x**3*np.sin(x) # Производная исходной функции\ny_pred = central_difference_first_derivative(f, x_0, x_end, h, N)\n                    '},
             17: {'theory': "\nМетод Эйлера — это численный метод решения задачи Коши для обыкновенных дифференциальных уравнений первого порядка. Его идея заключается в приближенном вычислении значений искомой функции \\( y(x) \\) в узловых точках \\( x_i \\), используя разложение функции в ряд Тейлора.\n\n1. **Формула разложения в ряд Тейлора в окрестности точти**\n\n  $$\n  f(x) = f(x_0) + f'(x_0)(x - x_0) + \\frac{f''(x_0)}{2!}(x - x_0)^2 + \\frac{f^{(3)}(x_0)}{3!}(x - x_0)^3 + \\dots + \\frac{f^{(n)}(x_0)}{n!}(x - x_0)^n + \\dots\n  $$\n\n  В точке x0 + h при малых значениях h достаточно использовать\n  только два слагаемых ряда, получим\n  $$ y(x) = y(x_0+h) = y(x_0) + y'(x_0) \\Delta x + O(h^2)$$\n  где O(h^2) – бесконечно малая величина порядка h^2.\n\n2. **Формула метода**:\n  \n  После преобразования получим:\n   $$y_{n+1} = y_n + hf(x_n, y_n)$$\n   где h — шаг интегрирования, f(x, y) — правая часть дифференциального уравнения.\n\n3. **Алгоритм**:\n   - Известны начальные условия x_0 и y_0.\n   - Значения y вычисляются последовательно для точек x_1 = x_0 + h, x_2 = x_1 + h, используя формулу выше.\n\n4. **Особенности**:\n   - **Простота реализации**: метод основывается на линейной аппроксимации.\n   - **Погрешность**:\n     - Локальная погрешность метода имеет порядок O(h^2).\n     - Глобальная погрешность O(h) .\n   - **Точность**: Увеличивается с уменьшением шага h, но слишком малый шаг увеличивает вычислительные затраты.\n                    ",
              'code': '\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef method_euler(f, x_0, x_end, y_0, N = 1000):\n    """\n    f: функция f(x, y), задающая производную исходной функции\n    x_0: начальное значение x\n    x_end: конечное значение x\n    y_0: начальное значение y\n    N: количество шагов\n    """\n    h = (x_end - x_0) / N\n    x = np.linspace(x_0, x_end, N+1)\n    y = np.zeros(N+1)\n    y[0] = y_0\n\n    for n in range(N):\n        y[n+1] = y[n] + h * f(x[n], y[n])\n\n    return x, y\n\ndef f(x, y):\n    return  3*x**2 * np.cos(x) - x**3*np.sin(x) # Производная исходной функции\nx_0 = 0\nx_end = 10\ny_0 = 0\nN = 100\nx, y = method_euler(f, x_0, x_end, y_0, N)\ny_true = x**3*np.cos(x) # Изначальная функция\n                    '},
             18: {'theory': '\nНа каждом шаге вводятся два этапа, использующих многошаговые методы:\n\n1. С помощью явного метода (**предиктора**) по известным значениям функции в предыдущих узлах находится начальное приближение:\n\n   $$\n   y_{i+1} = y_{i+1}^{(0)}\n   $$\n\n   в новом узле.\n\n2. Используя неявный метод (**корректора**), в результате итераций находятся приближения:\n\n   $$\n   y_{i+1}^{(1)}, \\, y_{i+1}^{(2)}, \\, \\dots\n   $$\n\nОдин из вариантов метода прогноза и коррекции может быть получен на основе метода Адамса четвертого порядка:\n\n- **На этапе предиктора**:\n\n  $$\n  y_{i+1} = y_i + \\frac{h}{24} \\left( 55f_i - 59f_{i-1} + 37f_{i-2} - 9f_{i-3} \\right)\n  $$\n\n- **На этапе корректора**:\n\n  $$\n  y_{i+1} = y_i + \\frac{h}{24} \\left( 9f_{i+1} + 19f_i - 5f_{i-1} + f_{i-2} \\right)\n  $$\n\nЯвная схема используется на каждом шаге один раз, а с помощью неявной схемы строится итерационный процесс вычисления $y_{i+1}$, поскольку это значение входит в правую часть выражения $f_{i+1} = f(x_{i+1}, y_{i+1})$. \n\nРасчёт по этому методу может быть начат только со значения $y_4$. Необходимые при этом $y_1, y_2, y_3$ находятся по методу Рунге-Кутта, $y_0$ задаётся начальным условием.                    \n                    ',
              'code': '\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef predictor_corrector(f, t0, y0, t_end, h):\n    """\n    Метод предиктора-корректора для решения ODE.\n\n    Параметры:\n    f    : функция, задающая правую часть уравнения dy/dt = f(t, y)\n    t0   : начальное время\n    y0   : начальное значение\n    t_end: конечное время\n    h    : шаг\n\n    Возвращает:\n    t : массив времени\n    y : массив решений\n    """\n    # Инициализация массивов времени и решений\n    t = np.arange(t0, t_end + h, h)\n    y = np.zeros_like(t)\n    y[0] = y0\n\n    # Основной цикл\n    for i in range(1, len(t)):\n        # Предиктор (явный метод Эйлера)\n        y_pred = y[i - 1] + h * f(t[i - 1], y[i - 1])\n\n        # Корректор (метод трапеций)\n        y[i] = y[i - 1] + h / 2 * (f(t[i - 1], y[i - 1]) + f(t[i], y_pred))\n\n    return t, y\n\n# Пример использования\ndef f(t, y):\n    return -2 * t * y  # Пример: dy/dt = -2 * t * y\n\nt0 = 0\ny0 = 1\nt_end = 5\nh = 0.1\n\nt, y = predictor_corrector(f, t0, y0, t_end, h)\n\n# График\nplt.plot(t, y, label=\'Predictor-Corrector\', marker=\'o\')\nplt.xlabel(\'t\')\nplt.ylabel(\'y\')\nplt.title(\'Solution of dy/dt = -2 * t * y using Predictor-Corrector\')\nplt.grid(True)\nplt.legend()\nplt.show()\n                    '},
             19: {'theory': "\n*референс: слайды 152-173*\n\\\n\\\nМетод Рунге–Кутты широко используется при интегрировании обыкновенных дифференциальных уравнений (ОДУ) вида:\n\n$$\n\\begin{cases}\ny'(t) = f\\bigl(t,\\,y(t)\\bigr),\\\\\ny(t_0) = y_0.\n\\end{cases}\n$$\n\nПо умолчанию, когда говорят «метод Рунге–Кутты», чаще всего имеют в виду метод Рунге–Кутты 4-го порядка точности. Однако существуют и методы 1-го, 2-го и 3-го порядков, имеющие ту же общую идею.\n\\\n\\\n**Общая идея построения**  \nДля построения разностной схемы интегрирования можно воспользоваться разложением искомой функции \\(y(x)\\) в ряд Тейлора:  \n$$\ny(x_{k+1}) \\;=\\; y(x_k) \\;+\\; y'(x_k)\\,h \\;+\\; \\frac{y''(x_k)}{2}\\,h^2 \\;+\\;\\dots\n$$\nгде $h = x_{k+1} - x_k$.  \n\nЧтобы заменить вторую производную $y''(x_k)$, мы используем приближение производной правой части $f(x,y)$. Например:  \n$$\ny''(x_k) \\;=\\; \\bigl(y'(x_k)\\bigr)' \\;=\\; f'(x_k,\\,y(x_k))\n\\;\\approx\\;\n\\frac{\\,f(\\tilde{x},\\,\\tilde{y}) \\;-\\; f\\bigl(x_k,\\,y(x_k)\\bigr)\\,}{\\,\\Delta x\\,},\n$$\nгде $\\tilde{x} = x_k + \\Delta x$ и $\\tilde{y} = y\\bigl(x_k + \\Delta x\\bigr)$.  \nВыбирая $\\Delta x$ подходящим образом, получают различные схемы **Рунге–Кутты**\n\\\n\\\n**Общая формула для метода Рунге–Кутты**  \n$$\ny_{k+1}\n\\;=\\;\ny_{k}\n\\;+\\;\nh\\,\\Bigl[\n(1-\\alpha)\\,f(x_k,y_k)\n\\;+\\;\n\\alpha\\,f\\Bigl(\nx_k + \\tfrac{h}{2\\alpha},\n\\;\ny_k + f(x_k,y_k)\\,\\tfrac{2h}{\\alpha}\n\\Bigr)\n\\Bigr].\n$$\n\nДля разных значений $\\alpha$ получаются различные частные случаи:\n\nПри $\\alpha=0$ получается метод Эйлера (или же метод Рунге-Кутты 1-го порядка):\n$$\ny_{k+1} \\;=\\; y_{k} \\;+\\; h\\,f\\bigl(x_k,\\,y_k\\bigr).\n$$\n\\\nПри $\\alpha=0.5$ получается классический метод Рунге–Кутты 2-го порядка (иногда называемый «метод Эйлера с пересчётом»):\n$$\ny_{k+1}\n\\;=\\;\ny_{k}\n\\;+\\;\n\\frac{h}{2}\\,\\Bigl[\nf\\bigl(x_k,\\,y_k\\bigr)\n\\;+\\;\nf\\bigl(x_k + h,\\,y_k + h\\,f(x_k,y_k)\\bigr)\n\\Bigr].\n$$\n\\\n\\\n**Метод Рунге–Кутты 1-го порядка (метод Эйлера)**  \nСамый простой вариант, фактически совпадает со схемой Эйлера:  \n$$\ny_{i} \\;=\\; y_{i-1} \\;+\\; h\\,f\\bigl(x_{i-1},\\,y_{i-1}\\bigr).\n$$\n\\\n\\\n**Метод Рунге–Кутты 2-го порядка**  \nЧастным случаем является схема (иногда её называют «Эйлера с пересчётом» или «метод Хойна»):\n\\\n\\\n$$\n\\begin{cases}\nK_1 = f\\bigl(x_{i-1},\\,y_{i-1}\\bigr),\\\\[6pt]\nK_2 = f\\!\\Bigl(x_{i-1} + h,\\;y_{i-1} + h\\,K_1\\Bigr),\\\\[6pt]\ny_i = y_{i-1} + \\frac{h}{2}\\,\\bigl(K_1 + K_2\\bigr).\n\\end{cases}\n$$\n\\\n**Метод Рунге–Кутты 4-го порядка (классический РК4)**  \nНа практике чаще всего применяют **метод Рунге–Кутты 4-го порядка**:\n:$$\n\\begin{cases}\nK_1 = f\\bigl(x_{i-1},\\,y_{i-1}\\bigr),\\\\[6pt]\nK_2 = f\\!\\Bigl(x_{i-1} + \\tfrac{h}{2},\\;y_{i-1} + \\tfrac{h}{2}\\,K_1\\Bigr),\\\\[6pt]\nK_3 = f\\!\\Bigl(x_{i-1} + \\tfrac{h}{2},\\;y_{i-1} + \\tfrac{h}{2}\\,K_2\\Bigr),\\\\[6pt]\nK_4 = f\\!\\Bigl(x_{i-1} + h,\\;y_{i-1} + h\\,K_3\\Bigr),\\\\[6pt]\ny_i = y_{i-1} + \\frac{h}{6}\\,\\bigl(K_1 + 2\\,K_2 + 2\\,K_3 + K_4\\bigr).\n\\end{cases}\n$$\n                    ",
              'code': "\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# Метод Рунге-Кутта 1-го порядка (Метод Эйлера)\ndef runge_kutta_1(f, t0, y0, h, n_steps):\n    t_values = np.linspace(t0, t0 + n_steps*h, n_steps + 1)\n    y_values = np.zeros(n_steps + 1)\n\n    y_values[0] = y0\n\n    for i in range(n_steps):\n        t = t_values[i]\n        y = y_values[i]\n        y_values[i+1] = y + h * f(t, y)\n\n    return t_values, y_values\n\n# Метод Рунге-Кутта 2-го порядка\ndef runge_kutta_2(f, t0, y0, h, n_steps):\n    t_values = np.linspace(t0, t0 + n_steps*h, n_steps + 1)\n    y_values = np.zeros(n_steps + 1)\n\n    y_values[0] = y0\n\n    for i in range(n_steps):\n        t = t_values[i]\n        y = y_values[i]\n\n        K1 = f(t, y)\n        K2 = f(t + h, y + h*K1)\n\n        y_values[i+1] = y + (h/2.0) * (K1 + K2)\n\n    return t_values, y_values\n\n# Метод Рунге-Кутта 3-го порядка\ndef runge_kutta_3(f, t0, y0, h, n_steps):\n    t_values = np.linspace(t0, t0 + n_steps*h, n_steps + 1)\n    y_values = np.zeros(n_steps + 1)\n\n    y_values[0] = y0\n\n    for i in range(n_steps):\n        t = t_values[i]\n        y = y_values[i]\n\n        K1 = f(t, y)\n        K2 = f(t + h/2.0, y + h/2.0*K1)\n        K3 = f(t + h, y - h*K1 + 2.0*h*K2)\n\n        y_values[i+1] = y + (h/6.0)*(K1 + 4.0*K2 + K3)\n\n    return t_values, y_values\n\n# сама функция Рунге-Кутта 4-го порядка\ndef runge_kutta_4(f, t0, y0, h, n_steps):\n    t_values = np.linspace(t0, t0 + n_steps*h, n_steps + 1)\n    y_values = np.zeros(n_steps + 1)\n\n    y_values[0] = y0\n\n    for i in range(n_steps):\n        t = t_values[i]\n        y = y_values[i]\n\n        K1 = f(t,         y)\n        K2 = f(t + h/2.0, y + h/2.0*K1)\n        K3 = f(t + h/2.0, y + h/2.0*K2)\n        K4 = f(t + h,     y + h*K3)\n\n        y_values[i+1] = y + (h/6.0)*(K1 + 2.0*K2 + 2.0*K3 + K4)\n\n    return t_values, y_values\n\n# Пример y'(t) = -2y,  y(0) = 1\ndef f_example(t, y):\n    return -2*y\n\nt0 = 0.0\ny0 = 1.0\nh = 0.1\nn_steps = 50\n\n# Расчеты для каждого метода\nt_vals_1, y_vals_1 = runge_kutta_1(f_example, t0, y0, h, n_steps)\nt_vals_2, y_vals_2 = runge_kutta_2(f_example, t0, y0, h, n_steps)\nt_vals_3, y_vals_3 = runge_kutta_3(f_example, t0, y0, h, n_steps)\nt_vals_4, y_vals_4 = runge_kutta_4(f_example, t0, y0, h, n_steps)\n\n# Точное решение\ny_exact = y0 * np.exp(-2 * t_vals_4)\n\n# Графики\nplt.plot(t_vals_1, y_vals_1, 'o-', label='РК1 (Эйлер)', alpha=0.5)\nplt.plot(t_vals_2, y_vals_2, 's-', label='РК2', alpha=0.5)\nplt.plot(t_vals_3, y_vals_3, '^-', label='РК3', alpha=0.5)\nplt.plot(t_vals_4, y_vals_4, 'o-', label='РК4', alpha=0.5)\nplt.plot(t_vals_4, y_exact, 'r--', label='Точное решение')\nplt.legend()\nplt.grid(True)\nplt.xlabel('t')\nplt.ylabel('y(t)')\nplt.title('Методы Рунге-Кутта')\nplt.show()\n                    "},
             20: {'theory': '\n**Явный метод Адамса:** Использует предыдущие значения $𝑦_𝑛\n, 𝑦_{𝑛−1}\n, … $ для\nаппроксимации следующего значения $𝑦_{𝑛+1}\n$.  \nФормула для трехшагового метода\nАдамса-Башфорта:  \n$$y_{n+1} = y_n + \\frac{h}{12}(23f(t_n, y_n) - 16f(t_{n-1}, y_{n-1}) + 5f(t_{n-2}, y_{n-2}))$$\n\n**Неявный метод Адамса:** Использует текущие и будущие значения для более\nточного результата.  \nФормула для трехшагового метода Адамса-Мултона:  \n$$y_{n+1} = y_n + \\frac{h}{12}(5f(t_{n+1}, y_{n+1}) + 8f(t_{n}, y_{n}) - f(t_{n-1}, y_{n-1}))$$\n\n**Явные методы**  \nПреимущества: Простота реализации и вычислительная эффективность.\nНедостатки: Ограниченная стабильность, особенно для жестких систем.  \n**Неявные методы**  \nПреимущества: Более высокая стабильность, подходящие для жестких\nсистем.  \nНедостатки: Более сложная реализация и необходимость решения\nнелинейных уравнений на каждом шаге.\n                    ',
              'code': '\ndef f(t, y):\n    return -y + np.sin(t)  # Пример функции\n\n# Явный метод Адамса (Адамса-Бэшфорта) для трехшагового метода\ndef adams_bashforth_3(f, t, y0, h):\n    n = len(t)\n    y = np.zeros(n)\n    y[0] = y0\n\n    # Начальные значения y1, y2 получаем методом Рунге-Кутты 2-го порядка\n    y[1] = y[0] + h * f(t[0], y[0])\n    y[2] = y[1] + h * f(t[1], y[1])\n\n    for i in range(2, n - 1):\n        y[i + 1] = y[i] + (h / 12) * (23 * f(t[i], y[i]) - 16 * f(t[i - 1], y[i - 1]) + 5 * f(t[i - 2], y[i - 2]))\n\n    return y\n\n# Неявный метод Адамса (Адамса-Мултона) для трехшагового метода\ndef adams_moulton_3(f, t, y0, h, tol=1e-6, max_iter=10):\n    n = len(t)\n    y = np.zeros(n)\n    y[0] = y0\n\n    # Начальные значения y1, y2 получаем методом Рунге-Кутты 2-го порядка\n    y[1] = y[0] + h * f(t[0], y[0])\n    y[2] = y[1] + h * f(t[1], y[1])\n\n    for i in range(2, n - 1):\n        # Предиктор: используем явный метод для начального приближения y[i+1]\n        y_pred = y[i] + (h / 12) * (23 * f(t[i], y[i]) - 16 * f(t[i - 1], y[i - 1]) + 5 * f(t[i - 2], y[i - 2]))\n\n        # Корректор: итерационно решаем нелинейное уравнение для y[i+1]\n        y_new = y_pred\n        for _ in range(max_iter):\n            y_next = y[i] + (h / 12) * (5 * f(t[i + 1], y_new) + 8 * f(t[i], y[i]) - f(t[i - 1], y[i - 1]))\n            if np.abs(y_next - y_new) < tol:\n                break\n            y_new = y_next\n\n        y[i + 1] = y_new\n\n    return y\n\n# Пример использования\nt = np.arange(0, 2, 0.1)  # Временные шаги\ny0 = 1.0  # Начальное условие\nh = 0.1   # Шаг\n                    '},
             21: {'theory': '\nМетод Милна относится к многошаговым методам и представляет один из методов\nпрогноза и коррекции (предиктора-корректора).\n\nРешение в следующей точке находится в два этапа.\n\nНа **первом этапе** осуществляется по специальной\nформуле **прогноз** значения функции, а\nзатем на **втором этапе** - **коррекция**\nполученного значения.\n\nЕсли полученное значение у после\nкоррекции существенно отличается от\nспрогнозированного, то проводят еще\nодин этап коррекции.\n\n------------\n\nМетод Милна имеет следующие вычислительные формулы:\\\nа) этап предположения (прогноза):\\\n$y_i^{пред} = y_{i-4} + \\frac{4*h}{3}(2f_{i-3} - f_{i-2} + 2f_{i-1})$\n\nгде для компактности записи использовано следующее обозначение\\\nf_i = f(x_i, y_i)\n\n\nб) этап коррекции:\\\n$y_i^{корр} = y_{i-2} + \\frac{h}{3}(f_{i-2} + 4f_{i-1} + f_i^{пред})$\n\n---------------\n\nМетод требует несколько меньшего количества вычислений (например, достаточно\nтолько два раза вычислить остальные запомнены с предыдущих этапов), но\nтребует дополнительного "расхода" памяти. Кроме этого, как уже указывалось выше,\nневозможно "запустить" метод: для этого необходимо предварительно получить\nодношаговыми методами первые три точки.\n\nПервые три точки можно получить с помощью метода Рунге-Кутты\n\n------------------\n                    ',
              'code': '\nimport numpy as np\n\ndef milne_method(f, x_0, x_n, y_0, N):\n    dx = (x_n - x_0) / N\n    x = np.linspace(x_0, x_n, N + 1)\n    y = np.zeros((N + 1, len(y_0)))\n    fn = np.zeros_like(y)\n    y[0, :] = y_0\n\n    # Инициализация методом Рунге-Кутта 4-го порядка для первых 4 точек\n    for n in range(3):\n        k1 = dx * f(x[n], y[n, :])\n        k2 = dx * f(x[n] + dx / 2, y[n, :] + k1 / 2)\n        k3 = dx * f(x[n] + dx / 2, y[n, :] + k2 / 2)\n        k4 = dx * f(x[n] + dx, y[n, :] + k3)\n        y[n + 1, :] = y[n, :] + 1 / 6 * (k1 + 2 * k2 + 2 * k3 + k4)\n\n    # Вычисляем значения функции f(x, y) для первых 4 точек\n    for n in range(4):\n        fn[n, :] = f(x[n], y[n, :])\n\n    # Основной цикл метода Милна\n    for n in range(3, N):\n        # Предсказание (Milne predictor)\n        y_pred = y[n - 3, :] + 4 * dx / 3 * (2 * fn[n, :] - fn[n - 1, :] + 2 * fn[n - 2, :])\n\n        # Коррекция (Milne corrector)\n        fn_pred = f(x[n + 1], y_pred)\n        y[n + 1, :] = y[n - 1, :] + dx / 3 * (fn_pred + 4 * fn[n, :] + fn[n - 1, :])\n        # Обновляем значение функции f(x, y) для нового шага\n        fn[n + 1, :] = f(x[n + 1], y[n + 1, :])\n\n    return x, y\n\ndef equations(x, y):\n    return np.array([np.arctan(1 / (1 + y[0]**2 + y[1]**2)), np.sin(y[0] * y[1])])\n\nx, y = milne_method(equations, -1.0, 5.0, [-1.0, -1.0], 60000)\n\nprint("Последние значения x:", x[-1])\nprint("Последние значения y:", y[-1])\n                    '},
             22: {'theory': '\n*референс слайды 227-229*\n\nПри рассмотрении методов численного решения ОДУ вида  \n$$\n\\sum_{i=0}^{k} \\alpha_i\\,y_{n+i}\n\\;=\\;\nh \\sum_{i=0}^{n} \\beta_i\\,f\\bigl(x_{n+i},\\,y_{n+i}\\bigr),\n\\quad\nn = 0,1,2,\\dots\n$$\nважными свойствами метода являются:\n\n\n1. **Согласованность**  \n\n   Пусть $y(x)$ — точное решение дифференциального уравнения. Подставим $y(x_{n+i})$ вместо $y_{n+i}$ в разностную схему. Тогда невязка\n   $$\n   \\rho_{n+k}\n   \\;=\\;\n   \\sum_{i=0}^k \\alpha_i \\,y(x_{n+i})\n   \\;-\\;\n   h \\sum_{i=0}^n \\beta_i \\,f\\bigl(x_{n+i},\\,y(x_{n+i})\\bigr)\n   $$\n   называется погрешностью аппроксимации (или локальной ошибкой метода).  \n   - Если $\\rho_{n+k} = O\\bigl(h^{\\,s+1}\\bigr)$, то говорят, что порядок аппроксимации (или степень разностного уравнения) равен $s$.  \n   - Соответственно, $r_{n+k} = \\rho_{n+k}\\,/\\,h$ называют погрешностью дискретизации.  \n\n   Метод называется **согласованным** с порядком $s$, если при $h \\to 0$ невязка $\\rho_{n+k}$ убывает как $O\\bigl(h^{\\,s+1}\\bigr)$ для любого точного решения.\n\n\n2. **Нуль-устойчивость**  \n\n   Рассмотрим характеристический многочлен\n   $$\n   \\rho(\\theta)\n   \\;=\\;\n   \\sum_{i=0}^{k} \\alpha_i\\,\\theta^{\\,i},\n   $$\n   возникающий при анализе однородной части разностной схемы.  \n   Метод называют **нуль-устойчивым**, если все корни $\\theta_i$ многочлена $\\rho(\\theta)$ лежат внутри или на единичной окружности в комплексной плоскости, причём корни на окружности являются простыми (не имеют кратности больше 1).   \n\n   Интуитивно говоря, нуль-устойчивость гарантирует, что ошибки (возникающие, например, при начальных условиях) не будут «разрастаться» при переходе от шага к шагу.\n\n\n3. **Сходимость**  \n\n   Пусть $y_{n}$ — численное решение (вычисленное методом), а $y(x)$ — точное решение задачи Коши. Говорят, что метод **сходится**, если\n   $$\n   y_n \\;\\to\\; y\\bigl(x_n\\bigr)\n   \\quad\n   \\text{при }\n   h \\to 0,\\,\n   n = \\frac{x - x_0}{h}.\n   $$\n   На практике это означает, что точность возрастает при уменьшении шага h.\n\n   **Основная теорема** (аналог теоремы Лакса в теории разностных схем) утверждает, что:\n   - Метод из данного класса **сходится** тогда и только тогда, когда он **согласован** и **нуль-устойчив**.  \n   - Порядок сходимости равен порядку согласованности $s$.\n\n\n4. **Интерпретация**  \n   - **Согласованность** определяет «уровень» локальной погрешности (насколько точно разностная схема аппроксимирует само дифференциальное уравнение).  \n   - **Нуль-устойчивость** описывает, как «накопленные» ошибки (начальные или вычислительные) будут вести себя по мере продвижения по шагам.  \n   - Если метод согласован и нуль-устойчив, то он **сходится**, то есть глобальная погрешность $y_n - y(x_n)$ исчезает при $h \\to 0$.\n                    ',
              'code': '\nсомневаюсь, что тут можно какой-то код придумать\n                    '},
             23: {'theory': '\n* Амплитуда ($A$): Максимальное отклонение волны от положения равновесия. Определяет "высоту" волны.\n\n* Период ($T$): Время, за которое волна совершает одно полное колебание. Измеряется в секундах.\n\n* Длина волны ($λ$): Пространственное расстояние между двумя соседними точками волны, колеблющимися в одинаковых фазах. Измеряется в метрах.\n\n* Частота ($f$): Количество полных колебаний, совершаемых волной за единицу времени. Измеряется в герцах (Гц). Связана с периодом соотношением: $f = 1/T.$\n\n* Угловая частота ($ω$): Скорость изменения фазы волны. Измеряется в радианах в секунду. Связана с частотой соотношением: $ω = 2πf.$\n\n* Фаза ($φ$): Определяет начальное положение точки волны в момент времени $t=0.$\n\n* Дискретизация: Процесс представления непрерывной функции (например, волны) в виде дискретного набора точек.\n\n* Частота дискретизации: Количество отсчетов, берущихся за единицу времени.\n\nРяды Фурье\n\nРяды Фурье позволяют изучать периодические (и непериодические)\nфункции при помощи представления их функциональными рядами. Комплексная форма ряда Фурье является наиболее употребимой в обработке сигналов.\n\nРассмотрим произвольную периодическую функцию $f(x)$ с периодом $2l$. Ее разложение в тригонометрический ряд Фурье имеет вид:\n\n\\begin{equation}\nf(x) = \\frac{a_0}{2} + \\sum_{n=1}^{\\infty} \\left[ a_n \\cos\\left(\\frac{n\\pi x}{l}\\right) + b_n \\sin\\left(\\frac{n\\pi x}{l}\\right) \\right]\n\\end{equation}\n\nДля более компактной записи ряда Фурье можно использовать следующую формулу:\n\n\\begin{equation}\nf(x) = \\frac{a_0}{2} + \\sum_{n=1}^{\\infty} A_n \\sin\\left(\\frac{n\\pi x}{l} + \\phi_n\\right)\n\\end{equation}\n\nгде:\n\n\\begin{align*}\nA_n &= \\sqrt{a_n^2 + b_n^2} \\\\\n\\phi_n &= \\arctan{\\frac{b_n}{a_n}}\n\\end{align*}\n\nВеличина $A_n$ называется амплитудой $n$-ой гармоники, а $\\phi_n$ – ее фазой. Совокупность всех амплитуд $A_n$ образует амплитудный спектр, а совокупность фаз $\\phi_n$ – фазовый спектр сигнала.\n                    ',
              'code': '\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# Параметры волны\nA = 1.0             # Амплитуда\nT = 1.0             # Период\nf = 1 / T           # Частота (Гц)\nomega = 2 * np.pi * f  # Угловая частота\nphi = 0.0           # Фаза (радианы)\nsampling_rate = 100  # Частота дискретизации (Гц)\nduration = 2.0       # Длительность сигнала (секунды)\n\n# Дискретизация времени\nt = np.linspace(0, duration, int(sampling_rate * duration), endpoint=False)\n\n# Моделирование синусоидальной волны\nwave = A * np.sin(omega * t + phi)\n\n# Визуализация волны\nplt.figure(figsize=(10, 4))\nplt.plot(t, wave, label="Синусоидальная волна")\nplt.title("Моделирование волны")\nplt.xlabel("Время (с)")\nplt.ylabel("Амплитуда")\nplt.grid(True)\nplt.legend()\nplt.show()\n\n# Применение ряда Фурье (быстрое преобразование Фурье)\nn = len(wave)\nfrequencies = np.fft.fftfreq(n, d=1/sampling_rate)\nfft_values = np.fft.fft(wave)\n\n# Модуль и фаза спектра\namplitude_spectrum = np.abs(fft_values) / n\nphase_spectrum = np.angle(fft_values)\n\n# Визуализация амплитудного спектра\nplt.figure(figsize=(10, 4))\nplt.stem(frequencies[:n // 2], amplitude_spectrum[:n // 2])\nplt.title("Амплитудный спектр (ряд Фурье)")\nplt.xlabel("Частота (Гц)")\nplt.ylabel("Амплитуда")\nplt.grid(True)\nplt.show()\n\n# Визуализация фазового спектра\nplt.figure(figsize=(10, 4))\nplt.stem(frequencies[:n // 2], phase_spectrum[:n // 2])\nplt.title("Фазовый спектр (ряд Фурье)")\nplt.xlabel("Частота (Гц)")\nplt.ylabel("Фаза (радианы)")\nplt.grid(True)\nplt.show()\n                    '},
             24: {'theory': '\nДискретное преобразование Фурье: $X(n) = \\sum_{k = 0}^{N - 1} x(k) * exp^{\\frac{-2j * \\pi * k * n} {N}}$ \\\nОбратное дискретное преобразование Фурье: $x(k) = \\frac{1}{N} \\sum_{n = 0}^{N - 1} X(n) * exp^{\\frac{2j * \\pi * k * n} {N}}$ \\\n**Ограничения**\n\n\n1. При перемножении сигналов их длины должны быть\nодинаковыми (N);\n\n2. Cуммирование элементов произведения должно\nпроизводиться по одному периоду (полученный результат\nназывается круговой сверткой спектров исходных\nсигналов).\n\n3. $O(N^{2})$ - время работы алгоритма\n\nСимметрия для действительных сигналов:\n\nЕсли\n$x[n]$ — действительный сигнал, то его спектр\n$X[k]$ обладает следующими свойствами:\n$X[N−k] = \\overline{X[k]}$, где $\\overline{X[k]}$ — комплексное сопряжение.\nЭто означает, что спектр симметричен относительно середины.\n\n                    ',
              'code': '\ndef dft(signal):\n    N = len(signal)\n    spectrum = np.zeros(N, dtype = complex)\n    for k in range(N):\n        X_k = 0\n        for n in range(N):\n            X_k += signal[n] * np.exp(-2j * np.pi * k * n / N)\n        spectrum[k] = X_k\n    return spectrum\n\ndef idft(spectrum):\n    N = len(spectrum)\n    signal = np.zeros(N)\n    for n in range(N):\n        x_n = 0\n        for k in range(N):\n            x_n += spectrum[k] * np.exp(2j * np.pi * k * n / N)\n        x_n = x_n / N\n        signal[n] = x_n.real\n    return signal\n                    '},
             25: {'theory': '\n**Принцип алгоритма БФП** \\\nБПФ можно разделить на две меньшие части. В отличие от прямого ДПФ, который требует  $O(N^{2})$  вычислений, алгоритм БПФ использует свойство симметрии и делит задачу на более мелкие подзадачи. Сначала разделяем исходное преобразование на два: одно для четных, другое для нечетных индексов. Каждый из этих подзадач похож на ДПФ меньшего размера (на $N/2$), и для их вычисления потребуется меньше операций. Этот процесс рекурсивно повторяется, пока размер подзадачи не станет слишком малым, чтобы применять дальнейшее разделение. Каждый шаг уменьшает количество вычислений, и в итоге общая сложность алгоритма становится\n$O(N\\log{N})$, что значительно быстрее прямого ДПФ.\n\n**Фильтрация сигнала** \\\nПрименить БПФ к сигналу, чтобы преобразовать его в частотную область. Дальше обнуляются или уменьшаются амплитуды на определенных частотах. Применяется обратное преобразование Фурье, чтобы вернуть сигнал в временную область.\n\n                    ',
              'code': '\ndef fft(signal):\n    n = len(signal)\n    if n <= 1:\n        return signal\n\n    even = fft(signal[::2])\n    odd = fft(signal[1::2])\n\n    combined = np.zeros(n, dtype=complex)\n    for k in range(n // 2):\n        t = np.exp(-2j * np.pi * k / n) * odd[k]\n        combined[k] = even[k] + t\n        combined[k + n // 2] = even[k] - t\n\n    return combined\n\ndef ifft(signal):\n    n = len(signal)\n    if n <= 1:\n        return signal\n\n    even = ifft(signal[::2])\n    odd = ifft(signal[1::2])\n\n    combined = np.zeros(n, dtype=complex)\n    for k in range(n // 2):\n        t = np.exp(2j * np.pi * k / n) * odd[k]\n        combined[k] = (even[k] + t) / 2\n        combined[k + n // 2] = (even[k] - t) / 2\n\n    return combined\n                    '},
             26: {'theory': '\n**Определение**  \nПусть $f,g:\\mathbb{R}^n \\to \\mathbb{R}$\xa0— две функции, на пространстве $\\mathbb{R}^n$. Тогда их свёрткой называется функция $f * g:\\mathbb{R}^n \\to \\mathbb{R}$, определённая формулой\n:$$\n(f * g)(x)\\\n\\stackrel{\\mathrm{def}}{=}\\ \\int \\limits_{\\mathbb{R}^n} f(y)\\, g(x-y)\\, dy =\n\\int \\limits_{\\mathbb{R}^n} f(x-y)\\, g(y)\\, dy.\n$$\nВ частности, при $n=1$ формула принимает вид\n:$$\n(f * g)(x)\\\n\\stackrel{\\mathrm{def}}{=}\\ \\int \\limits_{-\\infty}^{\\infty} f(y)\\, g(x-y)\\, dy =\n\\int \\limits_{-\\infty}^{\\infty} f(x-y)\\, g(y)\\, dy.\n$$\n\n**Связь с преобразованием Фурье**  \nОдним из наиболее значимых свойств свёртки является её связь с преобразованием Фурье. Свёртка функций в пространстве $R^n$ преобразуется в произведение их преобразований Фурье:\n\n$$F(f∗g)(ξ)=F(f)(ξ)⋅F(g)(ξ),$$\n\nгде\n$F(f)$ — преобразование Фурье функции\n$f$. Это свойство позволяет вычислять свёртку быстрее, особенно в случае высокоразмерных данных, используя быстрое преобразование Фурье (FFT).\n\n**Дискретная свёртка**  \nДля цифровой обработки данных используется дискретная свёртка. Пусть\n$f[k]$ и $g[k]$ — дискретные последовательности. Тогда их дискретная свёртка определяется как:\n$$(f∗g)[n]= \\sum^∞ _{k=−∞} f[k]⋅g[n−k].$$\n\n**Преимущества свёртки через FFT**  \nПрямая вычислительная свёртка имеет сложность\n$O(N\n^2\n )$ для сигналов длины N.\nИспользуя FFT, свёртка может быть выполнена с вычислительной сложностью\n$O(NlogN)$, что существенно ускоряет обработку.\n                    ',
              'code': '\nfrom scipy.signal import convolve\n\n# Пример функций\ndef f(x):\n    return np.exp(-x**2)  # Гауссиана\n\ndef g(x):\n    return np.sin(2 * np.pi * x)  # Синусоида\n\n# Дискретизация\nx = np.linspace(-5, 5, 500)  # Интервал дискретизации\nf_discrete = f(x)  # Дискретизация функции f\ng_discrete = g(x)  # Дискретизация функции g\n\n# Прямая свёртка\ndirect_convolution = np.convolve(f_discrete, g_discrete, mode="same")\n\n# Свёртка с использованием FFT\nfft_f = np.fft.fft(f_discrete)\nfft_g = np.fft.fft(g_discrete)\nfft_convolution = np.fft.ifft(fft_f * fft_g).real\n\n# Встроенная дискретная свёртка (Scipy)\nscipy_convolution = convolve(f_discrete, g_discrete, mode="same")\n                    '},
             27: {'theory': '\n**Дискретную свёртку** можно представить как умножение матрицы на вектор:\n\n$z_i = \\sum_{j=0}^{n-1}x_jy_{i-j}\\Leftrightarrow z = Ax$\n\nгде элементы матрицы ${A}$ равны ${a_{ij}} = y_{i-j}$, то есть они зависят только от разности между индексами строки и столбца.\n\n------------------------\n\n**Тёплицевы матрицы**\\\n**Матрица Тёплица** (диагонально-постоянная матрица) — матрица, в которой на всех диагоналях, параллельных главной, стоят равные элементы. То есть выполняется соотношение:\n\n$$a_{ij} = t_{i - j}.$$\n\n- Тёплицева матрица полностью определяется первой строкой и первым столбцом (то есть $2n-1$ параметр).\n\n- Это плотная матрица, однако она имеет структуру, то есть определяется $\\mathcal{O}(n)$ параметрами (сравните с разреженными матрицами)\n\n- Основная операция для вычисления дискретной свёртки – это произведение Тёплицевой матрицы на вектор.\n\nОбщий вид:\n\n$\nT =\n\\begin{bmatrix}\nt_0 & t_{-1} & t_{-2} & \\cdots & t_{-n+1} \\\\\nt_1 & t_0 & t_{-1} & \\cdots & t_{-n+2} \\\\\nt_2 & t_1 & t_0 & \\cdots & t_{-n+3} \\\\\n\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\\nt_{n-1} & t_{n-2} & t_{n-3} & \\cdots & t_0\n\\end{bmatrix}.\n$\n\n------------------\n\n**Ганкелевы матрицы**\\\nГанкелева матрица — это матрица, у которой на всех диагоналях, перпендикулярных главной, стоят равные элементы.\\\nОбщий вид:\n\n$H =\n\\begin{bmatrix}\nh_0 & h_1 & h_2 & \\cdots & h_{n-1} \\\\\nh_1 & h_2 & h_3 & \\cdots & h_n \\\\\nh_2 & h_3 & h_4 & \\cdots & h_{n+1} \\\\\n\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\\nh_{n-1} & h_n & h_{n+1} & \\cdots & h_{2n-2}\n\\end{bmatrix}.\n$\n                    ',
              'code': '\nfrom scipy.linalg import toeplitz, hankel\n\n# Дискретная свёртка через матричное умножение (Тёплицева матрица)\ndef discrete_convolution(x, y):\n    n = len(x)\n    m = len(y)\n    # Создаем Тёплицеву матрицу на основе y\n    first_col = np.concatenate(([y[0]], np.zeros(n - 1)))\n    first_row = np.concatenate((y, np.zeros(n - 1)))\n    toeplitz_matrix = toeplitz(first_col, first_row[:n])\n\n    # Умножаем Тёплицеву матрицу на вектор x\n    return toeplitz_matrix @ x\n\n# Генерация Тёплицевой матрицы\ndef generate_toeplitz(first_col, first_row):\n    return toeplitz(first_col, first_row)\n\n# Генерация Ганкелевой матрицы\ndef generate_hankel(first_col, last_row):\n    return hankel(first_col, last_row)\n                    '},
             28: {'theory': "\nЦиркулянтные матрицы = циркулянты\n\n- Матрица $C$ называется циркулянтом, если\n\n$$C_{ij} = c_{i - j \\mod n},$$\n\nто есть\n\n$$C = \\begin{bmatrix} c_0 & c_1 & c_2 & c_3 \\\\\n c_3 & c_0 & c_1 & c_2 \\\\\n c_2 & c_3 & c_0 & c_1 \\\\\n c_1 & c_2 & c_3 & c_0 \\\\\n \\end{bmatrix}.\n $$\n\n ### Матрица Фурье\n\nМатрица Фурье определяется как:\n\n$$\nF_n =\n\\begin{pmatrix}\n1 & 1 & 1 & \\dots & 1 \\\\\n1 & w^{1\\cdot 1}_n & w^{1\\cdot 2}_n & \\dots & w^{1\\cdot (n-1)}_n\\\\\n1 & w^{2\\cdot 1}_n & w^{2\\cdot 2}_n & \\dots & w^{2\\cdot (n-1)}_n\\\\\n\\dots & \\dots & \\dots &\\dots &\\dots \\\\\n1 & w^{(n-1)\\cdot 1}_n & w^{(n-1)\\cdot 2}_n & \\dots & w^{(n-1)\\cdot (n-1)}_n\\\\\n\\end{pmatrix},\n$$\n\nили эквивалентно\n\n$$ F_n = \\{ w_n^{kl} \\}_{k,l=0}^{n-1}, $$\n\nгде\n\n$$w_n = e^{-\\frac{2\\pi i}{n}}.$$\n\n**Свойства:**\n\n* Симметричная (но не эрмитова!)\n* Унитарна с точностью до нормализующего множителя: $F_n^* F_n = F_n F_n^* = nI$ (проверьте этот факт!). Поэтому $F_n^{-1} = \\frac{1}{n}F^*_n$\n* Может быть умножена на вектора (произведение называется дискретным преобразованием Фурье (DFT)) за  <font color='red'>$\\mathcal{O}(n \\log n)$</font> операций (операция называется быстрым преобразованием Фурье или <font color='red'>FFT</font>)!\n* FFT помогает анализировать спектр сигнала и, как мы увидим далее, позволяет быстро умножать некоторый класс матриц на вектор.\n\n### Спектральная теорема для циркулянтов\n\n**Теорема**\n\nЛюбая циркулянтная матрица может быть представлена в виде\n\n$$C = \\frac{1}{n} F^* \\Lambda F,$$\n\nгде $F$ – **матрица Фурье** с элементами\n\n$$F_{kl} = w_n^{kl}, \\quad k, l = 0, \\ldots, n-1, \\quad w_n = e^{-\\frac{2 \\pi i}{n}},$$\n\nа матрица $\\Lambda = \\text{diag}(\\lambda)$ диагональная и\n\n$$\\lambda = F c, $$\n\nгде $c$ – первый столбец циркулянта $C$.\n                    ",
              'code': '\nfrom scipy.linalg import circulant, dft\n\n# Создание циркулянтной матрицы\ndef create_circulant(c):\n    """\n    Создает циркулянтную матрицу из первого столбца c.\n    """\n    return circulant(c)\n\n# Создание матрицы Фурье\ndef create_fourier_matrix(n):\n    """\n    Создает матрицу Фурье размера n x n.\n    """\n    w_n = np.exp(-2j * np.pi / n)  # корень из единицы\n    F = np.array([[w_n**(k * l) for l in range(n)] for k in range(n)])\n    return F\n\n# Проверка спектральной теоремы для циркулянтов\ndef spectral_theorem_circulant(c):\n    """\n    Проверяет спектральную теорему для циркулянтной матрицы.\n    """\n    n = len(c)\n    C = create_circulant(c)  # Циркулянтная матрица\n    F = create_fourier_matrix(n)  # Матрица Фурье\n    F_star = np.conj(F.T)  # Сопряжённо-транспонированная матрица Фурье\n\n    # Собственные значения\n    lambdas = F @ c  # Диагональные элементы λ = F @ c\n    Lambda = np.diag(lambdas)  # Диагональная матрица\n\n    # Реконструкция циркулянта через спектральную теорему\n    C_reconstructed = (1 / n) * F_star @ Lambda @ F\n\n    return C, C_reconstructed, lambdas\n\n# Пример использования\nc = np.array([1, 2, 3, 4])  # Первый столбец циркулянтной матрицы\n\n# Циркулянт и его спектральная теорема\nC, C_reconstructed, lambdas = spectral_theorem_circulant(c)\n\n# Матрица Фурье\nF = create_fourier_matrix(len(c))\n                    '},
             29: {'theory': '\nЦиркулянт (circulant) — это специальный тип матрицы, в которой каждый ряд является циклическим сдвигом предыдущего ряда. Циркулянты часто встречаются в различных областях науки и инженерии, включая обработку сигналов, теорию управления и численные методы.\n\nБыстрый метод решения систем линейных уравнений с циркулянтными матрицами называется "Быстрый матвек с циркулянтом". Этот метод использует свойства циркулянтных матриц и преобразование Фурье для значительного ускорения вычислений.\n\nСвойства циркулянтных матриц\n1. **Диагонализация с помощью преобразования Фурье**: - Циркулянтные матрицы могут быть диагонализированы с помощью матрицы преобразования Фурье. Если $ F $ — матрица преобразования Фурье, то: $$ C = F^* \\Lambda F $$ где $ \\Lambda $ — диагональная матрица, содержащая собственные значения $ C $.\n2. **Собственные значения и векторы**: - Собственные значения циркулянтной матрицы $ C $ являются значениями дискретного преобразования Фурье (DFT) первого столбца $ c $. - Собственные векторы циркулянтной матрицы $ C $ являются столбцами матрицы преобразования Фурье $ F $.\n\nБыстрый матвек с циркулянтом   \n\nДля решения системы линейных уравнений $ Cx = b $, где $ C $ — циркулянтная матрица, можно использовать быстрый метод, основанный на преобразовании Фурье.\n\n#### Алгоритм\n\n1. **Вычисление DFT первого столбца $ c $**: - Вычислите DFT первого столбца $ c $ циркулянтной матрицы $ C $ для получения собственных значений $ \\lambda $.\n\n2. **Вычисление DFT правой части $ b $**: - Вычислите DFT правой части $ b $ для получения $ \\hat{b} $.\n\n3. **Поэлементное деление**: - Выполните поэлементное деление $ \\hat{b} $ на собственные значения $ \\lambda $ для получения $ \\hat{x} $.\n\n4. **Вычисление обратного DFT**: - Вычислите обратное DFT $ \\hat{x} $ для получения решения $ x $.  \n\nДОДЕЛАТЬ (КАРТИНКА)\n                    ',
              'code': "\nimport numpy as np\nimport scipy as sp\nimport scipy.linalg\n\ndef circulant_matvec(c, x):\n    return np.fft.ifft(np.fft.fft(c) * np.fft.fft(x))\n\nn = 5000\nc = np.random.random(n)\nC = sp.linalg.circulant(c)\nx = np.random.randn(n)\n\n\ny_full = C.dot(x)\nfull_mv_time = %timeit -q -o C.dot(x)\nprint('Full matvec time =', full_mv_time.average)\n\n\ny_fft = circulant_matvec(c, x)\nfft_mv_time = %timeit -q -o circulant_matvec(c, x)\nprint('FFT time =', fft_mv_time.average)\n\nprint('Relative error =', (np.linalg.norm(y_full - y_fft)) / np.linalg.norm(y_full))\n                    "},
             30: {'theory' : 'Для следующей матрицы определите с точностью e = 0.001 методом непосредественного развертывания собственные значения и собственные вектора матрицы, отобразите их на комплексной плоскости.',
                  'code' : "\ndef matrix_mult(A, B):\n    # Инициализация результирующей матрицы нулями\n    result = [[0 for _ in range(len(B[0]))] for _ in range(len(A))]\n\n    # Умножение матриц A и B\n    for i in range(len(A)):  # Проходим по строкам матрицы A\n        for j in range(len(B[0])):  # Проходим по столбцам матрицы B\n            for k in range(len(B)):  # Считаем сумму произведений\n                result[i][j] += A[i][k] * B[k][j]\n\n    return result  # Возвращаем результирующую матрицу\n\ndef matrix_norm(v):\n    # Вычисление евклидовой нормы вектора v\n    return math.sqrt(sum(x**2 for x in v))  # Суммируем квадраты элементов и берем корень\n\ndef normalize(v):\n    # Нормализация вектора v\n    norm = matrix_norm(v)  # Вычисляем норму вектора\n    return [x / norm for x in v]  # Делим каждый элемент на норму\n\ndef power_method(A, e=0.001, max_iterations=1000):\n    n = len(A)  # Размерность матрицы A\n    x = [random.random() for _ in range(n)]  # Инициализация случайного вектора\n    x = normalize(x)  # Нормализация вектора\n    eigenvalue = 0  # Инициализация переменной для хранения собственного значения\n\n    for _ in range(max_iterations):  # Цикл для максимального количества итераций\n        # Умножаем матрицу A на вектор x\n        x_new = matrix_mult(A, [[val] for val in x])\n\n        # Вычисляем новое собственное значение\n        eigenvalue_new = matrix_norm([val[0] for val in x_new])\n\n        # Нормализуем новый вектор\n        x_new = normalize([val[0] for val in x_new])\n\n        # Проверка на сходимость (разница между новыми и старыми собственными значениями)\n        if abs(eigenvalue_new - eigenvalue) < e:\n            break  # Если разница меньше заданной точности, выходим из цикла\n\n        eigenvalue = eigenvalue_new  # Обновляем собственное значение\n        x = x_new  # Обновляем вектор\n\n    return eigenvalue, x  # Возвращаем найденное собственное значение и собственный вектор\n\ndef decompose_matrix(A):\n    eigenvalues = []  # Список для хранения собственных значений\n    eigenvectors = []  # Список для хранения собственных векторов\n\n    for _ in range(len(A)):  # Повторяем процесс для каждого собственного значения\n        eigenvalue, eigenvector = power_method(A)  # Находим собственное значение и вектор\n        eigenvalues.append(eigenvalue)  # Добавляем собственное значение в список\n        eigenvectors.append(eigenvector)  # Добавляем собственный вектор в список\n\n        # Обновляем матрицу A, вычитая из нее произведение найденного собственного значения и вектора\n        A = [[A[i][j] - eigenvalue * eigenvector[i] * eigenvector[j] for j in range(len(A))] for i in range(len(A))]\n\n    return eigenvalues, eigenvectors  # Возвращаем списки собственных значений и векторов\n\n# Определяем матрицу A\nA = [[2.4, 1, 1.4, 0.4],\n     [1, 2.3, 1.4, 0.5],\n     [1.4, 1.2, 1, 2.2],\n     [0.6, 0.3, 1.6, 3.3]]\n\n# Находим собственные значения и собственные векторы матрицы A\neigenvalues, eigenvectors = decompose_matrix(A)\n\n# Визуализация собственных значений на комплексной плоскости\nplt.figure()\nfor val in eigenvalues:\n    plt.scatter(val, 0, color='blue')  # Отображаем каждое собственное значение как точку на графике\n\nplt.title('Собственные значения на комплексной плоскости')  # Заголовок графика\nplt.xlabel('Действительная часть')  # Подпись оси X\nplt.ylabel('Мнимая часть')  # Подпись оси Y\nplt.axhline(0, color='black', linewidth=0.5, ls='--')  # Горизонтальная линия на оси Y=0\nplt.axvline(0, color='black', linewidth=0.5, ls='--')  # Вертикальная линия на оси X=0\nplt.grid()  # Включение сетки на графике\nplt.show()  # Отображение графика\n"},
             31 : {'theory' : 'Найти приближенное решение задачи Коши на отрезке [-1;5] с шагом h = 0.0001 методом Эйлера. При ул(-1) = 1, Y2(-1) = 1. Постройте соответствующий фазовый портрет.',
                   'code' : "\n# Начальные условия\nx_start = 1.0\nx_end = 6.0\nh = 0.01\ny1_initial = 0.2\ny2_initial = -0.5\n\n# Определяем функцию для метода Эйлера\ndef euler_method(x_start, x_end, h, y1_initial, y2_initial):\n    x_values = np.arange(x_start, x_end, h)\n    y1_values = np.zeros(len(x_values))\n    y2_values = np.zeros(len(x_values))\n\n    # Устанавливаем начальные условия\n    y1_values[0] = y1_initial\n    y2_values[0] = y2_initial\n\n    # Метод Эйлера\n    for i in range(1, len(x_values)):\n        x = x_values[i - 1]\n        y1 = y1_values[i - 1]\n        y2 = y2_values[i - 1]\n\n        # Вычисляем производные\n        dy1_dx = np.cos(x + y2**2)\n        dy2_dx = np.log(x) * np.sin(x - y1)**3\n\n        # Обновляем значения\n        y1_values[i] = y1 + h * dy1_dx\n        y2_values[i] = y2 + h * dy2_dx\n\n    return x_values, y1_values, y2_values\n\n# Запуск метода Эйлера\nx_values, y1_values, y2_values = euler_method(x_start, x_end, h, y1_initial, y2_initial)\n\n# Построение фазового портрета\nplt.figure(figsize=(10, 6))\nplt.plot(y1_values, y2_values, label='Фазовый портрет')\nplt.title('Фазовый портрет системы уравнений')\nplt.xlabel('y_1')\nplt.ylabel('y_2')\nplt.grid()\nplt.legend()\nplt.show()\n"},
             32 : {'theory' : 'Для следующей матрицы определите с точностью e = 0,001 методом вращений собственные значения и собственные векторы, отобразите их на комплексной плоскости. Для умножения матриц используйте наивный алгоритм:',
                   'code' : "\ndef rotate(matrix, p, q, theta):\n       #вращение\n       c = np.cos(theta)\n       s = np.sin(theta)\n       ro = np.array(matrix)\n       ro[p, p] = c * c * matrix[p, p] + s * s * matrix[q, q] - 2 * s * c * matrix[p, q]\n       ro[q, q] = s * s * matrix[p, p] + c * c * matrix[q, q] + 2 * s * c * matrix[p, q]\n       ro[p, q] = 0\n       ro[q, p] = 0\n       for r in range(len(matrix)):\n           if r != p and r != q:\n               ro[r, p] = c * matrix[r, p] - s * matrix[r, q]\n               ro[r, q] = s * matrix[r, p] + c * matrix[r, q]\n       return ro\n\ndef values_and_vectors(matrix, tol=0.001):\n       #собств знач и векторы\n       n = len(matrix)\n       eigvec = np.eye(n)\n       for _ in range(100):\n           for p in range(n - 1):\n               for q in range(p + 1, n):\n                   if np.abs(matrix[p, q]) > tol:\n                       theta = 0.5 * np.arctan2(2 * matrix[p, q], matrix[q, q] - matrix[p, p])\n                       matrix = rotate(matrix, p, q, theta)\n                       eigvec[:, [p, q]] = eigvec[:, [p, q]] @ np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])\n       return np.diag(matrix), eigvec\n\nmatrix = np.array([[1.6, 0.7, 1.4, 0.4],\n                      [0.7, 1.6, 1.4, 0.5],\n                      [0.8, 0.3, 1, 2.2],\n                      [0.6, 0.3, 1.6, 3.3]])\n\nvalues, vectors = values_and_vectors(matrix)\n\n# Визуализация на комплексной плоскости\nplt.scatter(values.real, values.imag, color='blue')\nplt.xlabel('Действительная часть')\nplt.ylabel('Мнимая часть')\nplt.title('Собственные значения на комплексной плоскости')\nplt.grid()\nplt.axhline(0, color='black', lw=0.5)\nplt.axvline(0, color='black', lw=0.5)\nplt.show()\n"},
             33 : {'theory' : 'Вычислите производную функции  аналитически и численно с помощью метода центральной разности. Сравните результаты.',
                   'code' : '\n#функция\ndef y(x):\n    return np.exp(np.sqrt(7 * x)) + 3\n\ndef dy_analytic(x):\n    return (7 / (2 * np.sqrt(7 * x))) * np.exp(np.sqrt(7 * x))\n\ndef dy_central(x, h=1e-5):\n    return (y(x + h) - y(x - h)) / (2 * h)\n\n# точка, в которой вычисляем производную\nx0 = 1.0\n\nanalytical_result = dy_analytic(x0)\nnumerical_result = dy_central(x0)\n\nprint(f"Аналитическая производная в точке x = {x0}: {analytical_result}")\nprint(f"Численная производная в точке x = {x0}: {numerical_result}")\n'},
             34 : {'theory' : 'Для следующей матрицы определите с точностью e = 0,001 методом итераций (степенным методом) собственные значения и собственные векторы, отобразите их на комплексной плоскости. Для умножения матриц используйте алгоритм Штрассена:',
                   'code' : '\nA = np.array([[1.6, 0.7, 0.8, 2.2],\n              [0.7, 1.6, 0.3, 1.2],\n              [0.8, 0.3, 1.6, 1.3],\n              [2.2, 1.2, 3.2, 3.3]])\n\ndef power_iteration(A, num_simulations: int):\n    #начальный случайный вектор\n    b_k = np.random.rand(A.shape[1])\n\n    for _ in range(num_simulations):\n        #умножение матрицы на вектор\n        b_k1 = np.dot(A, b_k)\n\n        # Нормализация вектора\n        b_k1_norm = np.linalg.norm(b_k1)\n        b_k = b_k1 / b_k1_norm\n\n\n    value = np.dot(b_k.T, np.dot(A, b_k))\n    return value, b_k\n\nvalue, vector = power_iteration(A, 1000)\n\nprint(f"Наибольшее собственное значение: {value:.3f}")\nprint(f"Соответствующий собственный вектор: {vector}")\n\n#комплексная плоскость\neigenvalues = np.linalg.eigvals(A)\n\nplt.figure(figsize=(6, 6))\nplt.scatter(eigenvalues.real, eigenvalues.imag, color=\'red\')\nplt.title(\'Собственные значения на комплексной плоскости\')\nplt.xlabel(\'Real\')\nplt.ylabel(\'Imaginary\')\nplt.grid(True)\nplt.axhline(0, color=\'black\',linewidth=0.5)\nplt.axvline(0, color=\'black\',linewidth=0.5)\nplt.show()\n'},
             35: {'theory' : 'Для следующей матрицы определите с точностью eps = 0,001 при помощи QR алгоритма собственные значения и собственные векторы, отобразите их на комплексной плоскости. Для умножения матриц используйте наивный алгоритм.',
                  'code' : '\n# QR-разложение\ndef qr_decomposition(A):\n    n, m = A.shape\n    Q = np.zeros((n, n))\n    R = np.zeros((n, m))\n    for j in range(m):\n        v = A[:, j]\n        for i in range(j):\n            R[i, j] = np.dot(Q[:, i], A[:, j])\n            v = v - R[i, j] * Q[:, i]\n        R[j, j] = np.linalg.norm(v)\n        Q[:, j] = v / R[j, j]\n    return Q, R\n\n# Функция для умножения матриц (наивный алгоритм)\ndef matrix_multiply(A, B):\n    n, m = A.shape\n    p = B.shape[1]\n    C = np.zeros((n, p))\n    for i in range(n):\n        for j in range(p):\n            for k in range(m):\n                C[i, j] += A[i, k] * B[k, j]\n    return C\n\n# Вычисление собственных значений и собственных векторов\ndef eigen_decomposition(A, eps=0.001):\n    n = A.shape[0]\n    # Используем QR-разложение для итерационного метода\n    Ak = A.copy()\n    for _ in range(100):  # 100 итераций для сходимости\n        Q, R = qr_decomposition(Ak)\n        Ak = matrix_multiply(R, Q)\n\n    eigenvalues = np.diag(Ak)  # Собственные значения на диагонали\n    eigenvectors = Q  # Собственные векторы\n\n    return eigenvalues, eigenvectors\n\n# Пример матрицы\nA = np.array([\n    [1.2, 2.0, 0.5, 0.8],\n    [1.6, 1.6, 1.7, 0.4],\n    [0.4, 0.3, 1.4, 2.0],\n    [0.5, 1.7, 1.6, 0.3]\n])\n\n# Вычисляем собственные значения и собственные векторы\neigenvalues, eigenvectors = eigen_decomposition(A)\n\nprint("Собственные значения:")\nprint(eigenvalues)\n\nprint("Собственные векторы:")\nprint(eigenvectors)\n\n# Отображение eigenvalues на комплексной плоскости\nplt.figure(figsize=(8, 6))\nplt.scatter(eigenvalues.real, eigenvalues.imag, marker=\'o\', color=\'blue\')\nplt.axhline(0, color=\'gray\', lw=0.5, ls=\'--\')\nplt.axvline(0, color=\'gray\', lw=0.5, ls=\'--\')\nplt.title("Собственные значения на комплексной плоскости")\nplt.xlabel("Реальная часть")\nplt.ylabel("Мнимая часть")\nplt.grid()\nplt.show()\n\n#умножение матриц\ndef naive_matrix_multiply(A, B):\n    n, m = A.shape\n    m2, p = B.shape\n    assert m == m2, "Матрицы нельзя перемножить"\n    C = np.zeros((n, p))\n    for i in range(n):\n        for j in range(p):\n            for k in range(m):\n                C[i, j] += A[i, k] * B[k, j]\n    return C\n\n# QR-разложение\ndef qr_decomposition(A):\n    n, m = A.shape\n    Q = np.zeros((n, n))\n    R = np.zeros((n, m))\n    for j in range(m):\n        v = A[:, j]\n        for i in range(j):\n            R[i, j] = np.dot(Q[:, i], A[:, j])\n            v = v - R[i, j] * Q[:, i]\n        R[j, j] = np.linalg.norm(v)\n        Q[:, j] = v / R[j, j]\n    return Q, R\n\n# QR нахождение собственных значений\ndef qr_algorithm(A, tol=0.001, max_iterations=1000):\n    n, _ = A.shape\n    Ak = np.copy(A)\n    for _ in range(max_iterations):\n        Q, R = qr_decomposition(Ak)\n        Ak_next = naive_matrix_multiply(R, Q)\n        if np.allclose(Ak, Ak_next, atol=tol):\n            break\n        Ak = Ak_next\n    values = np.diag(Ak)\n    return values\n\ndef qr_algorithm(A, tol=0.001, max_iterations=1000):\n    n, _ = A.shape\n    Ak = np.copy(A)\n    Q_total = np.eye(n)  #для начала матрица собственных векторов — единичная матрица\n\n    for _ in range(max_iterations):\n        Q, R = qr_decomposition(Ak)\n        Ak_next = naive_matrix_multiply(R, Q)\n        Q_total = naive_matrix_multiply(Q_total, Q)\n\n        if np.allclose(Ak, Ak_next, atol=tol):\n            break\n        Ak = Ak_next\n\n    values = np.diag(Ak)\n    vectors = Q_total\n\n    return values, vectors\n\nvalues, vectors = qr_algorithm(A)\n\nA = np.array([\n    [1.2, 2.0, 0.5, 0.8],\n    [1.6, 1.6, 1.7, 0.4],\n    [0.4, 0.3, 1.4, 2.0],\n    [0.5, 1.7, 1.6, 0.3]\n])\n\n#собственные значения и векторы\nvalues, vectors = qr_algorithm(A)\n\nprint("Собственные значения (NumPy):", values)\nprint("Собственные векторы (NumPy):n", vectors)\n\nplt.figure(figsize=(6, 6))\nplt.scatter(values.real, values.imag, color=\'red\')\nplt.axhline(0, color=\'black\', lw=0.5)\nplt.axvline(0, color=\'black\', lw=0.5)\nplt.title(\'Собственные значения на комплексной плоскости\')\nplt.xlabel(\'Действительная часть\')\nplt.ylabel(\'Мнимая часть\')\nplt.grid(True)\nplt.show()\n'},
             36: {'theory' : 'Вычислите производную функцию аналитически и численно с помощью метода прямой разности. Сравните результаты. Реализуй решение  с помошью питона',
                  'code' : '\nimport numpy as np\nimport math\n\n# Определяем функцию\ndef f(x):\n    return np.cos(math.pi * x)**3\n\n# Аналитическая производная\ndef f_prime_analytical(x):\n    return 3 * (np.cos(math.pi * x)**2) * (-np.sin(math.pi * x)) * math.pi\n\n# Численное вычисление производной (метод прямой разности)\ndef f_prime_numerical(x, h):\n    return (f(x + h) - f(x)) / h\n\n# Задаем значение точки и шаг\nx_value = 3.0\nh = 1e-5\n\n# Вычисляем аналитическую и численную производные\nanalytical_result = f_prime_analytical(x_value)\nnumerical_result = f_prime_numerical(x_value, h)\n\n# Выводим результаты\nprint(f"Аналитическая производная в точке x={x_value}: {analytical_result}")\nprint(f"Численная производная в точке x={x_value} с шагом h={h}: {numerical_result}")\n'},
             1.1: {'code' : " def matmul(a, b):\n    '''\n    Умножение двух матриц или матрицы на вектор.\n    Args:\n        a (numpy.ndarray): Матрица (2D массив).\n        b (numpy.ndarray): Матрица (2D массив) или вектор (1D массив).\n    Returns:\n        numpy.ndarray: Результат умножения.\n    '''\n    n, k = a.shape  # Размеры первой матрицы\n\n    # Проверка на тип b (матрица или вектор)\n    if len(b.shape) == 1:  # b — это вектор\n        m = 1\n        b = b.reshape(k, 1)  # Преобразуем вектор в столбец для унификации\n    else:  # b — это матрица\n        m = b.shape[1]\n\n    # Инициализация результата\n    c = np.zeros((n, m))\n\n    # Перемножение\n    for i in range(n):\n        for j in range(m):\n            for s in range(k):\n                c[i, j] += a[i, s] * b[s, j]\n\n    # Если b был вектором, вернуть результат как вектор\n    if m == 1:\n        return c.flatten()\n\n    return c "},
             1.2: {'code' : " import numpy as np\n\n\n# **для матриц с порядком НЕ степень 2, для пар матрица - вектор**\ndef strassen(A, B):\n    '''\n    Реализация алгоритма Штрассена для умножения матриц A и B.\n\n    Если размер матриц не является степенью двойки, они дополняются до ближайшего большего размера.\n    Поддерживает умножение матрицы на вектор.\n\n    Args:\n        A (numpy.ndarray): Первая матрица.\n        B (numpy.ndarray): Вторая матрица или вектор.\n\n    Returns:\n        numpy.ndarray: Результат умножения матриц или матрицы на вектор.\n    '''\n    # Если B — вектор, преобразуем его в столбец\n    is_vector = False\n    if len(B.shape) == 1:\n        is_vector = True\n        B = B.reshape(-1, 1)\n\n    # Убедимся, что матрицы квадратные и имеют одинаковый размер\n    assert A.shape[0] == A.shape[1] and A.shape[1] == B.shape[0], 'Матрицы должны быть согласованы для умножения.'\n\n    n = A.shape[0]\n\n    # Если размер не степень двойки, дополняем матрицы\n    def next_power_of_two(x):\n        return 1 if x == 0 else 2**(x - 1).bit_length()\n\n    new_size = next_power_of_two(n)\n\n    if new_size != n:\n        A_padded = np.zeros((new_size, new_size))\n        B_padded = np.zeros((new_size, B.shape[1]))\n        A_padded[:n, :n] = A\n        B_padded[:n, :B.shape[1]] = B\n        A, B = A_padded, B_padded\n\n    n = len(A)  # Новый размер матриц\n\n    if n <= 2:\n        return (np.dot(A, B).flatten() if is_vector else np.dot(A, B))\n\n    # Разделим матрицы на подматрицы\n    mid = n // 2\n    A11 = A[:mid, :mid]\n    A12 = A[:mid, mid:]\n    A21 = A[mid:, :mid]\n    A22 = A[mid:, mid:]\n    B11 = B[:mid, :]\n    B21 = B[mid:, :]\n\n    # Рекурсивное умножение\n    P1 = strassen(A11, B11 - B21)\n    P2 = strassen(A11 + A12, B21)\n    P3 = strassen(A21 + A22, B11)\n    P4 = strassen(A22, B21 - B11)\n    P5 = strassen(A11 + A22, B11 + B21)\n    P6 = strassen(A12 - A22, B21)\n    P7 = strassen(A11 - A21, B11)\n\n    # Соединим результаты в матрицу или вектор\n    C11 = P5 + P4 - P2 + P6\n    C21 = P3 + P4\n\n    C = np.vstack((C11, C21))\n\n    # Вернем результат, обрезав лишние нули, если матрицы были дополнены\n    C = C[:A.shape[0], :B.shape[1]]\n\n    # Если B был вектором, вернуть результат как вектор\n    return C.flatten() if is_vector else C\n\n# Пример использования\nnp.random.seed(42)\nA = np.random.random((200, 200))\nB_matrix = np.random.random((200, 200))\nB_vector = np.random.random(200)\n\n# Умножение матрицы на матрицу\nresult_matrix = strassen(A, B_matrix)\nprint('Результат умножения матриц:\n', result_matrix)\n\n# Умножение матрицы на вектор\nresult_vector = strassen(A, B_vector)\nprint('Результат умножения матрицы на вектор:\n', result_vector)\n\n\n# **для матриц с порядком степень 2**\nimport numpy as np\n\ndef strassen(A, B):\n    n = len(A)\n\n    if n <= 2:\n        return np.dot(A, B)\n\n    # Разделим матрицы на подматрицы\n    mid = n // 2\n    A11 = A[:mid, :mid]\n    A12 = A[:mid, mid:]\n    A21 = A[mid:, :mid]\n    A22 = A[mid:, mid:]\n    B11 = B[:mid, :mid]\n    B12 = B[:mid, mid:]\n    B21 = B[mid:, :mid]\n    B22 = B[mid:, mid:]\n\n    # Рекурсивное умножение\n    P1 = strassen(A11, B12 - B22)\n    P2 = strassen(A11 + A12, B22)\n    P3 = strassen(A21 + A22, B11)\n    P4 = strassen(A22, B21 - B11)\n    P5 = strassen(A11 + A22, B11 + B22)\n    P6 = strassen(A12 - A22, B21 + B22)\n    P7 = strassen(A11 - A21, B11 + B12)\n\n    # Соединим результаты в матрицу С\n    C11 = P5 + P4 - P2 + P6\n    C12 = P1 + P2\n    C21 = P3 + P4\n    C22 = P5 + P1 - P3 - P7\n\n    C = np.vstack((np.hstack((C11, C12)), np.hstack((C21, C22))))\n\n    return C\n\nA = np.random.uniform(1, 10, size=(8, 8))\nB = np.random.uniform(1, 10, size=(8, 8))\n\nstrassen(A, B) "},
             1.3: {'code' : ' def sign(x):\n    if x < 0:\n        return -1\n    if x==0:\n        return 0\n    if x > 0:\n        return 1\n\ndef outer(a, b):\n    return a[:, None] * b\n\n# Отражение Хаусхолдера:\ndef householder(x):\n  e1 = np.zeros_like(x)\n  e1[0] = 1\n\n  # вычисление alpha\n  alpha = sign(x[0])*np.sqrt(np.sum(x**2))\n\n  # матрица отражения u\n  u = x - alpha*e1\n  # нормализация u\n  norm_u = np.sum(u**2)\n\n  if norm_u > 1e-10:\n    u = u/np.sqrt(norm_u)\n  else:\n    u = np.zeros_like(u)\n\n  return u\n\n# QR-разложение\ndef qr_dec(A):\n  n, m = A.shape\n\n  # единичная матрица Q\n  Q = np.eye(n)\n  R = A.copy()\n\n  # итерация разложения\n  for i in range(min(n, m)):\n    # подмножество x:\n    x = R[i:, i]\n    # вычисление u - отражение для x:\n    u = householder(x)\n\n    # нахождение матрицы Хаусхолдера:\n    H = np.eye(n)\n    H[i:, i:] -= 2*outer(u, u)\n\n    # обновление R и Q:\n    R = matmul(H, R)\n    Q = matmul(Q, H.T)\n\n  return Q, R\n\n# реализация QR алгоритма:\ndef qr(A, iter=100):\n  for i in range(iter):\n    # Разложение:\n    Q, R = qr_dec(A)\n    # Перемножение матриц:\n    A = matmul(R, Q)\n\n  return A\n\n# реализация неявного QR алгоритма:\ndef implict_qr(A, iter=100):\n  n = A.shape[0]\n  for i in range(iter):\n    # сдвиг\n    shift = A[n-1, n-1]\n    Q, R = qr_dec(A - shift*np.eye(n))\n    A = matmul(R, Q) + shift*np.eye(n)\n\n  return A '},
             1.4: {'code' : " import numpy as np\nfrom scipy.sparse import identity, csr_matrix\n\ndef schur_decomposition(A, max_iter=1000, tol=1e-10):\n    '''\n    Вычисляет разложение Шура A = UTU^*, где:\n    - U: унитарная матрица (приближение матрицы собственных векторов),\n    - T: верхнетреугольная матрица (приближение собственных значений).\n\n    Args:\n        A (numpy.ndarray): Квадратная матрица.\n        max_iter (int): Максимальное число итераций.\n        tol (float): Допустимая ошибка для сходимости.\n\n    Returns:\n        U (numpy.ndarray): Унитарная матрица.\n        T (numpy.ndarray): Верхнетреугольная матрица.\n    '''\n    n, m = np.shape(A)\n    assert n == m, 'Матрица должна быть квадратной.'\n\n    # Инициализация\n    T = np.array(A, dtype=np.float64)  # Копируем A в T\n    U = np.eye(n)  # Унитарная матрица (изначально единичная)\n\n    for i in range(max_iter):\n        # Разложение T = QR\n        Q = np.eye(n)\n        R = np.zeros_like(T)\n\n        for j in range(n - 1):\n            # Givens-вращение для зануления элементов под диагональю\n            a = T[j, j]\n            b = T[j + 1, j]\n            if abs(b) < tol:\n                continue\n            \n            r = np.sqrt(a**2 + b**2)\n            c = a / r\n            s = -b / r\n\n            G = np.eye(n)\n            G[j, j], G[j, j + 1] = c, s\n            G[j + 1, j], G[j + 1, j + 1] = -s, c\n\n            # Применяем вращение к T\n            T = np.dot(G.T, T)\n            T = np.dot(T, G)\n\n            # Накапливаем вращение в U\n            U = np.dot(U, G)\n\n        # Проверяем сходимость (изменение T)\n        off_diag_norm = np.sqrt(np.sum(T**2) - np.sum(np.diag(T)**2))\n        if off_diag_norm < tol:\n            break\n\n    return U, T\n\n# Пример использования:\nnp.random.seed(42)\nA = np.random.random((4, 4))\n\nU, T = schur_decomposition(A)\nprint('Матрица U (унитарная):\n', U)\nprint('Матрица T (верхнетреугольная):\n', T)\nprint('Проверка: A ≈ UTU*:\n', np.allclose(A, U @ T @ U.T, atol=1e-8)) "},
             1.5: {'code' : " import numpy as np\nimport matplotlib.pyplot as plt\n\nn = 3\nfig, ax = plt.subplots(1, 1)\na = np.array([[5, 1, 1], [1, 0, 0.5], [2, 0, 10]])\n\na += 2*np.random.randn(n, n)\n\nxg = np.diag(a).real\nyg = np.diag(a).imag\nrg = np.zeros(n)\nev = np.linalg.eigvals(a)\n\nfor i in range(n):\n    rg[i] = np.sum(np.abs(a[i, :])) - np.abs(a[i, i])\n    crc = plt.Circle((xg[i], yg[i]), radius = rg[i], fill = False)\n    ax.add_patch(crc)\nplt.scatter(ev.real, ev.imag, color = 'r', label = 'Eigenvalues')\nplt.axis('equal') "},
             2.1: {'code' : " import numpy as np\nimport matplotlib.pyplot as plt\n\ndef predictor_corrector(f, t0, y0, t_end, h):\n    '''\n    Метод предиктора-корректора для решения ODE.\n\n    Параметры:\n    f    : функция, задающая правую часть уравнения dy/dt = f(t, y)\n    t0   : начальное время\n    y0   : начальное значение\n    t_end: конечное время\n    h    : шаг\n\n    Возвращает:\n    t : массив времени\n    y : массив решений\n    '''\n    # Инициализация массивов времени и решений\n    t = np.arange(t0, t_end + h, h)\n    y = np.zeros_like(t)\n    y[0] = y0\n\n    # Основной цикл\n    for i in range(1, len(t)):\n        # Предиктор (явный метод Эйлера)\n        y_pred = y[i - 1] + h * f(t[i - 1], y[i - 1])\n\n        # Корректор (метод трапеций)\n        y[i] = y[i - 1] + h / 2 * (f(t[i - 1], y[i - 1]) + f(t[i], y_pred))\n\n    return t, y\n\n# Пример использования\ndef f(t, y):\n    return 2*y - t**2  \n\nt0 = 0\ny0 = 1\nt_end = 3\nh = 0.05\n\nt, y = predictor_corrector(f, t0, y0, t_end, h)\n\n# График\nplt.plot(t, y, label='Predictor-Corrector', marker='o')\nplt.xlabel('t')\nplt.ylabel('y')\nplt.title('Solution of dy/dt = -2 * t * y using Predictor-Corrector')\nplt.grid(True)\nplt.legend()\nplt.show() "},
             2.2: {'code' : " def method_euler(f, x_0, x_n, y_0, N):\n    '''\n    Реализация метода Эйлера для численного решения ОДУ.\n\n    f    : функция правой части ОДУ, f(x, y), где y может быть вектором\n    x_0  : начальное значение x (левая граница)\n    x_n  : конечное значение x (правая граница)\n    y_0  : начальные условия, передаются в виде списка или массива\n    N    : количество шагов интегрирования (чем больше, тем точнее)\n\n    Возвращает:\n    x    : массив значений x (разбиение области интегрирования)\n    y    : массив значений y, посчитанных методом Эйлера\n    '''\n    dx = (x_n - x_0) / N  # Шаг интегрирования\n    x = np.linspace(x_0, x_n, N+1)  # Создаем массив точек x с равномерным разбиением\n    y = np.zeros((N+1, len(y_0)))  # Инициализируем массив y с нулями\n    y[0, :] = y_0  # Задаем начальные условия: y_0 в начальной точке x_0\n\n    # Основной цикл метода Эйлера\n    for n in range(N):\n        # y[n+1] = y[n] + dx * f(x[n], y[n]) — основная формула метода\n        y[n+1, :] = y[n, :] + dx * f(x[n], y[n, :])\n\n    return x, y  # Возвращаем массивы x и y\n\ndef solve_sin_example():\n    '''\n    Пример применения метода Эйлера для решения уравнения y'(x) = -sin(x).\n    '''\n    # Определяем функцию правой части: y'(x) = -sin(x)\n    def f(x, y):\n        return -np.sin(x)\n\n    # Решение задачи при шаге h = 0.1 (N=5 шагов)\n    x_5, y_5 = method_euler(f, 0.0, 0.5, [1.0], 5)\n\n    # Решение задачи при шаге h = 0.005 (N=100 шагов)\n    x_100, y_100 = method_euler(f, 0.0, 0.5, [1.0], 100)\n\n    # Вывод результатов для сравнения с точным решением\n    print('Решение в x = 0.5 при h = 0.1 - {}.'.format(y_5[-1, 0]))\n    print('Решение в x = 0.5 при h = 0.005 - {}.'.format(y_100[-1, 0]))\n    print('Точное решение в x = 0.5 - {}.'.format(np.cos(0.5)))\n\n    # Исследуем сходимость метода\n    N = np.array([2**i for i in range(3, 12)])  # Генерируем массив значений N\n    dx = 0.5 / N  # Вычисляем шаг dx для каждого N\n    err = np.zeros_like(dx)  # Создаем массив для хранения ошибок\n\n    for i in range(len(N)):\n        n = N[i]  # Текущее число шагов\n        x, y = method_euler(f, 0.0, 0.5, [1.0], n)  # Решаем ОДУ методом Эйлера\n        err[i] = np.abs(np.cos(0.5) - y[-1, 0])  # Ошибка в конечной точке\n\n    # Линейная аппроксимация зависимости log(dx) от log(err)\n    p = np.polyfit(np.log(dx[:-1]), np.log(err[:-1]), 1)\n\n    # Построение графика сходимости\n    fig = plt.figure(figsize=(10, 6))\n    ax = fig.add_subplot(111)\n    ax.loglog(dx, err, 'kx', label='Посчитанные ошибки')  # Ошибки\n    ax.loglog(dx, np.exp(p[1]) * dx**p[0], label='Наклон линии {:.2f}'.format(p[0]))  # Линия тренда\n    ax.set_xlabel('dx')  # Подпись оси x\n    ax.set_ylabel('$\\|$ Ошибка $\\|$')  # Подпись оси y\n    ax.set_title('Сходимость метода Эйлера')  # Заголовок\n    ax.legend(loc=2)  # Легенда\n    plt.show()  # Показываем график\n\n\n'''Система:\n\n$x_{i+1} = -y_i$ <br>\n$y_{i+1} = x_i$\n\nx(0) = 1 <br>\ny(0) = 0\n\nВ полярных координатах это r = 0, phi = 1\n\nВозьмём шаг h = 0.1, рассчитав 500 шагов\n'''\n\n\ndef solve_circular_motion():\n    '''\n    Пример решения системы уравнений кругового движения.\n    '''\n    # Определяем функцию для правых частей системы уравнений\n    def motion(x, y):\n        dfdt = np.zeros_like(y)  # Инициализация массива для производных\n        dfdt[0] = -y[1]  # dx/dt = -y\n        dfdt[1] = y[0]   # dy/dt = x\n        return dfdt\n\n    y0 = np.array([1.0, 0.0])  # Начальные условия: x=1, y=0\n\n    # Решение для h=0.1 (500 шагов)\n    t_01, y_01 = method_euler(motion, 0.0, 50, y0, 500)\n\n    # Решение для h=0.01 (5000 шагов)\n    t_001, y_001 = method_euler(motion, 0.0, 50, y0, 5000)\n\n    # Построение графиков траекторий\n    fig = plt.figure(figsize=(10, 6))\n    ax1 = fig.add_subplot(121)  # Левый график\n    ax1.plot(y_01[:, 0], y_01[:, 1], label='$h = 0.1$')\n    ax1.set_xlabel('$x$')\n    ax1.set_ylabel('$y$')\n    ax1.set_title('Траектория при h=0.1')\n\n    ax2 = fig.add_subplot(122)  # Правый график\n    ax2.plot(y_001[:, 0], y_001[:, 1], label='$h = 0.01$')\n    ax2.set_xlabel('$x$')\n    ax2.set_ylabel('$y$')\n    ax2.set_title('Траектория при h=0.01')\n\n    plt.show()\n\n    # Анализ сходимости\n    N = np.array([500 * 2**i for i in range(0, 10)])  # Массив значений N\n    h = 0.5 / N  # Шаг интегрирования\n    err = np.zeros_like(h)  # Ошибки\n\n    for i in range(len(N)):\n        n = N[i]\n        x, y = method_euler(motion, 0.0, 50, y0, n)  # Решаем методом Эйлера\n        err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)  # Вычисляем ошибку\n\n    p = np.polyfit(np.log(h), np.log(err), 1)  # Линейная аппроксимация\n\n    # Построение графика сходимости\n    fig = plt.figure(figsize=(10, 6))\n    ax = fig.add_subplot(111)\n    ax.loglog(h, err, 'kx', label='Посчитанные ошибки')\n    ax.loglog(h, np.exp(p[1]) * h**p[0], label='Наклон линии {:.2f}'.format(p[0]))\n    ax.set_xlabel('h')\n    ax.set_ylabel('$\\|$ Ошибка $\\|$')\n    ax.set_title('Сходимость метода Эйлера')\n    ax.legend(loc=2)\n    plt.show()\n\nsolve_sin_example()\nsolve_circular_motion() "},
             2.3: {'code' : " import numpy as np\nimport matplotlib.pyplot as plt\n\n# Рунге-Кутта 4-го порядка для системы уравнений\ndef runge_kutta_4_system(f, t0, y0, h, n_steps):\n    t_values = np.linspace(t0, t0 + n_steps * h, n_steps + 1)\n    y_values = np.zeros((n_steps + 1, len(y0)))\n\n    y_values[0] = y0\n\n    for i in range(n_steps):\n        t = t_values[i]\n        y = y_values[i]\n\n        K1 = f(t, y)\n        K2 = f(t + h / 2.0, y + h / 2.0 * K1)\n        K3 = f(t + h / 2.0, y + h / 2.0 * K2)\n        K4 = f(t + h, y + h * K3)\n\n        y_values[i + 1] = y + (h / 6.0) * (K1 + 2.0 * K2 + 2.0 * K3 + K4)\n\n    return t_values, y_values\n\n# Система дифференциальных уравнений\ndef system(t, y):\n    x, y = y  # Распаковываем переменные\n    dxdt = x + y\n    dydt = y - x\n    return np.array([dxdt, dydt])\n\n# Параметры\nt0 = 0.0\ny0 = [1.0, 0.0]  # Начальные условия: x(0) = 1, y(0) = 0\nh = 0.1\nn_steps = 200\n\n# Решение\nt_vals, y_vals = runge_kutta_4_system(system, t0, y0, h, n_steps)\n\n# Построение фазового портрета\nplt.figure(figsize=(8, 6))\nplt.plot(y_vals[:, 0], y_vals[:, 1], label='Фазовая траектория', color='blue')\nplt.scatter(y0[0], y0[1], color='red', label='Начальная точка')  # Начальная точка\nplt.xlabel('x')\nplt.ylabel('y')\nplt.title('Фазовый портрет')\nplt.grid(True)\nplt.legend()\nplt.show()\n\n# Построение графиков x(t) и y(t)\nplt.figure(figsize=(8, 6))\nplt.plot(t_vals, y_vals[:, 0], label='x(t)', color='green')\nplt.plot(t_vals, y_vals[:, 1], label='y(t)', color='orange')\nplt.xlabel('t')\nplt.ylabel('Значение')\nplt.title('Графики x(t) и y(t)')\nplt.grid(True)\nplt.legend()\nplt.show()\n "},
             2.4: {'code' : " import numpy as np\nimport matplotlib.pyplot as plt\n\ndef am5_method(f, x_0, x_n, y_0, N):\n    # Шаг интегрирования\n    dx = (x_n - x_0) / N\n    x = np.linspace(x_0, x_n, N + 1)\n    y = np.zeros((N + 1, len(y_0)))\n    fn = np.zeros_like(y)\n    y[0, :] = y_0\n\n    # Используем метод Рунге-Кутты 4-го порядка для начальной прогонки\n    for n in range(4):\n        fn[n, :] = f(x[n], y[n, :])\n        k1 = dx * fn[n, :]\n        k2 = dx * f(x[n] + dx / 2, y[n, :] + k1 / 2)\n        k3 = dx * f(x[n] + dx / 2, y[n, :] + k2 / 2)\n        k4 = dx * f(x[n] + dx, y[n, :] + k3)\n        y[n + 1, :] = y[n, :] + (k1 + 2 * k2 + 2 * k3 + k4) / 6\n\n    # Коэффициенты для методов Адамса-Бэшфорта (AB4) и Адамса-Мултона (AM5)\n    b_ab4 = np.array([55, -59, 37, -9]) / 24\n    b_am5 = np.array([251, 646, -264, 106, -19]) / 720\n\n    # Основной цикл\n    for n in range(4, N):\n        fn[n, :] = f(x[n], y[n, :])\n        # Предиктор (AB4)\n        yp = y[n, :] + dx * sum(b_ab4[i] * fn[n - i, :] for i in range(4))\n        # Корректор (AM5)\n        y[n + 1, :] = y[n, :] + dx * (\n            b_am5[0] * f(x[n + 1], yp)\n            + sum(b_am5[i + 1] * fn[n - i, :] for i in range(4))\n        )\n\n    return x, y\n\ndef fun_sin(x, y):\n    return -np.sin(x)\n\nx_5, y_5 = am5_method(fun_sin, 0, 0.5, [1], 5)\nx_50, y_50 = am5_method(fun_sin, 0, 0.5, [1], 50)\n\nprint(f'Результат при N=5: {y_5[-1][0]}')\nprint(f'Результат при N=50: {y_50[-1][0]}')\n\ndef motion(x, y):\n    # Уравнения движения\n    dfdt = np.zeros_like(y)\n    dfdt[0] = -y[1]\n    dfdt[1] = y[0]\n    return dfdt\n\ny0 = np.array([1, 0])\n\n# Решение с разными шагами\nt_01, y_01 = am5_method(motion, 0, 50, y0, 500)  # Шаг 0.1\nt_001, y_001 = am5_method(motion, 0, 50, y0, 200)  # Шаг 0.25\n\n# Графики фазовых траекторий\nplt.figure(figsize=(10, 4))\nplt.plot(y_01[:, 0], y_01[:, 1], label='h = 0.1', color='blue')\nplt.plot(y_001[:, 0], y_001[:, 1], label='h = 0.25', color='orange')\nplt.xlabel('x')\nplt.ylabel('y')\nplt.title('Фазовые траектории')\nplt.grid(True, linestyle=':')\nplt.legend()\nplt.show()\n\n# Ошибка радиуса\nplt.figure(figsize=(10, 4))\nplt.plot(t_01, np.sqrt(y_01[:, 0]**2 + y_01[:, 1]**2) - 1, label='h = 0.1', color='blue')\nplt.plot(t_001, np.sqrt(y_001[:, 0]**2 + y_001[:, 1]**2) - 1, label='h = 0.25', color='orange')\nplt.xlabel('Время')\nplt.ylabel('Ошибка радиуса')\nplt.title('Сходимость радиуса')\nplt.grid(True, linestyle=':')\nplt.legend()\nplt.show()\n\nN = np.array([50 * 2**i for i in range(3, 10)])  # Число точек\ndx = 50 / N\nerr = np.zeros_like(dx)\n\n# Вычисление ошибки\nfor i, n in enumerate(N):\n    x, y = am5_method(motion, 0, 50, y0, n)\n    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1)\n\n# Логарифмический график ошибки\np = np.polyfit(np.log(dx), np.log(err), 1)\n\nplt.figure(figsize=(8, 5))\nplt.loglog(dx, err, 'kx', label='Глобальная ошибка')\nplt.loglog(dx, np.exp(p[1]) * dx**p[0], label=f'Наклон: {p[0]:.2f}')\nplt.xlabel('Шаг (dx)')\nplt.ylabel('Ошибка')\nplt.title('Сходимость метода Адамса')\nplt.grid(True, linestyle=':')\nplt.legend()\nplt.show() "},
             2.5: {'code' : " # работает некорректно в случае слишком большого шага, Если функция содержит разрывы или крутые изменения\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom scipy.integrate import solve_ivp\n\ndef predictor_corrector(f, t0, y0, t_end, h):\n    '''\n    Метод предиктора-корректора для решения ODE.\n\n    Параметры:\n    f    : функция, задающая правую часть уравнения dy/dt = f(t, y)\n    t0   : начальное время\n    y0   : начальное значение\n    t_end: конечное время\n    h    : шаг\n\n    Возвращает:\n    t : массив времени\n    y : массив решений\n    '''\n    # Инициализация массивов времени и решений\n    t = np.arange(t0, t_end + h, h)\n    y = np.zeros_like(t)\n    y[0] = y0\n\n    # Основной цикл\n    for i in range(1, len(t)):\n        # Предиктор (явный метод Эйлера)\n        y_pred = y[i - 1] + h * f(t[i - 1], y[i - 1])\n\n        # Корректор (метод трапеций)\n        y[i] = y[i - 1] + h / 2 * (f(t[i - 1], y[i - 1]) + f(t[i], y_pred))\n\n    return t, y\n\n# Пример функции\ndef f(t, y):\n    return 2 * y - t**2  \n\n# Получение точного решения через scipy\ndef exact_solution(f, t0, y0, t_end, t_points):\n    '''\n    Решение дифференциального уравнения с помощью solve_ivp.\n\n    Параметры:\n    f       : правая часть уравнения dy/dt = f(t, y)\n    t0      : начальное время\n    y0      : начальное значение\n    t_end   : конечное время\n    t_points: точки, в которых нужно вычислить решение\n\n    Возвращает:\n    y : массив значений точного решения\n    '''\n    sol = solve_ivp(f, [t0, t_end], [y0], t_eval=t_points, method='RK45')\n    return sol.y[0]\n\n# Параметры задачи\nt0 = 0\ny0 = 1\nt_end = 1\n\n# Разные значения шага\nh_values = [0.1, 0.05, 0.025, 0.0125]\nerrors = []\n\n# Вычисление решения и ошибки для каждого шага\nfor h in h_values:\n    t, y = predictor_corrector(f, t0, y0, t_end, h)\n    y_exact = exact_solution(f, t0, y0, t_end, t)\n    max_error = np.max(np.abs(y - y_exact))\n    errors.append(max_error)\n\n# График зависимости ошибки от шага\nplt.figure(figsize=(8, 6))\nplt.loglog(h_values, errors, 'o-', label='Ошибка', color='blue')\nplt.xlabel('Шаг h')\nplt.ylabel('Максимальная ошибка')\nplt.title('Сходимость метода предиктора-корректора')\nplt.grid(True, which='both', linestyle='--')\nplt.legend()\nplt.show()\n "},
             2.6: {'code' : " import numpy as np\nimport matplotlib.pyplot as plt\n\ndef milne_method(func, x_start, x_end, y_start, steps):\n    # Шаг интегрирования\n    step_size = (x_end - x_start) / steps\n    x_vals = np.linspace(x_start, x_end, steps + 1)\n    y_vals = np.zeros((steps + 1, len(y_start)))\n    y_vals[0, :] = y_start\n\n    # Инициализация с помощью метода Эйлера\n    for n in range(3):\n        y_predict = y_vals[n, :] + step_size * func(x_vals[n], y_vals[n, :])\n        y_vals[n + 1, :] = y_vals[n, :] + step_size / 2 * (\n            func(x_vals[n], y_vals[n, :]) + func(x_vals[n + 1], y_predict)\n        )\n\n    # Основной цикл метода Милна\n    for k in range(4, steps + 1):\n        y_pred = y_vals[k - 4, :] + (4 * step_size / 3) * (\n            2 * func(x_vals[k - 3], y_vals[k - 3, :])\n            - func(x_vals[k - 2], y_vals[k - 2, :])\n            + 2 * func(x_vals[k - 1], y_vals[k - 1, :])\n        )\n        y_corr = y_vals[k - 2, :] + (step_size / 3) * (\n            func(x_vals[k - 2], y_vals[k - 2, :])\n            + 4 * func(x_vals[k - 1], y_vals[k - 1, :])\n            + func(x_vals[k], y_pred)\n        )\n        y_vals[k, :] = y_corr\n\n    return x_vals, y_vals\n\ndef test_func(x, y):\n    return -np.sin(x)\n\nx5, y5 = milne_method(test_func, 0, 0.5, [1], 5)\nx50, y50 = milne_method(test_func, 0, 0.5, [1], 50)\n\nprint(f'Результат для N=5: {y5[-1, 0]}')\nprint(f'Результат для N=50: {y50[-1, 0]}')\n\n# Количество шагов\nstep_counts = np.array([2**i for i in range(3, 12)])\nstep_sizes = 0.5 / step_counts\nerrors = np.zeros_like(step_sizes)\n\nfor idx, n_steps in enumerate(step_counts):\n    x_vals, y_vals = milne_method(test_func, 0, 0.5, [1], n_steps)\n    errors[idx] = np.abs(np.cos(0.5) - y_vals[-1, 0])\n\n# Построение зависимости ошибки\ncoeffs = np.polyfit(np.log(step_sizes[:-1]), np.log(errors[:-1]), 1)\n\nplt.figure(figsize=(8, 5))\nplt.loglog(step_sizes, errors, 'o-', label='Вычисленные ошибки')\nplt.loglog(step_sizes, np.exp(coeffs[1]) * step_sizes**coeffs[0], label=f'Наклон: {coeffs[0]:.2f}')\nplt.xlabel('Шаг интегрирования')\nplt.ylabel('Ошибка')\nplt.title('Сходимость метода Милна')\nplt.legend()\nplt.grid(True, which='both', linestyle='--', linewidth=0.5)\nplt.show() "},
             3.1: {'code' : " import numpy as np\n\ndef dft(signal):\n    N = len(signal)\n    spectrum = np.zeros(N, dtype = complex)\n    for k in range(N):\n        X_k = 0\n        for n in range(N):\n            X_k += signal[n] * np.exp(-2j * np.pi * k * n / N)\n        spectrum[k] = X_k\n    return spectrum\n\ndef idft(spectrum):\n    N = len(spectrum)\n    signal = np.zeros(N)\n    for n in range(N):\n        x_n = 0\n        for k in range(N):\n            x_n += spectrum[k] * np.exp(2j * np.pi * k * n / N)\n        x_n = x_n / N\n        signal[n] = x_n.real\n    return signal\n\ndef F(k):\n    if abs(k) <= 3:\n        return k * np.sin(3 * k) * np.arctan(2 * k)\n    else:\n        return 0\n\n# Генерация ряда k и соответствующих значений F(k)\nk_values = np.linspace(-4, 4, 100)  # Диапазон значений k от -3 до 3\nF_values = [F(k) for k in k_values]\n\n# Построение графика\nplt.plot(k_values, F_values, label='F(k)', color='blue')\nplt.axhline(0, color='black', linestyle='--', linewidth=0.7)\nplt.title('График функции F(k)')\nplt.xlabel('k')\nplt.ylabel('F(k)')\nplt.grid()\nplt.legend()\nplt.show()\n\ndft_values = dft(F_values)\n\nplt.plot(k_values, dft_values, label='dft', color='blue')\nplt.axhline(0, color='black', linestyle='--', linewidth=0.7)\nplt.title('График преобразованного сигнала')\nplt.xlabel('k')\nplt.ylabel('F(k)')\nplt.grid()\nplt.legend()\nplt.show()\n\nidft_values = idft(dft_values)\n\nplt.plot(k_values, idft_values, label='idft', color='blue')\nplt.axhline(0, color='black', linestyle='--', linewidth=0.7)\nplt.title('График восстановленного сигнала')\nplt.xlabel('k')\nplt.ylabel('F(k)')\nplt.grid()\nplt.legend()\nplt.show() "},
             3.2: {'code' : " def f(t):\n    return np.sin(2 * np.pi * t) + np.cos(6 * np.pi * t)\n\ndef dft(signal):\n    N = len(signal)\n    spectrum = np.zeros(N, dtype=complex)\n    for k in range(N):\n        X_k = 0\n        for n in range(N):\n            X_k += signal[n] * np.exp(-2j * np.pi * k * n / N)\n        spectrum[k] = X_k\n    return spectrum\n\ndef idft(spectrum):\n    N = len(spectrum)\n    signal = np.zeros(N)\n    for n in range(N):\n        x_n = 0\n        for k in range(N):\n            x_n += spectrum[k] * np.exp(2j * np.pi * k * n / N)\n        x_n = x_n / N\n        signal[n] = x_n.real\n    return signal\n\ndef compute_frequencies(N, fs):\n    return np.array([k * fs / N if k <= N // 2 else (k - N) * fs / N for k in range(N)])\n\nfs = 50  # Частота дискретизации (Гц)\nT = 1    # Длительность сигнала (с)\nN = fs * T  # Количество дискретных точек\nt = np.linspace(0, T, N, endpoint=False)  # Массив времени\nsignal = f(t)\n\nspectrum = dft(signal)\n\nfreqs = compute_frequencies(N, fs)\n\nfiltered_spectrum = spectrum.copy()\nfor i in range(N):\n    if abs(freqs[i]) > 5:  # Обнуление частот выше 5 Гц\n        filtered_spectrum[i] = 0\n\nreconstructed_signal = idft(filtered_spectrum)\n\nplt.figure(figsize=(12, 8))\n\nplt.subplot(3, 1, 1)\nplt.plot(t, signal, label='Исходный сигнал', color='blue')\nplt.title('Исходный сигнал')\nplt.xlabel('Время (с)')\nplt.ylabel('Амплитуда')\nplt.grid()\nplt.legend()\n\nplt.subplot(3, 1, 2)\nplt.stem(freqs[:N//2], np.abs(spectrum[:N//2]), linefmt='r-', markerfmt='ro', basefmt='k')\nplt.title('Амплитудный спектр')\nplt.xlabel('Частота (Гц)')\nplt.ylabel('Амплитуда')\nplt.grid()\n\nplt.subplot(3, 1, 3)\nplt.plot(t, reconstructed_signal, label='Восстановленный сигнал (частоты < 5 Гц)', color='green')\nplt.title('Восстановленный сигнал')\nplt.xlabel('Время (с)')\nplt.ylabel('Амплитуда')\nplt.grid()\nplt.legend()\n\nplt.tight_layout()\nplt.show() "},
             }
m_to_dict = {0 : 'theory', 1 : 'code'}
themes = '''
1.   Наивное умножение матрицы на вектор и умножение матриц.
2.   Ииерархия памяти, план кеша и LRU, промахи в обращении к кешу.
3.   Алгоритм Штрассена.
4.   Собственные векторы, собственные значения (важность, Google PageRank).
5.   Разложение Шура и QR-алгоритм.
6.   Степенной метод.
7.   Круги Гершгорина.
8.   Разложение Шура, теорема Шура.
9.   Нормальные матрицы, эрмитовы матрицы, унитарно диагонализуемые матрицы, верхне-гессенбергова форма матриц.
10.  Спектр и псевдоспектр.
11.  Неявный QR алгоритм (со сдвигами).
12.  Алгоритм на основе стратегии "разделяй и властвуй".
13.  Разреженные матрицы, форматы хранения разреженных матриц, прямые методы для решения больших разреженных систем.
14.  Обыкновенные дифференциальные уравнения, задача Коши.
15.  Локальная, глобальная ошибки.
16.  Метод центральной разности.
17.  Метод Эйлера.
18.  Метод предиктора-корректора.
19.  Метод Рунге-Кутты 1-4 порядков.
20.  Методы Адамса-Мултона, методы Адамса-Бэшфорта.
21.  Метод Милна.
22.  Согласованность, устойчивость, сходимость, условия устойчивости.
23.  Моделирование волны с использованием математических инструментов (амплитуда, период, длина волны, частота, Герц, дискретизация, частота дискретизации, фаза, угловая частота).
24.  Дискретное преобразование Фурье, обратное дискретное преобразование Фурье их ограничения, симметрии в дискретном преобразовании Фурье.
25.  Быстрое преобразование Фурье, его принципы, фильтрация сигнала с использованием быстрого преобразования Фурье.
26.  Операции свёртки, связь с быстрым преобразованием Фурье, операции дискретной свёртки.
27.  Дискретная свёртка и Тёплицевы матрицы (Ганкелевы матрицы).
28.  Циркулянтные матрицы. Матрицы Фурье.
29.  Быстрый матвек с циркулянтом
30.  АКР_вар1_№1 (eigenvalues, развертывание)
31.  АКР_вар1_№2 (Коши)
32.  АКР_вар2_№1 (eigenvalues, вращения)
33.  АКР_вар2_№2 (найдите производную, центральная разность)
34.  АКР_вар3_№1 (eigenvalues, метод Эйлера)
35.  АКР_вар4_№1 (eigenvalues, QR)
36.  АКР_вар4_№2 (производная, прямая разность)
1.1 Перемножение матриц, умножение матрицы на вектор
1.2 Штрассен, график сходимости
1.3 QR
1.4 Шура
1.5 Круги Гершгорина
2.1 Коши, предиктор-корректор, график решения
2.2 Эйлер, анализ сходимости, система
2.3 Рунге-Кутта, фазовый портрет
2.4 Мултона
2.5 предиктор корректор сходимость
2.6 Метод Милна, сходимость
3.1 DFT
3.2 ДФТ, спектр, удаление частот
'''

import pyperclip as pc

def info():
    '''
    Добавляет в буфер обмена список тем, по которым потом обращаться при помощи функции get(n, m), где n - номер темы, m = 0 => теория, m = 1 => практика
    '''
    pc.copy(themes)

def info_cl():
    '''
    Создает класс, в документации которого список тем, по которым потом обращаться при помощи функции get(n, m), где n - номер темы, m = 0 => теория, m = 1 => практика
    '''
    class sol():
        __doc__ = themes
        
    return sol()

def get(n, m : int):
    '''
    Добавляет в буфер обмена ответ по теме (n - номер темы; m = 0 => теория, m = 1 => практика)
    '''
    if 0 < n < 37:
        if -1 < m < 2:
            pc.copy(questions[n][m_to_dict[m]])
        else:
            pc.copy('Неправильный выбор типа задания')
    else:
        pc.copy('Неправильный выбор номера темы')


def get_cl(n, m):
    '''
    Создает объект класса, в документации (shift + tab) которого лежит ответ по теме (n - номер темы; m = 0 => теория, m = 1 => практика)
    '''
    class sol:
        def __init__(self, n, m):
            self.n = n
            self.m = m
            self.doc = questions[self.n][m_to_dict[self.m]] 

        @property
        def __doc__(self):
            return self.doc  

    return sol(n, m)