import pyperclip

def t1():
    """
    Ответ на вопрос:
    - Работа с числами с плавающей точкой
    """

    answer = """
    Ответ на вопрос:
    - Работа с числами с плавающей точкой

    Работа с числами с плавающей точкой в компьютере принципиально отличается от идеальных математических вычислений из-за ограниченной точности представления чисел.

    В отличие от вычислений "вручную", где можно использовать любую степень точности, в компьютере числа хранятся в ячейках памяти с фиксированной разрядностью (например, float16, float32, float64). Это накладывает ограничения как на диапазон представляемых значений, так и на их точность. Например, для стандартного 64-битного числа (double precision) диапазон значений ограничен: 10⁻³⁰⁷ < |x| < 10³⁰⁷, и оно хранит не более 15-16 значащих цифр.

    Это приводит к тому, что при выполнении арифметических операций производятся округления, вносящие так называемую вычислительную погрешность. При большом количестве операций эти малые ошибки могут накапливаться, приводя к значительным отклонениям в конечном результате. Таким образом, компьютерная арифметика, связанная с представлением чисел в CPU/GPU, отличается от обычной, что необходимо учитывать при разработке и анализе численных методов.

    ---
    *Информация для ответа взята со слайдов 15, 18, 23, 118.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t2():
    """
    Ответ на вопрос:
    - Архитектура памяти.
    """

    answer = """
    Современные вычислительные системы используют иерархическую архитектуру памяти для баланса между скоростью, стоимостью и объёмом. Эта иерархия включает в себя несколько уровней: от самых быстрых и малых (регистры, кэш-память L1, L2, L3) до более медленных и объёмных (оперативная память RAM) и, наконец, до самого медленного, но и самого большого по объёму хранилища (SSD/HDD). Вычисления могут производиться только с данными, находящимися на вершине иерархии (в регистрах и кэше).

    Производительность численных методов напрямую зависит от того, насколько эффективно используется эта иерархия. Ключевым фактором является соотношение числа вычислительных операций (flops) к числу обращений к памяти. Библиотеки, такие как BLAS, оптимизированы для максимизации этого соотношения. Например, операции BLAS Уровня 3 (матрично-матричные) выполняют O(n³) вычислений при O(n²) обращений к памяти, что позволяет эффективно использовать быстрый кэш и минимизировать дорогостоящие обращения к медленной оперативной памяти. Этот принцип, известный как "принцип локальности", критически важен для достижения высокой производительности в численных расчетах.

    ---
    *Информация для ответа взята со слайдов 237-240. На слайде 239 приведена наглядная диаграмма иерархии памяти.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t3():
    """
    Ответ на вопрос:
    - Переполнение (overflow), потеря точности (underflow).
    """

    answer = """
    Переполнение (overflow) и потеря точности (underflow) — это ошибки, возникающие, когда результат арифметической операции выходит за пределы диапазона, представимого типом данных с плавающей точкой.

    **Переполнение (Overflow)** происходит, когда абсолютное значение результата превышает максимально возможное число для данного формата. Например, для 64-битного числа это происходит, если |x| > 10³⁰⁷. Такая операция обычно возвращает специальное значение, такое как бесконечность (Inf), что приводит к некорректной работе алгоритма.

    **Потеря точности (Underflow)** происходит, когда результат операции по абсолютной величине оказывается меньше минимального положительного числа, которое можно представить в данном формате (например, |x| < 10⁻³⁰⁷). В этом случае результат обычно округляется до нуля. Это может привести к потере значимой информации и последующим ошибкам, например, к делению на ноль. Оба этих явления являются следствием ограниченной разрядности компьютерной арифметики.

    ---
    *Информация для ответа взята со слайда 15.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t4():
    """
    Ответ на вопрос:
    - Ошибка округления в арифметике с плавающей точкой, накопление ошибок округления, потеря значимости.
    """

    answer = """
    **Ошибка округления** — это погрешность, возникающая из-за того, что результат арифметической операции не может быть точно представлен в памяти компьютера с фиксированной разрядностью и округляется до ближайшего доступного значения.

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

    **Потеря значимости** — это катастрофическое увеличение относительной погрешности при вычитании двух близких по значению чисел. При такой операции старшие разряды чисел взаимно уничтожаются, и результат определяется младшими, менее точными разрядами, которые могут содержать накопленную ошибку. Из-за этого относительная погрешность результата может стать очень большой, что делает его недостоверным. Эта проблема особенно видна из формулы предельной относительной погрешности разности: Δu = (Δx - Δy) / |x - y|, где знаменатель стремится к нулю.

    ---
    *Информация для ответа взята со слайдов 15, 18, 23, 33.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t5():
    """
    Ответ на вопрос:
    - Суммирование по Кахану.
    """

    answer = """
Суммирование по Кахану 
Стандартное суммирование чисел с плавающей запятой страдает от накопления ошибок, особенно когда к большой текущей сумме добавляется малое число – его младшие, значащие разряды могут быть отброшены.

Алгоритм Кахана решает эту проблему, вводя компенсирующую поправку.

На каждом шаге:

Очередное слагаемое сначала корректируется на величину поправки, вычисленной на предыдущем шаге. Это "возвращает" ранее потерянную точность.

Скорректированное слагаемое добавляется к основной сумме. При этом снова может возникнуть ошибка округления.

Вычисляется новая поправка: это разница между тем, что фактически было добавлено к сумме, и тем, что должно было быть добавлено (из скорректированного слагаемого). Эта разница и есть новая "потерянная" часть, которая будет учтена на следующей итерации.

Основная сумма обновляется.

Таким образом, поправка постоянно отслеживает и "переносит" на следующий шаг ту часть числа, которая теряется из-за ограниченной точности, что радикально снижает общую ошибку суммирования.
        """
    pyperclip.copy(answer)
    pyperclip.paste()


