Вычисление полинома от нескольких переменных

В самом общем виде степенной полином от нескольких переменных можно записать формулой


P_n(x_1,x_2,\ldots,x_D)=\displaystyle\sum_{l_1,\ldots,l_D=0}^{n}C_{l_1,\ldots,l_D}\prod_{j=1}^{D} x^{l_j},    \displaystyle\sum_{j=1}^{D}l_j \le n~~~~(1)

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

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

На самом деле каждый одночлен порядка k может быть вычислен по одному из одночленов порядка k-1 с помощью только одного умножения. Например, одночлены первого порядка x_1, x_2, \ldots, x_D получаются домножением одночлена нулевого порядка (единицы) на одну из переменных. Домножив снова каждый из этих одночленов на одну из переменных, получим все возможные одночлены второго порядка и т.д. Одночлены k-1-го порядка

p_k=\displaystyle\prod_{j=1}^{k-1}x_{i_j}, ~~i_j\in [1,D]~~~~(2)

множим на одну из переменных и получаем одночлены k-го порядка.

Однако, начиная со второго порядка появляется проблема: одночлен x_ix_j, i\ne j будет получен два раза. Собственно, сколько есть вариантов перестановки сомножителей в формуле (2), столько раз и будет получен каждый одночлен. Чтобы не производить подобных повторных вычислений, договоримся, из всех последовательностей вычисления одночлена (2) выбирать такой, при котором индексы переменных i_j упорядочены по возрастанию. Чтобы достичь этого, будем домножать на x_i только те одночлены, которые не содержат переменных с номерами больше i. Тогда на x_1 домножаются только те, которые являются степенями x_1, на x_2 — только содержащие x_1 и x_2 и т.д.

При таком подходе каждый одночлен служит для вычисления сразу нескольких других. Степени x_1 множатся на все D переменных. Одночлены, содержащие x_1 и x_2 на все переменные, начиная с x_2. И т.д. Процесс вычисления в таком случае можно представить в виде дерева:

Дерево вычисления одночленов многомерного полинома

Рис. 1. Процесс вычисления одночленов многомерного полинома, представленный в виде дерева. Каждый узел соответствует одночлену, а каждое ребро — домножению на одну из переменных.

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

//***********************************************************
//* Процедура для рекурсивного вычисления массива одночленов.
//* Один вызов процедуры - подсчет одного одночлена.
//***********************************************************/
procedure OneMonomial(
    //Ссылка на вещественнозначный массив, куда записываются 
    //вычисленные одночлены.
    var Monomials: array of extended; 

    //Номер предыдущего одночлена (по которому будет производится
    // вычисление текущего).
    Parent: integer;

    //Ссылка на массив значений переменных
    var x: array of extended;

    //Количество переменных
    D: integer;

    //Номер переменной, на которую надо домножить родительский одночлен
    xnum: integer;

    //Количество уже вычисленных одночленов
    //(чтобы знать куда записывать новый).
    var MN: integer;

    //Порядок одночлена, он же глубина рекурсии
    Order: integer;
    
    //Порядок полинома, он же максимальная глубина рекурсии
    n: integer;
    );
var
  i: integer;
begin
  MN := MN + 1;
  Monomials[MN] := Monomials[Parent] * x[xnum];
  if Order < n then
  begin
    Parent := MN; //Запоминаем значение текущего
    for i := xnum to D-1 do
      OneMonomial(Monomials, Parent, x, D, i, MN, Order + 1, n);
  end;
end;

Общее количество одночленов в полиноме порядка n от D переменных можно рассчитать по формуле

Number = C_{n+D}^D = \displaystyle \frac{(n+D)!}{n!D!}~~~~(3)

Вычисление массива одночленов с помощью приведенной выше процедуры выглядит так:

SetLength(Monomials, Number);
Monomials[0] := 1;
MN := 0;
if n > 0 then
  for i := 0 to D-1 do
    OneMonomial(Monomials, 0, x, D, i, MN, 1, n);

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

Соответствующий код выглядит так:

//Создадим ту же процедуру OneMonomial, но вместо вычисления
//одночлена будем лишь запоминать параметры Parent и xnum
procedure OneMonomial(
    Parent: integer;
    D: integer;
    xnum: integer;
    var MN: integer;
    Order: integer;
    n: integer;

    //Ссылка на массив для запоминания номеров родительских одночленов
    Parents: array of ineger;

    //Ссылка на массив для запоминания номеров переменных
    VarNumbers: array of integer;
    );
var
  i: integer;
