Как известно, представление графа в памяти преимущественно осуществляется двумя способами: матрицей смежности и списком смежности. Остановимся на первом из них.

Матрица смежности графа

Стоит отметить, что размер, выделяемый для хранения обыкновенной матрицы смежности, равняется n^2 где n- количество вершин в графе.

Рассмотрим ориентированный граф, состоящий из четырех вершин:

Ориентированный граф
Ориентированный граф

Матрица смежности для такого графа будет иметь вид: (INF означает отсутствие ребра)

INF

7

INF

5

INF

INF

INF

9

INF

INF

INF

INF

INF

INF

2

INF

Битовая матрица смежности

Предположим, что нас не интересуют веса ребер графа, то есть будем говорить лишь о наличии ребра из одной вершины в другую. В таком случае размер, выделяемый под матрицу смежности ориентированного графа, можно уменьшить в 8 раз, соответственно до: (n^2/8) + 1. Назовем такую матрицу смежности битовой матрицей смежности для графа. Изначально она инициализирована нулями.

  1. Рассмотрим функцию добавления значения в битовую матрицу смежности:

index = (line * numberOfVertices + column) / 8;
binaryMask = 128 >> ((line * numberOfVertices + column) % 8);
matrix[index] |= binaryMask;

Число 128 выбрано не просто так: его двоичное запись = 100000002. Здесь binaryMask представляет собой двоичную маску наличия ребра для текущей пары вершин line и column, формируемую побитовым сдвигом вправо. Дальнейшее выполнение "побитового или" позволяет добавить эту информацию в матрицу.

  1. Рассмотрим функцию получения значения из битовой матрицы смежности:

index = (line * numberOfVertices + column) / 8;
binaryMask = 128 >> ((line * numberOfVertices + column) % 8);
binaryMask &= matrix[index];

Отличий от функции добавления значения в матрицу немного: вместо того, чтобы совершать операцию "побитого или" для матрицы, мы совершаем операцию "побитового и" для двоичной маски, которая даёт возможность узнать информацию о наличии ребра между вершинами line и column: если binaryMask == 0, то ребра нет, иначе - ребро есть.

Замечу, что в функциях, описанных выше, я полагал по определению, что line и column - это вершины графа, номера которых >= 1.

Пример работы с битовой матрицей смежности:

Пусть дан следующий ориентированный граф:

Произвольный ориентированный граф
Произвольный ориентированный граф

Размер, выделяемый для хранения битовой матрицы смежности будет равняться: size = (4^2 /8)+1 = 3

А значит такая матрица имеет вид: (индексация ячеек матрицы с нуля)

0

0

0

Добавим ребро 1-2:
index = (1 * 4 + 2) / 8 = 0
binaryMask = 128 >> 6 = 2 = 000000102

0 0 0 0 0 0 1 0

0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0

Добавим ребро 1-4:
index = (1 * 4 + 4) / 8 = 1
binaryMask = 128 >> 0 = 128 = 100000002

0 0 0 0 0 0 1 0

1 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0

Добавим ребро 2-4:
index = (2 * 4 + 4) / 8 = 1
binaryMask = 128 >> 4 = 8 = 000010002

0 0 0 0 0 0 1 0

1 0 0 0 1 0 0 0

0 0 0 0 0 0 0 0

Наконец, добавим ребро 4-3:
index = (4 * 4 + 3) / 8 = 2
binaryMask = 128 >> 3 = 16 = 000100002

0 0 0 0 0 0 1 0

1 0 0 0 1 0 0 0

0 0 0 1 0 0 0 0

Теперь, используя полученную матрицу смежности, попробуем ответить на следующий вопрос: есть ли в этом графе ребро между вершинами 1-4?
index(1;4) = (1*4+4)/8=1
binaryMask(1;4) = 128 >> 0 = 128

10001000 & 10000000 = 1 => ребро есть.

А между вершинами 1-3?
index(1;3) = (1*4+3)/8=0
binaryMask(1;3) = 128 >> 7 = 1

