В этой статье мы будем рассматривать решения СЛАУ вида Ax = b, где A - симметричная разреженная матрица. Такие матрицы появляются, например, при решении задач методом наименьших квадратов. Для симметричных СЛАУ разработаны специальные методы, такие, как метод Холецкого и LDLT разложение ( см. [1][2] ). Так как первый из них применим к более узкому классу матриц, чем второй, поэтому далее будем рассматривать только LDLT разложение, хотя выводы этой статьи применимы к обоим методам.
При LDLT разложении матрица A представляется в виде произведения LDLT, где L - нижняя треугольная матрица с единицами на главной диагонали, D - диагональная матрица,
LT- транспонированная матрица L. Затем последовательно решаются уравнения Ly = b и DLTx = y. При этом затрачивается около n3/6 умножений ( где n - размерность матрицы А ),
что в 2 раза меньше, чем в методе Гаусса. Это неудивительно, так как при большой размерности в симметричной матрице содержится приблизительно в 2 раза меньше информации, чем в матрице общего вида.
Далее, чтобы уменьшить количество вычислений, воспользуемся разреженностью матрицы А. Элементы матриц D и L вычисляются по формулам:
Из этих формул следует, что если в какой-то строке матрицы А первые k элементов были нулями, то и в матрице L в той же строке первые k элементов тоже будут нулями. А это значит, что эти элементы вычислять не надо, и, если их много, то получается большая экономия в вычислениях. Это основная идея этой статьи. Назовём эти нули - "лидирующими нулями".
Ниже приведена программа на языке С++, которая решает СЛАУ с учётом лидирующих нулей. Это реальная программа с проверками на переполнение и исчезновение порядка
для чисел с плавающей точкой. Матрица А поступает на вход программы в компактном виде. Во-первых, опускаются лидирующие нули, а во-вторых, указываются элементы только до диагонального, так как матрица симметричная. Новые матрицы L и D записываются на место исходной матрицы А, причём для матрицы D записываются обратные диагональные элементы.
Входные параметры функции slu_LDLt:
n - размерность матрицы А,
m - массив с количеством лидирующих нулей по строкам,
a - матрица А записанная в виде массива по строкам фрагментами от первого ненулевого до диагонального элемента,
b - массив свободных членов,
x - массив неизвестных.
typedef unsigned int nat;
template<class T> inline const T & _max ( const T & i1, const T & i2 )
{
return i1 > i2 ? i1 : i2;
}
bool slu_LDLt ( nat n, const nat * m, double * a, const double * b, double * x )
{
nat i, j, k, l, nn = 0;
// Заполнение индексов диагональных элементов
std::vector<nat> di ( n );
for ( i = 0; i < n; ++i )
{
if ( m[i] > i )
return false;
di[i] = nn += i - m[i];
++nn;
}
// Построение матриц L и D
for ( i = l = 0; i < n; ++i )
{
const nat mi = m[i];
const nat ni = di[i] - i;
const double * ai = a + ni;
for ( j = mi; j <= i; ++j )
{
double * aj = a + di[j] - j;
double x = ai[j];
if ( i == j )
{
for ( k = mi; k < j; ++k )
{
double & ajk = aj[k];
const double y = ajk;
ajk *= a[di[k]];
x -= y * ajk;
}
const double ax = fabs ( x );
if ( ax < 1e-100 )
{
return false;
}
a[l] = ax > 1e100 ? 0 : 1 / x;
}
else
{
for ( k = _max ( mi, m[j] ); k < j; ++k ) x -= ai[k] * aj[k];
const double ax = fabs ( x );
if ( ax > 1e100 )
{
return false;
}
a[l] = ax < 1e-100 ? 0 : x;
}
++l;
}
}
// Решение системы Ly = b
for ( i = j = 0; i < n; ++i, ++j )
{
double t = b[i];
for ( k = m[i]; k < i; ++k ) t -= a[j++] * x[k];
x[i] = t;
}
// Решение системы DLtx = y;
for ( i = 0; i < n; ++i ) x[i] *= a[di[i]];
for ( l = nn; --i > 0; )
{
const double t = x[i];
const double * p = a + ( l -= i + 1 );
for ( l += k = m[i]; k < i; ++k ) x[k] -= p[k] * t;
}
return true;
}
В случае отсутствия лидирующих нулей - это будет обычная программа для LDLT разложения. А если их будет много, то время работы программы резко уменьшится по сравнению с обычной. В следующей статье будет показано, как можно увеличить количество лидирующих нулей, переставляя столбцы и строки исходной матрицы А.
Литература.
Уилкинсон, Райнш. "Справочник алгоритмов на языке АЛГОЛ. Линейная алгебра".
Голуб, Ван Лоун. "Матричные вычисления".
Комментарии (9)
alexejisma
29.10.2024 16:01Подскажите, пожалуйста, системы с какими матрицами можно решить методом LDL, которые нельзя решить методом Холецкого (разложение матрицы в виде L*D*L^t, где L нижнетреугольная, D диагональная с +1/-1 на диагонали)?
shevchenko_a_d Автор
29.10.2024 16:01Ниже уже написали о том, что в методе Холецкого матрица А должна быть положительно определенной. В этом методе используется квадратный корень, и когда для него получаются отрицательные числа, то матрица L получается комплексной, а это уже другая история.
shevchenko_a_d Автор
29.10.2024 16:01Вообще под методом Холецкого подразумевается разложение L*L^t, а ваш вариант с L*D*L^t я не рассматривал.
alexejisma
29.10.2024 16:01Когда я учился в университете нам рассказывали именно такой вариант. Для этого метода матрица не обязательна быть положительно определенной. Когда возникает необходимость извлекать квадратный корень sqrt(x), то извлекается корень из модуля числа sqrt(abs(x)), а диагональный элемент принимает значение знака того числа из модуля которого извлекается корень sign(x). Это существенно расширяет класс "хороших" матриц.
andy_p
29.10.2024 16:01Вы не упомянули о том, что в методе Холецкого матрица А должна быть положительно определена.
alexejisma
29.10.2024 16:01Почему выбор пал на точный метод вместо итерационного? Разве не будет расход памяти огромный?
shevchenko_a_d Автор
29.10.2024 16:01Вначале для своих задач я пробовал метод сопряжённых градиентов. Но он давал неважную точность, а увеличение количества итераций съедало преимущество по времени по сравнению с обычным LDLt. Далее я придумал убирать лидирующие нули. Получилось и быстрее, и точнее. Что касается памяти, то матрица записывается в компактном виде без лидирующих нулей, и в моей практике её размерность редко превосходила нескольких тысяч. В общем с памятью проблем не было.
Anton_Menshov
Для разреженных матриц огромная часть алгоритмов реализована в SuiteSparse. В вашем случае - в подразделе для LDL:
https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/LDL и документация https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/dev/LDL/Doc/ldl_userguide.pdf
Стандартный подход: 1) анализ как разрежена матрица 2) символьное разложение LDL (только учитывая что (i,j)-элемент ненулевой 3) численное разложение LDL.
Не очень понимаю мотивацию данной статьи, которая сваливает все в одну кучу, не делит на этапы и мало что рассказывает об алгоритме.