Предположим, требуется найти все вещественные корни полинома:

P(x) = a_0 + a_1 x + a_2 x^2 + \dots + a_n x^n.

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

  • КРИТЕРИЙ 1: Алгоритм должен быть абсолютно надежен, тоесть для любых коеффициентов, являющихся вещественными числами, все корни полинома гарантированно должны быть найдены. Исключение могут составлять случаи, когда корни настолько велики, что значение полинома в них не может быть вычислено по тем или иным причинам.

  • КРИТЕРИЙ 2: Количество операций (арифметических и прочих), требуемое для нахождения всех корней полинома с заданной точностью, как функция от коеффициентов, ограничена сверху. Другими словами, ситуация, в которой с приближением вектора коеффициентов к некоторой точке в \mathbb{R}^n, время работы алгоритма неограничено растет, недопустима. В зависимости от численного метода, такими точками может быть, например, множество всех точек, в которых полином имеет кратные корни (такое подмножество \mathbb{R}^nназывается дискриминантной поверхностью), или точки, в которых один или несколько старших коеффициентов становятся равны нулю (с приближением к таким точкам некоторые корни устремляются в бесконечность).

  • КРИТЕРИЙ 3: Алгоритм должен быть вычислительно устойчив. Переход от вещественных чисел к числам с ограниченной точностью не должен нарушать принципы работы алгоритма. Если в результаты работы алгоритма и вносятся некоторые погрешности (при таком переходе), то они должны уменьшаться с увеличением точности представления чисел.

  • КРИТЕРИЙ 4: Асимптотическая сложность алгоритма, как функция от степени полинома, количества итераций, и размера чисел, в которых производятся вычисления, должна быть полиномиальной (или лучше) по любой из переменных.

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

Индекс точки

Основой метода, который предлагается в этой статье, является некоторая целочисленная ступенчатая функция \mathcal{I}(x) от вещественного x, соответствующая рассматриваемому полиному. Будем в дальнейшем называть ее "индекс точки".

Приведем принцип ее вычисления. Для этого введем последовательность:

