Re: Представление подмножеств
От: andrey.desman  
Дата: 05.06.25 08:06
Оценка: 2 (1) +2
Здравствуйте, pva, Вы писали:

pva>На текущий момент пытаюсь сделать это через std::optional<std::list<std::variant<*>>>>, но как-то громоздко получается, что ли.


Как минимум для начала стоит посмотреть на boost icl.
Re: Представление подмножеств
От: Chorkov Россия  
Дата: 06.06.25 16:43
Оценка: 14 (2)
Здравствуйте, 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) Храним дерево операций, с помощью которых получено множество.
template<typename T>
struct points_set
{
   std::function< bool(T) > impl;
 //...
   points_set operator & ( const points_set& rhs) const // добавить && по вкусу
   {
      return points_set { [l=this->impl, r=rhs.impl] (T i)->bool { return l(i) && r(i); } };
   }
     
   bool contains(T i) const { return impl(i); }
};

Можно тоже самое на 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) точку рассмотрим как диапазон единичной длины. Пересекающиеся (по началу/концу) диапазоны, делим на либо с совпадающими концами, либо на не пересекающиеся.

template<typename T>
struct points_set
{
   std::flat_map< T, std::flat_map< T, std::flat_set<T> > data; // data[begin][divider] -> remainders set 

   bool contains(T key) const 
   { 
      auto i = data.upper_bound(key);
      if(i==data.begin()) return false;
      --i;
      assert( i->first <= key ); 
      assert( i+1 == data.end() || (i+1)->first > key ); 

      for( auto & [divider, remainders] : i->second )
         if( remainders.contains( key % divider ))
            return true;
      return false;
   }
};

Минусы: операции "^" и "/" — дорогие.
Отредактировано 06.06.2025 16:56 Chorkov . Предыдущая версия .
Re[5]: Представление подмножеств
От: rg45 СССР  
Дата: 05.06.25 11:55
Оценка: 4 (1)
Здравствуйте, 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);
}
--
Справедливость выше закона. А человечность выше справедливости.
Отредактировано 05.06.2025 12:22 rg45 . Предыдущая версия . Еще …
Отредактировано 05.06.2025 12:04 rg45 . Предыдущая версия .
Отредактировано 05.06.2025 12:04 rg45 . Предыдущая версия .
Отредактировано 05.06.2025 11:58 rg45 . Предыдущая версия .
Re: Представление подмножеств
От: Кодт Россия  
Дата: 05.06.25 22:41
Оценка: 2 (1)
Здравствуйте, pva, Вы писали:

pva>поделитесь, пожалуйста, мыслями или идеями как можно эффективно представлять множества целых и некоторые операции над ними.

pva>Размер: на текущий момент можно ограничиться до 64-bit числами.

Эффективность сильно привязана к предметной области. Что за операции, какие характерные размеры этих множеств, какие характерные диапазоны...
В самом худшем случае ведь это попросту битовый вектор размером в 2^64 элемента — с хаотично расставленными там единичками и ноликами.
Память жрёт, зато проверка вхождения мгновенная.

Можно попробовать подойти с такой стороны.
Множество изоморфно предикату "входит ли элементв в него".
А предикаты можно строить как дерево вычислений.

Листья в этом дереве — это
— сплошной диапазон
— битсет в диапазоне
— упорядоченный массив в диапазоне

Операция над множествами — или сразу упрощается (например, пересечение диапазонов — это диапазон, объединение смежных диапазонов — опять диапазон), или формирует узел дерева, который будет вычислять функцию вхождения каждый раз.

Дерево может иметь жёсткую структуру — дизъюнктивную нормальную форму, например.
Или разумное ограничение по высоте.
Или какой-то умный балансировщик, эвристически определяющий, что лучше — выращивать дерево или форсировать вычисление.
Перекуём баги на фичи!
Re: Представление подмножеств
От: rg45 СССР  
Дата: 05.06.25 05:30
Оценка: +1
Здравствуйте, 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, почему бы просто не выбрать целый тип максимальной ёмкости.
--
Справедливость выше закона. А человечность выше справедливости.
Re: Представление подмножеств
От: DiPaolo Россия  
Дата: 05.06.25 08:57
Оценка: +1
Можно тут подходящее поискать https://github.com/fffaraz/awesome-cpp?tab=readme-ov-file#containers
Патриот здравого смысла
Re[4]: Представление подмножеств
От: rg45 СССР  
Дата: 05.06.25 10:14
Оценка: +1
Здравствуйте, sergii.p, Вы писали:

pva>>Вторая — размер важен


SP>так std::variant всё равно будет занимать место максимального элемента.

SP>Так std::variant<i8, u8, u64> будет занимать все 64 бита + ещё поле селектора. После выравнивания получите все 16 байт.

