Перейти до основного вмісту

Метод Гаусса розв'язання системи лінійних рівнянь

Дано систему з nn лінійних алгебраїчних рівнянь (СЛАР) із mm невідомими. Потрібно розв'язати систему: визначити, чи не має вона розв'язків, має рівно один розв'язок або нескінченну кількість розв'язків. А якщо вона має хоча б один розв'язок — знайти будь-який із них.

Формально задача формулюється так: розв'язати систему:

a11x1+a12x2++a1mxm=b1a21x1+a22x2++a2mxm=b2an1x1+an2x2++anmxm=bn\begin{align} a_{11} x_1 + a_{12} x_2 + &\dots + a_{1m} x_m = b_1 \\ a_{21} x_1 + a_{22} x_2 + &\dots + a_{2m} x_m = b_2\\ &\vdots \\ a_{n1} x_1 + a_{n2} x_2 + &\dots + a_{nm} x_m = b_n \end{align}

де коефіцієнти aija_{ij} (для ii від 1 до nn, jj від 1 до mm) і bib_i (ii від 1 до nn) відомі, а змінні xix_i (ii від 1 до mm) — невідомі.

Ця задача також має просте матричне подання:

Ax=b,Ax = b,

де AA — матриця розміру n×mn \times m з коефіцієнтів aija_{ij}, а bb — вектор-стовпець розміру nn.

Варто зауважити, що описаний у цій статті метод можна застосувати й для розв'язання рівняння за модулем будь-якого числа p, тобто:

a11x1+a12x2++a1mxmb1(modp)a21x1+a22x2++a2mxmb2(modp)an1x1+an2x2++anmxmbn(modp)\begin{align} a_{11} x_1 + a_{12} x_2 + &\dots + a_{1m} x_m \equiv b_1 \pmod p \\ a_{21} x_1 + a_{22} x_2 + &\dots + a_{2m} x_m \equiv b_2 \pmod p \\ &\vdots \\ a_{n1} x_1 + a_{n2} x_2 + &\dots + a_{nm} x_m \equiv b_n \pmod p \end{align}
Коли підходить цей алгоритм?
  • Потрібно розв'язати систему лінійних рівнянь Ax=bAx=b (над дійсними числами або за модулем) і з'ясувати кількість розв'язків?
  • Цікавить лише кількість лінійно незалежних рівнянь / стовпців, а не сам розв'язок? (якщо так → Ранг матриці)
  • Потрібен лише визначник квадратної матриці, без розв'язку? (якщо так → Визначник методом Гаусса)

Гаусс

Строго кажучи, описаний нижче метод варто було б назвати «метод Гаусса-Жордана», або виключенням за Гауссом-Жорданом, оскільки це варіація методу Гаусса, описана Жорданом 1887 року.

Огляд

Алгоритм полягає в послідовному виключенні змінних у кожному рівнянні доти, доки в кожному рівнянні не залишиться лише одна змінна. Якщо n=mn = m, можна уявляти це як перетворення матриці AA на одиничну матрицю та розв'язання рівняння в цьому очевидному випадку, де розв'язок єдиний і дорівнює коефіцієнту bib_i.

Метод Гаусса ґрунтується на двох простих перетвореннях:

  • Можна поміняти місцями два рівняння.
  • Будь-яке рівняння можна замінити лінійною комбінацією цього рядка (з ненульовим коефіцієнтом) і деяких інших рядків (з довільними коефіцієнтами).

На першому кроці алгоритм Гаусса-Жордана ділить перший рядок на a11a_{11}. Потім алгоритм додає перший рядок до решти рядків так, щоб коефіцієнти в першому стовпці стали нульовими. Щоб цього досягти, до ii-го рядка ми маємо додати перший рядок, помножений на ai1- a_{i1}. Зауважимо, що цю операцію треба також виконувати над вектором bb. У певному сенсі це поводиться так, ніби вектор bb був m+1m+1-м стовпцем матриці AA.

У результаті після першого кроку перший стовпець матриці AA складатиметься з 11 у першому рядку та 00 в інших рядках.

