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

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

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

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

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

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


person Community    schedule 07.08.2008    source источник


Ответы (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;
person Mark Harrison    schedule 07.08.2008
comment
как насчет производительности получения диапазона элементов из этого или проверки того, полностью ли диапазон находится в массиве? - person Ivan G.; 20.03.2012
comment
Реализация оператора ‹неверна. Рассмотрим тройной {1,2,3} и тройной {3,2,1}, ни один из них не будет меньше другого. Правильная реализация будет проверять x, затем y, затем z последовательно, а не все сразу. - person Www; 27.06.2014
comment
Поскольку это не было исправлено в течение длительного времени, я взял на себя смелость заменить его правильной реализацией. - person celtschk; 06.05.2015
comment
Секунды всего на миллион элементов? Это кажется довольно плохим. Вам следует подумать об использовании хеш-функции и unordered_map. Ознакомьтесь с github.com/victorprad/InfiniTAM. Они используют хэш ((x * 73856093u) ^ (y * 19349669u) ^ (z * 83492791u)) и могут интегрировать миллионы образцов в разреженная трехмерная сетка при хорошей частоте кадров. - person masterxilo; 11.03.2017

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

Однако создание строк для этого напрасно расточительно. Если размер неизвестен во время компиляции (и, следовательно, 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://stackoverflow.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
}
person Konrad Rudolph    schedule 02.09.2008
comment
ака. unordered_map - person Andrew; 12.10.2017
comment
@ Андрей Не совсем так. Этот ответ предшествует C ++ 11 и, следовательно, std::unordered_map, на долгое время. В настоящее время я бы рекомендовал использовать последнее, но он не заменяет мой ответ, поскольку std::vector, похоже, не обеспечивает подходящей реализации для std::hash. Тем не менее, если размер индекса на самом деле не является переменным (что маловероятно), std::unordered_map<std::tuple<…>> фактически должен работать из коробки (хотя интерфейс определенно можно улучшить). - person Konrad Rudolph; 12.10.2017
comment
@KonradRudolph, разве временная сложность unordered_map не коллизия, Big 0, 1? Если да, то разве это не то же самое с хеш-таблицей? - person Cloud Cho; 23.12.2018
comment
@CloudCho Не понимаю, что вы имеете в виду, но std::unordered_map хеш-таблица. - person Konrad Rudolph; 23.12.2018
comment
@KonradRudolph Я думал, вы предложили Vector, а не Unoreded_map для хеш-таблицы. - person Cloud Cho; 25.12.2018

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

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

person Nic Strong    schedule 21.08.2008

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

person Community    schedule 29.12.2014

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

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
person Mat Noguchi    schedule 19.08.2008

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

person nlucaroni    schedule 07.08.2008

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

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

Словарь ключей (DOK)

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

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

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

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

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

Сжатая разреженная строка (формат CSR, CRS или Йельского университета)

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

person Community    schedule 07.12.2016

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

person JSN    schedule 08.08.2008
comment
LAPACK - это библиотека для плотных матриц. Стандартный BLAS также предназначен для плотных матриц. Существует пакет Sparse BLAS (через NIST), но он отличается от стандартного BLAS. - person codehippo; 08.08.2009

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

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

person Community    schedule 27.09.2009

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

// 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, если вы хотите разумно эффективные «вставки» (изменение нуля на ненулевое).

person Community    schedule 23.06.2014

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

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.

person Community    schedule 12.10.2016