\mathcal{H} = \{ P(x), P'(x), P''(x), \dots, P^{(n)}(x) \}.

Тогда функция \mathcal{I}(x) в точке x равна числу перемен знака в ряду значений элементов \mathcal{H}, взятых в точке x. Если какие либо значения равны при этом нулю, они удаляются из списка перед подсчетом числа перемен знака.

Индекс точки обладает совокупностью свойств, ключевых в поиске корней полинома:

  • В точке корня полинома кратности k индекс точки убывает на k единиц.

  • В точке корня кратности k производной порядка m индекс точки убывает на четное число единиц, близжайшее кратности k.

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

  • Количество единиц, на которое (в сумме) убывает индекс точки на всей числовой прямой в точности равняется степени полинома. Другими словами, индекс точки, монотонно убывая, изменяется от n до нуля.

  • В точках корней полинома и его производных индекс точки доопределен так, что интервалы постоянства его значения открыты вправо.

Рассмотрим поведение индекса точки на примере полинома

2x^5 -3x^4 + 2x^3 - 2x^2 + 1
масштаб для значения индекса равен 1/5
масштаб для значения индекса равен 1/5

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

Доказательство (корень полинома)

Докажем, что в точке s кратного корня полинома индекс точки убывает на величину k кратности этого корня. В этом случае справедливо представление:

P(x) = (x-s)^k A(x), \quad A(s) \neq 0.

Где A(x) полином, очевидно не равный нулю в точке s, иначе кратность была бы выше по крайней мере на единицу. Найдем производную:

P'(x) = (x-s)^{k-1}B(x), \\ \qquad \qquad \qquad B(x) = kA(x)+(x-s)A'(x).

В текущем контексте полином B(x) можно заменить на полином A(x). Справедливо это по той причине, что в точке s, а значит и в некоторой ее окрестности эти полиномы не равны нулю и при этом совпадают по знаку. Значит, и перемена знака между P(x) и P'(x) при такой замене останется прежней в некоторой окрестности корня s.

После такой замены между P(x) и P'(x) имеется общий множитель, равный:

(x-s)^{k-1} A(x).

Очевидно, что в перемене знака между двумя числами, имеющими общий множитель, его можно сократить без изменения такой перемены знака. Сократив P(x) и P'(x) на указанный множитель, имеем перемену знака между x\!-\!s и 1. Ясно, что такая перемена всегда убывает (пропадает) в точке s.

Нетрудно заметить, что все те же размышления можно распространить на первые kперемен знака. Остальные перемены знака можно не рассматривать, так как они будут затрагивать элементы \mathcal{H}, могущие относиться к другим корням соответствующих кратностей производных соответствующих порядков.

Доказательство (корень производной)

Теперь докажем, что в точке s корня кратности k производной порядка m индекс точки убывает на четное число, близшайшее величине кратности корня.

По доказанному ранее, имеем, что последовательно k перемен знака, начиная с перемены под номером m\!+\!1, всегда пропадают в точке s. Остается выяснить, как себя ведет перемена знака под номером m.

Здесь мы заметим, что элемент \mathcal{H}, предшествующий производной порядка m, в отличие от нее не равен нулю в точке s, иначе опять же кратность k была бы выше по крайней мере на единицу. Это значит, что в некоторой окрестности точки корня предшествующий элемент по знаку идентичен ненулевой константе.

Рассмотрим случай, когда k четно. Нетрудно показать, что в этом случае m-ная производная не изменит знак своего значения в точке корня, тоесть идентична по знаку константе в некоторой выколотой окрестности корня. Следовательно, и соответствующая перемена знака между двумя знаковыми константами не изменится в точке s.

Рассмотрим случай, когда k нечетно. Нетрудно показать, что в этом случае m-ная производная всегда меняет знак своего значения в точке корня. Следовательно, соответствующая перемена знака всегда либо пропадает, либо появляется.

Таким образом, в первом случае индекс точки убудет ровно на k единиц, в то время как во втором на k\!+\!1 или на k\!-\!1единиц, тоесть на одно из близжайших k четных чисел.

Доказательство (значение в корнях)

Индекс точки определен не только в промежутках между корнями элементов \mathcal{H}, но и в этих точках непосредственно: достаточно из последовательности значений этих элементов извлечь нули, сохранив порядок оставшихся чисел. Докажем, что интервалы постоянства индекса точки открыты вправо, тоесть имеют следующий вид:

(-\infty,s_1), [s1,s2), [s2,s3),\dots,[s_j,\infty).

В точке корня P(x) кратности k первыеk элементов \mathcal{H} равны нулю, значит, первые k перемен знака попросту не будут рассматриваться, так как соответствующие нули будут извлечены. В то же время, в правом (относительно точки корня) интервале постоянства индекса те же k перемен знака пропадают (убывают). Следовательно, значение индекса в точке корня полинома доопределяется его правым значением (его значением в правом интервале постоянства).

В точке корня кратности k производной P(x) порядка m соответствующие k перемен знака также выпадают из рассмотрения. Зато перемена знака между производными m\!-\!1 и m порядков заменяется на перемену знака между производными m\!-\!1 и m\!+\!k порядков. Остается показать, что правое значение производной m-ого порядка и значение (в точке корня) m\!+\!k-той производной совпадают по знаку. Действительно, дифференцируя k раз представление

получаем, что это верно, так как (x-s)^k для x, бОльших s положителен, а A_{k}(x) все совпадают по знаку в некоторой окрестности точки s.

Доказательство (суммарное изменение)

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

Пусть произвольный полином S(x) и его производная S'(x) имеют вид:

S(x) = (x-a_1)(x-a_2)\dots(x-a_k)A(x), \\ S'(x) = (x-b_1)(x-b_2)\dots(x-b_m)B(x).

Здесь a_{i} и b_{i} все корни этих полиномов. Тогда полиномы A(x) и B(x)не имеют корней, значит, сохраняют знак на всей вещественной прямой, а так как оба, очевидно, имеют старший коеффициент одного и того же знака, всюду совпадают по знаку. Поэтому можем сначала заменить B(x) на A(x) (без изменения перемены знака между S(x) и S'(x)), а затем сократить A(x) как общий ненулевой множитель (так же без изменения соответствующей перемены знака).

По итогу имеем равнозначную перемену знака между следующими полиномами:

U(x) = (x-a_1)(x-a_2)\dots(x-a_k), \\ V(x) = (x-b_1)(x-b_2)\dots(x-b_m).

Тогда для любого x, большего всех a_{i} и b_{i} одновременно, оба этих полинома положительны. Значит, двигаясь вправо, начиная с определенного x, перемена знака между полиномом и его производной равна нулю.

Теперь возьмем x, меньший всех a_{i} и b_i одновременно. В этом случае все множители, входящие в U(x) и V(x) отрицательны. Воспользуемся тем, что количество вещественных корней полинома совпадает по четности с его степенью (это понятно исходя из того, что корни появляются парами, и если степень нечетная, то всегда есть по крайней мере один корень). Тогда выходит, что количества k и m (отрицательных) множителей не совпадают по четности, из чего следует, что двигаясь влево, начиная с определенного x, перемена знака между полиномом и его производной равна единице.

Так как для полинома степени n индекс точки состоит из n перемен знака, имеем, что индекс точки полинома действительно убывает от n до нуля.

Прочие теоретические выводы

Все элементы последовательности \mathcal{H} непрерывные функции, значит знак своего значения они могут изменить только в точках корней. Но так как любая перемена знака изменяется при изменении знака одного из входящих в нее чисел, имеем, что индекс точки, как сумма перемен знака, не может изменяться нигде кроме точек корней элементов \mathcal{H}, тем самым он действительно если и изменяется, то только убывает.

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

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

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

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

Алгоритм поиска корней №1

Пусть дан начальный интервал вида (a,b], границами которого могут быть как конкретные вещественные числа, так и условные бесконечности соответствующих знаков. Нужно найти все корни полинома, содержащиеся в таком интервале.

Очевидно, что для произвольного интервала (u,v] разность \Delta\mathcal{I} = \mathcal{I}(u) \! - \! \mathcal{I}(v) равна сумме количеств вещественных и побочных корней, содержащихся в этом интервале.

Далее по тексту величина \mathrm{mid}\{x,\!y\}обозначает в каком-то смысле середину интервала (x,y]. Для простоты вначале ее можно считать равной среднему арифметическому x и y. Величина \mathrm{eps}\{x,\!y\}обозначает в каком-то смысле ширину интервала. Ее можно считать равной y\!-\!x.

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

\mathrm{mid}\{a,\!b\} = \sqrt{ab}, \qquad \mathrm{eps}\{a,\!b\} = \frac{b}{a}.

Вычислим значение индекса точки на границах начального интервала. Поделим затем этот интервал надвое точкой m, равной \mathrm{mid}\{a,\!b\}. Вычислив значение индекса и в этой точке, будем знать количества корней (вещественных плюс побочных) в соответствующих интервалах(a,m]и(m,b]. Каждый из них затем можно также разбить надвое, продолжая процесс рекурсивного разбиения интервалов далее.

Если для очередного интервала, подлежащего разбиению, величина \Delta \mathcal{I} становится равной нулю, то интервал отбрасывается, как не имеющий никаких корней.

Из того, что для очередного интервала (u,v], подлежащего разбиению, величина \mathrm{eps}\{u,\!v\}становится меньше некоторого заранее заданного \varepsilon, следует, что процесс рекурсивного деления требуется прекратить. Далее такой интервал считается как потенциально содержащий вещественные корни.

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

Поступим таким образом. Если на границах интервала (u,v] значение полинома имеет противоположные знаки, или что то же самое, если разность \mathcal{I}(u) \! - \! \mathcal{I}(v)нечетна, то считается, что в интервале есть по крайней мере один вещественный корень, значение которого принимается равным \mathrm{mid}\{u,\!v\}.

По окончании процесса рекурсивного деления считается, что все корни найдены.

Замечу, что из того, что величина \Delta \mathcal{I} для очередного интервала равна единице, следует, что в этом интервале содержится единственный вещественный корень, который может быть выгоднее приблизить обычным методом бисекции.

Алгоритм поиска корней №2

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

Пронумеруем ступени следующим образом: ступень под номером i находится в точке, где индекс точки изменяет значение с i на i\!-\!1. Соответственно номер ступени это целое число от 1 до \deg \{P\}включительно.

Пусть ступень под номером j ищется на интервале (a,b], гарантированно содержащем искомую ступень, где a и b могут быть как конкретными числами, так и условными бесконечностями соответствующих знаков. На каждой итерации интервал делится надвое точкой m, равной \mathrm{mid}\{a,\!b\}, в которой вычисляется индекс точки.

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

Поэтому в первом случае a заменяется на m, а во втором b заменяется на m. В результате интервал, содержащий искомую ступень становится (m,b] или [a,m) соответственно. Замечу, что такой принцип определения направления возможен только благодаря доказанному свойству монотонности убывания индекса точки.

Как и ранее, итерационный процесс прекращается, если \mathrm{eps}\{a,\!b\}становится меньше заранее заданного \varepsilon. Далее ступень считается относящейся к вещественному корню полинома, если на границах интервала значение полинома имеет разные знаки, или если разность значений индекса точки на границах интервала нечетна.

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

Частичный индекс точки

Пусть для инервала (u,v] вычислены величины \mathcal{I}(u) и \mathcal{I}(v). Тем самым, на границах этого интервала также известны значения полинома и всех его производных. Значит, имеем возможность (без дополнительных арифметических вычислений) определить на границах интервала индекс точки для любой производной полинома. В этом случае индекс точки производной порядка m будем называть частичным индексом порядка m.

Пусть значение частичного индекса порядка m совпадает на границах рассматриваемого интервала и равно \mathcal{I}^{\{m\}}_{uv}. Исходя из того, что индекс точки если и изменяется, то только убывает, имеем, что частичный индекс порядка m равен \mathcal{I}^{\{m\}}_{ab}в любой точке \mathrm{mid}\{u,\!v\} интервала (u,v]. Поэтому для определения индекса в любой такой точке останется вычислить только первые mпроизводных. Для минимизации вычислительных затрат очевидно взять m максимально малым из подходящих.

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

Вырождение полинома

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

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

Считается, что при любых значениях внешних переменных "реальная" степень полинома не может превышать фиктивной.

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

Ограничение точности вычислений

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

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

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

На практике повсеместно используется формат представления вещественных чисел с ограниченной точностью, называемый "floating point", или ФПЗ (формат чисел с плавающей запятой). В таком формате (со значительными упрощениями) двоичное число \mathcal{X} определенной ширины взаимно-однозначно и монотонно отображается в вещественное число  \tilde{x}(\mathcal{X}), так, что максимальная относительная ошибка (называемая ошибкой округления), возникающая в результате такого преобразования, не превышает \varepsilon.

Тем самым, совокупность всех значений (от 0 до 2^n\!\!-\!1) двоичного числа \mathcal{X} битовой ширины n под действием такого отображения превращается в сетку  \tilde{x} \in \mathbb{R}, расстояние между узлами которой пропорционально их величине (с коефф. 2 \varepsilon).

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

Рассмотрим реальный пример: дан полином с корнями x\!=\!1,2,3,4,5вида:

x^5-15x^4+85x^3-225x^2+274x-120.

Требуется вычислить значение (прежде всего знак значения) полинома в очень малой окрестности третьего корня (x\!=\!3), используя ФПЗ единичной точности (32 бита). Замечу, что коеффициенты этого полинома целые числа допустимой величины, могущие быть точно представлены в указанном формате. Имеем картину:

Здесь по горизонтальной оси двоичное число \mathcal{X}, увеличивающееся на единицу с каждой точкой на изображении. По вертикальной же оси вычисляемое (по схеме Горнера) значение \mathrm{sgn}(P( \tilde{x}(\mathcal{X}))). Синяя черта соответствует \mathcal{X}, для которого \tilde{x}\!=\!3.

Видим, что знак значения меняется отнюдь не раз в окрестности выбранного корня, более того, оказывается полином имеет в этой окрестности аж восемь "корней". Для справки, ширина окрестности колебания знака на изображении составляет \approx 0.000047.

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

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

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

Из этого прямо следует, что в некоторой окрестости (назовем ее переходной зоной) каждого корня любого из элементов \mathcal{H}, значение индекса точки, вычисляемое по схеме Горнера, может не соответствовать действительности:

Покажем, что если в точке s несколько элементов \mathcal{H} имеют корни, такие, что в этой точке индекс уменьшается с i_1на i_2, то вычисляемое по схеме Горнера значение индекса точки (в переходной зоне точки s) заключено между i_1 и i_2 включительно.

Рассмотрим корень в точке s полинома P(x)кратности k. В точке такого корня k перемен знака, относящихся к нему, убывают. Значит, вычисляя эти перемены знака по схеме Горнера, их сумма должна быть ограничена (в переходной зоне точки s) между 0 и k включительно. Действительно, какие k чисел мы не выбрали бы, сумма kперемен знака (в ряду этих чисел, в конце которого добавлено ненулевое число) не может быть меньше 0 или быть больше k.

Рассмотрим теперь корень sкратности k производной порядка m. В этом случае из k\!+\!1перемен знака k перемен убывают в точке корня всегда. Оставшаяся перемена всегда меняет значение в любом направлении (k нечетно) или не изменяется (k четно).

q_\uparrow, z_1, z_2, \dots, z_k, q_\downarrow.

В изображенном ряду z_iзначения производных, которые (в переходной зоне точки s) могут иметь произвольный знак (в том числе быть равными нулю). Значения q_\uparrow и q_\downarrowобозначают граничащие с указанными производными значения элементов \mathcal{H}. Таковые могут иметь произвольный знак (кроме нуля), но сохраняют его в переходной зоне. Обозначим за \mathcal{V}_kсумму перемен знака в этом ряду чисел.

Приведу несколько фактов, доказательство которых элементарно:

  • Если q_\uparrow и q_\downarrow совпадают по знаку, а k четно, то \mathcal{V}_k \in [0,k].

  • Если q_\uparrow и q_\downarrow совпадают по знаку, а k нечетно, то \mathcal{V}_k \in [0,k\!+\!1].

  • Если q_\uparrow и q_\downarrow отличаются по знаку, а k четно, то \mathcal{V}_k \in [1,k\!+\!1].

  • Если q_\uparrow и q_\downarrow отличаются по знаку, а k нечетно, то \mathcal{V}_k \in [1,k].

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

Вопрос о численной устойчивости изложенных здесь алгоритмов возникает, когда интервал (a,b] требуется поделить надвое точкой m\!=\! \mathrm{mid}\{a,\!b\}, дабы затем вычислить в ней индекс точки. Точнее тогда, когда точка mпопадает в переходную зону.

Рассмотрим на втором алгоритме. Пусть искомая ступень x\!=\!s заведомо содержится в интервале(a,b]. Если точка mоказалась вне переходных зон, то решение о том, в каком из интервалов (a,m] или (m,b] находится искомая ступень, всегда принимается верно.

Если точка m оказалась в переходной зоне ступени x\!=\!r, которая не является искомой, то, по показанному ранее, это никак не помешает выбрать тот подынтервал, который содержит искомую ступень. Действительно, пусть r, как и m, находятся левее s. Тогда, как было показано ранее, вычисляемый по схеме Горнера индекс \mathcal{I}(m) будет ограничен соответствующим образом и по-прежнему всегда будет больше или равен номера ступени s. Ровно также можно поступить, если r, как и m, находятся правее s.

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

С первым алгоритмом размышления аналогичны.

Почему не теорема Штурма ?

Хорошо известно, что еще в 1829 году Штурм привел совокупность свойств, которыми должны обладать элементы \mathcal{H}, чтобы число перемен знака в ряду их значений, взятых в точке x, равнялось количеству вещественных корней, больших x. Впоследствии он получил также конкретный способ вычисления элементов \mathcal{H}, удовлетворяющих таким свойствам. Фактически он нашел способ вычислить \mathcal{I}^*(x), ступенчатую функцию, на единицу убывающую в точке каждого простого корня полинома, и нигде более.

Принцип ее вычисления заключается в том, что первые два элемента функциональной последовательности, как и прежде, являются P(x) и P'(x) соответственно, а каждый последующий берется как (умноженный на минус единицу) остаток от Евклидова деления предыдущего полинома на текущий.

Пусть полиномы A(x) и B(x), расположенные последовательно, имеют вид:

A(x) = a x^n + b x^{n-1} + \dots \\ \quad \! \! B(x) = u x^{n-1} + v x^{n-2} + \dots

Тогда, применяя алгоритм Евклида, имеем, что полином C(x), в последовательности следующий за полиномами A(x) и B(x), вычисляется по несложной формуле:

C(x) = \frac{1}{u^2} \bigl[ (au)x+(bu-av) \bigr] B(x) - A(x).

Очевидно, сложность вычисления коеффициентов полинома C(x) пропорциональна степени n, (предполагая, что каждая арифметическая операция производится точно и занимает фиксированное время). Следовательно, вычисление всех полиномов последовательности займет время, пропорциональное n^2.

Теорема Штурма является важным теоретическим результатом алгебры. Однако, несмотря на некоторые преимущества, она не сыскала широкого применения на практике (по крайней мере, применительно к рассматриваемой задаче) ни в 19 веке, ни сейчас.

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

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

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

Сепаратор полинома

Индекс точки \mathcal{I}(x)в текущем определении несовершенен в том смысле, что не все точки, в которых он убывает, соответствуют вещественным корням полинома. Такой недостаток хорошо проявляется, например, в случае, когда требуется найти минимальный корень полинома. Придется найти несколько побочных корней, чтобы "добраться" до первого вещественного корня. В целом с увеличением степени полинома статистически все меньшее количество корней полинома являются вещественными.

Как было сказано ранее, Штурм нашел один из способов вычисления \mathcal{I}^*(x), индекса, который лишен такого недостатка, тоесть если и убывает, то только в точках корней полинома P(x)непосредственно.

Здесь для решения той же проблемы я предлагаю поступить немного иначе:

Cепаратором S(x)полинома P(x)будем называть полином, который, если и имеет корни, то только по одному в каждом промежутке между корнями P(x). Тем самым, корни S(x), чередуясь с корнями P(x), как-бы отделяют корни полинома P(x)друг от друга.

Будем считать, что сепаратор имеет степень на один меньше степени полинома, что возможно, так как количество корней полинома и сепаратора отличаются на единицу. Если зафиксировать корни сепаратора в промежутках между корнями P(x), а также зафиксировать комплексно-сопряженные любым образом, то сепаратор, как полином, по-прежнему имеет одну степень свободы, заключающуюся в своем старшем коеффициенте. Выберем его таким, чтобы он, как и у P'(x), совпадал по знаку со старшим коеффициентом P(x).

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

Видим, что в каждой точке корня P(x)перемена убывает, а в точке корня S(x), снова появляется (на изображении в верхних строках таблиц указан знак полинома в окрестности корня, в нижних указан знак сепаратора).

Допустим, индекс \mathcal{I}^*_S(x) сепаратора S(x)известен. Покажем, что для того, чтобы получить искомый индекс \mathcal{I}^*_P(x)полинома P(x)достаточно прибавить к \mathcal{I}^*_S(x)перемену знака между полиномом P(x)и его сепаратором:

\mathcal{I}^*_{P}(x) = \mathcal{I}^*_{S}(x) + \mathcal{V}(P(x),S(x)).

Действительно, пусть в точке s имеет место корень полинома P(x). Тогда соответствующая перемена знака в такой точке убывает на единицу, когда \mathcal{I}^*_S(x)в той же точке не изменяется. Тем самым, как и ожидается, указанная сумма убывает в точке простого корня полинома P(x)на единицу.

Пусть теперь в точке s имеет место корень сепаратора. Тогда соответствующая перемена знака в такой точке возрастает на единицу, когда \mathcal{I}^*_S(x)в той же точке убывает на ту же единицу. Имеем, что в такой точке s указанная сумма не изменяется, значит, как и ожидалось, такая сумма не изменяется нигде кроме корней P(x).

Таким образом, для вычисления \mathcal{I}^*(x)для полинома P(x)требуется знать \mathcal{I}^*(x)для сепаратора S(x)(обозначим его S_1(x)) . В свою очередь, требуется знать

Пусть полином P(x)пятой степени имеет три корня. Тогда S_1(x)должен иметь два корня, являясь полиномом четвертой степени. Далее S_2(x)должен иметь один корень, являясь полиномом третьей степени. Продолжая, S_3(x)не должен иметь кореней, являясь полиномом второй степени. Но в случае S_4(x), он также не должен иметь корней, вопреки тому, что имеет первую (нечетную!) степень.

Исходя из такого противоречия можно сделать вывод о том, что в отличие от ряда Штурма, всегда имеющего nполиномов, ряд

\mathcal{H} = \{ P(x), S_1(x), S_2(x), \dots, S_k(x) \}

должен иметь полиномов ровно на один больше того, сколько корней уP(x).

Можно заметить, что случай кратных корней у P(x)здесь не был рассмотрен. Определение сепаратора полинома легко расширить, например, так: если два корня совпадают в точке s, то промежутком между ними будет считаться сама точка s(соответственно, если совпадают три корня, возникает два одноточечных промежутка, и т.д). Тем самым, в точке корня P(x)кратности kиндекс \mathcal{I}^*_P(x)убывает на величину кратности k.

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

Поиск на всей числовой прямой

Нередко из определения задачи следует, что корни полиномиального уравнения требуется найти на всей вещественной прямой, а не только на некотором интервале. Существует несколько подходов, позволяющих решить эту проблему.

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

Поступим другим образом. Выполним замену x\!=\!1/t в полиноме P(x). Пересчитав коеффициенты, получим полином P(t). Его коеффициенты оказываются в точности коеффициентами исходного полинома, записанные в обратном порядке. Найдем корни полиномов P(x) и P(t) в ограниченных интервалах x \in (-1,1]и t \in (-1,1]соответственно, которые назовем x_i, и t_jсоответственно. Тогда обратные величины корней t_jпо существу будут равны недостающим корням P(x), тоесть корням, принадлежащим множеству x\in(-\infty,-1] \cup (1,\infty].

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

Можно поступить немного иначе. Выберем легковычислимую замену переменной вида x\!=\!f(t), такую, которая конечный интервал t \in (-1,1] взаимнооднозначно отображает на всю числовую ось. Тоесть f(t) должна быть непрерывной строго монотонно возрастающей функцией. Простейший пример такой замены:

x \leftarrow \frac{t}{1-t^2}, \qquad t \in (-1,1] \ \Rightarrow \ x \in (-\infty, \infty].

Не стоит непосредственно подставлять вместо x указанную формулу и пересчитывать коеффициенты. Это приведет как к излишним вычислениям, так и повысит степень вдвое. Изложенные ранее алгоритмы применяются по переменной t. В процессе работы индекс точки полинома P(t)в точке t_0 \in (-1,1]считается равным индексу полинома P(x), взятому в точке x_0 \in (-\infty, \infty], где x_0\!=\!f(t_0). Имея вычисленными корни t_k, искомые корни x_k получаются по той же формуле.

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

\mathrm{mid}\{a,\!b\} =  \tilde{x} \! \left( \frac{\mathcal{X}(a)+\mathcal{X}(b)}{2} \right), \quad \mathrm{eps}\{a,\!b\} = \mathcal{X}(b)-\mathcal{X}(a).

так как такой формат предусматривает значения для бесконечностей обоих знаков, которые очень упрощенно можно считать 0 для отрицательной бесконечности и 2^n\!-\!1для положительной, где nчисло двоичных разрядов числа \mathcal{X}.

Полиномиальная оценка

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

В первом случае число хранимых коеффициентов растет квадратично со степенью полинома, значит уже степени на 10 никакого регистрового файла (даже с SIMD) для хранения этих коеффициентов не хватит. Значит, будут обращения к кешу первого уровня, каждое из которых занимает 5-7 циклов, в то время как всего за один такт процессоры способны выполнять до четырех двойных инструкций double FMA (умножение с накоплением). Во втором же случае усложнение с дополнительными умножениями способно замедлить алгоритм по крайней мере раза в два.

Существует более выгодный алгоритм вычисления всех производных полинома в одной точке, открытый, по-видимому, также Горнером (или Безу, источник мне теперь не известен) и найденный мной в книге "The art of computer programming" Дональда Кнута:

Для
m от 0 до n\!-\!1 выполнить:
Для
k от n\!-\!1 до m выполнить:
a_k := a_k + x*a_{k+1}.
закончить.
закончить.

В оригинальном изложении алгоритм предназначен для пересчета коеффициентов (по t) полинома p(t\!+\!x)степени n. Тоесть, пусть до выполнения алгоритма переменные a_k(где k степень при t) были коеффициентами полинома p(t), тогда после выполнения такого алгоритма они становятся коеффициентами (по t) полинома p(t\!+\!x)при тех же степенях.

Связь между производными полинома p(t), взятыми в точке x и коеффициентами по t полинома p(t\!+\!x)очень просто установить. Достаточно разложить p(t\!+\!x)по формуле Тейлора в соответствующую сумму:

p(t\!+\!x) = p(x) + p'(x)t + \frac{p''(x)}{2} t^2 + \dots + \frac{p^{(n)}(x)}{n!} t^n.

Таким образом, если до выполнения алгоритма переменные a_kбыли коеффициентами полинома P(t), то после его выполнения:a_k k!\! = \! P^{(k)}(x). Очевидно, что для получения самих производных необязательно a_kдомножать на факториальный коеффициент, так как в текущем контексте нас интересует только знак значения производных.

Разберем алгоритм подробнее на примере полинома:

P(x) = ax^5 + bx^4 + cx^3 + dx^2 + ex + f.

Тогда последовательность операций выглядит следующим образом:

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

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

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

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

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

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

Для m от n\!-\!1 до 0 выполнить:
Для k от m до n\!-\!1 выполнить:
a_k = a_k + x * a_{k+1}.
закончить.
закончить.

Для обоих записей изложенного алгоритма верно, что для вычисления значения полинома и всех его производных потребуется (n^2\!+\!n)/2 операций умножения и столько же операций сложения. Существует способ снизить количество умножений до 2n\!-\!1, который актуален для вычислений с произвольной точностью, где операция умножения, в отличие от операции сложения, занимает квадратичное от размера числа время. Для этого каждая производная порядка m формально домножается на x^m. Теперь возможно внести множители x^mв переменные a_k, тоесть выполнить a_k := a_k x^m. Затем достаточно применить алгоритм Безу к коеффициентам a_kс xравным 1. Так как в текущем контексте нас интересует только знак производных, впоследствии деление a_k на x^k можно заменить умножением на \mathrm{sign}(x^k), которое не требует как таковых вычислений.

Комплексные корни (формальное решение)

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

Тем самым, он нашел способ вычисления \hat{\mathcal{I}}(x), индекса, который отличается от \mathcal{I}(x) тем, что побочные корни заменяются вещественными частями комплексно-сопряженных корней полинома. Для его вычисления достаточно применить (по t) алгоритм Раусса к полиному P(t\!+\!x), вычисление коеффициентов которого, как было замечено ранее, практически эквивалентно вычислению всех производных полинома P(t)в точке x.

Ползуясь алгоритмом №1 или №2, заменив индекс \mathcal{I}(x) на индекс \hat{\mathcal{I}}(x), можно найти все вещественные части корней полинома. Мнимые их части затем ищутся элементарно.

Замечу, что я по-прежнему не рекомендую такой подход (считаю его лишь формальным решением задачи), так как по сути он использует теорему Штурма, не лишенную критических недостатков, однако, на мой взгляд, это может быть лучше, чем из системы уравнений

\mathrm{Re} \{ P(x+yi) \} = 0, \quad \mathrm{Im} \{ P(x+yi) \} = 0.

исключать переменную y, пользуясь теорией исключения (результантами), или тем более применять базисы Гребнера.

Асимптотическая сложность

Определим асимптотическую сложность приведенных здесь алгоритмов. Будем считать, что время работы алгоритма f зависит от n- степени полинома, k- количества итераций (при строго линейной сходимости оно пропорционально количеству верных цифр в результатах работы алгоритмов), и от \omega- количества значащих цифр в числах, с которыми производятся вычисления.

Во втором алгоритме для степени n придется вычислить n ступеней, каждая из которых требует k итераций, а каждая итерация требует (n^2\!+\!n)/2 умножений и сложений. Учтя, что сложность умножения квадратична по \omega, имеем:

f(n,k,\omega) \in O(n^3 k \ \omega^2).

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

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

Заключение

Здесь я не привел сравнение этого метода с другими, достаточно известными методами, также теми, которые применяются в системах компьютерной алгебры (таких как Maxima, Mathematica, Wolfram, и др.). Не сравнил то, как общие и локальные оптимизации влияют на время вычислений. Не рассказал об истории проблемы отделения корней, тоесть о правиле знаков Декарта, о теореме Штурма, теореме Будана-Фурье, и о многом другом.

Если у меня будет время и, что немаловажно, желание читателей, я постараюсь написать вторую часть, в которой, надеюсь, изложу указанное. Надеюсь также, что многие из тех читателей, кто задавался вопросом "как численно и надежно, и за заранее ограниченное время решить такое-то уравнение такой-то степени", теперь имеют на него ответ.

Поправки, предложения и конструктивная критика приветствуются, с учетом того, что это моя первая статья на Хабре.

Комментарии (5)


  1. OBIEESupport
    24.08.2024 22:27
    +3

    Немного царапают глаз опечатки в математических терминах. Мы все-таки пишем "коэффициент" и "Теорема Штурма является важным теоретическим результатом алгебры.", наверное не только алгебры, но и всей науки математики в целом. Поправьте, пожалуйста: "Таким образом, в первом случае индекс точки убудет ровно на k единиц, в то время как во втором на k\!+\!1 или на k\!-\!1единиц, тоесть на одно из близжайших k четных чисел." "Тем самым, корни S(x), чередуясь с корнями P(x), как-бы отделяют корни полинома P(x)друг от друга." "Несмотря на простое определение сепаратора, увы, мне до сих пор не удалось найти разумный способ его построения. Под разумным я понимаю такой, где каждый коеффициент сепаратора является многочленом (возможно, однородным) от коеффициентов исходного полинома." - спорное утверждение, особенно на вещественной прямой с конечной точностью вычисления. Скорее всего, здесь придется использовать какую-либо быстро сходящуюся формулу нахождения корней по схеме Ньютона с численными производными.


  1. amishaa
    24.08.2024 22:27

    По окончании процесса рекурсивного деления считается, что все корни найдены.

    Противоречит требованию

    КРИТЕРИЙ 1: Алгоритм должен быть абсолютно надежен, тоесть для любых коеффициентов, являющихся вещественными числами, все корни полинома гарантированно должны быть найдены. Исключение могут составлять случаи, когда корни настолько велики, что значение полинома в них не может быть вычислено по тем или иным причинам.

    А именно, корни кратности два всегда будут пропущены.


  1. wataru
    24.08.2024 22:27
    +1

    Метод интересный, но слишком переусложненный. Этот ваш индекс весьма интересный объект, и его свойства стоит исследовать. Но есть более простой метод нахождения корней не уступающий вашему.

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

    Еще могут быть корни левее первой и правее последней экстремальной точки. Вы этот вопрос обсуждаете в секции "на всей числовой прямой". Но тут тоже есть более простое решение. Надо посмотреть на знак производной и полинома снаружи от крайней точки. Если они разные - то там есть корень. Потом надо взять интервал длиной, например, 1 снаружи от крайней точки. Потом надо удваивать длину этого интервала, пока полином не поменяет знак на второй границе и потом опять же бинпоиском найти корень. Вот так за O(log (M/eps)) вы найдете сначала интервал, где корень есть, а потом и сам корень. M тут - максимальное значение корня по модулю. Такая же оценка как в вашем алгоритме.


    1. amishaa
      24.08.2024 22:27
      +1

      Технически, метод бинарного поиска последовательно по производным (как и метод, предложенный автором) плохо работает с многочленами вида (x-a)^(2n) + b, где b очень мало по модулю (по сравнению с a). И это, в общем-то понятно - очень малое изменение свободного члена (с a^2n - b до a^2n + b) сильно влияет на корни (либо они есть, либо их нет). Собственно, никакой численный метод, кажется, за заранее фиксированное число шагов не может ответить на вопрос, есть ли корень, или нет (и это то, зачем нужен метод Штурма).


  1. nickolaym
    24.08.2024 22:27

    Хабр - торт!

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