def t6():
    """
    Ответ на вопрос:
    - Абсолютная и относительная погрешности.
    """

    answer = """
    Абсолютная и относительная погрешности — это две основные меры для оценки точности приближённых вычислений.

    **Абсолютной погрешностью (Δ)** приближенного значения `ap` называется модуль разности между точным значением `a` и приближенным:
    Δ = |a - ap|
    Она показывает величину ошибки в тех же единицах, что и измеряемая величина, но не даёт представления о её значимости.

    **Относительной погрешностью (δ)** называется отношение абсолютной погрешности к модулю точного значения:
    δ = Δ / |a| = |a - ap| / |a|
    Эта безразмерная величина (часто выражается в процентах) лучше характеризует качество измерения, так как показывает ошибку относительно самой величины. На практике, так как точное значение `a` обычно неизвестно, используют предельную абсолютную (Δa) и относительную (δa) погрешности, которые являются гарантированной верхней оценкой для Δ и δ соответственно.

    ---
    *Информация для ответа взята со слайдов 20-22, 26.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t7():
    """
    Ответ на вопрос:
    - Округление и значащие цифры в записи приближенного числа.
    """

    answer = """
    **Значащими цифрами** в записи приближенного числа называются все ненулевые цифры, а также нули, содержащиеся между ними или являющиеся представителями сохраненных разрядов при округлении. Нули в начале числа (например, в `0.0357`) не являются значащими. Так, в числе `2.305` — 4 значащие цифры, в `0.0357` — 3 значащие цифры, а в `0.035300` (если это результат округления) — 5 значащих цифр.

    **Округление** числа до `n` значащих цифр — это замена его близким числом с меньшим количеством значащих цифр. Правила округления зависят от первой отбрасываемой цифры:
    1.  Если она меньше 5, последняя сохраняемая цифра не меняется.
    2.  Если она больше 5, последняя сохраняемая цифра увеличивается на единицу.
    3.  Если она равна 5 и за ней следуют ненулевые цифры, последняя цифра также увеличивается.
    4.  Если она равна 5, а за ней нули, используется **правило четной цифры**: последняя сохраняемая цифра округляется до ближайшего четного числа (например, 2.5 -> 2, 3.5 -> 4). Это делается для компенсации знаков ошибок при массовых вычислениях.

    ---
    *Информация для ответа взята со слайдов 24, 25.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t8():
    """
    Ответ на вопрос:
    - Верные в строгом (узком) смысле цифры числа, верные в широком смысле цифры числа.
    """

    answer = """
    Понятие "верной цифры" позволяет оценить точность приближенного числа, связывая его абсолютную погрешность с разрядом цифр.

    **Верная в строгом (узком) смысле** цифра — это цифра, для которой абсолютная погрешность числа не превосходит половины единицы разряда, в котором стоит эта цифра. Например, для числа `46.852 ± 0.007`, цифра 8 (разряд десятых) верна в строгом смысле, так как погрешность `0.007` больше, чем `0.005` (половина от разряда сотых `0.01/2`), но меньше, чем `0.05` (половина от разряда десятых `0.1/2`). Цифра 5 (разряд сотых) уже не будет верной, так как `0.007 > 0.005`.

    **Верная в широком смысле** цифра — это цифра, для которой абсолютная погрешность не превышает единицы разряда, в котором она стоит. Это менее строгое требование. Например, в числе `а = 2.91385` с погрешностью `Δa = 0.0097`, верными в широком смысле будут цифры 2, 9 и 1, так как погрешность меньше единицы разряда тысячных (0.01).

    Запись числа считается **правильной**, если все его цифры верны.

    ---
    *Информация для ответа взята со слайдов 27-29.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t9():
    """
    Ответ на вопрос:
    - Сложность алгоритмов и нотация big-O.
    """

    answer = """
    **Сложность алгоритма** — это зависимость количества ресурсов, требуемых алгоритмом (обычно времени выполнения или памяти), от размера входных данных `n`. Время выполнения оценивается путем подсчета "базовых операций" (сложение, умножение, сравнение и т.д.).

    **Нотация Big-O (O-нотация)** используется для асимптотического анализа сложности, то есть для описания поведения алгоритма при больших `n` (когда `n → ∞`). Она позволяет абстрагироваться от конкретного "железа" и деталей реализации. Big-O описывает верхнюю границу роста сложности, отбрасывая константные множители и члены низшего порядка. Например, если точное число операций T(n) = 4n² + 2n + 2, то в нотации Big-O сложность записывается как O(n²), так как при больших `n` член n² доминирует.

    Распространенные классы сложности: O(1) – константная, O(log n) – логарифмическая, O(n) – линейная, O(n log n), O(n²) – квадратичная, O(2ⁿ) – экспоненциальная. Алгоритмы с полиномиальной сложностью (например, O(n^c)) считаются эффективными, в то время как экспоненциальная сложность делает алгоритм непрактичным для больших данных.

    ---
    *Информация для ответа взята со слайдов 37, 41-55.*


    # Код для графика со слайда 52
    n = np.linspace(1, 10, 400)
    log_n = np.log(n)
    lin_n = n
    quad_n = n**2

    plt.figure(figsize=(8, 6))
    plt.plot(n, quad_n, 'r-', label='O(n²)', linewidth=3)
    plt.plot(n, lin_n, 'g-', label='O(n)', linewidth=3)
    plt.plot(n, log_n, 'b-', label='O(log n)', linewidth=3)

    plt.xlabel('Input size', fontsize=16)
    plt.ylabel('Time', fontsize=16)
    plt.title('Algorithm Complexity Comparison', fontsize=18)
    plt.legend(fontsize=16, frameon=False)
    plt.ylim(0, 20)
    plt.xlim(0, 10)
    plt.xticks([])
    plt.yticks([])
    plt.grid(False)

    # Добавляем текст прямо на график
    plt.text(5, 18, 'O(n²)', fontsize=16, color='red')
    plt.text(8, 9, 'O(n)', fontsize=16, color='green')
    plt.text(7, 3, 'O(log n)', fontsize=16, color='blue')

    plt.show()"""

    pyperclip.copy(answer)
    pyperclip.paste()


def t10():
    """
    Ответ на вопрос:
    - Профилирование кода в Python.
    """

    answer = """Профилирование кода — это процесс анализа программы для определения времени выполнения и частоты вызовов её отдельных частей (функций, строк кода). Цель профилирования — найти "узкие места" (bottlenecks), то есть участки кода, которые потребляют больше всего времени или других ресурсов, и оптимизировать их.

    Концептуально, при профилировании в Python мы как бы "наблюдаем" за кодом во время его работы. Один из элегантных способов это сделать — использовать декораторы.

    Декоратор для профилирования, по сути, "оборачивает" интересующую нас функцию. Перед тем как оригинальная функция начнет свое выполнение, декоратор может, например, запустить таймер или увеличить счетчик вызовов. Затем выполняется сама оригинальная функция со всеми её аргументами. После того как она завершит работу и вернет результат, декоратор снова перехватывает управление: он останавливает таймер, фиксирует итоговое время и может вывести информацию о длительности выполнения или количестве вызовов. При этом сам код оригинальной функции остается неизменным.Магические команды %time и %timeit:

    Концепция: Это не совсем декораторы в классическом понимании Python, а специальные команды Jupyter. Однако они позволяют очень быстро "обернуть" одну строку или ячейку кода для замера времени.

    %time: Концептуально, он однократно выполняет указанный код и сообщает время. Это как временный, невидимый декоратор для мгновенного замера.

    %timeit: Более точный. Он многократно выполняет код (концептуально, как бы много раз "оборачивая" его невидимым декоратором) и предоставляет статистику (среднее время, стандартное отклонение), что помогает получить более надежную оценку производительности, сглаживая случайные флуктуации.

    cProfile через магическую команду %prun:

    Концепция: cProfile – это встроенный в Python мощный профилировщик. В Jupyter его удобно вызывать через %prun. Хотя вы не пишете @decorator над функцией, команда %prun my_function() концептуально запускает my_function() под полным "надзором" cProfile. Он отслеживает время, потраченное в каждой функции, вызванной в ходе выполнения my_function(), и количество вызовов. Результат – детальный отчет.
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t11():
    """
    Ответ на вопрос:
    - Представление чисел с плавающей точкой (стандарт IEEE 754), ошибки представления.
    """

    answer = """
    Тема "Стандарт IEEE 754" напрямую не раскрывается в предоставленном материале, но его концепции лежат в основе обсуждаемых проблем.

    **Стандарт IEEE 754** — это наиболее распространенный технический стандарт для представления чисел с плавающей точкой в компьютерах. Число представляется в виде трёх частей: **знак**, **порядок (экспонента)** и **мантисса**. Такая структура позволяет представлять очень большие и очень маленькие числа в компактной форме.

    **Ошибки представления** возникают из-за того, что не все вещественные числа могут быть точно представлены в этом формате. Это связано с двумя основными причинами:
    1.  **Ограниченная разрядность**: Мантисса и экспонента имеют конечное число бит, что ограничивает точность и диапазон чисел. Это приводит к **ошибкам округления**, когда число округляется до ближайшего представимого значения.
    2.  **Двоичная система**: Некоторые конечные десятичные дроби (например, 0.1) становятся бесконечными периодическими дробями в двоичной системе, и их точное представление невозможно.

    Эти ошибки представления являются источником вычислительных погрешностей, переполнения (overflow) и потери значимости (underflow), которые необходимо учитывать при работе с численными методами.

    ---
    *Ответ построен на основе общей информации и данных со слайдов 15, 18.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t12():
    """
    Ответ на вопрос:
    - Способы изолирования корней нелинейных функций.
    """

    answer = """
    Изолирование (или отделение) корня нелинейного уравнения `f(x) = 0` — это первый этап его численного решения, заключающийся в нахождении отрезка `[a, b]`, на котором содержится ровно один корень. Существует два основных способа для этого.

    1.  **Аналитический способ**: Основан на теореме о промежуточном значении. Если функция `f(x)` непрерывна на отрезке `[a, b]` и принимает на его концах значения разных знаков (т.е. `f(a) * f(b) < 0`), то на этом отрезке существует хотя бы один корень. Если дополнительно функция `f(x)` монотонна на `[a, b]` (т.е. ее производная `f'(x)` сохраняет знак), то корень на этом отрезке единственный.

    2.  **Графический способ**: Этот метод часто более нагляден. Можно построить график функции `y = f(x)` и визуально определить отрезки, где график пересекает ось Ox. Для упрощения задачи исходное уравнение `f(x) = 0` часто преобразуют к виду `f1(x) = f2(x)`, где `f1` и `f2` — более простые функции. Тогда абсциссы точек пересечения их графиков и будут искомыми корнями.

    ---
    *Информация для ответа взята со слайдов 60-63, 67, 69-71.*


    # Код для графика со слайда 67
    x = np.linspace(0.1, 5, 400)
    y1 = 1/x
    y2 = np.log10(x)

    plt.figure(figsize=(7, 5))
    plt.plot(x, y1, label='y = 1/x')
    plt.plot(x, y2, label='y = lg x')
    plt.axhline(0, color='black', linewidth=0.5)
    plt.axvline(0, color='black', linewidth=0.5)
    plt.title("Графическое отделение корня уравнения 1/x = lg(x)")
    plt.xlabel('x')
    plt.ylabel('y')
    plt.ylim(-1, 2)
    plt.grid(True)
    plt.legend()
    plt.show()"""

    pyperclip.copy(answer)
    pyperclip.paste()


def t13():
    """
    Ответ на вопрос:
    - Сжимающие отображения.
    """

    answer = """
    Тема "Сжимающие отображения" напрямую не названа в презентации, но ее концепция является теоретической основой метода простой итерации для решения уравнений вида `x = φ(x)`.

    Отображение `φ(x)` называется **сжимающим** на отрезке `[a, b]`, если для любых двух точек `x1` и `x2` из этого отрезка выполняется условие:
    `|φ(x1) - φ(x2)| ≤ q * |x1 - x2|`, где `0 ≤ q < 1`.
    Константа `q` называется коэффициентом сжатия.

    Если функция `φ(x)` дифференцируема, то это условие эквивалентно требованию `|φ'(x)| ≤ q < 1` для всех `x` на отрезке. Именно это условие `|φ'(x)| < 1` является ключевым для сходимости метода простой итерации. Оно гарантирует, что на каждом шаге итерации `xk+1 = φ(xk)` расстояние между последовательными приближениями будет уменьшаться, и последовательность `xk` будет сходиться к единственной неподвижной точке (корню уравнения) внутри отрезка.

    ---
    *Информация для ответа взята со слайдов 79, 81, 82, 87.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t14():
    """
    Ответ на вопрос:
    - Погрешность и критерии сходимости, константа Липшица.
    """

    answer = """
    При решении нелинейных уравнений итерационными методами ключевыми являются понятия погрешности и критериев сходимости.

    **Погрешность** — это отклонение текущего приближения от истинного корня. Итерационный процесс считается **сходящимся**, если эта погрешность стремится к нулю. На практике, поскольку истинный корень неизвестен, сходимость оценивают по разности между двумя последовательными приближениями. Критерием остановки (сходимости) обычно служит условие `|xk - xk-1| < ε`, где `ε` — заданная точность.

    Для метода простой итерации `x = φ(x)` основной **критерий сходимости** связан с **константой Липшица**. Функция `φ(x)` удовлетворяет условию Липшица с константой `L`, если `|φ(x1) - φ(x2)| ≤ L * |x1 - x2|`. Если `L < 1`, то отображение является сжимающим, и метод сходится. Для дифференцируемой функции `φ(x)` роль константы Липшица играет максимум модуля ее производной, и условие сходимости упрощается до `max|φ'(x)| < 1` на интервале изоляции корня.

    ---
    *Информация для ответа взята со слайдов 79-82.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t15():
    """
    Ответ на вопрос:
    - Скорость сходимости итерационного алгоритма.
    """

    answer = """
    Скорость сходимости итерационного алгоритма показывает, насколько быстро последовательность приближений `xk` стремится к точному решению `x*`. Формально, если ошибка на k-м шаге равна `εk = |xk - x*|`, то скорость сходимости определяется соотношением `εk+1 ≈ C * (εk)^p`, где `p` — порядок сходимости.

    *   **Линейная сходимость (p=1)**: Ошибка на каждом шаге уменьшается примерно в постоянное число раз (`C < 1`). Это относительно медленная сходимость. Примером является метод простой итерации, где `C` равно `|φ'(x*)|`.
    *   **Квадратичная сходимость (p=2)**: Количество верных значащих цифр на каждом шаге примерно удваивается. Это очень быстрая сходимость. Классическим примером является метод Ньютона.
    *   **Сверхлинейная сходимость**: Скорость выше линейной, но ниже квадратичной. Примером является метод секущих.

    Чем выше порядок сходимости `p`, тем меньше итераций требуется для достижения заданной точности. Например, метод Ньютона (квадратичная сходимость) обычно значительно быстрее метода половинного деления (линейная сходимость).

    ---
    *Информация для ответа взята со слайдов 77 (упоминание медленной сходимости), 93 (модификация Ньютона), 105 (связь с итерациями), 108 (условие сходимости Ньютона), 261 (скорость степенного метода).*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t16():
    """
    Ответ на вопрос:
    - Стабильность и распространение ошибки в методах численного решения нелинейных уравнений.
    """

    answer = """
    Тема "Стабильность и распространение ошибки" в контексте решения нелинейных уравнений напрямую не освещается в презентации так, как для дифференциальных уравнений. Однако можно провести аналогии.

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

    **Распространение ошибки** определяется свойствами итерационной функции. В методе простой итерации `xk+1 = φ(xk)` ошибка распространяется в соответствии с множителем `q = |φ'(x*)|`.
    *   Если `|q| < 1` (сжимающее отображение), метод является стабильным: любая ошибка на k-м шаге будет уменьшена на (k+1)-м.
    *   Если `|q| > 1`, метод нестабилен: малая ошибка будет экспоненциально нарастать с каждой итерацией, и алгоритм быстро разойдется.
    *   Случай `|q| = 1` требует дополнительного анализа.

    Таким образом, для итерационных методов решения нелинейных уравнений стабильность по сути эквивалентна выполнению условия сходимости.

    ---
    *Ответ построен на основе анализа слайдов 79, 81, 82, 87.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()



def t17():
    """
    Ответ на вопрос:
    - Теория сжимающих отображений.
    """

    answer = """
    Теория сжимающих отображений, также известная как принцип Банаха о неподвижной точке, является фундаментальной для доказательства сходимости многих итерационных методов. Отображение (функция) `φ` называется **сжимающим** на некотором множестве, если оно "сближает" любые две точки из этого множества. Формально, для любых `x1` и `x2` должно выполняться `|φ(x1) - φ(x2)| ≤ q * |x1 - x2|`, где `0 ≤ q < 1` — коэффициент сжатия.

    **Принцип сжимающих отображений** гласит, что если `φ` является сжимающим отображением на полном метрическом пространстве (например, на замкнутом отрезке `[a, b]`), которое отображает это пространство в себя, то существует единственная неподвижная точка `x*` такая, что `x* = φ(x*)`. Более того, итерационная последовательность `xk+1 = φ(xk)` сходится к этой точке `x*` при любом начальном приближении `x0`.

    Этот принцип напрямую применяется в методе простой итерации для решения нелинейных уравнений, где условие `|φ'(x)| < 1` на отрезке гарантирует, что отображение является сжимающим и, следовательно, итерационный процесс сойдется.

    ---
    *Информация для ответа взята со слайдов 79, 81, 82, 87.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t18():
    """
    Ответ на вопрос:
    - Интерполяция, экстраполяция, аппроксимация.
    """

    answer = """
    Эти три термина описывают задачу построения функции на основе набора известных данных, но различаются целями и условиями.

    **Аппроксимация** — это самый общий термин, означающий замену одного математического объекта (например, сложной или таблично заданной функции `f(x)`) другим, более простым (аппроксимирующей функцией `F(x)`).

    **Интерполяция** — это частный случай аппроксимации, при котором требуется, чтобы аппроксимирующая функция `F(x)` точно проходила через все заданные точки (узлы). То есть, `F(xi) = f(xi)` для всех узлов `xi`. Цель интерполяции — найти значение функции *между* известными точками.

    **Экстраполяция** — это также вид аппроксимации, но в этом случае задача состоит в оценке значения функции *вне* интервала, на котором заданы исходные точки. Экстраполяция, как правило, менее точна, чем интерполяция, так как поведение функции за пределами известного диапазона может быть непредсказуемым.

    ---
    *Информация для ответа взята со слайдов 153-159, 166.*
    """

    code = """
import numpy as np
import matplotlib.pyplot as plt

# Данные для примера
x_known = np.array([2, 3, 5, 6])
y_known = np.array([12, 15, 14, 15])
x_full_range = np.linspace(1, 8, 100)
y_true_func = lambda x: 10 + 2*x + 5*np.sin(1.5*x) # Пример истинной функции

# Интерполяция (линейная)
from scipy.interpolate import interp1d
interp_func = interp1d(x_known, y_known, kind='linear', fill_value="extrapolate")
y_interp_extrap = interp_func(x_full_range)

# Аппроксимация (полином 2-й степени)
approx_poly = np.poly1d(np.polyfit(x_known, y_known, 2))
y_approx = approx_poly(x_full_range)

plt.figure(figsize=(12, 7))

# График
plt.plot(x_full_range, y_true_func(x_full_range), 'k--', label='Истинная функция (неизвестна)', alpha=0.5)
plt.plot(x_known, y_known, 'ro', label='Узлы (известные данные)', markersize=8)
plt.plot(x_full_range, y_interp_extrap, 'g-', label='Интерполяция/Экстраполяция')
plt.plot(x_full_range, y_approx, 'b-.', label='Аппроксимация (полином)')

# Обозначение областей
plt.axvspan(x_known.min(), x_known.max(), color='yellow', alpha=0.2, label='Область интерполяции')
plt.axvspan(x_full_range.min(), x_known.min(), color='cyan', alpha=0.2, label='Область экстраполяции')
plt.axvspan(x_known.max(), x_full_range.max(), color='cyan', alpha=0.2)

plt.title('Интерполяция, Экстраполяция и Аппроксимация', fontsize=16)
plt.xlabel('X')
plt.ylabel('Y')
plt.legend()
plt.grid(True)
plt.show()
    """
    pyperclip.copy(answer + "\n\nКод для построения графика:\n" + code)
    pyperclip.paste()

