三重限制的正整数组合的非递归枚举

Non-recursive enumeration of triply restricted positive integer compositions

本文关键字:整数 组合 枚举 递归 三重      更新时间:2023-10-16

在创建一个迭代(非递归)函数后,该函数以字典顺序枚举正整数的双重限制组合,对于具有非常少量RAM(但较大的EPROM)的微控制器,我不得不将限制的数量扩展到3,即:

  1. 对构图长度的限制
  2. 对元素最小值的限制
  3. 对元素最大值的限制

下面列出了生成双重限制组合的原始函数:

void GenCompositions(unsigned int myInt, unsigned int CompositionLen, unsigned int MinVal)
{
if ((MinVal = MinPartitionVal(myInt, CompositionLen, MinVal, (unsigned int) (-1))) == (unsigned int)(-1)) // Increase the MinVal to the minimum that is feasible.
return;
std::vector<unsigned int> v(CompositionLen);
int pos = 0;
const int last = CompositionLen - 1;

for (unsigned int i = 1; i <= last; ++i) // Generate the initial composition
v[i] = MinVal;
unsigned int MaxVal = myInt - MinVal * last;
v[0] = MaxVal;
do
{
DispVector(v);
if (pos == last)
{
if (v[last] == MaxVal)
break;
for (--pos; v[pos] == MinVal; --pos);  //Search for the position of the Least Significant non-MinVal (not including the Least Significant position / the last position).
//std::cout << std::setw(pos * 3 + 1) << "" << "v" << std::endl;    //DEBUG
--v[pos++];
if (pos != last)
{
v[pos] = v[last] + 1;
v[last] = MinVal;
}
else
v[pos] += 1;
}
else
{
--v[pos];
v[++pos] = MinVal + 1;
}
} while (true);
}

此功能的示例输出为:

GenCompositions(10,4,1);:
7, 1, 1, 1
6, 2, 1, 1
6, 1, 2, 1
6, 1, 1, 2
5, 3, 1, 1
5, 2, 2, 1
5, 2, 1, 2
5, 1, 3, 1
5, 1, 2, 2
5, 1, 1, 3
4, 4, 1, 1
4, 3, 2, 1
4, 3, 1, 2
4, 2, 3, 1
4, 2, 2, 2
4, 2, 1, 3
4, 1, 4, 1
4, 1, 3, 2
4, 1, 2, 3
4, 1, 1, 4
3, 5, 1, 1
3, 4, 2, 1
3, 4, 1, 2
3, 3, 3, 1
3, 3, 2, 2
3, 3, 1, 3
3, 2, 4, 1
3, 2, 3, 2
3, 2, 2, 3
3, 2, 1, 4
3, 1, 5, 1
3, 1, 4, 2
3, 1, 3, 3
3, 1, 2, 4
3, 1, 1, 5
2, 6, 1, 1
2, 5, 2, 1
2, 5, 1, 2
2, 4, 3, 1
2, 4, 2, 2
2, 4, 1, 3
2, 3, 4, 1
2, 3, 3, 2
2, 3, 2, 3
2, 3, 1, 4
2, 2, 5, 1
2, 2, 4, 2
2, 2, 3, 3
2, 2, 2, 4
2, 2, 1, 5
2, 1, 6, 1
2, 1, 5, 2
2, 1, 4, 3
2, 1, 3, 4
2, 1, 2, 5
2, 1, 1, 6
1, 7, 1, 1
1, 6, 2, 1
1, 6, 1, 2
1, 5, 3, 1
1, 5, 2, 2
1, 5, 1, 3
1, 4, 4, 1
1, 4, 3, 2
1, 4, 2, 3
1, 4, 1, 4
1, 3, 5, 1
1, 3, 4, 2
1, 3, 3, 3
1, 3, 2, 4
1, 3, 1, 5
1, 2, 6, 1
1, 2, 5, 2
1, 2, 4, 3
1, 2, 3, 4
1, 2, 2, 5
1, 2, 1, 6
1, 1, 7, 1
1, 1, 6, 2
1, 1, 5, 3
1, 1, 4, 4
1, 1, 3, 5
1, 1, 2, 6
1, 1, 1, 7

在添加第三个限制(关于元素的最大值)后,函数的复杂性显着增加。下面列出了此扩展功能:

