我有值池,我想通过从某些池中选择来生成每种可能的无序组合。
例如,我想从池0,池0和池1中进行选择:
>>> pools = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
>>> part = (0, 0, 1)
>>> list(product(*(pools[i] for i in part)))
[(1, 1, 2), (1, 1, 3), (1, 1, 4), (1, 2, 2), (1, 2, 3), (1, 2, 4), (1, 3, 2), (1, 3, 3), (1, 3, 4), (2, 1, 2), (2, 1, 3), (2, 1, 4), (2, 2, 2), (2, 2, 3), (2, 2, 4), (2, 3, 2), (2, 3, 3), (2, 3, 4), (3, 1, 2), (3, 1, 3), (3, 1, 4), (3, 2, 2), (3, 2, 3), (3, 2, 4), (3, 3, 2), (3, 3, 3), (3, 3, 4)]
这会通过从池0,池0和池1中进行选择来生成所有可能的组合。
但是顺序对我来说并不重要,因此许多组合实际上都是重复的。例如,由于我使用的是笛卡尔积,因此(1, 2, 4)
和(2, 1, 4)
都会生成。
我想出了一种简单的方法来缓解此问题。对于从单个池中挑选的成员,我选择时不使用combinations_with_replacement
进行排序。我计算要从每个池中抽奖的次数。代码如下:
cnt = Counter()
for ind in part: cnt[ind] += 1
blocks = [combinations_with_replacement(pools[i], cnt[i]) for i in cnt]
return [list(chain(*combo)) for combo in product(*blocks)]
如果我碰巧多次从同一个池中进行选择,这将减少重复项的排序。但是,所有池都有很多重叠,并且在合并的多个池上使用combinations_with_replacement
会生成一些无效的组合。有没有更有效的方法来生成无序组合?
编辑:有关输入的额外信息:零件和池的数量很小(〜5和〜20),为简单起见,每个元素都是整数。我已经解决了实际问题,所以这只是出于学术目的。假设每个池中有成千上万个整数,但是有些池很小,只有几十个。因此,某种结合或相交似乎是可行的方法。
答案 0 :(得分:8)
这是一个难题。我认为一般情况下,最好的选择是实现一个hash table
,其中键是一个multiset
,值是您的实际组合。这类似于@ErikWolf提到的内容,但是此方法避免了首先产生重复项,因此不需要过滤。当我们遇到multisets
时,它还会返回正确的结果。
我现在正在嘲笑一种更快的解决方案,但可以保存以备后用。忍受我。
如评论中所述,一种可行的方法是合并所有池,并简单地生成此合并池的组合,然后选择池的数量。您将需要一种能够生成多集组合的工具,据我所知,该工具在python
中可用。它在sympy
库from sympy.utilities.iterables import multiset_combinations
中。这样做的问题是,我们仍然会产生重复的值,更糟糕的是,我们会产生类似的set
和product
组合无法获得的结果。例如,如果我们要进行排序和合并OP中的所有池之类的操作并应用以下内容:
list(multiset_permutations([1,2,2,3,3,4,4,5]))
其中两个结果将是[1 2 2]
和[4 4 5]
,它们都是无法从[[1, 2, 3], [2, 3, 4], [3, 4, 5]]
获得的。
除特殊情况外,我看不到如何避免检查所有可能的产品。我希望我错了。
算法概述
主要思想是将向量乘积的组合映射为唯一组合,而不必过滤出重复项。 OP给出的示例(即(1, 2, 3)
和(1, 3, 2)
)应仅映射到一个值(因为顺序无关紧要,所以可以是两者之一)。我们注意到,两个向量是相同的集合。现在,我们还有类似的情况:
vec1 = (1, 2, 1)
vec2 = (2, 1, 1)
vec3 = (2, 2, 1)
我们需要vec1
和vec2
来映射到相同的值,而vec3
需要映射到它自己的值。这是集合的问题,因为所有这些都是等效的sets(对于集合,元素是唯一的,因此{a, b, b}
和{a, b}
是等效的。)
这是multisets发挥作用的地方。对于多集,(2, 2, 1)
和(1, 2, 1)
是不同的,但是(1, 2, 1)
和(2, 1, 1)
是相同的。很好现在,我们有了一种生成唯一密钥的方法。
由于我不是python
程序员,所以我将继续学习C++
。
如果我们尝试按原样实现上述所有内容,则会遇到一些问题。据我所知,您不能将std::multiset<int>
作为std::unordered_map
的关键部分。但是,我们可以使用常规的std::map
。它的性能不如下面的哈希表(实际上是red-black tree),但仍然可以提供不错的性能。在这里:
void cartestionCombos(std::vector<std::vector<int> > v, bool verbose) {
std::map<std::multiset<int>, std::vector<int> > cartCombs;
unsigned long int len = v.size();
unsigned long int myProd = 1;
std::vector<unsigned long int> s(len);
for (std::size_t j = 0; j < len; ++j) {
myProd *= v[j].size();
s[j] = v[j].size() - 1;
}
unsigned long int loopLim = myProd - 1;
std::vector<std::vector<int> > res(myProd, std::vector<int>());
std::vector<unsigned long int> myCounter(len, 0);
std::vector<int> value(len, 0);
std::multiset<int> key;
for (std::size_t j = 0; j < loopLim; ++j) {
key.clear();
for (std::size_t k = 0; k < len; ++k) {
value[k] = v[k][myCounter[k]];
key.insert(value[k]);
}
cartCombs.insert({key, value});
int test = 0;
while (myCounter[test] == s[test]) {
myCounter[test] = 0;
++test;
}
++myCounter[test];
}
key.clear();
// Get last possible combination
for (std::size_t k = 0; k < len; ++k) {
value[k] = v[k][myCounter[k]];
key.insert(value[k]);
}
cartCombs.insert({key, value});
if (verbose) {
int count = 1;
for (std::pair<std::multiset<int>, std::vector<int> > element : cartCombs) {
std::string tempStr;
for (std::size_t k = 0; k < len; ++k)
tempStr += std::to_string(element.second[k]) + ' ';
std::cout << count << " : " << tempStr << std::endl;
++count;
}
}
}
使用长度从4到8的8个向量的测试用例填充从1到15的随机整数,上述算法在我的计算机上运行约5秒钟。考虑到我们正在从我们的产品中获得近250万个总结果,这还不错,但是我们可以做得更好。但是如何?
std::unordered_map
使用恒定时间构建的密钥可提供最佳性能。我们上面的密钥建立在对数时间(multiset, map and hash map complexity)中。所以问题是,我们如何克服这些障碍?
我们知道我们必须放弃std::multiset
。我们需要某种具有commutative类型属性,同时又能提供独特结果的对象。
输入Fundamental Theorem of Arithmetic
它指出,每个数字都可以用质数的乘积唯一地表示(直到因子的顺序)。有时称为素数分解。
因此,现在,我们可以像以前一样简单地进行操作,但无需构造多集,而是将每个索引映射到素数并将结果相乘。这将为我们的密钥提供恒定的时间构造。这是一个示例,显示了此技术在我们之前创建的示例中的作用(下面的N.B. P
是质数列表... (2, 3, 5, 7, 11, etc.)
:
Maps to Maps to product
vec1 = (1, 2, 1) -->> P[1], P[2], P[1] --->> 3, 5, 3 -->> 45
vec2 = (2, 1, 1) -->> P[2], P[1], P[1] --->> 5, 3, 3 -->> 45
vec3 = (2, 2, 1) -->> P[2], P[2], P[1] --->> 5, 5, 3 -->> 75
太棒了!! vec1
和vec2
映射到相同的数字,而vec3
映射到我们希望的其他值。
void cartestionCombosPrimes(std::vector<std::vector<int> > v,
std::vector<int> primes,
bool verbose) {
std::unordered_map<int64_t, std::vector<int> > cartCombs;
unsigned long int len = v.size();
unsigned long int myProd = 1;
std::vector<unsigned long int> s(len);
for (std::size_t j = 0; j < len; ++j) {
myProd *= v[j].size();
s[j] = v[j].size() - 1;
}
unsigned long int loopLim = myProd - 1;
std::vector<std::vector<int> > res(myProd, std::vector<int>());
std::vector<unsigned long int> myCounter(len, 0);
std::vector<int> value(len, 0);
int64_t key;
for (std::size_t j = 0; j < loopLim; ++j) {
key = 1;
for (std::size_t k = 0; k < len; ++k) {
value[k] = v[k][myCounter[k]];
key *= primes[value[k]];
}
cartCombs.insert({key, value});
int test = 0;
while (myCounter[test] == s[test]) {
myCounter[test] = 0;
++test;
}
++myCounter[test];
}
key = 1;
// Get last possible combination
for (std::size_t k = 0; k < len; ++k) {
value[k] = v[k][myCounter[k]];
key *= primes[value[k]];
}
cartCombs.insert({key, value});
std::cout << cartCombs.size() << std::endl;
if (verbose) {
int count = 1;
for (std::pair<int, std::vector<int> > element : cartCombs) {
std::string tempStr;
for (std::size_t k = 0; k < len; ++k)
tempStr += std::to_string(element.second[k]) + ' ';
std::cout << count << " : " << tempStr << std::endl;
++count;
}
}
}
在上面的示例中,该示例将产生近250万个产品,上述算法在不到0.3秒的时间内返回了相同的结果。
后一种方法有两个警告。我们必须让素数生成一个先验,并且如果我们在笛卡尔积中有许多矢量,则密钥可能会超出int64_t
的范围。由于存在许多可用于生成质数的资源(库,查找表等),第一个问题应该不难克服。我不太确定,但我读到对于python
来说,后一个问题应该不是问题,因为整数具有任意精度(Python integer ranges)。
我们还必须处理以下事实:我们的源向量可能不是具有较小值的 nice 整数向量。在继续进行之前,可以通过对所有向量中的所有元素进行排名来解决此问题。例如,给定以下向量:
vec1 = (12345.65, 5, 5432.11111)
vec2 = (2222.22, 0.000005, 5)
vec3 = (5, 0.5, 0.8)
排名靠前,我们将获得:
rank1 = (6, 3, 5)
rank2 = (4, 0, 3)
rank3 = (3, 1, 2)
现在,可以使用这些值代替实际值来创建密钥。唯一会更改的代码部分是用于构建密钥的for循环(当然还有需要创建的rank
对象):
for (std::size_t k = 0; k < len; ++k) {
value[k] = v[k][myCounter[k]];
key *= primes[rank[k][myCounter[k]]];
}
修改:
正如一些评论者所指出的那样,上述方法掩盖了必须生成所有产品的事实。我应该说是第一次。就个人而言,鉴于许多不同的演示,我看不出如何避免这种情况。
另外,万一有人好奇,这是我上面使用的测试用例:
[1 10 14 6],
[7 2 4 8 3 11 12],
[11 3 13 4 15 8 6 5],
[10 1 3 2 9 5 7],
[1 5 10 3 8 14],
[15 3 7 10 4 5 8 6],
[14 9 11 15],
[7 6 13 14 10 11 9 4]
它应该返回162295
个唯一的组合。
答案 1 :(得分:7)
一种节省工作的方法可能是生成前k个选定池的重复数据消除组合,然后将其扩展到前k + 1个池的重复数据消除组合。这样一来,您就可以避免分别生成和拒绝从前两个池中选择2, 1
而不是1, 2
的所有长度为20的组合:
def combinations_from_pools(pools):
# 1-element set whose one element is an empty tuple.
# With no built-in hashable multiset type, sorted tuples are probably the most efficient
# multiset representation.
combos = {()}
for pool in pools:
combos = {tuple(sorted(combo + (elem,))) for combo in combos for elem in pool}
return combos
尽管您使用的是输入大小,但是无论生成组合的效率如何,您都将永远无法处理所有组合。即使有20个相同的1000个元素池,也将有496432432432489450355564471512635900731810050组合(1019按星条形图选择20),或大约5e41。如果您征服了地球,并将全人类所有计算设备的全部处理能力都投入到了这项任务中,那么您仍然无法屈服。您需要找到一种更好的方法来解决基础任务。
答案 2 :(得分:5)
到目前为止已发布的答案(包括Tim Peters的lazy lexicographic one-at-a-time generation)在最坏情况下的空间复杂度与输出的大小成正比。我将概述一种方法,该方法将建设性地生成所有唯一的无序组合,而不会对内部生成的中间数据进行重复数据删除。我的算法按字典顺序生成组合。与较简单的算法相比,它具有计算开销。但是,它可以并行化(以便可以同时产生不同范围的最终输出)。
想法如下。
因此,我们有N
个池{P 1 ,...,P N },必须从中提取组合。
我们可以轻松地确定最小的组合(相对于上述字典顺序)。设为(x 1 ,x 2 ...,x N-1 ,x N )(其中x 1 <= x 2 <= ... <= x N-1 <= x N ,并且每个x j 只是池{P i }中之一的最小元素。此最小组合后跟零个或多个组合,其中前缀x 1 ,x 2 ...,x N-1 是相同,最后一个位置的值序列不断增加。我们如何识别该序列?
让我们介绍以下定义:
给出一个组合前缀C =(x 1 ,x 2 ...,x K-1 ,x K )(其中K
(如果是前缀C,则称池P i 相对于C ,称为 免费) )可以从其余的泳池中提取。
识别给定前缀的空闲池很容易地解决了在二部图中找到最大matchings的问题。具有挑战性的部分是有效地做到这一点(利用我们案件的具体内容)。但是我将其保存以备后用(这项工作正在进行中,有一天会变成Python程序)。
因此,对于第一个组合的前缀(x 1 ,x 2 ...,x N-1 ),我们可以标识所有空闲池{FP i }。它们中的任何一个都可以用来为最后一个位置选择一个元素。因此,感兴趣序列是{FP 1 U FP 2 U ...}中大于或等于x N-的元素的排序集合。 1 。
当最后一个位置用尽时,我们必须增加最后一个但只有一个位置,然后我们将重复查找最后一个位置的可能值的过程。毫不奇怪,枚举最后一个(以及其他任何一个)位置的值的过程是相同的-唯一的区别是组合前缀的长度,必须根据该长度来标识空闲池。
因此,以下递归算法可以完成工作:
答案 3 :(得分:3)
您可以实现可哈希列表,并使用python set()过滤所有重复项。 您的哈希函数只需要忽略列表中的顺序即可,这可以通过使用collections.Counter
来实现。from collections import Counter
class HashableList(list):
def __hash__(self):
return hash(frozenset(Counter(self)))
def __eq__(self, other):
return hash(self) == hash(other)
x = HashableList([1,2,3])
y = HashableList([3,2,1])
print set([x,y])
这将返回:
set([[1, 2, 3]])
答案 4 :(得分:2)
这是我想出的:
class Combination:
def __init__(self, combination):
self.combination = tuple(sorted(combination))
def __eq__(self, other):
return self.combination == self.combination
def __hash__(self):
return self.combination.__hash__()
def __repr__(self):
return self.combination.__repr__()
def __getitem__(self, i):
return self.combination[i]
然后
pools = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
part = (0, 0, 1)
set(Combination(combin) for combin in product(*(pools[i] for i in part)))
输出:
{(1, 1, 2),
(1, 1, 3),
(1, 1, 4),
(1, 2, 2),
(1, 2, 3),
(1, 2, 4),
(1, 3, 3),
(1, 3, 4),
(2, 2, 2),
(2, 2, 3),
(2, 2, 4),
(2, 3, 3),
(2, 3, 4),
(3, 3, 3),
(3, 3, 4)}
不确定这是否是您真正要寻找的东西。