begin
  MN := MN + 1;
  //Вычислений теперь не делаем
  //Monomials[MN] := Monomials[Parent] * x[xnum];
  //Вместо этого:
  Parents[MN] := Parent;
  VarNumbers[MN] := xnum;
  
  if Order < n then
  begin
    Parent := MN; //Запоминаем значение текущего
    for i := xnum to D-1 do
      OneMonomial(Parent, D, i, MN, Order + 1, n, Parents, VarNumbers);
  end;
end;

Вызов этой процедуры производим так:

SetLength(Parents, Number);
SetLength(VarNumbers, Number);
MN := 0;
if n > 0 then
  for i := 0 to D-1 do
    OneMonomial(0, D, i, MN, 1, n, Parents, VarNumbers);

А вычисление массива одночленов для каждого значения переменных x так:

SetLength(Monomials, Number);
Monomials[0] := 1;
for i := 1 to Number-1 do
  Monomials[i] := Monomials[Parents[i]] * x[VarNumbers[i]];

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

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


P_n(x_1,x_2,\ldots,x_D)=c_0+\displaystyle\sum_{i_1=1}^{D}x_{i_1}(c_{i_1}+\displaystyle\sum_{i_2=i_1}^{D}x_{i_2}(c_{i_1,i_2}+\displaystyle\sum_{i_3=i_2}^{D}x_{i_3}(c_{i_1,i_2,i_3}+...)))~~~~(4)

Здесь индексы i_1, i_2, \ldots, i_n - номера ветвей дерева по которым мы доходим до соответствующего одночлена.

Данная формула является обобщением на многомерный случай хорошо известной схемы Горнера для вычисления одномерного полинома:

P_n(x)=c_0+x(c_1+x(c_2+x(c_3+\ldots))). ~~~~(5)

Функция, производящая вычисления в соответствии с формулой (4), представлена ниже:

function Horner(
    //Коэффициенты, упорядоченные также как одночлены,
    //вычисляемые приведенной выше процедурой.
    var c: array of extended;

    //Аргументы полинома
    var x: array of extended;

    //Размерность полинома (количество аргументов)
    D: integer;

    //Порядок полинома
    n: integer;

    //Номер очередного одночлена
    var MN: integer;

    //Глубина рекурсии (она же - порядок очередного одночлена)
    depth: integer;

    //Номер переменной, начиная с которого производится домножение.
    //В зависимости от глубины рекурсии соответствует i_1, ..., i_n
    //в формуле (4).
    i_depth: integer;
);
var
  i: integer;
  P: extended;
begin
  P := c[MN]
  MN := MN + 1;
  if depth < n then
    for i := i_depth to D-1 do
      P := P + x[i] * Horner(c, x, D, n, MN, depth+1, i);
  Horner := P;
end;

Чтобы скрыть служебные в общем-то параметры MN, depth, i_depth следует вызывать эту функцию через другую:

function Polynomial(
    //Коэффициенты
    var c: array of extended;
    //Аргументы полинома
    var x: array of extended;
    //Размерность полинома (количество аргументов)
    D: integer;
    //Порядок полинома
    n: integer;
);
var
  MN: integer;
begin
  MN := 0;
  Polynomial := Horner(c, x, D, n, MN, 0, 0);
end;

12 комментариев

  1. Илья

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

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

  2. Taras

    Присылай.

    Подсветка будет. Я пользуюсь SyntaxHighlighter‘м. Там нет фортрана, но найду и для него скрипт. Будет код, будет и подсветка.

  3. Илья

    Код прислал.

  4. Фаниль

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

  5. Taras

    >>рассуждения о сложности вычисления

    Вычисление каждого одночлена требует одного умножения. Всего одночленов
    Number = \displaystyle \frac{(n+D)!}{n!D!}
    штук.

  6. Фаниль

    спасибо! не ожидал, оперативно работаете)

  7. Валерий

    Схема Горнера является расходящейся. Хотя сам вычисляю по ней.Ваше представление о многомерных полиномах неполно. Еще в 1992 году Я уже все разработал. Сейчас вернулся и кое что добавил. Кол-во неизвестных равно не (n+d)!/n!/d!. а можно свести к N*d

  8. Taras

    Поделитесь ссылкой, если не трудно.

  9. Bambucia

    Ребят помогите пождалуитс, ооочень срочно нужна задача на C++ Наити Значение многочлена по схеме Горнера

  10. and1

    матричная теория полиномов
    Схема Горнера устарела

Добавить комментарий