如何改进我的算法以生成一个多重集合的组合?

7
我应该如何优化以下生成器中的next()hasNext()方法,用于生成有界多重集合的组合?(我在C++和Java上发布此内容,因为代码可兼容C++并且没有不转换为C++的特定于Java的元素。)
算法中存在问题的特定区域是整个hasNext()方法,可能过于复杂,并且以下这一行: if( current[xSlot] > 0 ) aiItemsUsed[current[xSlot]]--; 其中包含一个if语句,我认为可以以某种方式删除它。我曾有过一个早期版本的算法,其中一些回溯发生在return语句之前,因此hasNext()测试要简单得多,但是我无法让那个版本正常工作。
该算法的背景是很难找到。例如,在Knuth 7.2.1.3中,他仅表示可以完成这项工作(并提供了一个练习来证明算法是可能的),但没有给出算法。同样,我拥有半打关于组合数学的高级文本(包括Papadimitriou和Kreher/Stimson),但其中没有一个提供了生成多重集合组合的算法。Kreher将其留给“读者作为练习”。无论如何,如果您可以改进上述算法或提供比我的实现更有效的工作实现的参考,请告诉我。请仅提供迭代算法(请勿使用递归)。
/** The iterator returns a 1-based array of integers. When the last combination is reached hasNext() will be false.
  * @param aiItems One-based array containing number of items available for each unique item type where aiItems[0] is the number of item types
  * @param ctSlots  The number of slots into which the items go
  * @return The iterator which generates the 1-based array containing the combinations or null in the event of an error.
  */
public static java.util.Iterator<int[]> combination( final int[] aiItems, final int ctSlots ){ // multiset combination into a limited number of slots
    CombinatoricIterator<int[]> iterator = new CombinatoricIterator<int[]>(){
        int xSlot;
        int xItemType;
        int ctItemType;
        int[] current = new int[ctSlots + 1];
        int[] aiItemsUsed = new int[aiItems[0] + 1];
        { reset(); current[0] = ctSlots; ctItemType = aiItems[0]; }
        public boolean hasNext(){
            int xUseSlot = ctSlots;
            int iCurrentType = ctItemType;
            int ctItemsUsed = 0;
            int ctTotalItemsUsed = 0;
            while( true ){
                int xUsedType = current[xUseSlot];
                if( xUsedType != iCurrentType ) return true;
                ctItemsUsed++;
                ctTotalItemsUsed++;
                if( ctTotalItemsUsed == ctSlots ) return false;
                if( ctItemsUsed == aiItems[xUsedType] ){
                    iCurrentType--;
                    ctItemsUsed = 0;
                }
                xUseSlot--;
            }
        }
        public int[] next(){
            while( true ){
                while( xItemType == ctItemType ){
                    xSlot--;
                    xItemType = current[xSlot];
                }
                xItemType++;
                while( true ){
                    while( aiItemsUsed[xItemType] == aiItems[xItemType] && xItemType != current[xSlot] ){
                        while( xItemType == ctItemType ){
                            xSlot--;
                            xItemType = current[xSlot];
                        }
                        xItemType++;
                    }
                    if( current[xSlot] > 0 ) aiItemsUsed[current[xSlot]]--;
                    current[xSlot] = xItemType;
                    aiItemsUsed[xItemType]++;
                    if( xSlot == ctSlots ){
                        return current;
                    }
                    xSlot++;
                }
            }

        }
        public int[] get(){ return current; }
        public void remove(){}
        public void set( int[] current ){ this.current = current; }
        public void setValues( int[] current ){
            if( this.current == null || this.current.length != current.length ) this.current = new int[current.length];
            System.arraycopy( current, 0, this.current, 0, current.length );
        }
        public void reset(){
            xSlot = 1;
            xItemType = 0;
            Arrays.fill( current, 0 ); current[0] = ctSlots;
            Arrays.fill( aiItemsUsed, 0 ); aiItemsUsed[0] = aiItems[0];
        }
    };
    return iterator;
}

附加信息

到目前为止,一些受访者似乎不理解集合和有界多重集之间的区别。有界多重集具有重复元素。例如,{ a,a,b,b,b,c }是一个有界多重集,在我的算法中将被编码为{ 3, 2, 3, 1 }。请注意,前导的“3”是集合中项类型(唯一项)的数量。如果您提供一个算法,则以下测试应该产生如下所示的输出。

    private static void combination_multiset_test(){
        int[] aiItems = { 4, 3, 2, 1, 1 };
        int iSlots = 4;
        java.util.Iterator<int[]> iterator = combination( aiItems, iSlots );
        if( iterator == null ){
            System.out.println( "null" );
            System.exit( -1 );
        }
        int xCombination = 0;
        while( iterator.hasNext() ){
            xCombination++;
            int[] combination = iterator.next();
            if( combination == null ){
                System.out.println( "improper termination, no result" );
                System.exit( -1 );
            }
            System.out.println( xCombination + ": " + Arrays.toString( combination ) );
        }
        System.out.println( "complete" );
    }


