E125. Метод Гаусса решения системы линейных уравнений

e-maxx algorithm original: C/C++ #algorithm #emaxx #linear-algebra #math #matrix
Task text is translated from Russian for the selected interface language. Code is left unchanged.

Источник: e-maxx.ru/algo, страница PDF 409.

Дана система

линейных алгебраических уравнений (СЛАУ) с

неизвестными. it is required решить эту

систему: определить, сколько решений она имеет (ни одного, одно или бесконечно много), а если она имеет хотя бы одно Solution, то find любое из них. Формально Task ставится следующим образом: решить систему:

где коэффициенты

и

известны, а

переменные

— искомые неизвестные. Удобно матричное представление этой задачи:

где

— матрица

, составленная из коэффициентов

,

и

— векторы-столбцы высоты

. Стоит отметить, что СЛАУ может быть не над полем действительных чисел, а над полем по модулю какого-

либо числа

, т.е.: — Algorithm Гаусса работает и для таких систем тоже (но этот случай будет рассмотрен ниже в отдельном разделе).

Algorithm Гаусса

Строго говоря, описываемый ниже метод правильно называть методом "Гаусса-Жордана" (Gauss-Jordan elimination), поскольку он является вариацией метода Гаусса, описанной геодезистом Вильгельмом Жорgivenм в 1887 г. (стоит отметить, что Вильгельм Жордан не является автором ни теоремы Жордана о кривых, ни жорgivenвой алгебры — всё это три разных учёных-однофамильца; кроме того, по всей видимости, более правильной является транскрипция "Йордан", но написание "Жордан" уже закрепилось в русской литературе). Также интересно заметить, что одновременно с Жорgivenм (а по некоторым данным даже раньше него) этот Algorithm придумал Класен (B.-I. Clasen).

Базовая схема

Кратко говоря, Algorithm заключается в последовательном исключении переменных из каждого уравнения до тех пор, пока в каждом уравнении не останется только по одной переменной. Если

, то

можно говорить, что Algorithm Гаусса-Жордана стремится привести матрицу

системы к единичной матрице —

ведь после того как матрица стала единичной, Solution системы очевидно — Solution единственно и

задаётся получившимися коэффициентами

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

. Затем Algorithm

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

-ой надо домножать её на

. При каждой операции с матрицей

(деление на number, прибавление к одной строке другой)

соответствующие операции производятся и с вектором

; в некотором смысле, он ведёт себя, как если бы он был

-ым столбцом матрицы

.

В итоге, по окончании первого шага первый столбец матрицы

станет единичным (т.е. будет содержать единицу в

первой строке и нули в остальных). Аналогично производится второй шаг Algorithmа, только теперь рассматривается второй столбец и вторая string:

сначала вторая string делится на

, а затем отнимается от всех остальных строк с такими коэффициентами,

чтобы обнулять второй столбец матрицы

. И так далее, пока мы не обработаем все строки или все столбцы матрицы

. Если

, то по построению

Algorithmа очевидно, что матрица

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

Поиск опорного elementа (pivoting)

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

-ом шаге element

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

столбце путём прибавления к ним

-ой строки. Чтобы сделать Algorithm работающим в таких случаях, как раз и существует процесс выбора опорного elementа (на английском языке это называется одним словом "pivoting"). Он заключается в том, что производится перестановка строк и/или столбцов матрицы, чтобы в нужном elementе оказалось ненулевое number. Заметим, что перестановка строк значительно проще реализуется на компьютере, чем перестановка столбцов: ведь при обмене местами двух каких-то столбцов надо запомнить, что эти две переменных обменялись местами, чтобы затем, при восстановлении ответа, правильно восстановить, какой ответ к какой переменной относится. При перестановке строк никаких таких дополнительных действий производить не надо. К счастью, для корректности метода достаточно одних только обменов строк (т.н. "partial pivoting", в отличие от "full pivoting", когда обмениваются и строки, и столбцы). Но какую же именно строку следует выбирать для обмена? И правда ли, что поиск опорного elementа надо делать только тогда, когда текущий element

нулевой?

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

не только когда это необходимо (т.е. не только тогда, когда

).

Иными словами, перед выполнением

-ой фазы Algorithmа Гаусса-Жордана с эвристикой partial pivoting необходимо

find в

-ом столбце среди elementов с индексами от

до

maximum по модулю, и обменять строку с

этим elementом с