Не, он не в этом смыле. Имеется в виду, что разные типы — это разные элементы множества даже если их значения совпадают. Хотя при таком способе хранения, оверхед по памяти налицо, конечно.
--
Справедливость выше закона. А человечность выше справедливости.
Представление подмножеств
От: pva  
Дата: 04.06.25 13:20
Оценка:
Привет,

поделитесь, пожалуйста, мыслями или идеями как можно эффективно представлять множества целых и некоторые операции над ними.
Размер: на текущий момент можно ограничиться до 64-bit числами.
Варианты элементов множеств: знаковое целое (8/16/32/64), беззнаковое целое (8/16/32/64), диаппазон целых с заданным шагом.
Операции: +,-,*,/,&,|,^

На текущий момент пытаюсь сделать это через std::optional<std::list<std::variant<*>>>>, но как-то громоздко получается, что ли.
newbie
Re[2]: Представление подмножеств
От: pva  
Дата: 05.06.25 06:22
Оценка:
Здравствуйте, rg45, Вы писали:

R>Почему list, a не set.

Один из возможных элементов коллекции "диаппазон целых с заданным шагом". И сколько будет занимать такой set для достаточно обширных диаппазонов?

R>Зачем нужен optional — почему нельзя просто проверить коллекцию на пустоту.

Пустое множество — допустимый вариант, в отличие от несуществующего.

R>Ну и наконец, зачем нужен variant, почему бы просто не выбрать целый тип максимальной ёмкости.

Одна из причин уже указана выше — тип "IntRange". Вторая — размер важен, 8l, 8u и 8ull — разные элементы.
newbie
Re[3]: Представление подмножеств
От: rg45 СССР  
Дата: 05.06.25 07:09
Оценка:
Здравствуйте, pva, Вы писали:

pva>Один из возможных элементов коллекции "диаппазон целых с заданным шагом". И сколько будет занимать такой set для достаточно обширных диаппазонов?

pva>Пустое множество — допустимый вариант, в отличие от несуществующего.
pva>Одна из причин уже указана выше — тип "IntRange". Вторая — размер важен, 8l, 8u и 8ull — разные элементы.

Ещё пара уточнений. Можно ли ввести какое-либо упорядочение элементов множества. И допускается ли дублирование элементов. Например, можно ли добавить в множество диапазон [1-10], а потом еще отдельно, например, 3.
--
Справедливость выше закона. А человечность выше справедливости.
Re[3]: Представление подмножеств
От: sergii.p  
Дата: 05.06.25 10:03
Оценка:
Здравствуйте, pva, Вы писали:

pva>Вторая — размер важен


так std::variant всё равно будет занимать место максимального элемента.
Так std::variant<i8, u8, u64> будет занимать все 64 бита + ещё поле селектора. После выравнивания получите все 16 байт.
Re[4]: Представление подмножеств
От: pva  
Дата: 05.06.25 11:35
Оценка:
Здравствуйте, 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 и интегрировать их.
newbie
Re[2]: Представление подмножеств
От: pva  
Дата: 07.06.25 07:36
Оценка:
Здравствуйте, Chorkov, Вы писали:

pva>Операции: +,-,*,/,&,|,^

C>А как еще будут запросы? (Кроме операций со множествами?)
Ха, спасибо за вопрос! Действительно, я не учел разночтения. Это не операции над объектами "множество", а поэлементные операции.
C>(1) Проверка принадлежности числа множеству. (2) Число элементов множества. (3) число элементов, попадающих в диапазон. (4) перечислить все элементы. (5) проверить что множество не пусто ... ?
Здесь достаточно стандартного набора: add, remove, contains, iterate, size
Логические операции не нужны, кроме OR (merge).

C>Для диапазонов с шагом, каков период шага? (Порядка единиц / десятоко / ... / миллионов ?)

C>Каковы типичные размеры диапазонов? Бывают ли на всю числовую ось шириной?
Опыты показали что диаппазоны, вероятно, можно выкинуть. Типовой размер множества — десяток-другой элементов, редко до тысячи.
Хотя, конечно, в некоторых случаях они были бы полезны. Но реализовать над ними базовую битовую арифметику будет долго и потребует, вероятно, реализации собственных итераторов (чтобы представить поэлементный AND, например) либо рассыпаться в стандартное поэлементное множество.

C>Как много операций происходит прежде чем будет сделан запрос?

Глубина дерева в среднем 5-7, то есть количество операций грубо 2^8.

Я извиняюсь за двусмысленность. Тема, похоже, свернула немного не туда. Прийдется глубже проработать пилот/архитектуру.
newbie
Отредактировано 07.06.2025 7:37 pva . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.