Как лучше всего создать разреженный массив в C++?

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

Короче говоря, мне нужно отслеживать относительно небольшое количество значений (обычно значение 1, а в редких случаях более 1) в море нулей в матрице (многомерный массив).

Разреженный массив позволяет пользователю хранить небольшое количество значений и предполагать, что все неопределенные записи являются предустановленными значениями. Поскольку физически невозможно сохранить все значения в памяти, мне нужно хранить только несколько ненулевых элементов. Это может быть несколько миллионов записей.

Скорость - это огромный приоритет, и я также хотел бы динамически выбирать количество переменных в классе во время выполнения.

В настоящее время я работаю над системой, которая использует двоичное дерево поиска (b-дерево) для хранения записей. Кто-нибудь знает систему получше?

Ответов (11)

Решение

Для C++ хорошо работает карта. Несколько миллионов объектов не проблема. 10 миллионов элементов заняли около 4,4 секунды и около 57 мегабайт на моем компьютере.

Мое тестовое приложение выглядит следующим образом:

#include <stdio.h>
#include <stdlib.h>
#include <map>

class triple {
public:
    int x;
    int y;
    int z;
    bool operator<(const triple &other) const {
        if (x < other.x) return true;
        if (other.x < x) return false;
        if (y < other.y) return true;
        if (other.y < y) return false;
        return z < other.z;
    }
};

int main(int, char**)
{
    std::map<triple,int> data;
    triple point;
    int i;

    for (i = 0; i < 10000000; ++i) {
        point.x = rand();
        point.y = rand();
        point.z = rand();
        //printf("%d %d %d %d\n", i, point.x, point.y, point.z);
        data[point] = i;
    }
    return 0;
}

Теперь, чтобы динамически выбирать количество переменных, самое простое решение - представить индекс в виде строки , а затем использовать строку в качестве ключа для карты. Например, элемент, расположенный в [23] [55], может быть представлен строкой «23,55». Мы также можем расширить это решение для более высоких измерений; например, для трех измерений произвольный индекс будет выглядеть как «34,45,56». Простая реализация этой техники выглядит следующим образом:

std::map data<string,int> data;
char ix[100];

sprintf(ix, "%d,%d", x, y); // 2 vars
data[ix] = i;

sprintf(ix, "%d,%d,%d", x, y, z); // 3 vars
data[ix] = i;

Eigen - это библиотека линейной алгебры C++, в которой реализована разреженная матрица. Он даже поддерживает матричные операции и решатели (факторизация LU и т. Д.), Которые оптимизированы для разреженных матриц.

Я бы посоветовал сделать что-то вроде:

typedef std::tuple<int, int, int> coord_t;
typedef boost::hash<coord_t> coord_hash_t;
typedef std::unordered_map<coord_hash_t, int, c_hash_t> sparse_array_t;

sparse_array_t the_data;
the_data[ { x, y, z } ] = 1; /* list-initialization is cool */

for( const auto& element : the_data ) {
    int xx, yy, zz, val;
    std::tie( std::tie( xx, yy, zz ), val ) = element;
    /* ... */
}

Чтобы сохранить ваши данные разреженными, вы можете захотеть написать подкласс unorderd_map, итераторы которого автоматически пропускают (и стирают) любые элементы со значением 0.

Вот относительно простая реализация, которая должна обеспечивать разумно быстрый поиск (с использованием хеш-таблицы), а также быструю итерацию по ненулевым элементам в строке / столбце.

// Copyright 2014 Leo Osvald
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef UTIL_IMMUTABLE_SPARSE_MATRIX_HPP_
#define UTIL_IMMUTABLE_SPARSE_MATRIX_HPP_

#include <algorithm>
#include <limits>
#include <map>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>

// A simple time-efficient implementation of an immutable sparse matrix
// Provides efficient iteration of non-zero elements by rows/cols,
// e.g. to iterate over a range [row_from, row_to) x [col_from, col_to):
//   for (int row = row_from; row < row_to; ++row) {
//     for (auto col_range = sm.nonzero_col_range(row, col_from, col_to);
//          col_range.first != col_range.second; ++col_range.first) {
//       int col = *col_range.first;
//       // use sm(row, col)
//       ...
//     }
template<typename T = double, class Coord = int>
class SparseMatrix {
  struct PointHasher;
  typedef std::map< Coord, std::vector<Coord> > NonZeroList;
  typedef std::pair<Coord, Coord> Point;

