Разделите список наборов по общим элементам
Вопрос
Вот в чем суть проблемы:Дан список наборов, таких как:
[ (1,2,3), (5,2,6), (7,8,9), (6,12,13), (21,8,34), (19,20) ]
Возвращает список групп наборов, таких, что наборы, имеющие общий элемент, находятся в одной группе.
[ [ (1,2,3), (5,2,6), (6,12,13) ], [ (7,8,9), (21,8,34) ], [ (19,20) ] ]
Обратите внимание на липкость - набор (6,12,13) не имеет общего элемента с (1,2,3), но они помещаются в одну группу из-за (5,2,6).
Чтобы усложнить ситуацию, я должен упомянуть, что на самом деле у меня нет этих аккуратных наборов, а скорее таблицы базы данных с несколькими миллионами строк, которая выглядит как:
element | set_id
----------------
1 | 1
2 | 1
3 | 1
5 | 2
2 | 2
6 | 2
и так далее.Поэтому мне бы хотелось найти способ сделать это на SQL, но я был бы доволен общим направлением решения.
Редактировать:Изменил имена столбцов таблицы на (element, set_id) вместо (key, group_id), чтобы сделать термины более согласованными.Обратите внимание, что в ответе Kev используются старые имена столбцов.
Решение
Проблема заключается именно в вычислении связных компонент гиперграфа:целые числа - это вершины, а множества - гиперребрики.Обычный способ вычисления подключенных компонентов заключается в заполнении их одного за другим:
- для всех i = от 1 до N выполните:
- если i был помечен каким-то j < i, затем продолжайте (я имею в виду переход к следующему i).
- else flood_from(i,я)
где flood_from(i, j) будет определен как
- для каждого набора S, содержащего i, если он еще не помечен j, то:
- пометьте S символом j и для каждого элемента k из S, если k еще не помечено символом j, пометьте его символом j и вызовите flood_from(k,j)
Затем теги наборов выдают вам подключенные компоненты, которые вы ищете.
В терминах баз данных алгоритм может быть выражен следующим образом:вы добавляете столбец ТЕГА в свою базу данных и вычисляете подключенный компонент набора i, выполняя
- S = выбрать все строки, где set_id == i
- установите для ТЕГА значение i для строк в S
- S' = выбрать все строки, где ТЕГ не задан и где элемент находится в элементах
- пока S' не пусто, выполните
- ---- установите для ТЕГА значение i для строк в S'
- ---- S" = выбрать все строки, где ТЕГ не задан и где элемент находится в элементе (Ах')
- ---- S = S объединение S'
- ---- S' = S"
- возвращает set_id(Ы)
Другим (теоретическим) способом представления этого алгоритма было бы сказать, что вы ищете фиксированные точки отображения:
- если A = {A1, ..., An} - это набор множеств, определяющий union(A) = A1 профсоюз ...объединение Аn
- если K = {k1, ..., кp} представляет собой набор целых чисел, определяющих инцидентности(K) = множество множеств, которые пересекают K
Затем, если S является множеством, связная составляющая S получается путем повторения (инциденций) o (объединения) на S до тех пор, пока не будет достигнута фиксированная точка:
- K = S
- K' = инциденты (объединение(K)).
- если K == K', то верните K, иначе K = K' и перейдите к 2.
Другие советы
Вы могли бы думать об этом как о задаче графа, где множество (1,2,3) связано с множеством (5,2,6) через 2.А затем используйте стандартный алгоритм для уточнения связанных подграфов.
Вот краткая реализация на python:
nodes = [ [1,2,3], [2,4,5], [6,7,8], [10,11,12], [7,10,13], [12], [] ]
links = [ set() for x in nodes ]
#first find the links
for n in range(len(nodes)):
for item in nodes[n]:
for m in range(n+1, len(nodes)):
if (item in nodes[m]):
links[n].add(m)
links[m].add(n)
sets = []
nodes_not_in_a_set = range(len(nodes))
while len(nodes_not_in_a_set) > 0:
nodes_to_explore = [nodes_not_in_a_set.pop()]
current_set = set()
while len(nodes_to_explore) > 0:
current_node = nodes_to_explore.pop()
current_set.add(current_node)
if current_node in nodes_not_in_a_set:
nodes_not_in_a_set.remove(current_node)
for l in links[current_node]:
if l not in current_set and l not in nodes_to_explore:
nodes_to_explore.append(l)
if len(current_set) > 0:
sets.append(current_set)
for s in sets:
print [nodes[n] for n in s]
выходной сигнал:
[[]]
[[6, 7, 8], [10, 11, 12], [7, 10, 13], [12]]
[[1, 2, 3], [2, 4, 5]]
Вероятно, это довольно неэффективно, но, по крайней мере, это должно сработать:Начните с ключа, выберите все группы, содержащие этот ключ, выберите все ключи из этих групп, выберите все группы, содержащие эти ключи, и т.д., И как только шаг не добавит новых ключей или групп, у вас будет список всех групп одного подграфа.Исключите их и повторяйте с самого начала, пока у вас не останется никаких данных.
С точки зрения SQL, я думаю, для этого потребовалась бы хранимая процедура.ИСПОЛЬЗОВАНИЕ RECURSIVE может вам как-то помочь, но у меня пока нет никакого опыта работы с этим, и я не уверен, что это доступно в вашем бэкэнде базы данных.
Поразмыслив об этом еще немного, я пришел к следующему выводу:
- Создайте таблицу под названием
groups
с колоннами(group_id, set_id)
- Отсортировать
sets
таблица поelement
.Теперь должно быть легко найти дублирующиеся элементы. - Выполните итерацию по таблице наборов, и когда вы найдете дублирующийся элемент, выполните:
- если один из
set_id
поля существуют вgroups
таблицу, добавьте другую с таким жеgroup_id
. - Если ни то , ни другое
set_id
существует вgroups
таблицу, сгенерируйте новый идентификатор группы и добавьте обаset_id
s кgroups
таблица.
- если один из
В конце концов, у меня должен быть groups
таблица, содержащая все наборы.
Это не чистый SQL, но похоже на O (nlogn), так что, думаю, я смогу с этим смириться.
Ответ Мэтта кажется более правильным, но я не уверен, как это реализовать в моем случае.