1: [4, 1, 1, 1, 2]
2: [4, 1, 1, 1, 3]
3: [4, 1, 1, 1, 4]
4: [4, 1, 1, 2, 2]
5: [4, 1, 1, 2, 3]
6: [4, 1, 1, 2, 4]
7: [4, 1, 1, 3, 4]
8: [4, 1, 2, 2, 3]
9: [4, 1, 2, 2, 4]
10: [4, 1, 2, 3, 4]
11: [4, 2, 2, 3, 4]
complete

请解释一下“有界多重集合”的含义。但是,您能否还澄清一下“iSlots”约束条件?它是指结果中的类型数量吗?还是指结果中多重性的总和?或者... - Apiwat Chantawibul
槽的数量是组合中元素的数量。如果您熟悉二项式关系 C(n,k),则 k 就是槽号。 - Tyler Durden
所以,您想要迭代每个特定基数的多重集合的每个子集,还是迭代每个特定基数或更小的多重集合的每个子集?因为您的测试代码输出似乎与您的问题描述不符。 - Yakk - Adam Nevraumont
需要改进的代码有个参数文档:“@param ctSlots——物品放置的插槽数量”。如果您熟悉组合中的“选择”符号,这类似于 C(n,k) 表示法。其中 k 是插槽数量。在这里,我使用变量“ctSlots”(插槽数量)。 - Tyler Durden
@TylerDurden 我在GitHub上有一个Java库,可以对多集合进行组合排列。这些类尝试利用变换顺序来提高效率。 - Christian Trimble
显示剩余2条评论
3个回答

1
我会编写一个简单的助手类,其中包含incrementhighbitfor_each_bit 方法。
首先,我会将一个无符号整数包装起来,并将其限制在32位以内,如果我感到有雄心壮志,可能会通过std::bitsetstd::vector<uint32_t>进行扩展。但是,只要拥有这3个方法,我就可以测试并使其正常工作。
在裸的32位int上,increment很容易实现。 highbit返回最高位设置的位位置。 for_each_bit在C++中具有此签名:
template<typename Lambda>
void for_each_bit( my_bignum const& num, Lambda&& func )

然后使用num中每个设置位的索引调用func

这应该最多只需要几分钟就可以写完。

抛弃hasNext,遵循迭代器概念 -- 你有一个begin子集和一个end子集,而end不是提取值的有效位置。解引用这些迭代器会产生相应的子集(或生成该子集的工厂)。

end现在很容易计算出来 -- 如果highbit大于等于集合中元素的数量,则超出了排列组合的末尾。

begin要么是零,要么是一,具体取决于是否希望包含空子集。

next只需递增bignum

生成子集只需调用for_each_bit,并将集合中的该项放入子集中。

接下来,改进increment以允许随机访问,然后您就可以并行实现对子集的迭代!

这解决了集合问题。要解决多重集合问题,首先解决派生集合问题(假装每个元素只有0或1个),并对其进行迭代。然后,在每次派生集合的迭代中,建立一个std::vector,包含每个元素的最大计数。然后执行以下操作:
#include <utility>
#include <cstddef>
#include <vector>

using std::size_t;

namespace details {
template<typename Lambda>
  void for_each_multiset_combo_worker( std::vector<size_t> const& counts, Lambda&& lambda, std::vector<size_t>& indexes, std::vector<size_t>& current )
  {
    if (depth >= counts.size()) {
      lambda( current );
      return;
    }
    for (size_t i = 0; i <= counts[depth]; ++i) {
      // Assert: current.size() == depth
      current.push_back(i);
      // Assert: current.back() == i
      // Assert: current.size() == dpeth+1
      for_each_multiset_combo_worker( counts, lambda, depth+1, current );
      // Assert: current.back() == i
      // Assert: current.size() == dpeth+1
      current.pop_back();
      // Assert: current.size() == depth
    }
  }
}
template<typename Lambda>
void for_each_multiset_combo( std::vector<size_t> const& counts, Lambda&& lambda )
{
  std::vector<size_t> current;
  current.reserve( counts.size() );
  details::for_each_multiset_combo_worker( counts, std::forward<Lambda>(lambda), 0, current );
}
#include <iostream>