void GenCompositions(unsigned int myInt, unsigned int CompositionLen, unsigned int MinVal, unsigned int MaxVal)
{
if ((MaxVal = MaxPartitionVal(myInt, CompositionLen, MinVal, MaxVal)) == 0) //Decrease the MaxVal to the maximum that is feasible.
return;
if ((MinVal = MinPartitionVal(myInt, CompositionLen, MinVal, MaxVal)) == (unsigned int)(-1))    //Increase the MinVal to the minimum that is feasible.
return;
std::vector<unsigned int> v(CompositionLen);
unsigned int last = CompositionLen - 1;
unsigned int rem = myInt - MaxVal - MinVal*(last-1);
unsigned int pos = 0;
v[0] = MaxVal;  //Generate the most significant element in the initial composition
while (rem > MinVal){   //Generate the rest of the initial composition (the highest in the lexicographic order). Spill the remainder left-to-right saturating at MaxVal
v[++pos] = ( rem > MaxVal ) ? MaxVal : rem;  //Saturate at MaxVal
rem -= v[pos] - MinVal; //Deduct the used up units (less the background MinValues)
}
for (unsigned int i = pos+1; i <= last; i++)    //Fill with MinVal where the spillage of the remainder did not reach.
v[i] = MinVal;

if (MinVal == MaxVal){  //Special case - all elements are the same. Only the initial composition is possible.
DispVector(v);
return;
}
do
{
DispVector(v);
if (pos == last)        
{       
for (--pos; v[pos] == MinVal; pos--) {  //Search backwards for the position of the Least Significant non-MinVal (not including the Least Significant position / the last position).
if (!pos)   
return;
}
//std::cout << std::setw(pos*3 +1) << "" << "v" << std::endl;  //Debug
if (v[last] >= MaxVal)  // (v[last] > MaxVal) should never occur
{
if (pos == last-1)  //penultimate position. //Skip the iterations that generate excessively large compositions (with elements > MaxVal).
{   
for (rem = MaxVal; ((v[pos] == MinVal) || (v[pos + 1] == MaxVal)); pos--) { //Search backwards for the position of the Least Significant non-extremum (starting from the penultimate position - where the previous "for loop" has finished).  THINK:  Is the (v[pos] == MinVal) condition really necessary here ?
rem += v[pos];  //Accumulate the sum of the traversed elements
if (!pos)
return;
}
//std::cout << std::setw(pos * 3 + 1) << "" << "v" << std::endl;    //Debug
--v[pos];
rem -= MinVal*(last - pos - 1) - 1;  //Subtract the MinValues, that are assumed to always be there as a background
while (rem > MinVal)    // Spill the remainder left-to-right saturating at MaxVal
{
v[++pos] = (rem > MaxVal) ? MaxVal : rem;   //Saturate at MaxVal
rem -= v[pos] - MinVal; //Deduct the used up units (less the background MinValues)
}
for (unsigned int i = pos + 1; i <= last; i++)  //Fill with MinVal where the spillage of the remainder did not reach.
v[i] = MinVal;
continue;   //The skipping of excessively large compositions is complete. Nothing else to adjust...
}
/* (pos != last-1) */
--v[pos];
v[++pos] = MaxVal;
v[++pos] = MinVal + 1;  //Propagate the change one step further. THINK: Why a CONSTANT value like MinVal+1 works here at all?
if (pos != last)
v[last] = MinVal;
}
else    // (v[last] < MaxVal)
{           
--v[pos++];
if (pos != last)
{
v[pos] = v[last] + 1;
v[last] = MinVal;
}
else
v[pos] += 1;
}
}
else    // (pos != last)
{
--v[pos];
v[++pos] = MinVal + 1;  // THINK: Why a CONSTANT value like MinVal+1 works here at all ?
}
} while (true);
}

此扩展函数的示例输出为:

GenCompositions(10,4,1,4);:
4, 4, 1, 1
4, 3, 2, 1
4, 3, 1, 2
4, 2, 3, 1
4, 2, 2, 2
4, 2, 1, 3
4, 1, 4, 1
4, 1, 3, 2
4, 1, 2, 3
4, 1, 1, 4
3, 4, 2, 1
3, 4, 1, 2
3, 3, 3, 1
3, 3, 2, 2
3, 3, 1, 3
3, 2, 4, 1
3, 2, 3, 2
3, 2, 2, 3
3, 2, 1, 4
3, 1, 4, 2
3, 1, 3, 3
3, 1, 2, 4
2, 4, 3, 1
2, 4, 2, 2
2, 4, 1, 3
2, 3, 4, 1
2, 3, 3, 2
2, 3, 2, 3
2, 3, 1, 4
2, 2, 4, 2
2, 2, 3, 3
2, 2, 2, 4
2, 1, 4, 3
2, 1, 3, 4
1, 4, 4, 1
1, 4, 3, 2
1, 4, 2, 3
1, 4, 1, 4
1, 3, 4, 2
1, 3, 3, 3
1, 3, 2, 4
1, 2, 4, 3
1, 2, 3, 4
1, 1, 4, 4

