Мое понимание проблемы: учитывая набор k
наборов Y
и набор m
наборов X
, мы хотели бы найти подмножество S
из Y
, такое, что для всех y in S
, существует x in X
st x
- это подмножество y
.
Я предполагаю, что множества представлены n
-векторами нулей и единицами, обозначающими включение. Вот установка:
import pandas as pd # for drop_duplicates & benchmarks
import numpy as np
np.random.seed(0)
n = 100 # 100 "atomic" elements
m = 1000 # small sets
k = 1000 # large sets
X = pd.DataFrame(np.random.randint(0, 2, size=(m, n))).drop_duplicates().values
Y = pd.DataFrame(np.random.randint(0, 2, size=(k, n))).drop_duplicates().values
# For each row y in Y, we would like to check if there exists a row x in X
# s.t. x represents a subset of Y
def naive(Y, X):
# O(k^2 + m^2)
for i, y in enumerate(Y):
for x in X:
found_subset = False
if (x <= y).all():
yield i
found_subset = True
if found_subset:
break
def naive_as_array(Y, X):
return np.array(list(naive(Y, X)))
Функция naive
выполняет итерацию по всем парам наборов, которые могут удовлетворять отношению включения и коротким замыканиям, когда это необходимо. Время выполнения равно O(m + k)
, где m = len(combs)
.
. В качестве альтернативы мы можем рассмотреть следующий рекурсивный алгоритм, обрабатывающий каждый элемент (от 1
до n
) одновременно:
def contains(Y, X):
"""
Y : k x n indicator array specifying sets
X : m x n indicator array specifying sets
output: subset Z of [0..k-1] s.t. i in Z iff there exists x in X s.t.
# x is a subset of Y[i]. Z is represented by a 1D numpy array.
"""
k, n = Y.shape
assert Y.shape[1] == X.shape[1]
detected = np.zeros(k, dtype=np.bool)
inds = np.arange(k)
# utility to account for sets that already have a subset detected
def account_for_detected(Y, inds):
mask = ~detected[inds]
Y = Y[mask]
inds = inds[mask]
return Y, inds
# inductively reduce Y.shape[1] (==X.shape[1])
def f(Y, X, inds):
if Y.shape[0] == 0 or X.shape[0] == 0:
# collection Y is empty; inculsions are impossible
return
# avoid redundant comparisons by dropping sets y in Y
# if it is already known that y contains some element of X
Y, inds = account_for_detected(Y, inds)
if Y.shape[1] == 1:
# Y and X are collections of singletons
Y = np.ravel(Y)
X = np.ravel(X)
X_vals = np.zeros(2, dtype=np.int)
X_vals[X] = 1
if X_vals[0] > 0:
detected[inds] = True
elif X_vals[1] > 0:
detected[inds[Y==1]] = True
return
else:
# make a recursive call
Ymask = Y[:,0] == 0
Xmask = X[:,0] == 0
# if x in X is a subset of y in Y, x[0] <= y[0]
f(Y[Ymask,1:], X[Xmask,1:], inds[Ymask])
# by now, detected is updated in the outer scope
# process the remaining Y's
f(Y[~Ymask,1:], X[:,1:], inds[~Ymask])
# done
# make call at root:
f(Y, X, inds)
# return indices
return np.where(detected)[0]
На шаге d
между 1
и n
мы разделяем множества Y
на Y0
и Y1
, где Y0
содержит множества в Y
, которые не содержат элемент d
и Y1
содержит наборы в Y
, которые содержат элемент d
. Аналогично, мы определяем X0
и X1
. Ключевое наблюдение заключается в том, что наборы в X1
не могут встречаться как подмножества наборов в Y0
. Следовательно, мы можем уменьшить количество сравнений в рекурсивном вызове.
Время:
%timeit contains(Y, X)
%timeit naive_as_array(Y, X)
10 loops, best of 3: 185 ms per loop
1 loop, best of 3: 2.39 s per loop