def t19():
    """
    Ответ на вопрос:
    - Глобальная и локальная интерполяция.
    """

    answer = """
    Различие между глобальной и локальной интерполяцией заключается в том, как используются известные точки (узлы) для построения интерполирующей функции.

    **Глобальная интерполяция** использует *все* доступные узлы `(x0, y0), ..., (xn, yn)` для построения *единой* функции `F(x)`, которая проходит через все эти точки. Классическим примером является полином Лагранжа. Недостатком глобальной полиномиальной интерполяции является то, что при большом числе узлов (обычно `n > 7`) она становится неустойчивой и может приводить к сильным осцилляциям между узлами (феномен Рунге).

    **Локальная (кусочная) интерполяция** строит итоговую функцию из нескольких "кусочков". Весь интервал разбивается на более мелкие подынтервалы, и на каждом из них строится своя, обычно простая, интерполирующая функция (например, линейная или кубическая), используя только несколько ближайших узлов. Примерами являются линейная интерполяция и сплайн-интерполяция. Такой подход более устойчив и не страдает от сильных осцилляций.

    ---
    *Информация для ответа взята со слайдов 169, 206, 207.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t20():
    """
    Ответ на вопрос:
    - Ступенчатая и линейная интерполяция.
    """

    answer = """
    Ступенчатая и линейная интерполяция — это простейшие виды локальной интерполяции, использующие соседние узлы для оценки значения функции.

    **Ступенчатая интерполяция (интерполяция нулевого порядка)** — самый простой способ. Для любой точки `x` на интервале `[xi, xi+1]` значение функции принимается равным значению в одном из узлов, например, в левом: `F(x) = f(xi)`. График такой функции представляет собой набор горизонтальных отрезков, "ступенек". Этот метод имеет низкую точность, и его график является разрывным.

    **Линейная интерполяция (интерполяция первого порядка)** является более точным методом. На каждом интервале `[xi, xi+1]` функция аппроксимируется прямой линией, соединяющей два соседних узла `(xi, yi)` и `(xi+1, yi+1)`. Формула для вычисления значения в точке `x` имеет вид:
    `y(x) = yi + (y_{i+1} - yi) * (x - xi) / (x_{i+1} - xi)`
    График представляет собой ломаную линию. Этот метод обеспечивает непрерывность функции, но её производная разрывна в узлах.

    ---
    *Информация для ответа взята со слайдов 169, 172-174.*
    """
    code = """
import numpy as np
import matplotlib.pyplot as plt

x_nodes = np.array([0, 1, 2, 3, 4, 5])
y_nodes = np.array([1, 2, 1.5, 3, 2.5, 4])
x_dense = np.linspace(0, 5, 500)

# Ступенчатая интерполяция
from scipy.interpolate import interp1d
step_func = interp1d(x_nodes, y_nodes, kind='previous')
y_step = step_func(x_dense)

# Линейная интерполяция
linear_func = interp1d(x_nodes, y_nodes, kind='linear')
y_linear = linear_func(x_dense)

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 8), sharex=True)

# График ступенчатой
ax1.plot(x_dense, y_step, 'b-', label='Ступенчатая интерполяция')
ax1.plot(x_nodes, y_nodes, 'ro', label='Узлы')
ax1.set_title('Ступенчатая интерполяция')
ax1.set_ylabel('Y')
ax1.legend()
ax1.grid(True)

# График линейной
ax2.plot(x_dense, y_linear, 'g-', label='Линейная интерполяция')
ax2.plot(x_nodes, y_nodes, 'ro', label='Узлы')
ax2.set_title('Линейная интерполяция')
ax2.set_xlabel('X')
ax2.set_ylabel('Y')
ax2.legend()
ax2.grid(True)

plt.tight_layout()
# Чтобы показать график, раскомментируйте plt.show()
# plt.show()

print("Код для построения графика сравнения ступенчатой и линейной интерполяции")
    """
    pyperclip.copy(answer + "\n\n" + code)
    pyperclip.paste()

def t21():
    """
    Ответ на вопрос:
    - Интерполяционные полиномы.
    """

    answer = """
    **Интерполяционный полином** — это полином `P(x)`, который используется для аппроксимации некоторой функции `f(x)` с условием точного прохождения через заданный набор узловых точек `(xi, yi)`. То есть, для всех узлов `i` должно выполняться равенство `P(xi) = yi`. Основная теорема утверждает, что для `n+1` различных узлов существует единственный полином степени не выше `n`, удовлетворяющий этим условиям.

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

    ---
    *Информация для ответа взята со слайдов 176, 177, 196.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t22():
    """
    Ответ на вопрос:
    - Квадратичная интерполяция.
    """

    answer = """
    **Квадратичная интерполяция** — это локальный метод интерполяции, при котором для оценки значения функции в некоторой точке `x*` используется парабола (полином второй степени), построенная по трем соседним узлам `(x_{i-1}, y_{i-1})`, `(xi, yi)` и `(x_{i+1}, y_{i+1})`. Интерполяционный полином имеет вид `φ(x) = a0 + a1*x + a2*x²`.

    Для нахождения неизвестных коэффициентов `a0, a1, a2` решается система из трех линейных уравнений, которая составляется из условия прохождения параболы через три узла:
    `φ(x_{i-1}) = y_{i-1}`
    `φ(xi) = yi`
    `φ(x_{i+1}) = y_{i+1}`

    Эта система всегда имеет единственное решение, так как ее определитель (определитель Вандермонда для различных узлов) отличен от нуля. Квадратичная интерполяция обеспечивает не только непрерывность самой функции, но и непрерывность ее первой производной, что делает график более гладким по сравнению с линейной интерполяцией.

    ---
    *Информация для ответа взята со слайдов 197-200.*
    """
    code = """
import numpy as np
import matplotlib.pyplot as plt

# Узлы для примера со слайда 199
x_nodes = np.array([1, 3, 4])
y_nodes = np.array([12, 4, 6])

# Находим коэффициенты полинома
# A * c = y  => c = A_inv * y
A = np.vander(x_nodes, increasing=True)
coeffs = np.linalg.solve(A, y_nodes) # [a0, a1, a2] -> a0 + a1*x + a2*x^2
# В презентации коэффициенты расположены наоборот [a2, a1, a0]
# coeffs_презентация = np.polyfit(x_nodes, y_nodes, 2)
# print(f"Коэффициенты из np.polyfit (a2, a1, a0): {coeffs_презентация}") # [ 2. -12.  22.]
# print(f"Коэффициенты из np.linalg.solve (a0, a1, a2): {coeffs}")     # [ 22. -12.   2.]

# Создаем полиномиальную функцию
p = np.poly1d(np.flip(coeffs)) # np.poly1d ожидает [a2, a1, a0]
print(f"Интерполяционный полином: \n{p}")

# Генерируем точки для гладкого графика
x_smooth = np.linspace(0.5, 4.5, 400)
y_smooth = p(x_smooth)

plt.figure(figsize=(8, 6))
plt.plot(x_smooth, y_smooth, label='Квадратичный полином P(x) = 2x² - 12x + 22')
plt.plot(x_nodes, y_nodes, 'ro', label='Узловые точки')
plt.title('Квадратичная интерполяция')
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True)
plt.legend()
# plt.show()
print("\\nКод для построения графика квадратичной интерполяции:")
    """
    pyperclip.copy(answer + "\n\n" + code)
    pyperclip.paste()

