如何在python中实现一个简单的基于greedy multiset的算法

时间:2017-04-15 18:28:28

标签: python algorithm combinatorics

我想实现以下算法。对于nk,请按排序顺序考虑所有重复组合,我们会从k中选择重复{0,..n-1}个数字。例如,如果我们有n=5k =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)

2 个答案:

答案 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

请注意,由于生成了大量组合,所有方法对nk的较大值都会变慢。

答案 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< =>的轻微复杂性。新分区规则。现在:

  • new_partition =>携带
  • 随身携带并不一定意味着 new_partition

只有一个条件 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