int main() {
  std::vector<size_t> multiset = {3, 2, 1, 1};
  size_t counter = 0;
  for_each_multiset_combo( multiset, [&]( std::vector<size_t> const& counts ){
    std::cout << counter << ": [";
    for(auto it = counts.begin(); it != counts.end(); ++it) {
      if (it != counts.begin()) {
        std::cout << ", ";
      }
      std::cout << *it;
    }
    std::cout << "]\n";
    ++counter;
  });
}

实时示例:http://ideone.com/8GN1xx

在这个实时示例中,我跳过了先进行集合迭代的优化,而是直接遍历了多重集合。

(限制条件:每种类型的元素不超过最大size_t元素,并且不超过std::vector不同类型元素的最大容量)。

我不需要前导的“多重集合中不同元素的数量”,所以我没有使用它。

以下是上述递归算法的迭代版本,使用通常的“将隐式递归栈转换为显式迭代栈”的技术:

#include <utility>
#include <cstddef>
#include <vector>

using std::size_t;

template<typename Lambda>
void for_each_multiset_combo( std::vector<size_t> const& counts, Lambda&& lambda )
{
  // below code is easier if I assume counts is non-empty:
  if (counts.empty())
  {
    lambda(counts);
    return;
  }
  // preallocate a buffer big enough to hold the output counts:
  std::vector<size_t> indexes;
  indexes.reserve( counts.size() );
  while(true) {
    // append 0s on the end of indexes if we have room:
    while (indexes.size() < counts.size()) {
      indexes.push_back(0);
    }
    // at this point, we have a unique element.  Pass it to the passed in lambda:
    lambda( indexes );
    // The advancement logic.  Advance the highest index.  If that overflows, pop it and
    // advance the next highest index:
    indexes.back()++;
    while (indexes.back() > counts[indexes.size()-1]) {
      indexes.pop_back();
      // we are done if we have managed to advance every index, and there are none left to advance:
      if (indexes.empty())
        return; // finished
      indexes.back()++;
    }
  }
}
#include <iostream>

int main() {
  std::vector<size_t> multiset = {3, 2, 1, 1};
  size_t counter = 0;
  for_each_multiset_combo( multiset, [&]( std::vector<size_t> const& counts ){
    std::cout << counter << ": [";
    for(auto it = counts.begin(); it != counts.end(); ++it) {
      if (it != counts.begin()) {
        std::cout << ", ";
      }
      std::cout << *it;
    }
    std::cout << "]\n";
    ++counter;
  });
}

http://ideone.com/x2Zp2f


我不太明白你在这里的意思,但你似乎和Mel Nicholson一样犯了同样的错误,没有理解集合和多重集之间的区别。生成多重集的组合比生成集合的组合要困难得多。 - Tyler Durden
@TylerDurden 没有注意到它是一个multiset,但在生成set的组合之上生成multiset的组合是一件微不足道的事情。假设你只有每个元素的1个而不是N个,并生成派生集合的组合。对于派生集合的每个组合,迭代每个元素计数的交叉乘积(不包括空元素)。你需要代码来迭代每个元素计数的交叉乘积吗? - Yakk - Adam Nevraumont
我想我不明白如何从集合组合转换为多重集合组合。如果这是如此“微不足道”,为什么没有一本书有这个算法?另外,如果你认为这很简单,为什么不展示算法呢?我只对比我已经展示的实现更有效率的实现感兴趣。 - Tyler Durden
如上所述,我正在寻找一种迭代(非递归)算法。 - Tyler Durden
@TylerDurden 添加了迭代版本。像大多数递归算法的迭代重写一样,递归理解起来要容易得多,但它在功能上是等效的。我只是记录了indexes大小中的深度,并手动维护了堆栈。现在我要回去添加注释了。 - Yakk - Adam Nevraumont
显示剩余2条评论

1

编辑: 根据澄清的问题调整了答案

主要思路: 同样,所得到的选择结果可以类比于自定义的数字系统进行编码。可以通过增加计数器并将其解释为选择来实现。

然而,由于还有一个大小等于target的附加限制。 一种天真的实现方法是仅检查所得到的选择的大小,并跳过不满足该限制的选择。但是这种方法很慢。

所以我只是做了一个稍微聪明一点的增量,直接跳转到具有正确大小的选择。

抱歉,代码是用Python编写的。 但我按照Java迭代器接口的方式进行了比较。 输入和输出格式为:

haves[i] := multiplicity of the i-th item in the collection
target := output collection must have this size