问题:我对元素最大值的限制在哪里出错,导致代码的大小和复杂性增加?
IOW:算法中的缺陷在哪里,导致在添加一个简单的<= MaxVal限制后出现这种代码膨胀? 可以在没有递归的情况下简化吗?

如果有人想实际编译它,下面列出了帮助程序函数:

#include <iostream>
#include <iomanip>
#include <vector> 
void DispVector(const std::vector<unsigned int>& partition)
{
for (unsigned int i = 0; i < partition.size() - 1; i++)       //DISPLAY THE VECTOR HERE ...or do sth else with it.
std::cout << std::setw(2) << partition[i] << ",";
std::cout << std::setw(2) << partition[partition.size() - 1] << std::endl;
}
unsigned int MaxPartitionVal(const unsigned int myInt, const unsigned int PartitionLen, unsigned int MinVal, unsigned int MaxVal)
{
if ((myInt < 2) || (PartitionLen < 2) || (PartitionLen > myInt) || (MaxVal < 1) || (MinVal > MaxVal) || (PartitionLen > myInt) || ((PartitionLen*MaxVal) < myInt ) || ((PartitionLen*MinVal) > myInt))  //Sanity checks
return 0;
unsigned int last = PartitionLen - 1;
if (MaxVal + last*MinVal > myInt)
MaxVal = myInt - last*MinVal;   //It is not always possible to start with the Maximum Value. Decrease it to sth possible
return MaxVal;
}
unsigned int MinPartitionVal(const unsigned int myInt, const unsigned int PartitionLen, unsigned int MinVal, unsigned int MaxVal)
{
if ((MaxVal = MaxPartitionVal(myInt, PartitionLen, MinVal, MaxVal)) == 0)   //Assume that MaxVal has precedence over MinVal
return (unsigned int)(-1);
unsigned int last = PartitionLen - 1;
if (MaxVal + last*MinVal > myInt)
MinVal = myInt - MaxVal - last*MinVal;  //It is not always possible to start with the Minimum Value. Increase it to sth possible
return MinVal;
}
//
// Put the definition of GenCompositions() here....
//
int main(int argc, char *argv[])
{
GenCompositions(10, 4, 1, 4);
return 0;
}

注意:由这些功能生成的作品的(从上到下)词典顺序不是可选的。跳过"do 循环"迭代也不会生成有效的组合。

算法

生成具有有限零件数以及最小值和最大值的组合的迭代算法并不复杂。固定长度和最小值的组合实际上使事情变得更容易;我们可以始终保持每个部分的最小值,只需移动"额外"值即可生成不同的组合。

我将使用此示例:

n=15, length=4, min=3, max=5

我们将首先创建一个具有最小值的合成:

3,3,3,3

然后我们将剩余值 15 - 12 = 3 分布在零件上,从第一部分开始,每次达到最大值时向右移动:

5,4,3,3

这是第一个构图。然后,我们将使用以下规则反复变换构图以获得反向词法顺序的下一个组合:

我们从找到值大于最小值的最右侧部分开始每一步。(实际上这可以简化;请参阅本答案末尾的更新代码示例。如果这部分不是最后一部分,我们从中减去 1,并在它右侧的部分加 1,例如:

5,4,3,3
^
5,3,4,3

这就是下一个构图。如果最右边的非最小部分是最后一个部分,事情会稍微复杂一些。我们将最后一部分的值降至最小,并将"额外"值存储在临时总计中,例如:

3,4,3,5
^
3,4,3,3   + 2

然后我们进一步向左移动,直到找到值大于最小值的下一部分:

3,4,3,3   + 2
^

如果这部分(2)右侧的零件数可以容纳临时总计加1,我们从当前部分减去1,并在临时总计中加1,然后分配临时总计,从当前部分右侧的部分开始:

3,3,3,3   + 3
^
3,3,5,4

这就是我们的下一个构图。如果非最小部分右侧的部分无法容纳临时总计加 1,我们将再次将该部分减少到最小值并将"额外"值添加到临时总计中,然后进一步向左看,例如(使用另一个示例,n=17):

5,3,4,5
^
5,3,4,3   + 2
^
5,3,3,3   + 3
^
4,3,3,3   + 4
^
4,5,5,3

这就是我们的下一个构图。如果我们向左移动以找到一个非最小值,但到达第一部分而没有找到一个,我们就超过了最后一个组合,例如:

3,3,4,5
^
3,3,4,3   + 2
^
3,3,3,3   + 3
?

这意味着3,3,4,5是最后的作品。

如您所见,这只需要一个合成和临时总计的空间,从右到左遍历每个合成一次以查找非最小部分,并从左到右迭代一次组合以分发临时总计。它创建的所有作品都是有效的,并且按字典顺序倒序排列。


代码示例

我首先将这个简单的翻译写成上面解释的算法C++。查找最右侧的非最小部分并在组合中分配值由两个辅助函数完成。代码遵循逐步说明,但这不是最有效的编码方式。有关改进版本,请参见下文。

#include <iostream>
#include <iomanip>
#include <vector>
void DisplayComposition(const std::vector<unsigned int>& comp)
{
for (unsigned int i = 0; i < comp.size(); i++)
std::cout << std::setw(3) << comp[i];
std::cout << std::endl;
}
void Distribute(std::vector<unsigned int>& comp, const unsigned int part, const unsigned int max, unsigned int value) {
for (unsigned int p = part; value && p < comp.size(); ++p) {
while (comp[p] < max) {
++comp[p];
if (!--value) break;
}
}
}
int FindNonMinPart(const std::vector<unsigned int>& comp, const unsigned int part, const unsigned int min) {
for (int p = part; p >= 0; --p) {
if (comp[p] > min) return p;
}
return -1;
}
void GenerateCompositions(const unsigned n, const unsigned len, const unsigned min, const unsigned max) {
if (len < 1 || min > max || n < len * min || n > len * max) return;
std::vector<unsigned> comp(len, min);
Distribute(comp, 0, max, n - len * min);
int part = 0;
while (part >= 0) {
DisplayComposition(comp);
if ((part = FindNonMinPart(comp, len - 1, min)) == len - 1) {
unsigned int total = comp[part] - min;
comp[part] = min;
while (part && (part = FindNonMinPart(comp, part - 1, min)) >= 0) {
if ((len - 1 - part) * (max - min) > total) {
--comp[part];
Distribute(comp, part + 1, max, total + 1);
total = 0;
break;
}
else {
total += comp[part] - min;
comp[part] = min;
}
}
}
else if (part >= 0) {
--comp[part];
++comp[part + 1];
}
}
}
int main() {
GenerateCompositions(15, 4, 3, 5);
return 0;
}

改进的代码示例

实际上,大多数对FindNonMinPart的调用都是不必要的,因为在重新分配值后,您确切地知道最右边的非最小部分在哪里,并且无需再次搜索它。重新分配额外值也可以简化,无需函数调用。

下面是一个更有效的代码版本,它考虑了这些事情。它在零件中左右走动,搜索非最小零件,重新分配额外价值并在完成后立即输出组合。它明显比第一个版本快(尽管对DisplayComposition的调用显然占用了大部分时间)。

#include <iostream>
#include <iomanip>
#include <vector>
void DisplayComposition(const std::vector<unsigned int>& comp)
{
for (unsigned int i = 0; i < comp.size(); i++)
std::cout << std::setw(3) << comp[i];
std::cout << std::endl;
}
void GenerateCompositions(const unsigned n, const unsigned len, const unsigned min, const unsigned max) {
// check validity of input
if (len < 1 || min > max || n < len * min || n > len * max) return;
// initialize composition with minimum value
std::vector<unsigned> comp(len, min);
// begin by distributing extra value starting from left-most part
int part = 0;
unsigned int carry = n - len * min;
// if there is no extra value, we are done
if (carry == 0) {
DisplayComposition(comp);
return;
}
// move extra value around until no more non-minimum parts on the left
while (part != -1) {
// re-distribute the carried value starting at current part and go right
while (carry) {
if (comp[part] == max) ++part;
++comp[part];
--carry;
}
// the composition is now completed
DisplayComposition(comp);
// keep moving the extra value to the right if possible
// each step creates a new composition
while (part != len - 1) {
--comp[part];
++comp[++part];
DisplayComposition(comp);
}
// the right-most part is now non-minimim
// transfer its extra value to the carry value
carry = comp[part] - min;
comp[part] = min;
// go left until we have enough minimum parts to re-distribute the carry value
while (part--) {
// when a non-minimum part is encountered
if (comp[part] > min) {
// if carry value can be re-distributed, stop going left
if ((len - 1 - part) * (max - min) > carry) {
--comp[part++];
++carry;
break;
}
// transfer extra value to the carry value
carry += comp[part] - min;
comp[part] = min;
}
}
}
}
int main() {
GenerateCompositions(15, 4, 3, 5);
return 0;
}

该算法可以使用深度优先搜索和递归算法非常容易地实现。因为不能使用递归,所以可以使用堆栈来模拟函数调用。

这是一种可能的解决方案:

void GenCompositions(
unsigned int value,
const unsigned int CompositionLen,
const unsigned int min,
const unsigned int max
) {
using composition_t = std::vector<int>;
using stackframe = std::pair<composition_t::iterator, unsigned int>;
// Create a vector with size CompositionLen and fill it with the
// minimum allowed value
composition_t composition(CompositionLen, min);
// Because we may have initialised our composition with non-zero values,
// we need to decrease the remaining value
value -= min*CompositionLen;
// Iterator to where we intend to manipulate our composition
auto pos = composition.begin();
// We need the callstack to implement the depth first search in an
// iterative manner without searching through the composition on
// every backtrace.
std::vector<stackframe> callstack;
// We know, that the composition has a maximum length and so does our
// callstack. By reserving the memory upfront, we never need to
// reallocate the callstack when pushing new elements.
callstack.reserve(CompositionLen);
// Our main loop
do {
// We need to generate a valid composition. To do this, we fill the
// remaining places of the composition with the maximum allowed
// values, until the remaining value reaches zero
for(
;
// Check if we hit the end or the total sum equals the value
pos != composition.end() && value > 0;
++pos
) {
// Whenever we edit the composition, we add a frame to our
// callstack to be able to revert the changes when backtracking
callstack.emplace_back(pos, value);
// calculate the maximum allowed increment to add to the current
// position in our composition
const auto diff = std::min(value,max-*pos);
// *pos might have changed in a previous run, therefore we can
// not use diff as an offset. Instead we have to assign
// the correct value.
*pos = min+diff;
// We changed our composition, so we have to change the
// remaining value as well
value -= diff;
}
// If the remaining value is zero we got a correct composition and
// display it to std::out
if(value == 0) {
DisplayVector(
composition,
std::distance(composition.begin(), pos),
min
);
}
// This is our backtracking step. To prevent values below the
// minimum in our composition we backtrack until we get a value that
// is higher than the minimum. That way we can decrease this value
// in the last step by one. Because our for loop that generates the
// valid composition increases pos once more before exit, we have to
// look at (pos-1).
while(*(pos-1) <= min) {
// If our callstack is empty, we can not backtrack further and
// terminate the algorithm
if(callstack.empty()) {
return;
}
// If backtracking is possible, we get tha last values from the
// callstack and reset our state
pos = callstack.back().first;
value = callstack.back().second;
// After this is done, we remove the last frame from the stack
callstack.pop_back();
}
// The last step is to decrease the value in the composition and
// increase the remaining value to generate the next composition
--*(pos-1);
++value;
} while(true);
}

我还更改了DisplayVector的接口,这是一个可能的实现:

// Because we stop stepping deeper in our DFS tree, if the remaining value
// is zero, the composition may have wrong values behind pos. This is no
// problem for us, because we know these values have to be the minimum
// allowed value.
void DisplayVector(const std::vector<int>& vector, size_t pos, int minval) {
// I prefere to print opening an closing brackets around sequences
std::cout << "{ ";
// The ostream_iterator is a addition, that comes with C++17.
std::ostream_iterator<int> out_it (std::cout, " ");
// If you can't use C++17, you can use a for loop with an index
// from 0 to pos to print from the composition
std::copy_n(vector.begin(), pos, out_it);
// And a for loop to print vector.size() - pos times the value of minval
// to fill the rest of your composition
std::fill_n(out_it, vector.size() - pos, minval);
std::cout << "}n";
}

要编译此代码,您需要将标准设置为 C++17 并包含以下标头:

#include <iostream>
#include <vector>
#include <iterator>

GenCompositions()不使用 C++17 功能,因此如果您无法使用现代编译器,则可以重新实现打印功能并继续。