大集合的第 n 个或任意组合

Posted

技术标签:

【中文标题】大集合的第 n 个或任意组合【英文标题】:n-th or Arbitrary Combination of a Large Set 【发布时间】:2013-02-10 02:53:33 【问题描述】:

假设我有一组来自[0, ....., 499] 的数字。目前正在使用 C++ std::next_permutation 顺序生成组合。作为参考,我拉出的每个元组的大小是 3,所以我返回的是顺序结果,例如 [0,1,2], [0,1,3], [0,1,4], ... [497,498,499]

现在,我想并行化它所在的代码,因此这些组合的顺序生成将不再起作用。是否有任何现有的算法可以从 500 个数字中计算出 3 的 ith 组合?

我想确保每个线程,无论它获得的循环迭代如何,都可以根据它正在迭代的i 计算一个独立的组合。因此,如果我想要线程 1 中 i=38 的组合,我可以计算 [1,2,5],同时将线程 2 中的 i=0 计算为 [0,1,2]

编辑下面的陈述无关紧要,我自己搞混了

我查看了利用阶乘从左到右缩小每个单独元素的算法,但我不能将这些用作 500!肯定不适合记忆。有什么建议吗?

【问题讨论】:

向我们展示涉及阶乘的计算。你可能只是看错了。当然,您会将一个阶乘分解为另一个,这通常意味着可以进行简化。 我想我需要重新表述我的问题。这不仅仅是 500 个数字的排列。这是 500 种可能性中 3 种的组合。但我希望能够从 500 中选择任意组合,选择 3 种可能。 可能类似于以下内容:code.google.com/p/strtk/source/browse/trunk/strtk.hpp#11622 @Gerdiner 如果我能接受你的回答,我会的。该代码更通用,并且也可以完全按照需要工作。那里有很棒的算法。谢谢! 【参考方案1】:

这是我的镜头:

int k = 527; //The kth combination is calculated
int N=500; //Number of Elements you have
int a=0,b=1,c=2; //a,b,c are the numbers you get out

while(k >= (N-a-1)*(N-a-2)/2)
    k -= (N-a-1)*(N-a-2)/2;
    a++;

b= a+1;
while(k >= N-1-b)
    k -= N-1-b;
    b++;


c = b+1+k;


cout << "["<<a<<","<<b<<","<<c<<"]"<<endl; //The result

想到在下一个数字增加之前有多少组合。但是,它仅适用于三个元素。我不能保证它是正确的。如果您将其与您的结果进行比较并提供一些反馈,那就太棒了。

【讨论】:

简单,快速,就我所知。我试图想出类似于 jacobm 所写的东西,但我确实喜欢这样!【参考方案2】:

如果您正在寻找一种方法来获取字典索引或唯一组合的排名而不是排列,那么您的问题属于二项式系数。二项式系数处理在总共有 N 个项目的 K 组中选择唯一组合的问题。

我用 C# 编写了一个类来处理处理二项式系数的常用函数。它执行以下任务:

    以适合任何 N 选择 K 的格式将所有 K 索引输出到文件。 K-indexes 可以替换为更具描述性的字符串或字母。

    将 K 索引转换为正确的词典索引或排序二项式系数表中条目的等级。这种技术比依赖迭代的旧已发布技术快得多。它通过使用帕斯卡三角形中固有的数学属性来做到这一点,并且与迭代集合相比非常有效。

    将已排序二项式系数表中的索引转换为相应的 K 索引。我相信它也比旧的迭代解决方案更快。

    使用Mark Dominus方法计算二项式系数,该方法不太可能溢出并且适用于较大的数字。

    该类是用 .NET C# 编写的,并提供了一种通过使用通用列表来管理与问题相关的对象(如果有)的方法。此类的构造函数采用一个名为 InitTable 的 bool 值,当它为 true 时,将创建一个通用列表来保存要管理的对象。如果此值为 false,则不会创建表。无需创建表即可使用上述 4 种方法。提供访问器方法来访问表。

    有一个关联的测试类显示如何使用该类及其方法。它已经过 2 个案例的广泛测试,没有已知的错误。

要了解该课程并下载代码,请参阅Tablizing The Binomial Coeffieicent。

以下测试代码将遍历每个独特的组合:

