Как известно, представление графа в памяти преимущественно осуществляется двумя способами: матрицей смежности и списком смежности. Остановимся на первом из них.
Матрица смежности графа
Стоит отметить, что размер, выделяемый для хранения обыкновенной матрицы смежности, равняется где - количество вершин в графе.
Рассмотрим ориентированный граф, состоящий из четырех вершин:
Матрица смежности для такого графа будет иметь вид: (INF
означает отсутствие ребра)
|
7 |
|
5 |
|
|
|
9 |
|
|
|
|
|
|
2 |
|
Битовая матрица смежности
Предположим, что нас не интересуют веса ребер графа, то есть будем говорить лишь о наличии ребра из одной вершины в другую. В таком случае размер, выделяемый под матрицу смежности ориентированного графа, можно уменьшить в 8 раз, соответственно до: . Назовем такую матрицу смежности битовой матрицей смежности для графа. Изначально она инициализирована нулями.
Рассмотрим функцию добавления значения в битовую матрицу смежности:
index = (line * numberOfVertices + column) / 8;
binaryMask = 128 >> ((line * numberOfVertices + column) % 8);
matrix[index] |= binaryMask;
Число 128 выбрано не просто так: его двоичное запись = 10000000
2. Здесь binaryMask представляет собой двоичную маску наличия ребра для текущей пары вершин line и column, формируемую побитовым сдвигом вправо. Дальнейшее выполнение "побитового или" позволяет добавить эту информацию в матрицу.
Рассмотрим функцию получения значения из битовой матрицы смежности:
index = (line * numberOfVertices + column) / 8;
binaryMask = 128 >> ((line * numberOfVertices + column) % 8);
binaryMask &= matrix[index];
Отличий от функции добавления значения в матрицу немного: вместо того, чтобы совершать операцию "побитого или" для матрицы, мы совершаем операцию "побитового и" для двоичной маски, которая даёт возможность узнать информацию о наличии ребра между вершинами line и column: если binaryMask == 0, то ребра нет, иначе - ребро есть.
Замечу, что в функциях, описанных выше, я полагал по определению, что line и column - это вершины графа, номера которых >= 1.
Пример работы с битовой матрицей смежности:
Пусть дан следующий ориентированный граф:
Размер, выделяемый для хранения битовой матрицы смежности будет равняться:
А значит такая матрица имеет вид: (индексация ячеек матрицы с нуля)
0 |
0 |
0 |
Добавим ребро 1-2:
2
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:
2
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:
2
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:
2
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
?
10001000
& 10000000
= 1
=> ребро есть.
А между вершинами 1-3
?
00000010
& 00000001
= 0
=> ребра нет.
Хранение половины (треугольной) матрицы смежности
В случае неориентированного графа мы получаем симметричную относительно главной диагонали матрицу:
|
4 |
8 |
4 |
|
17 |
8 |
17 |
|
Размер, выделяемый под треугольную матрицу смежности неориентированного графа, будет равняться: , если нам нужно хранить её главную диагональ.
Треугольная матрицы смежности такого графа примет вид:
|
4 |
|
8 |
17 |
|
Рассмотрим функцию получения индекса для этой матрицы:
maxVertex = max(line, column);
minVertex = min(line, column);
index = (maxVertex * (maxVertex - 1) / 2) + minVertex - 1;
Добавление и получение значений из треугольной матрицы смежности аналогично обычной матрице смежности, но по индексу, полученному выше.
Замечу, что здесь я так же предполагал line и column вершинами графа, номера которых >= 1.
Итоги
Эту статью я написал, потому что в своё время сам нуждался в данной информации. Благодарю всех читателей и надеюсь, что помог кому-нибудь.
Визуализацию графов проводил с помощью сайта: graphonline.ru.
Комментарии (7)
Tiriet
13.04.2022 08:32Есть у математиков такое понятие, как Sparse matrix- разреженные матрицы. Матрица, большая часть элементов которой- нули. Они очень распространены в задачах вычислительной математики и численных методах математической физики, в частности, любая практически задача, решаемая методом конечных элементов (МКЭ) в итоге сводится к разреженной матрице и какой-то операции с ней (как правило- итерационного решения, которое в свою очередь, сводится к многократному перемножению таких матриц). Мат-аппарат этого дела довольно хорошо развит, а то, что изложили Вы- очень напоминает "портрет матрицы".
У меня складывается ощущение, что велосипедостроение получило какой-то бонус в небесной канцелярии и стало массовым увлечением на хабре.
aleex
13.04.2022 13:32Да, это распространенная вещь для задач с размерностью выше единицы. И они реализованы например в Eigen, MTL, данные в соответствующем формате(CSR или CSC) идут на вход функций Intel MKL.
wataru
13.04.2022 16:59+1Вся статья заменяется одним предлождением: "матрица смежности состоит из чисел 0 и 1. Их можно хранить в битах большого массива". Или вообще одной фразой "битовое сжатие".
Rsa97
Не говоря уже о том, что вы изобрели велосипед, вы ещё и неправильно его изобрели.
Добавим по вашей формуле ребро (а вовсе не вершину!) 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, это тоже общепринятая практика, так как биты нумеруются с младшего (нулевого).
murloteg Автор
Вы правы, я ошибся в примере. Спасибо, что указали на это. Про магическую константу учту :)
randomsimplenumber
Если уж речь о хранении достаточно сложного объекта, и пишется какой то код на С(++), стоило бы оформить это в виде структуры или класса.