 public:
  typedef T ValueType;
  typedef Coord CoordType;
  typedef typename NonZeroList::mapped_type::const_iterator CoordIter;
  typedef std::pair<CoordIter, CoordIter> CoordIterRange;

  SparseMatrix() = default;

  // Reads a matrix stored in MatrixMarket-like format, i.e.:
  // <num_rows> <num_cols> <num_entries>
  // <row_1> <col_1> <val_1>
  // ...
  // Note: the header (lines starting with '%' are ignored).
  template<class InputStream, size_t max_line_length = 1024>
  void Init(InputStream& is) {
    rows_.clear(), cols_.clear();
    values_.clear();

    // skip the header (lines beginning with '%', if any)
    decltype(is.tellg()) offset = 0;
    for (char buf[max_line_length + 1];
         is.getline(buf, sizeof(buf)) && buf[0] == '%'; )
      offset = is.tellg();
    is.seekg(offset);

    size_t n;
    is >> row_count_ >> col_count_ >> n;
    values_.reserve(n);
    while (n--) {
      Coord row, col;
      typename std::remove_cv<T>::type val;
      is >> row >> col >> val;
      values_[Point(--row, --col)] = val;
      rows_[col].push_back(row);
      cols_[row].push_back(col);
    }
    SortAndShrink(rows_);
    SortAndShrink(cols_);
  }

  const T& operator()(const Coord& row, const Coord& col) const {
    static const T kZero = T();
    auto it = values_.find(Point(row, col));
    if (it != values_.end())
      return it->second;
    return kZero;
  }

  CoordIterRange
  nonzero_col_range(Coord row, Coord col_from, Coord col_to) const {
    CoordIterRange r;
    GetRange(cols_, row, col_from, col_to, &r);
    return r;
  }

  CoordIterRange
  nonzero_row_range(Coord col, Coord row_from, Coord row_to) const {
    CoordIterRange r;
    GetRange(rows_, col, row_from, row_to, &r);
    return r;
  }

  Coord row_count() const { return row_count_; }
  Coord col_count() const { return col_count_; }
  size_t nonzero_count() const { return values_.size(); }
  size_t element_count() const { return size_t(row_count_) * col_count_; }

 private:
  typedef std::unordered_map<Point,
                             typename std::remove_cv<T>::type,
                             PointHasher> ValueMap;

  struct PointHasher {
    size_t operator()(const Point& p) const {
      return p.first << (std::numeric_limits<Coord>::digits >> 1) ^ p.second;
    }
  };

  static void SortAndShrink(NonZeroList& list) {
    for (auto& it : list) {
      auto& indices = it.second;
      indices.shrink_to_fit();
      std::sort(indices.begin(), indices.end());
    }

    // insert a sentinel vector to handle the case of all zeroes
    if (list.empty())
      list.emplace(Coord(), std::vector<Coord>(Coord()));
  }

  static void GetRange(const NonZeroList& list, Coord i, Coord from, Coord to,
                       CoordIterRange* r) {
    auto lr = list.equal_range(i);
    if (lr.first == lr.second) {
      r->first = r->second = list.begin()->second.end();
      return;
    }

    auto begin = lr.first->second.begin(), end = lr.first->second.end();
    r->first = lower_bound(begin, end, from);
    r->second = lower_bound(r->first, end, to);
  }

  ValueMap values_;
  NonZeroList rows_, cols_;
  Coord row_count_, col_count_;
};

#endif  /* UTIL_IMMUTABLE_SPARSE_MATRIX_HPP_ */

Для простоты это так immutable, но вы можете сделать его изменяемым; обязательно измените std::vector на, std::set если вы хотите разумно эффективные «вставки» (изменение нуля на ненулевое).

Полный список решений можно найти в википедии. Для удобства я процитировал соответствующие разделы следующим образом.

https://en.wikipedia.org/wiki/Sparse_matrix#Dictionary_of_keys_.28DOK.29

Словарь ключей (ДОК)

DOK состоит из словаря, который отображает пары (строка, столбец) на значения элементов. Элементы, отсутствующие в словаре, считаются равными нулю. Формат хорош для постепенного построения разреженной матрицы в случайном порядке, но плохой для перебора ненулевых значений в лексикографическом порядке. Обычно матрица создается в этом формате, а затем преобразуется в другой, более эффективный формат для обработки. [1]

Список списков (LIL)

LIL хранит по одному списку на строку, каждая запись содержит индекс столбца и значение. Обычно эти записи отсортированы по индексу столбца для более быстрого поиска. Это еще один формат, подходящий для построения инкрементальной матрицы. [2]

Список координат (COO)