public void Test10Choose5()

   String S;
   int Loop;
   int N = 500;  // Total number of elements in the set.
   int K = 3;  // Total number of elements in each group.
   // Create the bin coeff object required to get all
   // the combos for this N choose K combination.
   BinCoeff<int> BC = new BinCoeff<int>(N, K, false);
   int NumCombos = BinCoeff<int>.GetBinCoeff(N, K);
   // The Kindexes array specifies the indexes for a lexigraphic element.
   int[] KIndexes = new int[K];
   StringBuilder SB = new StringBuilder();
   // Loop thru all the combinations for this N choose K case.
   for (int Combo = 0; Combo < NumCombos; Combo++)
   
      // Get the k-indexes for this combination.  
      BC.GetKIndexes(Combo, KIndexes);
      // Verify that the Kindexes returned can be used to retrive the
      // rank or lexigraphic order of the KIndexes in the table.
      int Val = BC.GetIndex(true, KIndexes);
      if (Val != Combo)
      
         S = "Val of " + Val.ToString() + " != Combo Value of " + Combo.ToString();
         Console.WriteLine(S);
      
      SB.Remove(0, SB.Length);
      for (Loop = 0; Loop < K; Loop++)
      
         SB.Append(KIndexes[Loop].ToString());
         if (Loop < K - 1)
            SB.Append(" ");
      
      S = "KIndexes = " + SB.ToString();
      Console.WriteLine(S);
   

您应该能够相当轻松地将此类移植到 C++。您可能不必移植类的通用部分来实现您的目标。您的 500 选择 3 的测试用例产生 20,708,500 个唯一组合,它们适合 4 字节的 int。如果 500 choose 3 只是一个示例,您需要选择大于 3 的组合,那么您将不得不使用 long 或定点 int。

【讨论】:

我一定会调查的。 500 选择 3 保证是我们正在查看的参数的最坏情况,所以我不太担心溢出。 我将您的代码@Bob Bryan 移植到了 Java。你可以在github.com/aalhossary/BinomialCoefficient找到它【参考方案3】:

您可以将 500 个对象中的 3 个特定选择描述为三元组(i, j, k),其中i 是从 0 到 499 的数字(第一个数字的索引),j 的范围从 0 到 498 (第二个的索引,跳过第一个数字),k 的范围从 0 到 497(最后一个的索引,跳过之前选择的两个数字)。鉴于此,枚举所有可能的选择实际上非常容易:从(0,0,0) 开始,递增k 直到达到最大值,然后递增j 并将k 重置为0,依此类推,直到@ 987654329@ 达到最大值,以此类推,直到j 达到自己的最大值;然后递增i 并重置jk 并继续。

如果这个描述听起来很熟悉,那是因为它与增加一个以 10 为底的数字的工作方式完全相同,只是底数更时髦,实际上底数因数而异 .您可以利用这种洞察力来实现该想法的一个非常紧凑的版本:对于从 0 到 500*499*498 的任何整数 n,您可以获得:

struct 
  int i, j, k;
 triple;

triple AsTriple(int n) 
  triple result;
  result.k = n % 498;
  n = n / 498;
  result.j = n % 499;
  n = n / 499;
  result.i = n % 500;  // unnecessary, any legal n will already be between 0 and 499
  return result;


void PrintSelections(triple t) 
  int i, j, k;
  i = t.i;
  j = t.j + (i <= j ? 1 : 0);
  k = t.k + (i <= k ? 1 : 0) + (j <= k ? 1 : 0);
  std::cout << "[" << i << "," << j << "," << k << "]" << std::endl;


void PrintRange(int start, int end) 
  for (int i = start; i < end; ++i) 
    PrintSelections(AsTriple(i));
  

现在要进行分片,您可以取 0 到 500*499*498 之间的数字,以任何您喜欢的方式将它们划分为子范围,然后让每个分片计算其子范围中每个值的排列。

这个技巧对于需要枚举子集的任何问题都非常方便。

【讨论】:

这里唯一的问题是我最终会出现重复。我需要 500 种选择 3 种组合(最坏情况),即约 2000 万种组合。这种情况没有重复发生,因此 (0,0,0) 被消除。谢谢你,我很感激你的回答! 不,我描述的方式没有重复。诀窍在于解释:(0,0,0) 代表第一个、第二个和第三个元素,因为对于每个数字,您都会跳过您已经选择的所有元素。 啊啊啊。我现在明白了。我会测试一下! 仅供参考,更新了上面的代码示例,使其更加完整,并希望阐明数字的解释方式。 这个答案不正确。您可以通过调用 PrintRange(0, 1) 轻松看到这一点。它打印 [0,1,1],这甚至不是一个有效的组合。它还会产生很多重复项。例如,它将 1 映射到 [0,1,2],将 498 映射到 [0,2,1]。正确解决这个问题需要更复杂的计算。

以上是关于大集合的第 n 个或任意组合的主要内容,如果未能解决你的问题,请参考以下文章

正则表达式

组合数据类型,英文词频统计

[Usaco2010 Mar]gather 奶牛大集会

正则表达式

求n个元素的任意组合

BZOJ1827 [Usaco2010 Mar]gather 奶牛大集会