поделитесь, пожалуйста, мыслями или идеями как можно эффективно представлять множества целых и некоторые операции над ними.
Размер: на текущий момент можно ограничиться до 64-bit числами.
Варианты элементов множеств: знаковое целое (8/16/32/64), беззнаковое целое (8/16/32/64), диаппазон целых с заданным шагом.
Операции: +,-,*,/,&,|,^
На текущий момент пытаюсь сделать это через std::optional<std::list<std::variant<*>>>>, но как-то громоздко получается, что ли.
Здравствуйте, pva, Вы писали:
pva>Привет,
pva>поделитесь, пожалуйста, мыслями или идеями как можно эффективно представлять множества целых и некоторые операции над ними. pva>Размер: на текущий момент можно ограничиться до 64-bit числами. pva>Варианты элементов множеств: знаковое целое (8/16/32/64), беззнаковое целое (8/16/32/64), диаппазон целых с заданным шагом. pva>Операции: +,-,*,/,&,|,^
pva>На текущий момент пытаюсь сделать это через std::optional<std::list<std::variant<*>>>>, но как-то громоздко получается, что ли.
Я, признаться, не совсем понял условие задачи. Почему list, a не set. Зачем нужен optional — почему нельзя просто проверить коллекцию на пустоту. Ну и наконец, зачем нужен variant, почему бы просто не выбрать целый тип максимальной ёмкости.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>Почему list, a не set.
Один из возможных элементов коллекции "диаппазон целых с заданным шагом". И сколько будет занимать такой set для достаточно обширных диаппазонов?
R>Зачем нужен optional — почему нельзя просто проверить коллекцию на пустоту.
Пустое множество — допустимый вариант, в отличие от несуществующего.
R>Ну и наконец, зачем нужен variant, почему бы просто не выбрать целый тип максимальной ёмкости.
Одна из причин уже указана выше — тип "IntRange". Вторая — размер важен, 8l, 8u и 8ull — разные элементы.
Здравствуйте, pva, Вы писали:
pva>Один из возможных элементов коллекции "диаппазон целых с заданным шагом". И сколько будет занимать такой set для достаточно обширных диаппазонов? pva>Пустое множество — допустимый вариант, в отличие от несуществующего. pva>Одна из причин уже указана выше — тип "IntRange". Вторая — размер важен, 8l, 8u и 8ull — разные элементы.
Ещё пара уточнений. Можно ли ввести какое-либо упорядочение элементов множества. И допускается ли дублирование элементов. Например, можно ли добавить в множество диапазон [1-10], а потом еще отдельно, например, 3.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, pva, Вы писали:
pva>На текущий момент пытаюсь сделать это через std::optional<std::list<std::variant<*>>>>, но как-то громоздко получается, что ли.
Как минимум для начала стоит посмотреть на boost icl.
Здравствуйте, pva, Вы писали:
pva>Вторая — размер важен
так std::variant всё равно будет занимать место максимального элемента.
Так std::variant<i8, u8, u64> будет занимать все 64 бита + ещё поле селектора. После выравнивания получите все 16 байт.
Здравствуйте, sergii.p, Вы писали:
pva>>Вторая — размер важен
SP>так std::variant всё равно будет занимать место максимального элемента. SP>Так std::variant<i8, u8, u64> будет занимать все 64 бита + ещё поле селектора. После выравнивания получите все 16 байт.
Не, он не в этом смыле. Имеется в виду, что разные типы — это разные элементы множества даже если их значения совпадают. Хотя при таком способе хранения, оверхед по памяти налицо, конечно.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>Ещё пара уточнений. Можно ли ввести какое-либо упорядочение элементов множества.
Да, можно. Выше я немного приврал что тип делает элементы строго отличными друг от друга. Попробую объяснить ниже.
Суть в том, что это a la дерево "symbolic calculation", в котором для узлов в процессе вычисления определяется множество вероятно принимаемых значений.
Например, в какой-то момент решатель видит что результат op(a, b)->bool. Соответственно, допустимые значения {0,1}. not({0,1})={1,0}. Порядок следования важен.
И так далее, двигаясь к корню мы в результате получаем некоторое множество (область) допустимых значений "функций" (МДЗ).
При этом, технически есть еще ограничение размера типа (битовая маска — Mask).
Грубо говоря, если у нас есть
MOV al,0a5h
SHL ax,3
То по итогу у нас Mask=11 бит, МДЗ={0x528} (технически это rax=0x528 & Mask). Теперь если сделать
AND ax,01a5h
Результатом будет Mask=16 бит, MДЗ={0x120}. Неопределенность затронутой части не играет роли. Но если бы мы _вместо_ предыдущего сделали
AND ax,0a5a5h
Результатом будет Mask=11 бит, а вот MДЗ={}, поскольку затронуты были _значимые_ неопределенные биты.
В общем, это напоминает проблемы с неинициализированной памятью в сях, обращение к которой дает неопределенный результат.
R>И допускается ли дублирование элементов. Например, можно ли добавить в множество диапазон [1-10], а потом еще отдельно, например, 3.
Дублирование элементов не критично. Их можно отсеивать со временем.
Пока писал сам осознал несколько дополнительных подводных камней. Вероятно, есть смысл посмотреть специализированные решатели типа z3 и интегрировать их.
Здравствуйте, pva, Вы писали:
pva>Здравствуйте, rg45, Вы писали:
R>>Ещё пара уточнений. Можно ли ввести какое-либо упорядочение элементов множества. pva>Да, можно. Выше я немного приврал что тип делает элементы строго отличными друг от друга. Попробую объяснить ниже. pva>Суть в том, что это a la дерево "symbolic calculation", в котором для узлов в процессе вычисления определяется множество вероятно принимаемых значений. pva>Например, в какой-то момент решатель видит что результат op(a, b)->bool. Соответственно, допустимые значения {0,1}. not({0,1})={1,0}. Порядок следования важен. pva>И так далее, двигаясь к корню мы в результате получаем некоторое множество (область) допустимых значений "функций" (МДЗ). pva>При этом, технически есть еще ограничение размера типа (битовая маска — Mask). pva>Грубо говоря, если у нас есть
MOV al,0a5h
pva>SHL ax,3
pva>То по итогу у нас Mask=11 бит, МДЗ={0x528} (технически это rax=0x528 & Mask). Теперь если сделать
AND ax,01a5h
pva>Результатом будет Mask=16 бит, MДЗ={0x120}. Неопределенность затронутой части не играет роли. Но если бы мы _вместо_ предыдущего сделали
AND ax,0a5a5h
pva>Результатом будет Mask=11 бит, а вот MДЗ={}, поскольку затронуты были _значимые_ неопределенные биты. pva>В общем, это напоминает проблемы с неинициализированной памятью в сях, обращение к которой дает неопределенный результат.
R>>И допускается ли дублирование элементов. Например, можно ли добавить в множество диапазон [1-10], а потом еще отдельно, например, 3. pva>Дублирование элементов не критично. Их можно отсеивать со временем.
pva>Пока писал сам осознал несколько дополнительных подводных камней. Вероятно, есть смысл посмотреть специализированные решатели типа z3 и интегрировать их.
Я тут набросал эскиз — просто для демонстрации идеи. Можно реализовать шаблон класса множества ориентированного на работу с отдельным целочисленным типом а потом сложить все такие множества в общую мапу с ключом std::type_index. Мапу инкапсулировать, конечно же. Для единообразия и упрощения простые числа можно хранить как интервалы. И это будет не хуже, чем вариант с variant (пардон за каламбур).
Вот с итераторами будет только лёгкий геморрой. Потому что итерироваться придется по элементам с разными типами. Но юзабельные компромисные решения тоже можно будет искать. В т.ч. и визиторы.
#include <any>
#include <concepts>
#include <iostream>
#include <map>
#include <set>
#include <typeindex>
template <std::integral T>
struct Interval // Вместо этого можно заюзать boost::interval
{
T lower{};
T upper{};
};
template <std::integral T>
class Set
{
public:
using Interval = Interval<T>;
void insert(T t) {insert({t, t});} // Простые числа тоже хранятся как интервалыvoid insert(const Interval&); // Вот тут нужно будет реализовать слияние интерваловbool contains(T t) const {
const auto it = m_set.lower_bound(t);
return it != m_set.end() and it->lower <= t;
}
private:
struct Comparer { // Хитрый компараторusing is_transparent = void;
bool operator()(const Interval& a, const Interval& b) const {return a.upper < b.upper;}
bool operator()(const Interval& a, T t) const { return a.upper < t; }
bool operator()(T t, const Interval& a) const { return t < a.upper; }
};
std::set<Interval, Comparer> m_set;
};
class SuperSet
{
public:
void insert(std::integral auto t) {get<decltype(t)>().insert(t);}
template<std::integral T> bool contains(T t) const {return get<T>() and get<T>()->contains(t);}
private:
template <std::integral T> Set<T>& get () {
std::any& any = m_pool[typeid(T)];
return any.has_value() ? std::any_cast<Set<T>&>(any) : any.emplace<Set<T>>();
}
template <std::integral T> const Set<T>* get() const;
std::map<std::type_index, std::any> m_pool;
};
int main() {
SuperSet s;
s.contains(42);
}
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, pva, Вы писали:
pva>поделитесь, пожалуйста, мыслями или идеями как можно эффективно представлять множества целых и некоторые операции над ними. pva>Размер: на текущий момент можно ограничиться до 64-bit числами.
Эффективность сильно привязана к предметной области. Что за операции, какие характерные размеры этих множеств, какие характерные диапазоны...
В самом худшем случае ведь это попросту битовый вектор размером в 2^64 элемента — с хаотично расставленными там единичками и ноликами.
Память жрёт, зато проверка вхождения мгновенная.
Можно попробовать подойти с такой стороны.
Множество изоморфно предикату "входит ли элементв в него".
А предикаты можно строить как дерево вычислений.
Листья в этом дереве — это
— сплошной диапазон
— битсет в диапазоне
— упорядоченный массив в диапазоне
Операция над множествами — или сразу упрощается (например, пересечение диапазонов — это диапазон, объединение смежных диапазонов — опять диапазон), или формирует узел дерева, который будет вычислять функцию вхождения каждый раз.
Дерево может иметь жёсткую структуру — дизъюнктивную нормальную форму, например.
Или разумное ограничение по высоте.
Или какой-то умный балансировщик, эвристически определяющий, что лучше — выращивать дерево или форсировать вычисление.
Здравствуйте, pva, Вы писали:
pva>Привет,
pva>поделитесь, пожалуйста, мыслями или идеями как можно эффективно представлять множества целых и некоторые операции над ними. pva>Размер: на текущий момент можно ограничиться до 64-bit числами. pva>Варианты элементов множеств: знаковое целое (8/16/32/64), беззнаковое целое (8/16/32/64), диаппазон целых с заданным шагом. pva>Операции: +,-,*,/,&,|,^
pva>На текущий момент пытаюсь сделать это через std::optional<std::list<std::variant<*>>>>, но как-то громоздко получается, что ли.
А как еще будут запросы? (Кроме операций со множествами?)
(1) Проверка принадлежности числа множеству. (2) Число элементов множества. (3) число элементов, попадающих в диапазон. (4) перечислить все элементы. (5) проверить что множество не пусто ... ?
Если никакие запросу не предусмотрены, можно ничего не хранить.
Для диапазонов с шагом, каков период шага? (Порядка единиц / десятоко / ... / миллионов ?)
Каковы типичные размеры диапазонов? Бывают ли на всю числовую ось шириной?
Как много операций происходит прежде чем будет сделан запрос?
Как соотносятся частоты запросов (принадлежность элемента (?)) и частоты операций над множествами?
Часто ли диапазоны пересекаются?
Варианты:
1) Храним дерево операций, с помощью которых получено множество.
Можно тоже самое на shared_ptr, для исключения копирования, или если нужно распечатать дерево, например.
Минусы: никакой оптимизации, если при промежуточных вычислениях, имеет место пустое множество. Не дружественно к памяти. Но не факт, что такая оптимизация нужна. Не дружественно к отладчику.
2) Храним множество, как набор точек и набор диапазонов
template<typename T>
struct points_set
{
struct range{ T begin, end, step; };
std::flat_set<T> points; // или unordered
std::vector<range> ranges; // Можно сгруппировать по делителям / остаткам от деления если делители часто повторяются
};
Минусы: если операция "не" — очень дорогая, особенно если шаг большой. По диапазонам вынуждены ходить полным перебором. (Предполагается, что диапазонов много меньше чем точек.)
3) Хранить битовую маску для значимого под диапазона данных + границы диапазона + значение для всех запросов за пределами диапазона.
template<typename T>
struct points_set
{
bool out_of_range_value=flase; // это поле нужно только для поддержки "не"
T begin, end;
std::vector<bool> subrange_points;
bool contains(T i) const { return ((i>=begin) && (i<end)) ? subrange_points.at(i-begin) : out_of_range_value; }
};
4) точку рассмотрим как диапазон единичной длины. Пересекающиеся (по началу/концу) диапазоны, делим на либо с совпадающими концами, либо на не пересекающиеся.
Здравствуйте, Chorkov, Вы писали:
pva>Операции: +,-,*,/,&,|,^ C>А как еще будут запросы? (Кроме операций со множествами?)
Ха, спасибо за вопрос! Действительно, я не учел разночтения. Это не операции над объектами "множество", а поэлементные операции. C>(1) Проверка принадлежности числа множеству. (2) Число элементов множества. (3) число элементов, попадающих в диапазон. (4) перечислить все элементы. (5) проверить что множество не пусто ... ?
Здесь достаточно стандартного набора: add, remove, contains, iterate, size
Логические операции не нужны, кроме OR (merge).
C>Для диапазонов с шагом, каков период шага? (Порядка единиц / десятоко / ... / миллионов ?) C>Каковы типичные размеры диапазонов? Бывают ли на всю числовую ось шириной?
Опыты показали что диаппазоны, вероятно, можно выкинуть. Типовой размер множества — десяток-другой элементов, редко до тысячи.
Хотя, конечно, в некоторых случаях они были бы полезны. Но реализовать над ними базовую битовую арифметику будет долго и потребует, вероятно, реализации собственных итераторов (чтобы представить поэлементный AND, например) либо рассыпаться в стандартное поэлементное множество.
C>Как много операций происходит прежде чем будет сделан запрос?
Глубина дерева в среднем 5-7, то есть количество операций грубо 2^8.
Я извиняюсь за двусмысленность. Тема, похоже, свернула немного не туда. Прийдется глубже проработать пилот/архитектуру.