COO хранит список кортежей (строка, столбец, значение). В идеале записи сортируются (по индексу строки, затем по индексу столбца), чтобы сократить время произвольного доступа. Это еще один формат, который подходит для построения инкрементальной матрицы. [3]

Сжатая разреженная строка (формат CSR, CRS или Yale)

Формат сжатой разреженной строки (CSR) или сжатого хранилища строк (CRS) представляет матрицу M тремя (одномерными) массивами, которые соответственно содержат ненулевые значения, экстенты строк и индексы столбцов. Он похож на COO, но сжимает индексы строк, отсюда и название. Этот формат обеспечивает быстрый доступ к строкам и умножение матрицы на вектор (Mx).

Хеш-таблицы имеют быструю вставку и поиск. Вы можете написать простую хеш-функцию, поскольку знаете, что будете иметь дело только с целочисленными парами в качестве ключей.

Лучший способ реализовать разреженные матрицы - не реализовывать их - по крайней мере, не самостоятельно. Я бы посоветовал BLAS (который, я думаю, является частью LAPACK), который может обрабатывать действительно огромные матрицы.

Небольшая деталь в сравнении индексов. Вам нужно провести лексикографическое сравнение, иначе:

a= (1, 2, 1); b= (2, 1, 2);
(a<b) == (b<a) is true, but b!=a

Изменить: так что сравнение, вероятно, должно быть:

return lhs.x<rhs.x
    ? true 
    : lhs.x==rhs.x 
        ? lhs.y<rhs.y 
            ? true 
            : lhs.y==rhs.y
                ? lhs.z<rhs.z
                : false
        : false

Boost имеет шаблонную реализацию BLAS под названием uBLAS, которая содержит разреженную матрицу.

https://www.boost.org/doc/libs/release/libs/numeric/ublas/doc/index.htm

В принятом ответе рекомендуется использовать строки для представления многомерных индексов.

Однако создание строк для этого напрасно расточительно. Если размер неизвестен во время компиляции (и, следовательно std::tuple, не работает), std::vector хорошо работает как индекс, как с хэш-картами, так и с упорядоченными деревьями. Ибо std::map это почти тривиально:

#include <vector>
#include <map>

using index_type = std::vector<int>;

template <typename T>
using sparse_array = std::map<index_type, T>;

Для std::unordered_map (или аналогичных словарей на основе хеш-таблиц) это немного больше работы, поскольку std::vector не специализируется std::hash :

#include <vector>
#include <unordered_map>
#include <numeric>

using index_type = std::vector<int>;

struct index_hash {
    std::size_t operator()(index_type const& i) const noexcept {
        // Like boost::hash_combine; there might be some caveats, see
        // <https://answacode.com/a/50978188/1968>
        auto const hash_combine = [](auto seed, auto x) {
            return std::hash<int>()(x) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
        };
        return std::accumulate(i.begin() + 1, i.end(), i[0], hash_combine);
    }
};

template <typename T>
using sparse_array = std::unordered_map<index_type, T, index_hash>;

В любом случае использование одинаково:

int main() {
    using i = index_type;

    auto x = sparse_array<int>();
    x[i{1, 2, 3}] = 42;
    x[i{4, 3, 2}] = 23;

    std::cout << x[i{1, 2, 3}] + x[i{4, 3, 2}] << '\n'; // 65
}

Поскольку только значения с [a] [b] [c] ... [w] [x] [y] [z] имеют значение, мы сохраняем только сам индекс, а не значение 1, которое почти везде - всегда то же самое + нет возможности его хешировать. Заметив, что проклятие размерности присутствует, предложите использовать какой-нибудь зарекомендовавший себя инструмент NIST или Boost, по крайней мере, прочитать источники для этого, чтобы избежать ненужной ошибки.

Если в работе необходимо зафиксировать распределения временной зависимости и параметрические тенденции неизвестных наборов данных, то карта или B-дерево с однозначным корнем, вероятно, нецелесообразны. Мы можем хранить только сами индексы, хешированные, если упорядочение (чувствительность для представления) может подчиняться сокращению временной области во время выполнения для всех значений 1. Поскольку ненулевых значений, отличных от единицы, немного, очевидным кандидатом для них является любая структура данных, которую вы можете легко найти и понять. Если набор данных действительно имеет размер огромной вселенной, я предлагаю какое-то скользящее окно, которое самостоятельно управляет файлом / диском / постоянным-io, перемещая части данных в область видимости по мере необходимости. (написание кода, который вы можете понять) Если вы обязуетесь предоставить реальное решение рабочей группе,