def t23():
    """
    Ответ на вопрос:
    - Интерполяция сплайнами.
    """

    answer = """
    **Интерполяция сплайнами** — это метод кусочно-полиномиальной интерполяции, который обеспечивает не только прохождение через узловые точки, но и гладкость итоговой функции в этих узлах. Сплайн — это функция, состоящая из нескольких полиномов (обычно невысокой степени), соединенных друг с другом так, чтобы в точках их соединения (узлах) были непрерывны не только значения функции, но и ее производные до определенного порядка.

    Наиболее распространен **кубический сплайн**, где на каждом сегменте `[xi, xi+1]` функция является полиномом третьей степени. Условия "сшивки" в узлах требуют непрерывности первой и второй производных. Это позволяет избежать изломов (как в линейной интерполяции) и чрезмерных осцилляций (как в глобальной полиномиальной), создавая гладкую и предсказуемую кривую. В отличие от полинома Лагранжа, сплайн требует только значения функции в узлах, но не ее производных, что делает его практичным.

    ---
    *Информация для ответа взята со слайдов 189, 208-213.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t24():
    """
    Ответ на вопрос:
    - Интерполяционный полином Лагранжа.
    """

    answer = """
    **Интерполяционный многочлен Лагранжа** — это метод глобальной полиномиальной интерполяции, который позволяет построить единственный полином степени не выше `n`, проходящий через `n+1` заданных узловых точек `(xi, yi)`. Его ключевая особенность — удобная формула, не требующая решения систем уравнений.

    Полином `L(x)` строится как взвешенная сумма базисных полиномов `li(x)`:
    `L(x) = Σ [yi * li(x)]` (сумма по i от 0 до n),
    где каждый базисный полином `li(x)` обладает свойством: он равен 1 в узле `xi` и 0 во всех остальных узлах `xj` (при `j ≠ i`).

    Формула для базисного полинома `li(x)`:
    `li(x) = Π [(x - xj) / (xi - xj)]` (произведение по j от 0 до n, где `j ≠ i`).

    Хотя формула элегантна, на практике полином Лагранжа редко используется для большого числа узлов из-за вычислительной сложности и склонности к осцилляциям (феномену Рунге).

    ---
    *Информация для ответа взята со слайдов 176-179, 191-195.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t25():
    """
    Ответ на вопрос:
    - Метод кубической сплайн-интерполяции.
    """

    answer = """
    **Метод кубической сплайн-интерполяции** является одним из самых популярных и эффективных методов интерполяции. Он заключается в построении функции, которая на каждом отрезке `[xi, xi+1]` является кубическим полиномом `Si(x)`, и при этом в узлах `xi` обеспечивается непрерывность не только самой функции, но и её первой и второй производных. Это создает очень гладкую кривую, проходящую через все заданные точки.

    На каждом i-м интервале полином имеет вид:
    `Si(x) = ai + bi*(x - xi) + ci*(x - xi)² + di*(x - xi)³`

    Коэффициенты полиномов находятся из системы линейных уравнений, составленной из следующих условий:
    1.  Прохождение через узлы: `Si(xi) = yi`.
    2.  Непрерывность функции, первой и второй производных в узлах: `S_{i-1}(xi) = Si(xi)`, `S'_{i-1}(xi) = S'i(xi)`, `S''_{i-1}(xi) = S''i(xi)`.
    3.  Дополнительные граничные условия на концах всего интервала (например, `S''(x0) = 0` и `S''(xn) = 0` для "естественного" сплайна).

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

    ---
    *Информация для ответа взята со слайдов 189-190, 210-219.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t26():
    """
    Ответ на вопрос:
    - Основные операции в вычислительной линейной алгебре.
    """

    answer = """
    Основные операции в вычислительной (плотной) линейной алгебре (DLA) играют ключевую роль во множестве научных и инженерных приложений. Их можно сгруппировать по задачам:

    1.  **Решение систем линейных уравнений (СЛУ): Ax = b.** Это фундаментальная задача, возникающая в материаловедении, электродинамике, статистике и многих других областях.

    2.  **Задачи на собственные значения и векторы: Ax = λx.** Эти задачи необходимы для анализа стабильности систем, в квантовой механике, для алгоритмов ранжирования (Google PageRank) и методов снижения размерности, таких как метод главных компонент (PCA).

    3.  **Метод наименьших квадратов: найти x, минимизирующий ||Ax - b||.** Применяется для аппроксимации данных, в эконометрике, обработке сигналов и машинном обучении.

    4.  **Сингулярное разложение (SVD): A = UΣV*.** Это мощный инструмент для анализа матриц, используемый в поиске информации, сжатии данных, вычислении псевдообратной матрицы и решении задач методом наименьших квадратов.

    Для эффективной реализации этих операций созданы стандартизированные библиотеки, такие как BLAS и LAPACK.

    ---
    *Информация для ответа взята со слайда 235.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t27():
    """
    Ответ на вопрос:
    - Эффективная реализация алгоритмов вычисления произведения матриц.
    """

    answer = """
    Эффективная реализация умножения матриц критически важна, так как это одна из самых вычислительно-интенсивных операций. Наивный алгоритм "строка на столбец" имеет сложность O(n³), но его практическая производительность сильно зависит от организации доступа к данным в иерархической памяти компьютера.

    Ключевой принцип оптимизации — **принцип локальности**: максимизация повторного использования данных, уже загруженных в быстрый кэш. Это достигается с помощью **блочного умножения матриц**. Матрицы-сомножители разбиваются на небольшие подматрицы (блоки), размер которых подбирается так, чтобы они помещались в кэш-память. Вычисления проводятся на уровне этих блоков, что значительно снижает количество дорогостоящих обращений к медленной оперативной памяти.

    Именно такие блочные алгоритмы реализованы в оптимизированных библиотеках, таких как BLAS (Basic Linear Algebra Subroutines), которые лежат в основе быстрых вычислений в NumPy и других научных пакетах. Это позволяет достичь производительности, близкой к пиковой для конкретного процессора, что на порядки быстрее наивной реализации.

    ---
    *Информация для ответа взята со слайдов 237, 238, 240.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t28():
    """
    Ответ на вопрос:
    - Алгоритм Штрассена, сложность метода Штрассена.
    """

    answer = """
    **Алгоритм Штрассена** — это рекурсивный алгоритм для умножения двух матриц размером n×n, который асимптотически быстрее наивного метода. Идея алгоритма заключается в разбиении исходных матриц на четыре подматрицы (блока) размера n/2 × n/2 и вычислении произведения с помощью 7 умножений этих подматриц вместо 8, как в стандартном блочном методе, но ценой увеличения числа сложений (18 вместо 4).

    Эта экономия на одном умножении на каждом шаге рекурсии приводит к снижению общей вычислительной сложности.

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

    ---
    *Информация для ответа взята со слайдов 240, 287, 292.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t29():
    """
    Ответ на вопрос:
    - Вычисление SVD.
    """

    answer = """
    Вычисление сингулярного разложения (SVD) матрицы A (A = UΣVᵀ) является одной из фундаментальных задач вычислительной линейной алгебры. Прямое вычисление через определение обычно не используется. Вместо этого SVD тесно связывают с задачей нахождения собственных значений.

    Сингулярные числа `σi` (диагональные элементы матрицы Σ) являются квадратными корнями из собственных значений `λi` симметричной положительно полуопределенной матрицы `AᵀA` (или `AAᵀ`). Столбцы матрицы `V` (правые сингулярные векторы) — это собственные векторы матрицы `AᵀA`, а столбцы `U` (левые сингулярные векторы) — собственные векторы `AAᵀ`.

    Практические алгоритмы вычисления SVD обычно двухэтапные:
    1.  Сначала матрица `A` приводится к более простой **бидиагональной форме** `B` с помощью ортогональных преобразований (например, отражений Хаусхолдера).
    2.  Затем для бидиагональной матрицы `B` итерационно вычисляются её сингулярные числа и векторы, например, с помощью модифицированного QR-алгоритма. Этот подход численно более устойчив и эффективен, чем прямое формирование и решение задачи на собственные значения для `AᵀA`.

    ---
    *Информация для ответа взята со слайдов 235, 314, 316, 332-340.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t30():
    """
    Ответ на вопрос:
    - Собственные векторы, собственные значения.
    """

    answer = """
    Для квадратной матрицы `A` размером n×n, **собственным вектором** называется ненулевой вектор `x`, который при умножении на матрицу `A` не меняет своего направления, а только масштабируется на некоторое число `λ`. Это число `λ` называется **собственным значением**, соответствующим данному вектору.

    Математически это определение записывается как уравнение:
    `Ax = λx`

    Собственные векторы определяют инвариантные направления для линейного преобразования, задаваемого матрицей `A`. Они показывают "оси", вдоль которых действие матрицы сводится к простому растяжению или сжатию. Собственные значения `λ` являются коэффициентами этого масштабирования. Нахождение собственных значений и векторов является одной из ключевых задач линейной алгебры, имеющей широкое применение в анализе динамических систем, вибрационном анализе, квантовой механике и алгоритмах машинного обучения.

    ---
    *Информация для ответа взята со слайдов 244, 245.*
    """

    code = """
import numpy as np
import matplotlib.pyplot as plt

A = np.array([[3, 1], [0, 2]])

# Собственные значения и векторы
eigenvalues, eigenvectors = np.linalg.eig(A)
v1 = eigenvectors[:, 0]
v2 = eigenvectors[:, 1]

# Несколько случайных векторов для сравнения
x1 = np.array([1, 1])
x2 = np.array([-1, 1])
x3 = np.array([0.5, -1])

# Применяем преобразование
Ax1 = A @ x1
Ax2 = A @ x2
Ax3 = A @ x3
Av1 = A @ v1
Av2 = A @ v2

plt.figure(figsize=(8, 8))
# Исходные векторы
plt.quiver(0, 0, x1[0], x1[1], angles='xy', scale_units='xy', scale=1, color='gray', label='Original Vectors')
plt.quiver(0, 0, x2[0], x2[1], angles='xy', scale_units='xy', scale=1, color='gray')
plt.quiver(0, 0, x3[0], x3[1], angles='xy', scale_units='xy', scale=1, color='gray')

# Преобразованные векторы
plt.quiver(0, 0, Ax1[0], Ax1[1], angles='xy', scale_units='xy', scale=1, color='blue', label='Transformed Vectors')
plt.quiver(0, 0, Ax2[0], Ax2[1], angles='xy', scale_units='xy', scale=1, color='blue')
plt.quiver(0, 0, Ax3[0], Ax3[1], angles='xy', scale_units='xy', scale=1, color='blue')

# Собственные векторы (исходный и преобразованный)
plt.quiver(0, 0, v1[0], v1[1], angles='xy', scale_units='xy', scale=1, color='red', label='Eigenvector 1')
plt.quiver(0, 0, Av1[0], Av1[1], angles='xy', scale_units='xy', scale=1, color='pink', linestyle='--')
plt.quiver(0, 0, v2[0], v2[1], angles='xy', scale_units='xy', scale=1, color='green', label='Eigenvector 2')
plt.quiver(0, 0, Av2[0], Av2[1], angles='xy', scale_units='xy', scale=1, color='lightgreen', linestyle='--')