00000010 & 00000001 = 0 => ребра нет.


Хранение половины (треугольной) матрицы смежности

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

Неориентированный граф
Неориентированный граф

INF

4

8

4

INF

17

8

17

INF

Размер, выделяемый под треугольную матрицу смежности неориентированного графа, будет равняться: n * (n+1)/2, если нам нужно хранить её главную диагональ.

Треугольная матрицы смежности такого графа примет вид:

INF

4

INF

8

17

INF

  1. Рассмотрим функцию получения индекса для этой матрицы:

maxVertex = max(line, column);
minVertex = min(line, column);
index = (maxVertex * (maxVertex - 1) / 2) + minVertex - 1;
  1. Добавление и получение значений из треугольной матрицы смежности аналогично обычной матрице смежности, но по индексу, полученному выше.

Замечу, что здесь я так же предполагал line и column вершинами графа, номера которых >= 1.

Итоги

Эту статью я написал, потому что в своё время сам нуждался в данной информации. Благодарю всех читателей и надеюсь, что помог кому-нибудь.


Визуализацию графов проводил с помощью сайта: graphonline.ru.

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


  1. Rsa97
    12.04.2022 23:01
    +4

    Не говоря уже о том, что вы изобрели велосипед, вы ещё и неправильно его изобрели.
    Добавим по вашей формуле ребро (а вовсе не вершину!) 3-4
    index = (3 * 3 + 4) / 8 = 13 / 8 = 1
    mask = 128 >> ((3 * 3 + 4) % 8) = 128 >> (13 % 8) = 128 >> 5 = 4
    Теперь добавим ребро 4-1
    index = (4 * 3 + 1) / 8 = 13 / 8 = 1
    mask = 128 >> ((4 * 3 + 1) % 8) = 128 >> (13 % 8) = 128 >> 5 = 4
    Опаньки. Это же одно и то же ребро!
    Проверять надо формулы перед публикацией.
    Вовсе не случайно в программировании используют индексы, начинающиеся с нуля.
    Если взять граф из N вершин, пронумерованных с единицы, то правильная формула для ребра X->Y примет вид:
    bit = (X — 1) * N + (Y — 1)
    index = bit / 8
    mask = 1 << (bit % 8)
    Да, и использование сдвига единицы, а не магической 128, это тоже общепринятая практика, так как биты нумеруются с младшего (нулевого).


    1. murloteg Автор
      13.04.2022 07:45
      +1

      Вы правы, я ошибся в примере. Спасибо, что указали на это. Про магическую константу учту :)


      1. randomsimplenumber
        13.04.2022 09:55

        Если уж речь о хранении достаточно сложного объекта, и пишется какой то код на С(++), стоило бы оформить это в виде структуры или класса.


  1. Tiriet
    13.04.2022 08:32

    Есть у математиков такое понятие, как Sparse matrix- разреженные матрицы. Матрица, большая часть элементов которой- нули. Они очень распространены в задачах вычислительной математики и численных методах математической физики, в частности, любая практически задача, решаемая методом конечных элементов (МКЭ) в итоге сводится к разреженной матрице и какой-то операции с ней (как правило- итерационного решения, которое в свою очередь, сводится к многократному перемножению таких матриц). Мат-аппарат этого дела довольно хорошо развит, а то, что изложили Вы- очень напоминает "портрет матрицы".

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


    1. aleex
      13.04.2022 13:32

      Да, это распространенная вещь для задач с размерностью выше единицы. И они реализованы например в Eigen, MTL, данные в соответствующем формате(CSR или CSC) идут на вход функций Intel MKL.


  1. wataru
    13.04.2022 16:59
    +1

    Вся статья заменяется одним предлождением: "матрица смежности состоит из чисел 0 и 1. Их можно хранить в битах большого массива". Или вообще одной фразой "битовое сжатие".


  1. VladimirKryukov
    13.04.2022 17:41

    Спасибо