Как найти корень уравнения, если формулы не помогают
ОП.10 Численные методы · Специальность 09.02.07 «Информационные системы и программирование»
Лекция 2 · 2 академических часа (90 минут)
Сегодня мы перейдём от теории погрешностей к настоящей «боевой» задаче — поиску корней уравнений. Вы узнаете, как компьютер находит решения там, где бессильна школьная алгебра, и напишете свои первые численные алгоритмы на Python.
В школе мы решали уравнения по формулам: квадратное — через дискриминант, линейное — переносом слагаемых. Но в реальной разработке 90% уравнений не имеют аналитического решения.
Главная мысль: умение численно решать уравнения — это базовый навык любого разработчика, работающего с математикой, физикой или оптимизацией.
Что мы разберём за одну пару:
Рассмотрим уравнение вида:
f(x) = 0
где f(x) — заданная функция, а x — неизвестная величина, которую нужно найти.
Корень уравнения — это такое значение x, при котором функция f(x) обращается в ноль. Геометрически — это точка пересечения графика функции с осью Ox (абсцисс — горизонтальной осью).
Уравнение x² − 4 = 0 имеет два корня: x = 2 и x = −2. На графике параболы y = x² − 4 это точки, где кривая пересекает горизонтальную ось.
Уравнение может иметь один корень, несколько корней, бесконечно много корней или не иметь корней вовсе. Наша задача — найти все действительные корни (или те, что нас интересуют).
Все уравнения делятся на два больших класса. Понимание этой разницы — ключ к выбору метода решения.
Функция f(x) — алгебраическая: содержит только сложение, вычитание, умножение, деление, возведение в степень.
Примеры:
x² − 5x + 6 = 0 (квадратное)x³ − 2x + 1 = 0 (кубическое)2x⁵ + x − 7 = 0 (пятой степени)Функция f(x) содержит тригонометрические, показательные, логарифмические или другие «неалгебраические» операции.
Примеры:
sin(x) = x/2e^x = 3xln(x) + x² = 5cos(x) − x = 0Важно: алгебраические уравнения степени 5 и выше не имеют общей формулы корней (теорема Абеля — Абеля Руффини, доказана в 1824 году). Трансцендентные уравнения почти всегда решаются только численно.
«Аналитически» — значит по формуле, точно, без приближений. Как x = (−b ± √D) / (2a) для квадратного уравнения.
cos(x) = x не имеют точной формулы решения.Выход: вместо точного решения мы ищем приближённое значение корня с заданной точностью (например, до 6 знаков после запятой). Это и есть численный метод.
Пример: уравнение cos(x) = x имеет корень x ≈ 0.7390851332. Точной формулы нет, но компьютер найдёт это значение за миллисекунды.
Численное решение любого уравнения всегда состоит из двух последовательных этапов. Пропустить один из них — нельзя.
Находим промежутки, на каждом из которых лежит ровно один корень.
Результат: «на отрезке [a, b] есть один корень».
Методы:
Вычисляем значение корня с заданной точностью ε (эпсилон).
Результат: «корень равен x ≈ 0.7391 с точностью до 0.0001».
Методы:
Аналогия из жизни: сначала вы ищете дом на улице (локализация — «дом где-то между №10 и №20»), а потом подходите к нужной двери (уточнение — «это дом №15»). Без первого этапа второй невозможен.
Самый наглядный способ — построить график функции y = f(x) и посмотреть, где он пересекает ось Ox.
x (например, от −5 до 5).f(x) в нескольких точках (с шагом 0.5 или 0.1).matplotlib).import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-5, 5, 400)
y = np.cos(x) - x # наше уравнение cos(x) = x
plt.axhline(0, color='black', linewidth=1)
plt.plot(x, y, label='f(x) = cos(x) - x')
plt.grid(True)
plt.legend()
plt.title('Графическая локализация корня')
plt.show()
На графике видно: кривая пересекает ось один раз, примерно в точке x ≈ 0.7. Значит, корень один, и он находится на отрезке [0, 1].
Если графика под рукой нет, используем математику. В основе — теорема о промежуточном значении (теорема Больцано — Коши).
Теорема: если функция f(x) непрерывна на отрезке [a, b] и на концах отрезка принимает значения разных знаков (то есть f(a) · f(b) < 0), то внутри отрезка существует хотя бы один корень.
[x₀, x₁] и шаг h.f(x₀), f(x₁), f(x₂), … с шагом h.Уравнение x³ − x − 1 = 0. Считаем:
f(1) = 1 − 1 − 1 = −1 (отрицательно)f(2) = 8 − 2 − 1 = 5 (положительно)Знак изменился → на отрезке [1, 2] есть корень.
Осторожно: если шаг h слишком большой, можно «проскочить» два корня, которые находятся близко. Поэтому шаг выбирают достаточно мелким.
Теперь, когда мы знаем, что на отрезке [a, b] есть ровно один корень, нужно его уточнить. Самый простой и надёжный метод — метод половинного деления (он же метод бисекции, от лат. bisectio — рассечение надвое).
Делим отрезок пополам, выбираем ту половину, где функция меняет знак, и повторяем. И так до тех пор, пока отрезок не станет достаточно малым.
[a, b] и точность ε.c = (a + b) / 2.f(c).f(c) = 0 (или |f(c)| < ε) — корень найден: x = c.f(a) · f(c) < 0 — корень в левой половине: b = c.a = c.|b − a| < ε.Преимущество: метод всегда сходится (если корень есть). Недостаток: работает медленно по сравнению с другими методами.
Визуально алгоритм выглядит так:
На каждом шаге длина отрезка уменьшается ровно в 2 раза. Через n шагов длина станет (b − a) / 2ⁿ. Чтобы получить точность ε = 10⁻⁶ на отрезке [0, 1], нужно около 20 шагов.
Метод бисекции имеет важную особенность — мы можем точно предсказать, сколько итераций (шагов) потребуется.
n ≥ log₂((b − a) / ε)
где n — число шагов, (b − a) — длина исходного отрезка, ε — требуемая точность.
Отрезок [1, 2] (длина 1), точность ε = 0.001.
n ≥ log₂(1 / 0.001) = log₂(1000) ≈ 9.97 → нужно 10 шагов
| Точность ε | Число шагов n |
|---|---|
| 10⁻² | 7 |
| 10⁻³ | 10 |
| 10⁻⁶ | 20 |
| 10⁻⁹ | 30 |
Видно: чтобы добавить ещё один верный знак после запятой, нужно примерно 3–4 дополнительных шага. Это плата за надёжность метода.
Решим уравнение cos(x) − x = 0 на отрезке [0, 1] с точностью ε = 10⁻⁶.
def f(x):
"""Наша функция: cos(x) - x"""
import math
return math.cos(x) - x
def bisection(a, b, epsilon):
"""Метод половинного деления"""
step = 0
while abs(b - a) > epsilon:
c = (a + b) / 2
step += 1
print(f"Шаг {step}: a={a:.6f}, b={b:.6f}, c={c:.6f}, f(c)={f(c):.6f}")
if f(c) == 0:
return c
elif f(a) * f(c) < 0:
b = c # корень в левой половине
else:
a = c # корень в правой половине
return (a + b) / 2
# Запуск
root = bisection(0, 1, 1e-6)
print(f"\nКорень найден: x ≈ {root:.8f}")
print(f"Проверка: f(x) = {f(root):.2e}")
Шаг 1: a=0.000000, b=1.000000, c=0.500000, f(c)=0.377583
Шаг 2: a=0.500000, b=1.000000, c=0.750000, f(c)=-0.018311
...
Шаг 20: a=0.739085, b=0.739086, c=0.739085, f(c)=0.000000
Корень найден: x ≈ 0.73908513
Проверка: f(x) = 2.22e-16
Второй по популярности метод — метод простой итерации. Он быстрее бисекции, но требует аккуратной подготовки уравнения.
Преобразуем уравнение f(x) = 0 к виду:
x = φ(x)
где φ(x) — некоторая функция, полученная из f(x). Затем строим итерационный процесс:
xₙ₊₁ = φ(xₙ)
Начинаем с некоторого x₀ (приближения) и последовательно вычисляем x₁, x₂, x₃, … до сходимости.
Уравнение x³ − x − 1 = 0. Преобразуем:
x³ = x + 1 → x = ∛(x + 1)
Теперь φ(x) = ∛(x + 1). Берём x₀ = 1 и считаем:
x₁ = ∛(1 + 1) = ∛2 ≈ 1.2599x₂ = ∛(1.2599 + 1) = ∛2.2599 ≈ 1.3122x₃ = ∛(1.3122 + 1) ≈ 1.3227Метод итерации не всегда сходится. Если преобразовать уравнение неудачно, процесс может «разбегаться» в бесконечность.
Теорема о сходимости (принцип сжимающих отображений): метод итерации xₙ₊₁ = φ(xₙ) сходится, если на отрезке локализации выполняется условие:
|φ'(x)| < 1
где φ'(x) — производная функции φ(x). Проще говоря, функция φ должна быть «пологой», а не крутой.
φ'(x).[a, b].|φ'(x)| < 1 на всём отрезке — метод сойдётся.|φ'(x)| ≥ 1 — нужно преобразовать уравнение по-другому.Уравнение x³ − x − 1 = 0. Преобразуем неудачно: x = x³ − 1. Тогда φ'(x) = 3x². При x ≈ 1.3 получаем φ'(x) ≈ 5.07 > 1 — метод разойдётся!
Правило: чем меньше |φ'(x)|, тем быстрее сходится метод. Идеально, когда φ'(x) ≈ 0 в окрестности корня.
Решим то же уравнение x³ − x − 1 = 0 методом простой итерации.
def phi(x):
"""Преобразованная функция: x = ∛(x + 1)"""
return (x + 1) ** (1/3)
def simple_iteration(x0, epsilon, max_iter=100):
"""Метод простой итерации"""
x = x0
for n in range(max_iter):
x_new = phi(x)
print(f"Итерация {n+1}: x = {x_new:.8f}")
if abs(x_new - x) < epsilon:
return x_new
x = x_new
print("Метод не сошёлся за заданное число итераций!")
return None
# Запуск
root = simple_iteration(1.0, 1e-6)
print(f"\nКорень найден: x ≈ {root:.8f}")
Итерация 1: x = 1.25992105
Итерация 2: x = 1.31229384
Итерация 3: x = 1.32271116
...
Итерация 28: x = 1.32471796
Итерация 29: x = 1.32471796
Корень найден: x ≈ 1.32471796
Метод сошёлся за 29 итераций. Для сравнения: метод бисекции на том же отрезке потребовал бы около 20 шагов, но итерации здесь быстрее — каждое вычисление проще.
Какой метод выбрать? Зависит от задачи. Давайте сравним два рассмотренных.
| Критерий | Метод бисекции | Метод итераций |
|---|---|---|
| Надёжность | ✅ Всегда сходится | ⚠️ Только если |φ'(x)| < 1 |
| Скорость сходимости | Линейная (медленно) | Линейная (быстрее) |
| Подготовка | Простая: нужен только отрезок | Сложнее: нужно преобразовать уравнение |
| Требования к функции | Непрерывность | Непрерывность + существование φ'(x) |
| Применение | Надёжный старт, «грубая» локализация | Быстрое уточнение, если есть хорошая φ(x) |
Практический совет: часто методы комбинируют. Сначала бисекция делает несколько шагов для надёжного приближения, а потом итерация «добивает» точность. Такой подход используется, например, в стандартной функции scipy.optimize.root_scalar в Python.
Скорость сходимости — это то, как быстро метод приближается к точному корню. Измеряется порядком метода p.
Если текущая ошибка 10⁻²:
10⁻³ (добавили 1 знак).10⁻⁴ (удвоили число знаков).Именно поэтому метод Ньютона (метод касательных) так популярен в научных вычислениях — он находит корень за 5–7 итераций с точностью до 15 знаков!
В реальных проектах не пишут свои методы с нуля — используют готовые библиотеки. В Python это scipy.optimize.
from scipy.optimize import root_scalar
import math
def f(x):
return math.cos(x) - x
# Метод бисекции
result = root_scalar(f, bracket=[0, 1], method='bisect', xtol=1e-8)
print(f"Бисекция: x = {result.root:.10f}, итераций: {result.iterations}")
# Метод Brent (комбинация бисекции, секущих и обратной квадратичной интерполяции)
result = root_scalar(f, bracket=[0, 1], method='brentq', xtol=1e-8)
print(f"Brent: x = {result.root:.10f}, итераций: {result.iterations}")
Бисекция: x = 0.7390851332, итераций: 27
Brent: x = 0.7390851332, итераций: 6
Метод Brent (Брента) — это «золотой стандарт» в научных вычислениях. Он сочетает надёжность бисекции и скорость других методов. Именно его использует функция root_scalar по умолчанию.
Но чтобы грамотно использовать библиотеки, нужно понимать, как работают базовые методы. Иначе вы не сможете выбрать правильный инструмент для задачи.
Рассмотрим реальную задачу из игровой разработки. Игрок бросает мяч с начальной скоростью v₀ = 20 м/с под углом α = 45°. Нужно найти, когда мяч упадёт на землю.
Уравнение движения по вертикали (с учётом сопротивления воздуха, пропорционального скорости):
y(t) = v₀·sin(α)·t − (g/2)·t² − k·t³ = 0
где g = 9.81 м/с² — ускорение свободного падения, k = 0.05 — коэффициент сопротивления.
Это кубическое уравнение относительно t. Аналитически решить можно, но сложно. Проще — численно.
import math
def y(t):
v0, alpha, g, k = 20, math.radians(45), 9.81, 0.05
return v0 * math.sin(alpha) * t - (g/2) * t**2 - k * t**3
# Локализация: проверяем значения
for t in [0, 1, 2, 3, 4]:
print(f"t={t}: y={y(t):.3f}")
# Видно: y(2) > 0, y(3) < 0 → корень на [2, 3]
# Уточняем бисекцией:
# ... (код из слайда 13)
Результат: мяч упадёт примерно через t ≈ 2.73 секунды. Именно так работают физические движки игр.
На следующей паре мы проведём Практическую работу №2 по теме «Решение алгебраических и трансцендентных уравнений методом половинного деления и методом итераций» (2 академических часа).
Что нужно подготовить:
numpy и matplotlib).f(x) = 0 к виду x = φ(x)?После изучения этой темы и выполнения практической работы вы должны:
scipy.optimize.Понимать: почему численные методы — это единственный способ решить большинство реальных уравнений, и как грамотно их применять в программных проектах.
x³ − 2x − 5 = 0 методом бисекции на отрезке [2, 3] с точностью ε = 0.1. Сделать 4 шага.cos(x) − x = 0. Вывести число итераций и время работы каждого метода (используйте модуль time).matplotlib.Подсказка для задания 3:
import time
start = time.time()
# ... ваш код ...
end = time.time()
print(f"Время работы: {end - start:.6f} секунд")
На практических работах часто встречаются одни и те же промахи. Избегайте их!
| Ошибка | Почему это плохо | Как правильно |
|---|---|---|
| Пропуск этапа локализации | Метод может сойтись не к тому корню или разойтись | Всегда сначала стройте график или делайте перебор |
| Слишком большой шаг при переборе | Можно «проскочить» два близких корня | Используйте шаг 0.1 или меньше |
Неправильное преобразование к x = φ(x) |
Метод итераций разойдётся | Проверяйте условие |φ'(x)| < 1 |
Сравнение float через == |
Условие никогда не сработает (см. лекцию 1) | Используйте abs(a - b) < epsilon |
| Бесконечный цикл | Программа «зависает» | Всегда ставьте ограничение на число итераций |
Золотое правило: любой итерационный процесс должен иметь «аварийный выход» — ограничение по числу шагов. Иначе одна ошибка в коде может привести к зависанию программы.
Сегодня мы сделали огромный шаг вперёд — перешли от теории погрешностей к реальным численным алгоритмам.
|φ'(x)| < 1).scipy.optimize.root_scalar.Главная мысль: численные методы — это не «костыли» вместо точной математики, а мощный инструмент, без которого не работает современная наука, инженерия и IT.
На следующей лекции — метод хорд и метод касательных (Ньютона).
Это самые быстрые методы уточнения корней!
Метод Ньютона даёт квадратичную сходимость — число верных знаков удваивается на каждом шаге. Именно его используют в научных вычислениях, машинном обучении и финансовых моделях.
Спасибо за внимание!
До встречи на практической работе №2!
💡 Подсказка: используйте клавиши ← → на клавиатуре для переключения слайдов