plt.xlim(-2, 4)
plt.ylim(-2, 4)
plt.axhline(0, color='black',linewidth=0.5)
plt.axvline(0, color='black',linewidth=0.5)
plt.grid(True)
plt.legend()
plt.title('Действие матрицы на векторы и собственные векторы')
# plt.show()
print("Код для визуализации собственных векторов:")
    """
    pyperclip.copy(answer + "\n\n" + code)
    pyperclip.paste()


def t31():
    """
    Ответ на вопрос:
    - Разложение по собственным векторам.
    """

    answer = """
    **Разложение по собственным векторам**, или спектральное разложение, применимо к диагонализуемым матрицам (например, к симметричным или имеющим `n` линейно независимых собственных векторов). Оно представляет матрицу `A` в виде произведения трех матриц:
    `A = VΛV⁻¹`

    Здесь:
    - `V` — это матрица, столбцами которой являются собственные векторы матрицы `A`.
    - `Λ` (Лямбда) — это диагональная матрица, на главной диагонали которой расположены соответствующие собственные значения `λi`.
    - `V⁻¹` — матрица, обратная к `V`.

    Это разложение очень полезно, так как оно сводит действие сложной матрицы `A` к трем более простым операциям: смена базиса (`V⁻¹`), простое масштабирование вдоль осей нового базиса (`Λ`) и возврат в исходный базис (`V`). Это упрощает многие вычисления, например, возведение матрицы в степень: `A^k = VΛ^k V⁻¹`, где `Λ^k` вычисляется тривиальным возведением в степень диагональных элементов.

    ---
    *Информация для ответа взята со слайдов 250, 262, 283.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t32():
    """
    Ответ на вопрос:
    - Задача Google PageRank.
    """

    answer = """
    **Задача Google PageRank** заключается в ранжировании веб-страниц для определения их важности. Алгоритм PageRank рассматривает Интернет как граф, где страницы — это вершины, а гиперссылки — направленные ребра. Важность страницы определяется рекурсивно: чем больше важных страниц ссылается на данную страницу, тем она важнее.

    Пусть `pi` — это важность (PageRank) i-ой страницы. Тогда она вычисляется как сумма долей PageRank'ов всех страниц `j`, которые ссылаются на страницу `i`:
    `pi = Σ [pj / L(j)]` (сумма по всем `j`, ссылающимся на `i`),
    где `L(j)` — общее число исходящих ссылок со страницы `j`.

    Эта система уравнений может быть записана в матричном виде `p = Gp`, где `p` — вектор PageRank'ов, а `G` — модифицированная матрица смежности графа. Таким образом, задача сводится к нахождению собственного вектора матрицы `G`, соответствующего собственному значению `λ=1`. Этот собственный вектор и является искомым вектором важности страниц.

    ---
    *Информация для ответа взята со слайдов 253, 254.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t33():
    """
    Ответ на вопрос:
    - Вычисление собственных значений с помощью характеристического многочлена.
    """

    answer = """
    Классический метод нахождения собственных значений матрицы `A` основан на решении её **характеристического уравнения**. Исходное уравнение `Ax = λx` преобразуется к виду `(A - λE)x = 0`, где `E` — единичная матрица. Эта система однородных линейных уравнений имеет ненулевое решение `x` только в том случае, если ее определитель равен нулю.

    Таким образом, мы получаем характеристическое уравнение:
    `det(A - λE) = 0`

    Раскрытие этого определителя дает полином `Pn(λ)` n-й степени относительно `λ`, который называется **характеристическим многочленом**. Корни этого полинома `λ₁, λ₂, ..., λn` и являются собственными значениями матрицы `A`.

    Хотя этот метод является теоретически фундаментальным, на практике он используется только для матриц очень малого порядка (n ≤ 4), так как аналитическое нахождение корней полиномов степени 5 и выше невозможно (теорема Абеля-Руффини), а численное решение является плохо обусловленной задачей.

    ---
    *Информация для ответа взята со слайдов 244, 245, 256, 257.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t34():
    """
    Ответ на вопрос:
    - Особенности степенного метода. Скорость сходимости.
    """

    answer = """
    **Степенной метод** — это итерационный алгоритм для нахождения одного, максимального по модулю, собственного значения (доминантного) и соответствующего ему собственного вектора. Его основная идея заключается в многократном умножении матрицы `A` на некоторый начальный вектор `x₀`. После каждой итерации `x_{k+1} = Ax_k` вектор `x_k` будет все больше "вытягиваться" вдоль направления собственного вектора, соответствующего доминантному собственному значению. Для предотвращения переполнения вектор на каждом шаге нормируется.

    **Особенности метода**:
    1.  Простота реализации.
    2.  Находит только одно, максимальное по модулю, собственное значение.
    3.  Требует, чтобы доминантное собственное значение было строго больше остальных по модулю.

    **Скорость сходимости** метода является линейной и определяется отношением модулей двух наибольших собственных значений: `q = |λ₂ / λ₁|`. Чем это отношение ближе к 1, тем медленнее сходится метод. Если `|λ₂| = |λ₁|`, метод не сходится. Из-за потенциально медленной сходимости он используется для грубой оценки или как часть более сложных алгоритмов.

    ---
    *Информация для ответа взята со слайдов 258-264.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t35():
    """
    Ответ на вопрос:
    - Круги Гершгорина, теорема Гершгорина.
    """

    answer = """
    **Теорема Гершгорина** предоставляет простой способ локализации собственных значений матрицы на комплексной плоскости, не вычисляя их. Она утверждает, что все собственные значения матрицы `A` лежат в объединении кругов, называемых **кругами Гершгорина**.

    Для квадратной матрицы `A` n×n, i-й круг Гершгорина `Ci` — это круг на комплексной плоскости с центром в диагональном элементе `aii` и радиусом `ri`, равным сумме модулей всех остальных элементов в i-й строке:
    `ri = Σ |aij|` (сумма по `j`, где `j ≠ i`).

    **Свойства**:
    1.  Все собственные значения матрицы находятся внутри хотя бы одного из этих `n` кругов.
    2.  Если объединение `k` кругов не пересекается с объединением остальных `n-k` кругов, то в первом объединении находится ровно `k` собственных значений.

    Этот метод особенно полезен для диагонально-доминантных матриц, так как позволяет легко оценить, что собственные значения близки к диагональным элементам и матрица невырождена.

    ---
    *Информация для ответа взята со слайдов 267, 268.*
    """

    code = """
import numpy as np
import matplotlib.pyplot as plt

def plot_gershgorin_circles(A):
    n = A.shape[0]
    plt.figure(figsize=(8, 8))
    ax = plt.gca()

    eigenvalues = np.linalg.eigvals(A)

    for i in range(n):
        center = A[i, i]
        radius = np.sum(np.abs(A[i, :])) - np.abs(center)

        circle = plt.Circle((center.real, center.imag), radius, color='gray', fill=False, linestyle='-', linewidth=2)
        ax.add_patch(circle)
        plt.plot(center.real, center.imag, 'ro') # Центры кругов

    plt.plot(eigenvalues.real, eigenvalues.imag, 'b*', markersize=10, label='Собственные значения')

    plt.title('Круги Гершгорина')
    plt.xlabel('Реальная часть')
    plt.ylabel('Мнимая часть')
    plt.grid(True)
    plt.axis('equal')
    plt.legend()
    # plt.show()

# Пример матрицы
A = np.array([[10, -1, 0, 1],
              [0.2, 8, 0.2, 0.2],
              [1, 1, 2, 1],
              [-1, -1, -1, -4]])

# plot_gershgorin_circles(A)
print("Код для построения кругов Гершгорина:")
    """
    pyperclip.copy(answer + "\n\n" + code)
    pyperclip.paste()


def t36():
    """
    Ответ на вопрос:
    - Теорема Шура.
    """

    answer = """
    **Теорема Шура** (или разложение Шура) является одним из центральных результатов в вычислительной линейной алгебре. Она утверждает, что для любой квадратной матрицы `A` с комплексными элементами существует унитарная матрица `U` (такая что `U*U = I`) и верхнетреугольная матрица `T` такие, что:
    `A = UTU*` или `U*AU = T`

    Ключевые моменты этой теоремы:
    1.  **Существование**: В отличие от диагонализации, разложение Шура существует для *любой* квадратной матрицы.
    2.  **Собственные значения**: Диагональные элементы верхнетреугольной матрицы `T` являются собственными значениями исходной матрицы `A`.
    3.  **Численная устойчивость**: Использование унитарных матриц в преобразованиях делает алгоритмы, основанные на разложении Шура (например, QR-алгоритм), численно устойчивыми.

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

    ---
    *Информация для ответа взята со слайдов 294, 295.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t37():
    """
    Ответ на вопрос:
    - Нормальные матрицы, унитарно диагонализуемые матрицы, унитарные матрицы, эрмитовы матрицы.
    """

    answer = """
    Эти термины описывают важные классы матриц со специальными свойствами, которые упрощают задачу нахождения собственных значений.

    - **Эрмитова матрица**: Квадратная матрица `A`, которая равна своему эрмитово-сопряжённому (транспонированному и комплексно-сопряжённому) аналогу: `A = A*`. Для вещественных матриц это эквивалентно понятию **симметричной матрицы** (`A = Aᵀ`). Эрмитовы матрицы всегда имеют вещественные собственные значения.

    - **Унитарная матрица**: Квадратная матрица `U`, для которой обратная матрица совпадает с её эрмитово-сопряжённой: `U*U = UU* = I`. Для вещественных матриц это **ортогональная матрица** (`UᵀU = I`). Модули всех собственных значений унитарной матрицы равны 1.

    - **Нормальная матрица**: Это наиболее общий класс. Матрица `A` называется нормальной, если она коммутирует со своей эрмитово-сопряжённой: `AA* = A*A`. Эрмитовы и унитарные матрицы являются частными случаями нормальных матриц.

    - **Унитарно диагонализуемая матрица**: Матрица `A`, которую можно представить в виде `A = UΛU*`, где `U` — унитарная, а `Λ` — диагональная. **Ключевая теорема гласит, что матрица унитарно диагонализуема тогда и только тогда, когда она нормальна.** Это означает, что для любой нормальной матрицы существует ортонормированный базис из её собственных векторов.

    ---
    *Информация для ответа взята со слайдов 250, 262, 296.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t38():
    """
    Ответ на вопрос:
    - Верхне-гессенбергова форма матрицы.
    """

    answer = """
    **Верхне-гессенбергова форма** — это специальная структура квадратной матрицы, близкая к треугольной. Матрица `H` находится в верхне-гессенберговой форме, если все её элементы ниже первой поддиагонали равны нулю. То есть, `hij = 0` для всех `i > j + 1`.

    Визуально это выглядит так:
    `H = [[*, *, *, *], [*, *, *, *], [0, *, *, *], [0, 0, *, *]]`

    Эта форма является важным промежуточным шагом во многих алгоритмах вычисления собственных значений, в частности, в QR-алгоритме. Приведение произвольной матрицы к гессенберговой форме (что всегда возможно с помощью унитарных преобразований) значительно снижает вычислительную сложность последующих итераций QR-алгоритма с O(n³) до O(n²) за шаг. Для симметричных матриц гессенбергова форма становится трёхдиагональной, что ещё больше упрощает вычисления.

    ---
    *Информация для ответа взята со слайдов 311, 312.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t39():
    """
    Ответ на вопрос:
    - Приведение произвольной матрицы к верхне-гессенберговой форме
    """

    answer = """
    Приведение произвольной квадратной матрицы `A` к верхне-гессенберговой форме `H` является первым шагом в современных QR-алгоритмах для нахождения собственных значений. Цель этого преобразования — "обнулить" элементы под первой поддиагональю. Это достигается с помощью последовательности унитарных преобразований подобия, которые сохраняют собственные значения исходной матрицы.

    Наиболее распространённым методом для этого является использование **отражений Хаусхолдера**. Процесс происходит пошагово, для каждого столбца `j` от 1 до `n-2`:
    1.  Для j-го столбца строится матрица отражения Хаусхолдера `Hj`, которая обнуляет все элементы под первой поддиагональю (с `j+2` по `n`).
    2.  К исходной матрице применяется преобразование подобия: `A := Hj A Hj*`.

    Это преобразование не "портит" нули, полученные на предыдущих шагах. В отличие от приведения к форме Шура, здесь на каждом шаге обнуляются не все поддиагональные элементы, а только те, что лежат ниже первой поддиагонали. Итоговая сложность этого приведения составляет O(n³) операций.

    ---
    *Информация для ответа взята со слайдов 311, 312, 313, 318-319.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t40():
    """
    Ответ на вопрос:
    - Отношение Релея.
    """

    answer = """
    **Отношение Релея** (или частное Релея) — это скалярная величина, определяемая для эрмитовой (или симметричной) матрицы `A` и ненулевого вектора `x` по формуле:
    `RA(x) = (x*Ax) / (x*x)`

    Это отношение имеет несколько важных свойств:
    1.  Его значение всегда лежит между минимальным и максимальным собственными значениями матрицы `A`: `λ_min ≤ RA(x) ≤ λ_max`.
    2.  Оно достигает своих экстремальных значений (минимума и максимума) тогда и только тогда, когда `x` является собственным вектором, соответствующим `λ_min` или `λ_max`.

    Благодаря этим свойствам, отношение Релея является мощным инструментом в вариационных методах поиска экстремальных собственных значений. Оно позволяет свести задачу нахождения собственных значений к задаче оптимизации — поиску максимума или минимума функции `RA(x)`. Также оно используется в **итерации Релея** для ускорения сходимости алгоритмов поиска собственных значений.

    ---
    *Информация для ответа взята со слайда 297.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t41():
    """
    Ответ на вопрос:
    - Зазор между собственными значениями в матрице, алгоритмы со сдвигами.
    """

    answer = """
    **Зазор (gap)** между собственными значениями — это разница между их модулями, например, `|λi| - |λj|`. Величина зазора напрямую влияет на скорость сходимости итерационных методов, таких как степенной метод или QR-алгоритм. Большой зазор между доминантным собственным значением и остальными ускоряет сходимость, в то время как маленький зазор (близкие по модулю собственные значения) её замедляет.

    **Алгоритмы со сдвигами** — это техника для искусственного увеличения зазора и, как следствие, ускорения сходимости. Идея состоит в том, чтобы на каждой итерации QR-алгоритма применять его не к матрице `Ak`, а к сдвинутой матрице `Ak - sk*I`, где `sk` — это "сдвиг", некоторое число, близкое к искомому собственному значению.

    Формула итерации выглядит так:
    1. `Ak - sk*I = Qk*Rk` (QR-разложение сдвинутой матрицы)
    2. `A_{k+1} = Rk*Qk + sk*I` (обратный сдвиг)

    Если сдвиг `sk` выбрать удачно (например, используя **смещение Уилкинсона**), то одно из собственных значений сдвинутой матрицы становится очень малым. Это создает большой относительный зазор, что приводит к очень быстрой (квадратичной или даже кубической) сходимости к одному из собственных значений матрицы `A`.

    ---
    *Информация для ответа взята со слайдов 304, 307, 308, 310.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


def t42():
    """
    Ответ на вопрос:
    - Отражения Хаусхолдера.
    """

    answer = """
    **Отражение Хаусхолдера** (или преобразование Хаусхолдера) — это линейное ортогональное преобразование, которое "отражает" вектор относительно гиперплоскости, перпендикулярной некоторому вектору `v` (называемому вектором Хаусхолдера). Матрица этого преобразования `H` задается формулой:
    `H = I - 2 * (v vᵀ) / (vᵀv)`

    Ключевые свойства матриц Хаусхолдера:
    1.  **Ортогональность**: `Hᵀ = H⁻¹`, что обеспечивает численную устойчивость преобразований.
    2.  **Симметричность**: `H = Hᵀ`.
    3.  **Инволютивность**: `H² = I`.

    Основное применение отражений Хаусхолдера в вычислительной линейной алгебре — это управляемое "обнуление" выбранных элементов в векторе или столбце матрицы. Путем подбора вектора `v` можно отразить любой вектор `x` так, чтобы его преобразованная версия `Hx` была коллинеарна первому базисному вектору, то есть все её компоненты, кроме первой, стали равны нулю. Это свойство широко используется для приведения матриц к верхне-гессенберговой или бидиагональной форме.

    ---
    *Информация для ответа взята со слайда 313.*
    """

    code = """
