В этой статье мы будем рассматривать решения СЛАУ вида 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 разложения. А если их будет много, то время работы программы резко уменьшится по сравнению с обычной. В следующей статье будет показано, как можно увеличить количество лидирующих нулей, переставляя столбцы и строки исходной матрицы А.

Литература.

  1. Уилкинсон, Райнш. "Справочник алгоритмов на языке АЛГОЛ. Линейная алгебра".

  2. Голуб, Ван Лоун. "Матричные вычисления".

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


  1. Anton_Menshov
    29.10.2024 16:01

    Для разреженных матриц огромная часть алгоритмов реализована в 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.

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


  1. alexejisma
    29.10.2024 16:01

    Подскажите, пожалуйста, системы с какими матрицами можно решить методом LDL, которые нельзя решить методом Холецкого (разложение матрицы в виде L*D*L^t, где L нижнетреугольная, D диагональная с +1/-1 на диагонали)?


    1. shevchenko_a_d Автор
      29.10.2024 16:01

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


      1. shevchenko_a_d Автор
        29.10.2024 16:01

        Вообще под методом Холецкого подразумевается разложение L*L^t, а ваш вариант с L*D*L^t я не рассматривал.


        1. alexejisma
          29.10.2024 16:01

          Когда я учился в университете нам рассказывали именно такой вариант. Для этого метода матрица не обязательна быть положительно определенной. Когда возникает необходимость извлекать квадратный корень sqrt(x), то извлекается корень из модуля числа sqrt(abs(x)), а диагональный элемент принимает значение знака того числа из модуля которого извлекается корень sign(x). Это существенно расширяет класс "хороших" матриц.


          1. shevchenko_a_d Автор
            29.10.2024 16:01

            Получается, что это третий метод из этой группы.


  1. andy_p
    29.10.2024 16:01

    Вы не упомянули о том, что в методе Холецкого матрица А должна быть положительно определена.


  1. alexejisma
    29.10.2024 16:01

    Почему выбор пал на точный метод вместо итерационного? Разве не будет расход памяти огромный?


    1. shevchenko_a_d Автор
      29.10.2024 16:01

      Вначале для своих задач я пробовал метод сопряжённых градиентов. Но он давал неважную точность, а увеличение количества итераций съедало преимущество по времени по сравнению с обычным LDLt. Далее я придумал убирать лидирующие нули. Получилось и быстрее, и точнее. Что касается памяти, то матрица записывается в компактном виде без лидирующих нулей, и в моей практике её размерность редко превосходила нескольких тысяч. В общем с памятью проблем не было.