如何改进生成多集组合的算法
How can I improve my algorithm for generating combinations of a multiset?
如何在以下生成有界多集组合的生成器中优化next()
和hasNext()
方法?(我在C++和Java上发布了这篇文章,因为代码是C++兼容的,并且没有不能直接转换为C++的Java特定元素
算法中有问题的特定领域是整个hasNext()
方法,它可能不必要地复杂,以及行:
if( current[xSlot] > 0 ) aiItemsUsed[current[xSlot]]--;
它有一个if语句,我认为可以以某种方式删除。我有一个早期版本的算法,在返回语句之前有一些回溯,因此有一个简单得多的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,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
编辑:根据澄清的问题调整答案
主要思想:同样,所产生的选择可以像自定义数字系统一样进行编码。可以增加一个计数器并将该计数器解释为一个选择。
然而,由于存在选择的大小==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
只是为了在调用__fill
的上下文中显示self.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
列表的大小(。
我会编写一个简单的助手类来执行increment
、highbit
和for_each_bit
。
我会首先包装一个unsigned int
,并将其限制在32位,如果我有野心的话,可能会用std::bitset
或std::vector<uint32_t>
来扩展它——但通过使用这三种方法,我可以测试它并使其工作。
increment
很简单,尤其是在裸32位内部上
CCD_ 26返回最高设置比特的比特位置。
for_each_bit
在C++中具有此签名:
template<typename Lambda>
void for_each_bit( my_bignum const& num, Lambda&& func )
并且它用CCD_ 29中的每个设置比特的索引来调用CCD_。
写出来最多需要几分钟。
抛开hasNext
,遵循迭代器的概念——您有一个begin
子集和一个end
子集,而end
对于提取的值是无效的。取消引用这些迭代器会生成有问题的子集(或为所述子集生成工厂(。
end
现在很容易计算了——如果highbit
是>=集合中元素的数量,那么就超过了排列的末尾。
begin
要么是零,要么是1,这取决于您是否希望包含空子集。
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
本文在第8页提供了一种生成多集排列的高效迭代算法
本文提供了另一种迭代算法,同样在第8页
- 构建可组合有向图(扫描仪生成器的汤普森构造算法)
- 计算数组重复次数的组合的有效算法,加起来达到给定的总和
- 为 C++11 算法组合多个谓词
- 双重释放或损坏(输出):使用向量的组合算法0x0000000001a880a0***
- Python到C++:使用递归列出背包的所有组合的算法
- 从给定的 IPv6:端口列表中搜索 IPv6:端口组合的最快搜索算法是什么 O(1) 时间一致性
- 给定数字与重复的组合的算法?C
- 排列和组合生成算法
- 加密++对称算法和经过身份验证的块模式组合
- 所有的组合算法和解决C++问题的一般方法
- 按字典顺序打印给定字符串的所有字母组合的算法
- 生成每个可能的 7 位数字组合的算法
- 运行整数数组所有组合的算法
- 根据组合框中的选定项目选择算法
- C++算法优化:从N个元素中求出K组合
- 递归算法将所有组合分成两组
- 数组算法的组合
- 按类型算法的列表组合
- 一个很好的算法来获得元素的闲置组合
- 是硬币变化算法,输出仍可由DP解决的所有组合