Аналогічно виконуємо другий крок алгоритму, де розглядаємо другий стовпець другого рядка. Спочатку рядок ділиться на a22a_{22}, потім віднімається від інших рядків так, щоб увесь другий стовпець став 00 (окрім другого рядка).

Продовжуємо цей процес для всіх стовпців матриці AA. Якщо n=mn = m, то AA стане одиничною матрицею.

Пошук опорного елемента

В описаній схемі залишилося багато деталей. На ii-му кроці, якщо aiia_{ii} дорівнює нулю, ми не можемо безпосередньо застосувати описаний метод. Натомість ми спочатку маємо вибрати опорний рядок: знайти такий рядок матриці, у якому ii-й стовпець ненульовий, а потім поміняти ці два рядки місцями.

Зауважимо, що тут ми міняємо місцями рядки, а не стовпці. Це тому, що якщо міняти стовпці, то, знайшовши розв'язок, доведеться не забути поміняти їх назад на правильні місця. Тож міняти місцями рядки набагато простіше.

У багатьох реалізаціях, навіть коли aii0a_{ii} \neq 0, можна побачити, що люди все одно міняють ii-й рядок із деяким опорним рядком, використовуючи певні евристики — наприклад, обираючи опорний рядок із максимальним абсолютним значенням ajia_{ji}. Ця евристика використовується, щоб зменшити діапазон значень матриці на подальших кроках. Без неї навіть для матриць розміру близько 2020 похибка буде надто великою й може спричинити переповнення для типів даних із рухомою комою в C++.

Вироджені випадки

У випадку, коли m=nm = n і система невироджена (тобто має ненульовий визначник і єдиний розв'язок), описаний вище алгоритм перетворить AA на одиничну матрицю.

Тепер розглянемо загальний випадок, де nn і mm не обов'язково рівні, а система може бути виродженою. У цих випадках опорний елемент на ii-му кроці може й не знайтися. Це означає, що в ii-му стовпці, починаючи з поточного рядка, всюди нулі. У цьому випадку або немає жодного можливого значення змінної xix_i (тобто СЛАР не має розв'язку), або xix_iнезалежна змінна й може набувати довільного значення. Реалізуючи метод Гаусса-Жордана, слід продовжити роботу для наступних змінних і просто пропустити ii-й стовпець (це еквівалентно видаленню ii-го стовпця матриці).

Отже, у процесі можна виявити, що деякі змінні є незалежними. Коли кількість змінних mm більша за кількість рівнянь nn, то знайдеться щонайменше mnm - n незалежних змінних.

Загалом, якщо ви знайшли хоча б одну незалежну змінну, вона може набувати будь-якого довільного значення, а решта (залежних) змінних виражаються через неї. Це означає, що, працюючи в полі дійсних чисел, система потенційно має нескінченно багато розв'язків. Але слід пам'ятати, що за наявності незалежних змінних СЛАР може взагалі не мати розв'язку. Це трапляється, коли в решти необроблених рівнянь є хоча б один ненульовий вільний член. Це можна перевірити, присвоївши всім незалежним змінним нулі, обчисливши інші змінні, а потім підставивши їх у початкову СЛАР, щоб перевірити, чи вони її задовольняють.

Реалізація

Нижче наведено реалізацію методу Гаусса-Жордана. Вибір опорного рядка виконується за евристикою: обираємо максимальне значення в поточному стовпці.

Вхідними даними функції gauss є матриця системи aa. Останній стовпець цієї матриці — це вектор bb.

Функція повертає кількість розв'язків системи (0,1,або )(0, 1,\textrm{або } \infty). Якщо існує хоча б один розв'язок, то він повертається у векторі ansans.

const double EPS = 1e-9;
const int INF = 2; // насправді це не обов'язково має бути нескінченність чи велике число

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;
}