import numpy as np
import matplotlib.pyplot as plt

# Исходный вектор
x = np.array([3, 4])

# Вектор, вдоль которого хотим отразить x, чтобы получить ||x||*e1
e1 = np.array([1, 0])
norm_x = np.linalg.norm(x)
sign_x1 = np.sign(x[0]) if x[0] != 0 else 1

# Вектор Хаусхолдера
v = x + sign_x1 * norm_x * e1

# Матрица Хаусхолдера
H = np.eye(2) - 2 * np.outer(v, v) / np.dot(v, v)

# Отраженный вектор
Hx = H @ x

print(f"Исходный вектор x: {x}")
print(f"Вектор Хаусхолдера v: {v}")
print(f"Матрица Хаусхолдера H: \n{H}")
print(f"Отраженный вектор Hx: {Hx}") # Должен быть [-5, 0] или [5,0]

plt.figure(figsize=(7, 7))
plt.quiver(0, 0, x[0], x[1], angles='xy', scale_units='xy', scale=1, color='b', label='Исходный вектор x')
plt.quiver(0, 0, Hx[0], Hx[1], angles='xy', scale_units='xy', scale=1, color='r', label='Отраженный вектор Hx')

# Гиперплоскость (в 2D это линия, перпендикулярная v)
line_x = np.linspace(-5, 5, 100)
line_y = (-v[0] / v[1]) * line_x
plt.plot(line_x, line_y, 'k--', label='Гиперплоскость отражения')

