我想实现以下算法。对于n
和k
,请按排序顺序考虑所有重复组合,我们会从k
中选择重复{0,..n-1}
个数字。例如,如果我们有n=5
和k =3
:
[(0,0,0),(0,0,1),(0,0,2),(0,0,3),(0,0,4),(0,1,1) ),(0, 1,2),(0,1,3),(0,1,4),(0,2,2),(0,2,3),(0,2,4),(0,3, 3),(0,3,4),(0,4,4),(1,1,1),(1,1,2),(1,1,3),(1,1,4) , (1,2,2),(1,2,3),(1,2,4),(1,3,3),(1,3,4),(1,4,4),(2) , 2,2,2(2,2,3),(2,2,4),(2,3,3),(2,3,4),(2,4,4),(3,3, 3),(3,3,4),(3,4,4),(4,4,4)]
从现在开始,我会将每个组合视为多重组合。我想贪婪地浏览这些多字节并对列表进行分区。分区具有属性,其中所有多个集合的交集大小必须至少为k-1
。所以在这种情况下我们有:
(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 0, 3), (0, 0, 4)
然后
(0, 1, 1), (0, 1, 2), (0, 1, 3), (0, 1, 4)
然后
(0, 2, 2), (0, 2, 3), (0, 2, 4)
然后
(0, 3, 3), (0, 3, 4)
然后
(0, 4, 4)
等等。
在python中,您可以按如下方式迭代组合:
import itertools
for multiset in itertools.combinations_with_replacement(range(5),3):
#Greedy algo
如何创建这些分区?
我遇到的一个问题是如何计算多重集合的交集大小。例如,多集(2,1,2)
和(3,2,2)
的交集大小为2。
以下是n=4, k=4
的完整答案。
(0, 0, 0, 0), (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)
(0, 0, 1, 1), (0, 0, 1, 2), (0, 0, 1, 3)
(0, 0, 2, 2), (0, 0, 2, 3)
(0, 0, 3, 3)
(0, 1, 1, 1), (0, 1, 1, 2), (0, 1, 1, 3)
(0, 1, 2, 2), (0, 1, 2, 3)
(0, 1, 3, 3)
(0, 2, 2, 2), (0, 2, 2, 3)
(0, 2, 3, 3), (0, 3, 3, 3)
(1, 1, 1, 1), (1, 1, 1, 2), (1, 1, 1, 3)
(1, 1, 2, 2), (1, 1, 2, 3)
(1, 1, 3, 3)
(1, 2, 2, 2), (1, 2, 2, 3)
(1, 2, 3, 3), (1, 3, 3, 3)
(2, 2, 2, 2), (2, 2, 2, 3)
(2, 2, 3, 3), (2, 3, 3, 3)
(3, 3, 3, 3)
答案 0 :(得分:3)
创建分区的一种方法是迭代迭代器,然后将每个multiset *与前一个multiset进行比较。我测试了4种方法**来比较多个集合,我发现最快的是测试成员资格in
前一个多集的迭代器,一旦成员资格测试失败就会消耗和短路。如果多集和前一个多集中的相等项的数量等于多集的长度减去1,则满足对它们进行分组的标准。然后构建list
s的结果输出生成器,其中append
项符合上一个list
的条件,并开始包含list
的新tuple
否则,yield
一次一个组以最小化内存使用:
import itertools
def f(n,k):
prev, group = None, []
for multiset in itertools.combinations_with_replacement(range(n),k):
if prev:
it = iter(prev)
for idx, item in enumerate(multiset):
if item not in it:
break
if idx == len(multiset) - 1:
group.append(multiset)
continue
if group:
yield group
group = [multiset]
prev = multiset
yield group
测试用例
输入:
for item in f(4,4):
print(item)
输出:
[(0, 0, 0, 0), (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)]
[(0, 0, 1, 1), (0, 0, 1, 2), (0, 0, 1, 3)]
[(0, 0, 2, 2), (0, 0, 2, 3)]
[(0, 0, 3, 3)]
[(0, 1, 1, 1), (0, 1, 1, 2), (0, 1, 1, 3)]
[(0, 1, 2, 2), (0, 1, 2, 3)]
[(0, 1, 3, 3)]
[(0, 2, 2, 2), (0, 2, 2, 3)]
[(0, 2, 3, 3), (0, 3, 3, 3)]
[(1, 1, 1, 1), (1, 1, 1, 2), (1, 1, 1, 3)]
[(1, 1, 2, 2), (1, 1, 2, 3)]
[(1, 1, 3, 3)]
[(1, 2, 2, 2), (1, 2, 2, 3)]
[(1, 2, 3, 3), (1, 3, 3, 3)]
[(2, 2, 2, 2), (2, 2, 2, 3)]
[(2, 2, 3, 3), (2, 3, 3, 3)]
[(3, 3, 3, 3)]
输入:
for item in f(5,3):
print(item)
输出:
[(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 0, 3), (0, 0, 4)]
[(0, 1, 1), (0, 1, 2), (0, 1, 3), (0, 1, 4)]
[(0, 2, 2), (0, 2, 3), (0, 2, 4)]
[(0, 3, 3), (0, 3, 4)]
[(0, 4, 4)]
[(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)]
[(1, 2, 2), (1, 2, 3), (1, 2, 4)]
[(1, 3, 3), (1, 3, 4)]
[(1, 4, 4)]
[(2, 2, 2), (2, 2, 3), (2, 2, 4)]
[(2, 3, 3), (2, 3, 4)]
[(2, 4, 4)]
[(3, 3, 3), (3, 3, 4)]
[(3, 4, 4), (4, 4, 4)]
*我称之为多字符串以匹配您的术语,但实际上是tuple
s(有序和不可变数据结构);使用collections.Counter
对象,例如Counter((0, 0, 0, 1))
return
s Counter({0: 3, 1: 1})
,并且递减就像一个真正的multiset方法但我发现这个更慢,因为使用该命令实际上是是有用的。
**提供与我测试的输出相同的其他较慢的函数:
def f2(n,k):
prev, group = None, []
for multiset in itertools.combinations_with_replacement(range(n),k):
if prev:
if sum(item1 == item2 for item1, item2 in zip(prev,multiset)) == len(multiset) - 1:
group.append(multiset)
continue
if group:
yield group
group = [multiset]
prev = multiset
yield group
def f3(n,k):
prev, group = None, []
for multiset in itertools.combinations_with_replacement(range(n),k):
if prev:
lst = list(prev)
for item in multiset:
if item in lst:
lst.remove(item)
else:
break
if len(multiset) - len(lst) == len(multiset) - 1:
group.append(multiset)
continue
if group:
yield group
group = [multiset]
prev = multiset
yield group
import collections
def f4(n,k):
prev, group = None, []
for multiset in itertools.combinations_with_replacement(range(n),k):
if prev:
if sum((collections.Counter(prev) - collections.Counter(multiset)).values()) == 1:
group.append(multiset)
continue
if group:
yield group
group = [multiset]
prev = multiset
yield group
示例时间:
from timeit import timeit
list(f(11,10)) == list(f2(11,10)) == list(f3(11,10)) == list(f4(11,10))
# True
timeit(lambda: list(f(11,10)), number = 10)
# 4.19157001003623
timeit(lambda: list(f2(11,10)), number = 10)
# 7.32002648897469
timeit(lambda: list(f3(11,10)), number = 10)
# 6.236868146806955
timeit(lambda: list(f4(11,10)), number = 10)
# 47.20136355608702
请注意,由于生成了大量组合,所有方法对n
和k
的较大值都会变慢。
答案 1 :(得分:1)
我们可以使用base n 查看要分区的集合/列表中的元组作为长度 k 的数字。从数字上看,您的算法在最小数量的基础上是贪婪的。让具有 k “digits”和base n 的所有数字的集合表示为 N(k,n)。忽略 N(k,n)不是您想要分区的列表的事实,我们可以按分区标准对 N(k,n)进行分区,贪婪地在最小的第一个基础上相当琐碎;通过从0开始计数(例如,在k = 5的情况下为00000),并且每当我们计数时存在进位时创建一个新分区(即从数字i溢出到数字i + 1) )。即规则是: carry< => new_partition
证明:假设 A 是进位后的值,进位进入第i 位。 A 在传输之前与前一个分区中的所有数字共享一个公共前缀,但不包括 i-th ,因此至少有一个不同。 A 仅在 i 之后与另一个(较小的)数字共享一个后缀,但该数字已经在一个分区中,其他数字与的差异超过1 ,所以 A 启动一个新分区。
然而,根据您的规范,我们只考虑N(k,n)
的子集; X
, X 中的 x , x [i]< = x [j]当i> Ĵ。这增加了上述 carry< =>的轻微复杂性。新分区规则。现在:
只有一个条件 carry 并不意味着 new_partition :刚刚有一个进位创建一个新的分区,然后还有另一个进位,由<当i> 1时,em> x [i]&lt; = x [j] j 规则。下一个进位不会导致多于一个的变化,因此并不意味着新的分区。
<强>实施强>
class ExpNum:
''' Represents a number with base @base, @size digits, and funny successor semantics. '''
def __init__(self, base, size):
if size <= 0 or base <= 1:
raise Exception("Bad args")
self.size = size
self.base = base
self.number = [0]*size
self.zero = [0]*size
def increment(self):
''' Increment number by one. If we carry return index of carry else return -1. '''
carried = -1
for i in reversed(range(0, len(self.number))):
self.number[i] = (self.number[i]+1)%self.base
if self.number[i] != 0:
break
carried = i
if carried >= 0:
self.pullup()
return carried
def pullup(self):
''' Ensure x[i] <= x[j] when i > j '''
for i in range(0, len(self.number)):
if self.number[i] == 0 and i > 0:
self.number[i] = self.number[i-1]
def out_by_one_partition(self):
''' Do the partition by counting from 0 to n**k '''
self.number = [0]*self.size
just_carried = False
partition = [list(self.number)]
carried = self.increment()
while self.number != self.zero:
# Check for exception to carry => new partition.
if carried >= 0 and not (just_carried and list(self.number)[carried] == (self.base -1) and len(partition) == 1):
yield(partition)
partition = []
partition += [list(self.number)]
just_carried = carried >= 0
carried = self.increment()
yield(partition)
<强>测试强>
from ExpNum import ExpNum
from timeit import timeit
from pprint import pprint
pprint(list(ExpNum(4,4).out_by_one_partition()))
print(timeit(lambda: list(ExpNum(11,10).out_by_one_partition()), number = 10))
测试结果:
[[[0, 0, 0, 0], [0, 0, 0, 1], [0, 0, 0, 2], [0, 0, 0, 3]],
[[0, 0, 1, 1], [0, 0, 1, 2], [0, 0, 1, 3]],
[[0, 0, 2, 2], [0, 0, 2, 3]],
[[0, 0, 3, 3]],
[[0, 1, 1, 1], [0, 1, 1, 2], [0, 1, 1, 3]],
[[0, 1, 2, 2], [0, 1, 2, 3]],
[[0, 1, 3, 3]],
[[0, 2, 2, 2], [0, 2, 2, 3]],
[[0, 2, 3, 3], [0, 3, 3, 3]],
[[1, 1, 1, 1], [1, 1, 1, 2], [1, 1, 1, 3]],
[[1, 1, 2, 2], [1, 1, 2, 3]],
[[1, 1, 3, 3]],
[[1, 2, 2, 2], [1, 2, 2, 3]],
[[1, 2, 3, 3], [1, 3, 3, 3]],
[[2, 2, 2, 2], [2, 2, 2, 3]],
[[2, 2, 3, 3], [2, 3, 3, 3]],
[[3, 3, 3, 3]]]
10.25355386902811