-ой строкой. Во-первых, эта эвристика позволит решить СЛАУ, даже если по ходу решения будет случаться так, что element . Во-вторых, что весьма немаловажно, эта эвристика улучшает численную устойчивость Algorithmа Гаусса-Жордана.

Без этой эвристики, даже если система такова, что на каждой

-ой фазе

— Algorithm Гаусса-

Жордана отработает, но в итоге накапливающаяся погрешность может оказаться настолько огромной, что даже

для матриц размера около

погрешность будет превосходить сам ответ.

Вырожденные случаи

Итак, если останавливаться на Algorithmе Гаусса-Жордана с partial pivoting, то, утверждается, если

и

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

(Proof этого, т.е. того,

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

Рассмотрим теперь общий случай — когда

и

не обязательно равны. Предположим, что опорный element на

-ом шаге не нашёлся. Это означает, что в

-ом столбце все строки, начиная с текущей, содержат нули. Утверждается,

что в этом случае эта

-ая переменная не может быть определена, и является независимой

переменной (может принимать произвольное значение). Чтобы Algorithm Гаусса-Жордана продолжил свою работу для всех последующих переменных, в такой ситуации надо просто пропустить текущий

-ый столбец,

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

когда количество

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

уравнений, то как минимум

переменных

обнаружатся независимыми. В целом, если обнаружилась хотя бы одна независимая переменная, то она может принимать произвольное значение, в то время как остальные (зависимые) переменные будут выражаться через неё. Это означает, что, когда мы работаем в поле действительных чисел, система потенциально имеет бесконечно много решений (если мы рассматриваем СЛАУ по модулю, то number решений будет равно этому модулю в степени количества независимых переменных). Впрочем, следует быть аккуратным: надо помнить о том, что даже если были обнаружены независимые переменные, тем не менее СЛАУ может не иметь решений вовсе. Это происходит, когда в оставшихся необработанными уравнениях (тех, до которых Algorithm Гаусса-Жордана не дошёл, т.е. это уравнения, в которых остались только независимые переменные) есть хотя бы один ненулевой свободный член. Впрочем, проще это проверить явной подстановкой найденного решения: всем независимыми переменным присвоить нулевые значения, зависимым переменным присвоить найденные значения, и подставить это Solution в текущую СЛАУ.

Implementation

Приведём здесь реализацию Algorithmа Гаусса-Жордана с эвристикой partial pivoting (выбором опорного elementа как максимума по столбцу).

На Input функции

передаётся сама матрица системы

. Последний столбец матрицы

— это в наших

старых обозначениях столбец

свободных коэффициентов (так сделано для удобства программирования — т.к. в

самом Algorithmе все операции со свободными коэффициентами

повторяют операции с матрицей

).

Функция returns number решений системы (

,

или

) (бесконечность обозначена в коде специальной

константой

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

оно returnsся в векторе

.

int gauss (vector < vector<double> > a, vector<double> & ans) {
int n = (int) a.size();
int m = (int) a[0].size() - 1;

vector<int> where (m, -1);

for (int col=0, row=0; col<m && row<n; ++col) {
int sel = row;
for (int i=row; i<n; ++i)
if (abs (a[i][col]) > abs (a[sel][col]))

sel = i;

if (abs (a[sel][col]) < EPS)

continue;

for (int i=col; i<=m; ++i)

swap (a[sel][i], a[row][i]);

where[col] = row;

for (int i=0; i<n; ++i)
if (i != row) {

double c = a[i][col] / a[row][col];

for (int j=col; j<=m; ++j)

a[i][j] -= a[row][j] * c;

}

++row;

}

ans.assign (m, 0);

for (int i=0; i<m; ++i)
if (where[i] != -1)

ans[i] = a[where[i]][m] / a[where[i]][i];

for (int i=0; i<n; ++i) {

double sum = 0;

for (int j=0; j<m; ++j)

sum += ans[j] * a[i][j];

if (abs (sum - a[i][m]) > EPS)
return 0;

}

for (int i=0; i<m; ++i)
if (where[i] == -1)
return INF;
return 1;

}

В функции поддерживаются два указателя — на текущий столбец

и текущую строку

.

Также заводится вектор

, в котором для каждой переменной записано, в какой строке должна она получиться (иными словами, для каждого столбца записан номер строки, в которой этот столбец отличен от нуля). Этот вектор нужен, поскольку некоторые переменные могли не "определиться" в ходе решения (т.е. это независимые переменные, которым можно присвоить произвольное значение — наExample, в приведённой реализации это нули). Implementation использует технику partial pivoting, производя поиск строки с максимальным по модулю elementом,

и переставляя затем эту строку в позицию

(хотя явную перестановку строк можно заменить обменом двух индексов в некотором arrayе, на практике это не даст реального выигрыша, т.к. на обмены тратится операций). В реализации в целях простоты текущая string не делится на опорный element — так что в итоге по окончании работы Algorithmа матрица становится не единичной, а диагональной (впрочем, по-видимому, деление строки на ведущий element позволяет несколько уменьшить возникающие погрешности). После нахождения решения оно подставляется обратно в матрицу — чтобы проверить, имеет ли система хотя бы одно Solution или нет. Если проверка найденного решения прошла успешно, то функция returns

или

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

Asymptotic complexity

Оценим асимптотику полученного Algorithmа. Algorithm состоит из

фаз, на каждой из которых происходит:

● поиск и перестановка опорного elementа — за время

при использовании эвристики "partial

pivoting" (поиск максимума в столбце)

● если опорный element в текущем столбце был найден — то прибавление текущего уравнения ко всем

остальным уравнениям — за время

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

более

раз — столько, сколько может быть зависимых переменных в СЛАУ.

Таким образом, итоговая Asymptotic complexity Algorithmа принимает вид

.

При

эта оценка превращается в

. Заметим, что когда СЛАУ рассматривается не в поле действительных чисел, а в поле по модулю два, то систему можно решать гораздо быстрее — об этом см. ниже в разделе "Solution СЛАУ по модулю".

Более точная оценка числа действий

Для простоты выкладок будем считать, что

. Как мы уже знаем, Running time всего Algorithmа фактически определяется временем, затрачиваемым на исключение текущего уравнения из остальных.

Это может происходить на каждом из

шагов, при этом текущее уравнение прибавляется ко всем

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

Дополнения

Ускорение Algorithmа: разделение его на прямой и обратный ход

Добиться двукратного ускорения Algorithmа можно, рассмотрев другую его версию, более классическую, когда Algorithm разбивается на фазы прямого и обратного хода. В целом, в отличие от описанного выше Algorithmа, можно приводить матрицу не к диагональному виду, а к треугольному виду — когда все elementы строго ниже главной диагонали равны нулю. Система с треугольной матрицей решается тривиально — сначала из последнего уравнения сразу находится значение последней переменной, затем найденное значение подставляется в предпоследнее уравнение и находится значение предпоследней переменной, и так далее. Этот процесс и называется обратным ходом Algorithmа Гаусса. Прямой ход Algorithmа Гаусса — это Algorithm, аналогичный описанному выше Algorithmу Гаусса-Жордана, за одним исключением: текущая переменная исключается не из всех уравнений, а только из уравнений после текущего. В результате этого действительно получается не диагональная, а треугольная матрица. Разница в том, что прямой ход работает быстрее Algorithmа Гаусса-Жордана — поскольку в среднем он делает в два раза меньше прибавлений одного уравнения к другому. Обратный ход работает за

, что в любом

случае асимптотически быстрее прямого хода.

Таким образом, если

, то данный Algorithm будет делать уже

операций — что в два раза

меньше Algorithmа Гаусса-Жордана.

Solution СЛАУ по модулю

Для решения СЛАУ по модулю можно применять описанный выше Algorithm, он сохраняет свою корректность. Разумеется, теперь становится ненужным использовать какие-то хитрые техники выбора опорного elementа — достаточно find любой ненулевой element в текущем столбце. Если модуль простой, то никаких сложностей вообще не возникает — происходящие по ходу работы Algorithmа Гаусса деления не создают особых проблем. Особенно замечателен модуль, равный двум: для него все операции с матрицей можно производить очень эффективно. НаExample, отнимание одной строки от другой по модулю два — это на самом деле их симметрическая разность ("xor"). Таким образом, весь Algorithm можно значительно ускорить, сжав всю матрицу в битовые маски и оперируя только ими. Приведём здесь новую реализацию основной части Algorithmа Гаусса- Жордана, используя стандартный контейнер C++ "bitset":

int gauss (vector < bitset<N> > a, int n, int m, bitset<N> & ans) {

vector<int> where (m, -1);

for (int col=0, row=0; col<m && row<n; ++col) {
for (int i=row; i<n; ++i)
if (a[i][col]) {

swap (a[i], a[row]);

break;

}

if (! a[row][col])

continue;

where[col] = row;

for (int i=0; i<n; ++i)
if (i != row && a[i][col])

a[i] ^= a[row];

++row;

} Как можно заметить, Implementation стала даже немного короче, при том, что она значительно быстрее старой реализации

— а именно, быстрее в

раза за счёт битового сжатия. Также следует отметить, что Solution систем по модулю два на практике работает очень быстро, поскольку случаи, когда от одной строки надо отнимать другую, происходят достаточно редко (на разреженных матрицах этот Algorithm может работать за время скорее порядка квадрата от размера, чем куба). Если модуль произвольный (не обязательно простой), то всё становится несколько сложнее. Понятно, что пользуясь Китайской теоремой об остатках, мы сводим задачу с произвольным модулем только к модулям вида "степень простого". [ дальнейший текст был скрыт, т.к. это непроверенная информация — возможно,

неправильный способ решения ]

Наконец, рассмотрим вопрос числа решений СЛАУ по модулю. Ответ на него достаточно прост:

number решений равно

, где

— модуль,

— number независимых переменных.

Немного о различных способах выбора опорного elementа

Как уже говорилось выше, однозначного ответа на этот вопрос нет. Эвристика "partial pivoting", которая заключалась в поиске максимального elementа в текущем столбце, работает на практике весьма неплохо. Также оказывается, что она даёт практически тот же результат, что и "full pivoting" — когда опорный element ищется среди elementов целой подматрицы — начиная с текущей строки и с текущего столбца. Но интересно отметить, что обе эти эвристики с поиском максимального elementа, фактически, очень зависят от того, насколько были промасштабированы исходные уравнения. НаExample, если одно из уравнений системы умножить на миллион, то это уравнение почти наверняка будет выбрано в качестве ведущего на первом же шаге. Это кажется достаточно странным, поэтому логичен переход к немного более сложной эвристике — так называемому "implicit pivoting". Эвристика implicit pivoting заключается в том, что elementы различных строк сравниваются так, как если бы обе строки были пронормированы таким образом, что maximum по модулю element в них был бы равен единице. Для реализации этой техники надо просто поддерживать текущий максимум в каждой строке (либо поддерживать каждую строку так, чтобы максимум в ней был равен единице по модулю, но это может привести к увеличению накапливаемой погрешности).

Улучшение найденного ответа

Поскольку, несмотря на различные эвристики, Algorithm Гаусса-Жордана всё равно может приводить к

большим погре

...

C# solution

auto-draft, review before submit
using System;
using System.Collections.Generic;
using System.Linq;

public static class AlgorithmDraft
{
    // Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
    int gauss (vector < vector<double> > a, vector<double> & ans) {
            int n = (int) a.size();
            int m = (int) a[0].size() - 1;
            List<int> where (m, -1);
            for (int col=0, row=0; col<m && row<n; ++col) {
                    int sel = row;
                    for (int i=row; i<n; ++i)
                            if (abs (a[i][col]) > abs (a[sel][col]))
                                    sel = i;
                    if (abs (a[sel][col]) < EPS)
                            continue;
                    for (int i=col; i<=m; ++i)
                            swap (a[sel][i], a[row][i]);
                    where[col] = row;
                    for (int i=0; i<n; ++i)
                            if (i != row) {
                                    double c = a[i][col] / a[row][col];
                                    for (int j=col; j<=m; ++j)
                                            a[i][j] -= a[row][j] * c;
                            }
                    ++row;
            }
            ans.assign (m, 0);
            for (int i=0; i<m; ++i)
                    if (where[i] != -1)
                            ans[i] = a[where[i]][m] / a[where[i]][i];
            for (int i=0; i<n; ++i) {
                    double sum = 0;
                    for (int j=0; j<m; ++j)
                            sum += ans[j] * a[i][j];
                    if (abs (sum - a[i][m]) > EPS)
                            return 0;
            }
            for (int i=0; i<m; ++i)
                    if (where[i] == -1)
                            return INF;
            return 1;
    }
    int gauss (vector < bitset<N> > a, int n, int m, bitset<N> & ans) {
            List<int> where (m, -1);
            for (int col=0, row=0; col<m && row<n; ++col) {
                    for (int i=row; i<n; ++i)
                            if (a[i][col]) {
                                    swap (a[i], a[row]);
                                    break;
                            }
                    if (! a[row][col])
                            continue;
                    where[col] = row;
                    for (int i=0; i<n; ++i)
                            if (i != row && a[i][col])
                                    a[i] ^= a[row];
                    ++row;
            }
}

C++ solution

matched/original
int gauss (vector < vector<double> > a, vector<double> & ans) {
        int n = (int) a.size();
        int m = (int) a[0].size() - 1;
        vector<int> where (m, -1);
        for (int col=0, row=0; col<m && row<n; ++col) {
                int sel = row;
                for (int i=row; i<n; ++i)
                        if (abs (a[i][col]) > abs (a[sel][col]))
                                sel = i;
                if (abs (a[sel][col]) < EPS)
                        continue;
                for (int i=col; i<=m; ++i)
                        swap (a[sel][i], a[row][i]);
                where[col] = row;
                for (int i=0; i<n; ++i)
                        if (i != row) {
                                double c = a[i][col] / a[row][col];
                                for (int j=col; j<=m; ++j)
                                        a[i][j] -= a[row][j] * c;
                        }
                ++row;
        }
        ans.assign (m, 0);
        for (int i=0; i<m; ++i)
                if (where[i] != -1)
                        ans[i] = a[where[i]][m] / a[where[i]][i];
        for (int i=0; i<n; ++i) {
                double sum = 0;
                for (int j=0; j<m; ++j)
                        sum += ans[j] * a[i][j];
                if (abs (sum - a[i][m]) > EPS)
                        return 0;
        }
        for (int i=0; i<m; ++i)
                if (where[i] == -1)
                        return INF;
        return 1;
}
int gauss (vector < bitset<N> > a, int n, int m, bitset<N> & ans) {
        vector<int> where (m, -1);
        for (int col=0, row=0; col<m && row<n; ++col) {
                for (int i=row; i<n; ++i)
                        if (a[i][col]) {
                                swap (a[i], a[row]);
                                break;
                        }
                if (! a[row][col])
                        continue;
                where[col] = row;
                for (int i=0; i<n; ++i)
                        if (i != row && a[i][col])
                                a[i] ^= a[row];
                ++row;
        }

Java solution

auto-draft, review before submit
import java.util.*;
import java.math.*;

public class AlgorithmDraft {
    // Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
    int gauss (vector < vector<double> > a, vector<double> & ans) {
            int n = (int) a.size();
            int m = (int) a[0].size() - 1;
            ArrayList<Integer> where (m, -1);
            for (int col=0, row=0; col<m && row<n; ++col) {
                    int sel = row;
                    for (int i=row; i<n; ++i)
                            if (abs (a[i][col]) > abs (a[sel][col]))
                                    sel = i;
                    if (abs (a[sel][col]) < EPS)
                            continue;
                    for (int i=col; i<=m; ++i)
                            swap (a[sel][i], a[row][i]);
                    where[col] = row;
                    for (int i=0; i<n; ++i)
                            if (i != row) {
                                    double c = a[i][col] / a[row][col];
                                    for (int j=col; j<=m; ++j)
                                            a[i][j] -= a[row][j] * c;
                            }
                    ++row;
            }
            ans.assign (m, 0);
            for (int i=0; i<m; ++i)
                    if (where[i] != -1)
                            ans[i] = a[where[i]][m] / a[where[i]][i];
            for (int i=0; i<n; ++i) {
                    double sum = 0;
                    for (int j=0; j<m; ++j)
                            sum += ans[j] * a[i][j];
                    if (abs (sum - a[i][m]) > EPS)
                            return 0;
            }
            for (int i=0; i<m; ++i)
                    if (where[i] == -1)
                            return INF;
            return 1;
    }
    int gauss (vector < bitset<N> > a, int n, int m, bitset<N> & ans) {
            ArrayList<Integer> where (m, -1);
            for (int col=0, row=0; col<m && row<n; ++col) {
                    for (int i=row; i<n; ++i)
                            if (a[i][col]) {
                                    swap (a[i], a[row]);
                                    break;
                            }
                    if (! a[row][col])
                            continue;
                    where[col] = row;
                    for (int i=0; i<n; ++i)
                            if (i != row && a[i][col])
                                    a[i] ^= a[row];
                    ++row;
            }
}

Материал разбит как Algorithmическая Task: изучить постановку, понять асимптотику и реализовать Algorithm на выбранном языке.

Vacancies for this task

Active vacancies with overlapping task tags are shown.

All vacancies
There are no active vacancies yet.