plt.xlim(-6, 6)
plt.ylim(-6, 6)
plt.axhline(0, color='gray',linewidth=0.5)
plt.axvline(0, color='gray',linewidth=0.5)
plt.grid(True)
plt.legend()
plt.axis('equal')
plt.title('Отражение Хаусхолдера')
# plt.show()
print("\\nКод для визуализации отражения Хаусхолдера:")
    """
    pyperclip.copy(answer + "\n\n" + code)
    pyperclip.paste()

def t43():
    """
    Ответ на вопрос:
    - Сходимость и сложность QR алгоритма.
    """

    answer = """
    **Сложность** базового QR-алгоритма для плотной матрицы `A` размером n×n складывается из двух этапов. Первый этап — приведение матрицы к верхне-гессенберговой форме, что требует O(n³) операций. Второй этап — итерационный процесс. Каждая QR-итерация для гессенберговой матрицы требует O(n²) операций. Поскольку для нахождения всех `n` собственных значений требуется O(n) итераций, общая сложность этого этапа также составляет O(n³). Таким образом, полная сложность QR-алгоритма для плотной несимметричной матрицы оценивается как O(n³). Для симметричных матриц сложность меньше, так как гессенбергова форма является трёхдиагональной.

    **Сходимость** QR-алгоритма линейная, но её можно значительно ускорить с помощью техники **сдвигов**. Сходимость зависит от **зазоров** между собственными значениями: чем они дальше друг от друга, тем быстрее сходимость. Использование сдвигов (например, смещения Уилкинсона) позволяет искусственно увеличить эти зазоры, достигая квадратичной или даже кубической сходимости, что делает QR-алгоритм очень эффективным на практике.

    ---
    *Информация для ответа взята со слайдов 300, 302, 307, 315.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t44():
    """
    Ответ на вопрос:
    - Метод главных компонент и поиск сингулярных значений, прикладные аспекты.
    """

    answer = """
    **Метод главных компонент (PCA)** — это статистический метод для снижения размерности данных при сохранении максимального объема информации (вариации). Он преобразует набор исходных, возможно, коррелирующих признаков в новый набор линейно независимых признаков, называемых **главными компонентами**. Первая главная компонента соответствует направлению наибольшей изменчивости данных, вторая — направлению наибольшей изменчивости в ортогональном подпространстве, и так далее.

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

    PCA тесно связан с **сингулярным разложением (SVD)**. Применение SVD к центрированной матрице данных `X = UΣVᵀ` напрямую дает главные компоненты (столбцы `V` — это направления, а `UΣ` — проекции данных на эти направления).

    **Прикладные аспекты**:
    - **Сжатие изображений**: Отбрасывая компоненты с малыми сингулярными числами, можно значительно уменьшить объем данных для хранения изображения с минимальной потерей качества.
    - **Визуализация многомерных данных**: Проецирование данных на первые 2-3 главные компоненты позволяет визуализировать их структуру.
    - **Ускорение машинного обучения**: Работа с данными меньшей размерности снижает вычислительные затраты и помогает бороться с "проклятием размерности".
    - **Шумоподавление**: Компоненты с малыми сингулярными значениями часто соответствуют шуму, и их удаление позволяет "очистить" данные.

    ---
    *Информация для ответа взята со слайдов 277-286, 288, 337.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t45():
    """
    Ответ на вопрос:
    - Сингулярное разложение (SVD).
    """

    answer = """
    **Сингулярное разложение (SVD)** — это одно из важнейших разложений матрицы в линейной алгебре, существующее для *любой* прямоугольной или квадратной матрицы `A`. Оно представляет матрицу `A` в виде произведения трех матриц:
    `A = UΣVᵀ` (или `UΣV*` для комплексных матриц)

    Здесь:
    - `U` — ортогональная (или унитарная) матрица, столбцы которой называются **левыми сингулярными векторами**. Они являются собственными векторами матрицы `AAᵀ`.
    - `Σ` (Сигма) — диагональная матрица того же размера, что и `A`. На её диагонали расположены неотрицательные числа `σi`, называемые **сингулярными числами**. Они являются квадратными корнями из собственных значений `AᵀA`.
    - `V` — ортогональная (или унитарная) матрица, столбцы которой называются **правыми сингулярными векторами**. Они являются собственными векторами `AᵀA`.

    SVD предоставляет глубокое понимание структуры матрицы, включая ее ранг, норму и четыре фундаментальных подпространства. Оно имеет широчайшее применение в статистике (метод главных компонент), обработке сигналов, сжатии данных и решении систем линейных уравнений.

    ---
    *Информация для ответа взята со слайдов 235, 314, 332-340.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t46():
    """
    Ответ на вопрос:
    - Плотные и разреженные матрицы, способы хранения разреженных матриц.
    """

    answer = """
    Тема "Способы хранения разреженных матриц" напрямую не рассматривается в предоставленном материале, но различие между типами матриц подразумевается.

    **Плотная матрица (Dense Matrix)** — это матрица, у которой большинство элементов не равны нулю. Для хранения такой матрицы размера m×n требуется память для всех `m*n` элементов. Большинство стандартных алгоритмов линейной алгебры, таких как в библиотеке LAPACK, разработаны именно для плотных матриц.

    **Разреженная матрица (Sparse Matrix)** — это матрица, у которой подавляющее большинство элементов равны нулю. Хранить такую матрицу как плотную (со всеми нулями) крайне неэффективно с точки зрения памяти и вычислений.

    **Способы хранения разреженных матриц** нацелены на хранение только ненулевых элементов и их позиций. Основные форматы:
    - **Coordinate (COO)**: Хранятся три массива — значения ненулевых элементов, их строковые индексы и их столбцовые индексы.
    - **Compressed Sparse Row (CSR)**: Хранятся значения, столбцовые индексы и указатели на начало каждой строки в массиве значений. Этот формат эффективен для умножения матрицы на вектор.
    - **Compressed Sparse Column (CSC)**: Аналогичен CSR, но сжатие идет по столбцам.

    Использование специализированных форматов и алгоритмов для разреженных матриц позволяет решать задачи огромной размерности, которые были бы невозможны при использовании методов для плотных матриц.

    ---
    *Ответ построен на основе общих знаний и контекста слайдов 235, 237, где упоминаются плотные матрицы.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t47():
    """
    Ответ на вопрос:
    - Обыкновенные дифференциальные уравнения, численное дифференцирование. Типы ОДУ.
    """

    answer = """
    **Обыкновенное дифференциальное уравнение (ОДУ)** — это уравнение, связывающее неизвестную функцию одной переменной `y(x)` с её производными: `F(x, y, y', y'', ..., y^(n)) = 0`. Порядок ОДУ определяется наивысшей производной.

    **Численное дифференцирование** — это процесс приближённого вычисления производной функции, когда она задана дискретно (в виде набора точек) или её аналитическое выражение слишком сложно. Вместо точной производной вычисляется её аппроксимация, например, через конечные разности.

    **Типы ОДУ** классифицируются по нескольким признакам:
    1.  **По порядку**: Первого порядка (содержит `y'`), второго порядка (содержит `y''`) и т.д.
    2.  **По линейности**: **Линейные**, если неизвестная функция и её производные входят в уравнение линейно. В противном случае — **нелинейные**.
    3.  **По типу зависимости от независимой переменной**: **Автономные**, если независимая переменная `x` не входит в уравнение в явном виде (например, `y' = y²`). В противном случае — **неавтономные** (например, `y' = x*y`).

    ---
    *Информация для ответа взята со слайдов 364, 368, 371.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t48():
    """
    Ответ на вопрос:
    - Метод прямой разности, метод обратной разности, метод центральной разности.
    """

    answer = """
    Это три основных способа аппроксимации первой производной функции `f'(x)` в точке `xi` с использованием значений в узлах дискретной сетки.

    1.  **Прямая разность (или правая разность)** использует значение в текущей и следующей точке. Она имеет первый порядок точности O(h).
        `f'(xi) ≈ (f(x_{i+1}) - f(xi)) / h`

    2.  **Обратная разность (или левая разность)** использует значение в текущей и предыдущей точке. Она также имеет первый порядок точности O(h).
        `f'(xi) ≈ (f(xi) - f(x_{i-1})) / h`

    3.  **Центральная разность** использует значения в симметричных точках вокруг `xi`. Этот метод является более точным и имеет второй порядок точности O(h²), так как ошибки, связанные с кривизной функции, взаимно компенсируются.
        `f'(xi) ≈ (f(x_{i+1}) - f(x_{i-1})) / (2*h)`

    Выбор метода зависит от задачи: центральная разность предпочтительнее по точности, но требует знания функции с обеих сторон от точки, что не всегда возможно, например, на границе интервала.

    ---
    *Информация для ответа взята со слайдов 378-381.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t49():
    """
    Ответ на вопрос:
    - Локальная и глобальная ошибки, правило Симпсона, ошибка сокращения и ошибка округления, накопление ошибок.
    """

    answer = """
    Эта группа понятий описывает различные источники и типы погрешностей в численных методах.

    - **Локальная ошибка** (или ошибка усечения/аппроксимации) — это погрешность, вносимая на *одном* шаге алгоритма из-за замены точной математической операции (например, производной) её дискретным аналогом (конечной разностью).

    - **Глобальная ошибка** — это итоговая, накопленная ошибка в конце вычислений. Она является результатом суммирования и распространения локальных ошибок на всех шагах. Порядок глобальной ошибки обычно на единицу ниже порядка локальной.

    - **Ошибка сокращения (truncation error)** — синоним локальной ошибки, возникает из-за "усечения" бесконечного ряда (например, ряда Тейлора) до конечного числа членов.

    - **Ошибка округления (round-off error)** возникает из-за ограниченной точности представления чисел в компьютере.

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

    - **Правило Симпсона** — это метод численного интегрирования, а не дифференцирования. Оно аппроксимирует функцию на отрезке параболой и имеет более высокий порядок точности (O(h⁴)), чем методы трапеций или прямоугольников.

    ---
    *Информация для ответа взята со слайдов 14, 15, 401.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t50():
    """
    Ответ на вопрос:
    - Сетка дифференцирования.
    """

    answer = """
    **Сетка** (или сеточная область) — это фундаментальное понятие в численных методах, которое позволяет перейти от непрерывной задачи к дискретной. Она представляет собой набор дискретных точек (узлов) `{x_i}`, в которых ищется приближенное решение.

    Основные характеристики сетки:
    - **Узлы**: Точки `x_i`, где вычисляются значения функции.
    - **Шаг сетки (h)**: Расстояние между соседними узлами.

    Типы сеток:
    1.  **Равномерная сетка**: Шаг `h` постоянен на всем интервале: `h_i = h = const`. Это наиболее простой и часто используемый тип сетки.
    2.  **Неравномерная сетка**: Шаг `h_i = x_{i+1} - x_i` может меняться. Такие сетки используются, когда поведение функции сильно различается на разных участках: сетку делают более густой там, где функция быстро меняется, и более редкой, где она меняется плавно.

    Выбор сетки — это компромисс между точностью (требуется мелкий шаг `h`) и вычислительными затратами (мелкий шаг увеличивает количество узлов и операций).

    ---
    *Информация для ответа взята со слайдов 371, 372, 374.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t51():
    """
    Ответ на вопрос:
    - Фазовые портреты, особые точки.
    """

    answer = """
    **Фазовый портрет** — это графический метод анализа систем дифференциальных уравнений, который позволяет визуализировать их поведение без нахождения явного решения. Он представляет собой набор **траекторий** в **фазовом пространстве**, где осями являются зависимые переменные системы (например, `x` и `y` для системы из двух ОДУ). Каждая траектория показывает, как состояние системы эволюционирует во времени из определенного начального условия.

    **Особые точки** (или точки равновесия) — это ключевые элементы фазового портрета. В этих точках производные всех переменных равны нулю (`x' = 0`, `y' = 0`), то есть система находится в состоянии покоя. Тип особой точки определяет локальное поведение траекторий вокруг нее:
    - **Узел**: Траектории либо сходятся к точке, либо расходятся от нее.
    - **Фокус**: Траектории закручиваются в спираль, сходясь к точке или расходясь.
    - **Седло**: Траектории приближаются к точке по одним направлениям и удаляются по другим.
    - **Центр**: Траектории образуют замкнутые кривые вокруг точки.

    Анализ фазового портрета и его особых точек позволяет определить устойчивость системы, наличие колебательных режимов и ее общее динамическое поведение.

    ---
    *Информация для ответа взята со слайдов 404, 405, 419-421.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t52():
    """
    Ответ на вопрос:
    - Неявные и явные методы численного дифференцирования.
    """

    answer = """
    При численном решении ОДУ вида `y' = f(x, y)` методы делятся на явные и неявные в зависимости от того, как вычисляется значение `y_{i+1}` на следующем шаге.

    **Явные методы** вычисляют `y_{i+1}` напрямую, используя только уже известные значения с предыдущих шагов (`y_i`, `y_{i-1}` и т.д.). Классический пример — **явный метод Эйлера**:
    `y_{i+1} = y_i + h * f(x_i, y_i)`
    Они просты в реализации, но часто имеют строгие ограничения на величину шага `h` для обеспечения устойчивости.

    **Неявные методы** для вычисления `y_{i+1}` используют уравнение, в которое входит само это неизвестное значение. Пример — **неявный метод Эйлера**:
    `y_{i+1} = y_i + h * f(x_{i+1}, y_{i+1})`
    На каждом шаге для нахождения `y_{i+1}` требуется решать (часто нелинейное) уравнение. Это делает их вычислительно более сложными, но они, как правило, обладают лучшими свойствами устойчивости и позволяют использовать значительно больший шаг `h`, что особенно важно для решения **жёстких систем ОДУ**.

    ---
    *Информация для ответа взята со слайдов 396, 397, 517, 525, 537.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t53():
    """
    Ответ на вопрос:
    - Многошаговые методы решения обыкновенных дифференциальных уравнений.
    """

    answer = """
    **Многошаговые методы**, в отличие от одношаговых (как Эйлер или Рунге-Кутта), используют для вычисления значения `y_{i+1}` информацию из нескольких предыдущих точек (`y_i`, `y_{i-1}`, `y_{i-2}` и т.д.). Идея этих методов заключается в аппроксимации интеграла от правой части ОДУ `f(x, y)` на отрезке `[x_i, x_{i+1}]` с помощью интерполяционного полинома, построенного по предыдущим значениям.

    Основное преимущество многошаговых методов — их вычислительная эффективность. После "разгона" (вычисления первых нескольких точек одношаговым методом) они требуют всего одно вычисление функции `f(x,y)` на шаге, в то время как, например, метод Рунге-Кутты 4-го порядка требует четыре.

    Многошаговые методы делятся на:
    - **Явные** (например, методы Адамса-Башфорта), где формула для `y_{i+1}` не содержит этого значения.
    - **Неявные** (например, методы Адамса-Мультона), которые более устойчивы, но требуют решения уравнения на каждом шаге.
    На практике часто используют их комбинацию в виде методов **предиктор-корректор**.

    ---
    *Информация для ответа взята со слайдов 533-542.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t54():
    """
    Ответ на вопрос:
    - Использование адаптивного шага.
    """

    answer = """
    Использование **адаптивного шага** — это продвинутая техника в численных методах решения ОДУ, позволяющая автоматически изменять размер шага `h` в процессе вычислений. Цель состоит в том, чтобы поддерживать локальную ошибку на заданном уровне, обеспечивая баланс между точностью и эффективностью.

    Основная идея:
    1.  На каждом шаге делается оценка локальной погрешности. Часто это делается путем сравнения результатов, полученных методами разного порядка точности, или путем расчета по одной и той же схеме с шагом `h` и двумя шагами `h/2` (метод Рунге).
    2.  Если оцененная ошибка больше заданной, шаг `h` уменьшается, и вычисление повторяется.
    3.  Если ошибка значительно меньше заданной, шаг `h` можно увеличить на следующей итерации для экономии вычислительных ресурсов.

    Адаптивный шаг особенно эффективен для уравнений, где решение меняется с разной скоростью: шаг уменьшается на участках с быстрым изменением функции и увеличивается на участках, где она меняется плавно. Одношаговые методы, такие как методы Рунге-Кутты, хорошо подходят для реализации с адаптивным шагом.

    ---
    *Информация для ответа взята со слайдов 532, 543-545.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t55():
    """
    Ответ на вопрос:
    - Понятия согласованности, устойчивости, сходимости алгоритмов.
    """

    answer = """
    Это три фундаментальных понятия, характеризующих качество численного метода для решения дифференциальных уравнений.

    1.  **Согласованность (Consistency)**: Метод называется согласованным, если его разностная схема правильно аппроксимирует исходное дифференциальное уравнение. Формально это означает, что локальная ошибка аппроксимации (невязка) стремится к нулю, когда шаг сетки `h` стремится к нулю.

    2.  **Устойчивость (Stability)**: Метод устойчив, если небольшие возмущения (например, ошибки округления или неточности в начальных данных) не приводят к неконтролируемому росту ошибок в решении. Устойчивость гарантирует, что ошибки остаются ограниченными в процессе вычислений.

    3.  **Сходимость (Convergence)**: Метод сходится, если его численное решение стремится к точному решению дифференциального уравнения при уменьшении шага сетки `h`.

    Эти три понятия связаны **Основной теоремой вычислительной математики (теоремой Лакса-Рябенького-Филипова)**, которая гласит: **Согласованность + Устойчивость = Сходимость**. То есть, для того чтобы численный метод давал правильный результат, он должен быть и согласованным, и устойчивым.

    ---
    *Информация для ответа взята со слайдов 387-392, 505, 511, 546-552.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t56():
    """
    Ответ на вопрос:
    - Строгая и нестрогая (слабая) устойчивость.
    """

    answer = """
    Понятия строгой и нестрогой устойчивости характеризуют поведение ошибок в численном методе на длительных интервалах времени. Они связаны с поведением собственных значений `λi` оператора перехода `R_h`.

    **Строгая устойчивость** требуется для моделирования долгосрочных процессов или при неопределённом количестве шагов. Метод строго устойчив, если ошибки гарантированно затухают со временем. Это соответствует условию, что модуль всех собственных значений оператора перехода строго меньше единицы: `|λi| < 1`. В этом случае любая ошибка со временем экспоненциально уменьшается.

    **Нестрогая (слабая) устойчивость** допустима для задач с конечным и известным числом шагов. Она требует, чтобы ошибки оставались ограниченными, но не обязательно затухали. Условием является `|λi| ≤ 1`, при этом для собственных значений, лежащих на единичной окружности (`|λi| = 1`), должны быть ограничены соответствующие корневые векторы. Это означает, что ошибка может расти, но не быстрее, чем полиномиально (например, линейно), что приемлемо на коротких интервалах.

    ---
    *Информация для ответа взята со слайдов 506, 509, 514, 515, 518.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t57():
    """
    Ответ на вопрос:
    - Детерминированный хаос, бифуркация, странные аттракторы.
    """

    answer = """
    Эти понятия описывают сложное, непредсказуемое поведение в детерминированных динамических системах.

    **Детерминированный хаос** — это поведение системы, которое, несмотря на полное отсутствие случайных факторов (система детерминирована), выглядит случайным и непредсказуемым. Ключевой его особенностью является **чувствительность к начальным условиям** ("эффект бабочки"): малейшие изменения в начальном состоянии приводят к экспоненциально расходящимся траекториям в будущем.

    **Бифуркация** — это качественное изменение поведения системы (например, смена устойчивого состояния на колебательное) при плавном изменении ее управляющего параметра. Точка, в которой происходит такое изменение, называется точкой бифуркации. Каскад бифуркаций удвоения периода является одним из классических сценариев перехода системы к хаосу.

    **Странный аттрактор** — это притягивающее множество в фазовом пространстве, к которому стремятся траектории хаотической системы. В отличие от простых аттракторов (точка, предельный цикл), странный аттрактор имеет фрактальную структуру. Траектория на странном аттракторе никогда не повторяется и не пересекает саму себя, бесконечно блуждая по этому сложному множеству. Примерами являются аттрактор Лоренца и аттрактор Рёсслера.

    ---
    *Информация для ответа взята со слайдов 562-582.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t58():
    """
    Ответ на вопрос:
    - Амплитуда, период, частота, длин волны, дискретизация, частота дискретизации, герц, угловая частота, фаза сигнала.
    """

    answer = """
    Эти термины являются основными характеристиками колебательных процессов и сигналов.

    - **Амплитуда**: Максимальное отклонение колеблющейся величины от положения равновесия.
    - **Период (T)**: Время одного полного колебания.
    - **Длина волны (λ)**: Пространственное расстояние, через которое повторяется форма волны.
    - **Частота (f)**: Число колебаний в единицу времени. Измеряется в **Герцах (Гц)**, где 1 Гц = 1 колебание в секунду. Связана с периодом как `f = 1/T`.
    - **Угловая (круговая) частота (ω)**: Скорость изменения фазы. Связана с обычной частотой как `ω = 2πf`. Измеряется в радианах в секунду.
    - **Фаза (φ)**: Определяет начальное состояние колебания в момент времени t=0. Показывает, на какую долю периода смещено колебание относительно начала отсчета.
    - **Дискретизация**: Процесс представления непрерывного сигнала в виде последовательности его значений (отсчетов), взятых в дискретные моменты времени.
    - **Частота дискретизации (Fs)**: Число отсчетов, взятых в секунду. Для корректного представления сигнала она должна быть как минимум в два раза больше максимальной частоты в спектре сигнала (теорема Котельникова).

    ---
    *Информация для ответа взята со слайдов 584, 586, 590, 601, 602, 659.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t59():
    """
    Ответ на вопрос:
    - Амплитудный спектр и частотный спектр.
    """

    answer = """
    **Частотный спектр** (или просто спектр) сигнала — это представление сигнала в частотной области, полученное с помощью преобразования Фурье. Он показывает, из каких гармонических колебаний (синусоид и косинусоид) и с какими параметрами состоит исходный сигнал. Для комплексного представления ряда Фурье `S(t) = Σ Ck * exp(j*ωk*t)`, спектр — это набор комплексных коэффициентов `Ck`.

    Поскольку коэффициенты `Ck` являются комплексными числами, их удобнее представлять в виде двух вещественных спектров:
    1.  **Амплитудный спектр**: Это зависимость амплитуд `|Ck|` гармоник от их частот `ωk`. Он показывает "силу" или "вклад" каждой частотной составляющей в общий сигнал.
    2.  **Фазовый спектр**: Это зависимость фаз `arg(Ck)` гармоник от их частот. Он показывает начальные сдвиги каждой гармоники относительно начала отсчета.

    Вместе амплитудный и фазовый спектры полностью описывают сигнал в частотной области и позволяют восстановить исходный сигнал без потерь информации. Анализ этих спектров является основой обработки сигналов.

    ---
    *Информация для ответа взята со слайдов 588, 601, 602.*
    """

    code = """