代码:

class Perm(object):
    def __init__(self,items,haves,target):
        assert sum(haves) >= target
        assert all(h > 0 for h in haves)
        self.items = items
        self.haves = haves
        self.target = target
        self.ans = None
        self.stop = False
    def __iter__(self):
        return self
    def reset(self):
        self.ans = [0]*len(self.haves)
        self.__fill(self.target)
        self.stop = False
    def __fill(self,n):
        """fill ans from LSB with n bits"""
        if n <= 0: return
        i = 0
        while n > self.haves[i]:
            assert self.ans[i] == 0
            self.ans[i] = self.haves[i]
            n -= self.haves[i]
            i += 1
        assert self.ans[i] == 0
        self.ans[i] = n
    def __inc(self):
        """increment from LSB, carry when 'target' or 'haves' constrain is broken"""
        # in fact, the 'target' constrain is always broken on the left most non-zero entry
        # find left most non-zero
        i = 0
        while self.ans[i] == 0:
            i += 1
        # set it to zero
        l = self.ans[i]
        self.ans[i] = 0
        # do increment answer, and carry
        while True:
            # increment to the next entry, if possible
            i += 1
            if i >= len(self.ans):
                self.stop = True
                raise StopIteration
            #
            if self.ans[i] == self.haves[i]:
                l += self.ans[i]
                self.ans[i] = 0
            else:
                l -= 1
                self.ans[i] += 1
                break
        return l
    def next(self):
        if self.stop:
            raise StopIteration
        elif self.ans is None:
            self.reset()
        else:
            l = self.__inc()
            self.__fill(l)
        return self.ans

请注意,items参数实际上并未被使用。 __init__中的assert是为了明确我的输入假设。 __fill中的assert只是展示了在调用__fillself.ans的一个方便属性。
这是一个测试代码的好框架:
test_cases = [([3,2,1], 3),
              ([3,2,1], 5),
              ([3,2,1], 6),
              ([4,3,2,1,1], 4),
              ([1,3,1,2,4], 4),
             ]

P = Perm(None,*test_cases[-1])
for p in P:
    print p
    #raw_input()

输入 ([1,3,1,2,4], 4) 的示例结果:

[1, 3, 0, 0, 0]
[1, 2, 1, 0, 0]
[0, 3, 1, 0, 0]
[1, 2, 0, 1, 0]
[0, 3, 0, 1, 0]
[1, 1, 1, 1, 0]
[0, 2, 1, 1, 0]
[1, 1, 0, 2, 0]
[0, 2, 0, 2, 0]
[1, 0, 1, 2, 0]
[0, 1, 1, 2, 0]
[1, 2, 0, 0, 1]
[0, 3, 0, 0, 1]
[1, 1, 1, 0, 1]
[0, 2, 1, 0, 1]
[1, 1, 0, 1, 1]
[0, 2, 0, 1, 1]
[1, 0, 1, 1, 1]
[0, 1, 1, 1, 1]
[1, 0, 0, 2, 1]
[0, 1, 0, 2, 1]
[0, 0, 1, 2, 1]
[1, 1, 0, 0, 2]
[0, 2, 0, 0, 2]
[1, 0, 1, 0, 2]
[0, 1, 1, 0, 2]
[1, 0, 0, 1, 2]
[0, 1, 0, 1, 2]
[0, 0, 1, 1, 2]
[0, 0, 0, 2, 2]
[1, 0, 0, 0, 3]
[0, 1, 0, 0, 3]
[0, 0, 1, 0, 3]
[0, 0, 0, 1, 3]
[0, 0, 0, 0, 4]

性能 每次next()调用的时间复杂度为O(h),其中h是项目类型的数量(即haves列表的长度)。


在您的例子中,(4+1)(3+1)(1+1)*(2+1) 的值是幂集(包括空集)中的条目数。这里的问题是将组合生成到固定数量的插槽中,而不是生成幂集。 - Tyler Durden
啊,我明白了,现在我想我理解你想要的是一样的,但是受到特定总大小的限制?等一下...我想我可以修改... - Apiwat Chantawibul
哈哈。我稍后会写一个更直观的算法描述。但目前,我想指出我的算法只使用1个辅助数组来跟踪当前迭代进度,而该数组也是答案!然而,你的算法使用了额外的一个数组。 - Apiwat Chantawibul

0

这篇论文在第8页提供了一个高效的迭代算法,用于生成多重集合排列

这篇论文也在第8页提供了另一个迭代算法


是的,我熟悉这两篇论文。它们迭代排列而不是组合。 - Tyler Durden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接