Зауваження щодо реалізації:

  • Функція використовує два вказівники — поточний стовпець colcol і поточний рядок rowrow.
  • Для кожної змінної xix_i значення where(i)where(i) — це рядок, у якому цей стовпець ненульовий. Цей вектор потрібен, бо деякі змінні можуть бути незалежними.
  • У цій реалізації поточний ii-й рядок не ділиться на aiia_{ii}, як описано вище, тож наприкінці матриця не є одиничною (хоча, вочевидь, ділення ii-го рядка може допомогти зменшити похибки).
  • Знайшовши розв'язок, його підставляють назад у матрицю — щоб перевірити, чи має система хоча б один розв'язок. Якщо пробний розв'язок успішний, то функція повертає 1 або inf\inf залежно від того, чи є хоча б одна незалежна змінна.

Складність

Тепер оцінимо складність цього алгоритму. Алгоритм складається з mm фаз, у кожній фазі:

  • Пошук і перестановка опорного рядка. Це займає O(n+m)O(n + m) при використанні згаданої вище евристики.
  • Якщо опорний елемент у поточному стовпці знайдено — то ми маємо додати це рівняння до всіх інших рівнянь, що займає час O(nm)O(nm).

Отже, остаточна складність алгоритму становить O(min(n,m).nm)O(\min (n, m) . nm). У випадку n=mn = m складність дорівнює просто O(n3)O(n^3).

Зауважимо, що коли СЛАР задана не над дійсними числами, а за модулем два, то систему можна розв'язати значно швидше, про що йдеться нижче.

Пришвидшення алгоритму

Попередню реалізацію можна пришвидшити вдвічі, поділивши алгоритм на дві фази: пряму та зворотну:

  • Пряма фаза: подібна до попередньої реалізації, але поточний рядок додається лише до рядків після нього. У результаті ми отримуємо трикутну матрицю замість діагональної.
  • Зворотна фаза: коли матриця трикутна, ми спочатку обчислюємо значення останньої змінної. Потім підставляємо це значення, щоб знайти значення наступної змінної. Потім підставляємо ці два значення, щоб знайти наступні змінні...

Зворотна фаза займає лише O(nm)O(nm), що набагато швидше за пряму фазу. У прямій фазі ми зменшуємо кількість операцій удвічі, тим самим скорочуючи час роботи реалізації.

Розв'язання модульної СЛАР

Для розв'язання СЛАР за деяким модулем ми все одно можемо використати описаний алгоритм. Однак у випадку, коли модуль дорівнює 2, ми можемо виконати виключення за Гауссом-Жорданом значно ефективніше, використовуючи бітові операції та тип даних bitset у C++:

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;
}
// Решта реалізації така сама, як вище
}

Оскільки ми використовуємо бітове стиснення, реалізація не лише коротша, а й у 32 рази швидша.

Невелике зауваження про різні евристики вибору опорного рядка

Загального правила, які евристики використовувати, не існує.

Евристика, використана в попередній реалізації, доволі добре працює на практиці. До того ж вона дає майже ті самі відповіді, що й «повний вибір опорного елемента» (full pivoting) — коли опорний рядок шукається серед усіх елементів усієї підматриці (від поточного рядка та поточного стовпця).

Утім, слід зауважити, що обидві евристики залежать від того, наскільки масштабовані початкові рівняння. Наприклад, якщо одне з рівнянь помножили на 10610^6, то це рівняння майже напевно буде обрано як опорне на першому кроці. Це здається доволі дивним, тож логічно перейти до складнішої евристики, яку називають неявним вибором опорного елемента (implicit pivoting).

Неявний вибір опорного елемента порівнює елементи так, ніби обидва рядки нормалізовано, щоб максимальний елемент дорівнював одиниці. Щоб реалізувати цю техніку, потрібно підтримувати максимум у кожному рядку (або підтримувати кожен рядок так, щоб максимум дорівнював одиниці, але це може призвести до зростання накопиченої похибки).

Покращення розв'язку

Попри різні евристики, алгоритм Гаусса-Жордана все одно може призводити до великих похибок у спеціальних матрицях навіть розміру 5010050 - 100.

Тому отриманий розв'язок Гаусса-Жордана іноді потрібно покращити, застосувавши простий чисельний метод — наприклад, метод простої ітерації.

Таким чином, розв'язання стає двокроковим: спочатку застосовується алгоритм Гаусса-Жордана, а потім чисельний метод, що бере початковий розв'язок як розв'язок із першого кроку.

Задачі для практики

Відеоматеріали