import numpy as np
import matplotlib.pyplot as plt

# Параметры сигнала
fs = 1000  # Частота дискретизации
t = np.linspace(0, 1, fs, endpoint=False)
# Сигнал = 10 Гц + 50 Гц
signal = np.sin(2 * np.pi * 10 * t) + 0.5 * np.sin(2 * np.pi * 50 * t + np.pi/4)

# Преобразование Фурье
fft_signal = np.fft.fft(signal)
freqs = np.fft.fftfreq(len(signal), 1/fs)

# Берем только положительную часть спектра
n = len(freqs)
positive_freqs = freqs[:n//2]
amplitude_spectrum = (2/n) * np.abs(fft_signal[:n//2])
phase_spectrum = np.angle(fft_signal[:n//2])

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 7))
fig.suptitle('Амплитудный и фазовый спектры')

# Амплитудный спектр
ax1.plot(positive_freqs, amplitude_spectrum)
ax1.set_title('Амплитудный спектр')
ax1.set_xlabel('Частота (Гц)')
ax1.set_ylabel('Амплитуда')
ax1.grid(True)
ax1.set_xlim(0, 100)

# Фазовый спектр
ax2.plot(positive_freqs, phase_spectrum)
ax2.set_title('Фазовый спектр')
ax2.set_xlabel('Частота (Гц)')
ax2.set_ylabel('Фаза (радианы)')
ax2.grid(True)
ax2.set_xlim(0, 100)

plt.tight_layout(rect=[0, 0.03, 1, 0.95])
# plt.show()
print("Код для построения амплитудного и фазового спектров:")
    """
    pyperclip.copy(answer + "\n\n" + code)
    pyperclip.paste()

def t60():
    """
    Ответ на вопрос:
    - Фильтрация сигналов.
    """

    answer = """
    **Фильтрация сигналов** — это процесс обработки сигнала с целью удаления или ослабления нежелательных компонентов (например, шума) и/или выделения полезных. В частотной области эта задача решается особенно эффективно.

    Процедура цифровой фильтрации с помощью преобразования Фурье состоит из трех основных шагов:
    1.  **Прямое преобразование Фурье**: Исходный зашумленный сигнал переводится из временной области в частотную, то есть вычисляется его спектр.
    2.  **Обработка спектра**: В полученном спектре "обнуляются" или ослабляются те частотные составляющие (гармоники), которые соответствуют шуму. Для этого спектр сигнала умножается на частотную характеристику фильтра (например, фильтра нижних частот, который пропускает низкие частоты и подавляет высокие).
    3.  **Обратное преобразование Фурье**: Модифицированный спектр преобразуется обратно во временную область. В результате получается отфильтрованный сигнал.

    Этот метод эффективен, когда полезный сигнал и шум занимают разные частотные диапазоны.

    ---
    *Информация для ответа взята со слайдов 603, 604, 605, 641.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t61():
    """
    Ответ на вопрос:
    - Применение преобразований Фурье с целью анализа сезонности во временных рядах.
    """

    answer = """
    Преобразование Фурье является мощным инструментом для анализа и удаления **сезонности** во временных рядах (например, в данных о потреблении электроэнергии). Сезонность проявляется в виде периодических колебаний (суточных, недельных, годовых), которые в частотном спектре соответствуют ярко выраженным пикам на определенных частотах.

    Процесс анализа и удаления сезонности ("десезонирования") включает следующие шаги:
    1.  **Вычисление спектра**: С помощью быстрого преобразования Фурье (БПФ) вычисляется спектр временного ряда.
    2.  **Идентификация сезонных частот**: На графике амплитудного спектра находятся пики, соответствующие периодическим компонентам (например, пик на частоте 1/24 часа для суточного цикла).
    3.  **Фильтрация**: Амплитуды этих сезонных гармоник в спектре обнуляются или значительно уменьшаются.
    4.  **Обратное преобразование Фурье**: Выполняется обратное БПФ отфильтрованного спектра. В результате получается "десезонированный" временной ряд, который показывает основной тренд и случайные колебания без периодических наслоений.

    Этот подход позволяет выделить и проанализировать долгосрочные тренды, которые были скрыты за сезонными колебаниями.

    ---
    *Информация для ответа взята из постановки домашнего задания #9 на слайдах 664-666.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()

def t62():
    """
    Ответ на вопрос:
    - Алгоритмическая сложность, сходимость всех методов, которые могут встретиться в задачах.
    """

    answer = """
    Оценка сложности и сходимости является ключевой для выбора подходящего численного метода.

    **1. Решение нелинейных уравнений:**
    - **Метод половинного деления**: Гарантированно сходится (линейная сходимость, `p=1`), но медленно. Сложность шага O(1).
    - **Метод простой итерации**: Линейная сходимость (`p=1`), сходится при `|φ'(x)| < 1`. Сложность шага O(1).
    - **Метод Ньютона**: Очень быстрая квадратичная сходимость (`p=2`). Сходится при достаточно хорошем начальном приближении. Сложность шага O(1), но требует вычисления производной.
    - **Метод секущих (хорд)**: Сверхлинейная сходимость (`p ≈ 1.618`). Не требует производной.

    **2. Интерполяция:**
    - **Полином Лагранжа/Ньютона**: Сложность построения O(n²), вычисления в точке O(n). Неустойчив при большом `n`.
    - **Кубические сплайны**: Сложность построения O(n) (решение трёхдиагональной системы), вычисления O(log n). Устойчив.

    **3. Линейная алгебра:**
    - **Умножение матриц**: Наивный/блочный - O(n³), Штрассена - O(n^2.807).
    - **QR-алгоритм (поиск всех собственных значений)**: Общая сложность O(n³). Сходимость быстрая (квадратичная со сдвигами).
    - **Степенной метод (поиск одного λ_max)**: Сложность шага O(n²) для плотных матриц. Сходимость линейная, зависит от зазора `|λ₂/λ₁|`.

    **4. Решение ОДУ:**
    - **Метод Эйлера**: Первый порядок точности (сходимости), `p=1`. Сложность шага O(1). Условно устойчив.
    - **Метод Рунге-Кутты 4-го порядка**: Четвёртый порядок точности, `p=4`. Сложность шага O(1), но требует 4 вычисления правой части. Более устойчив, чем метод Эйлера.

    **5. Преобразование Фурье:**
    - **Дискретное преобразование Фурье (ДПФ)**: Сложность O(N²).
    - **Быстрое преобразование Фурье (БПФ)**: Значительно эффективнее, сложность O(N log N).

    ---
    *Ответ является обобщением информации со всех слайдов презентации.*
    """
    pyperclip.copy(answer)
    pyperclip.paste()


