【问题标题】:How to efficiently generate all combinations (at all depths) whose sum is within a specified range如何有效地生成总和在指定范围内的所有组合(在所有深度)
【发布时间】:2013-10-13 22:47:14
【问题描述】:

假设您有一组值 (1,1,1,12,12,16),您将如何生成所有可能的组合(不重复),其总和在预定义范围[min,max]。例如,以下是范围在1317 之间的所有组合(所有深度):

112

1112

11112

16

116

这假设相同值的每个项目是无法区分的,因此您在最终输出中没有1 12 的三个结果。蛮力是可能的,但在项目数量很大的情况下,所有深度的组合数量是天文数字。在上面的示例中,所有深度都有 (3 + 1) * (2 + 1) * (1 + 1) = 24 种组合。因此,总组合是任何给定值的项目数 + 1 的乘积。当然,我们可以在逻辑上抛出部分和大于最大值的大量组合(例如集合1612已经大于17 的最大值,因此请跳过其中包含1612 的任何组合。

我最初认为我可以将输入数组转换为两个数组并像里程表一样增加它们。但是我完全陷入了这种提前中断的递归算法上。有什么建议吗?

{
    int uniqueValues = 3;
    int[] maxCounts = new int[uniqueValues];
    int[] values = new int[uniqueValues];

    // easy code to bin the data, just hardcoding for example
    maxCounts[0] = 3;
    values[0] = 1;
    maxCounts[1] = 2;
    values[1] = 12;
    maxCounts[2] = 1;
    values[2] = 16;

    GenerateCombinationsHelper(new List<int[]>(), 13, 17, 0, 0, maxCounts, new int[3], values);
}

private void GenerateCombinationsHelper(List<int[]> results, int min, int max, int currentValue, int index, int[] maxValues, int[] currentCombo, int[] values)
{
    if (index >= maxValues.Length)
    {
        return;
    }

    while (currentCombo[index] < maxValues[index])
    {
        currentValue += values[index];

        if (currentValue> max)
        {                   
            return;
        }

        currentCombo[index]++;

        if (currentValue< min)
        {                    
            GenerateCombinationsHelper(results, min, max, currentValue, index + 1, maxValues, currentCombo, values);
        }
        else
        {
            results.Add((int[])currentCombo.Clone());
        }
    }
}

编辑

整数值仅用于演示。它可以是任何具有某种数值的对象(intdoublefloat 等...)

通常只有少数唯一值(约 10 个左右),但总共可能有数千个项目。

【问题讨论】:

  • 那么,范围的顶部是独占的吗?如果不是,{1,16} 应该是有效的。
  • 糟糕,我会解决的
  • 你能假设所有的值都是正数吗?
  • 是的,所有的值都是正数(显然非零)
  • 最小值和最大值如何?有什么限制吗?如果不是,我认为下面的两个答案是正确的:)

标签: c# algorithm recursion combinatorics


【解决方案1】:

将主调用切换为:

GenerateCombinationsHelper2(new List<int[]>(), 13, 17, 0, maxCounts, new int[3], values);

然后添加这段代码:

private void GenerateCombinationsHelper2(List<int[]> results, int min, int max, int index, int[] maxValues, int[] currentCombo, int[] values)
{
    int max_count = Math.Min((int)Math.Ceiling((double)max / values[index]), maxValues[index]);

    for(int count = 0; count <= max_count; count++)
    {
        currentCombo[index] = count;
        if(index < currentCombo.Length - 1)
        {
            GenerateCombinationsHelper2(results, min, max, index + 1, maxValues, currentCombo, values);
        }
        else
        {
            int sum = Sum(currentCombo, values);
            if(sum >= min && sum <= max)
            {
                int[] copy = new int[currentCombo.Length];
                Array.Copy(currentCombo, copy, copy.Length);
                results.Add(copy);
            }
        }
    }
}

private static int Sum(int[] combo, int[] values)
{
    int sum = 0;
    for(int i = 0; i < combo.Length; i++)
    {
        sum += combo[i] * values[i];
    }
    return sum;
}

它返回 5 个有效答案。

【讨论】:

    【解决方案2】:

    这类问题的一般趋势是,会出现的值相对较少,但每个值都会出现很多很多次。因此,您首先要创建一个数据结构,以有效地描述将加起来为所需值的组合,然后才能找出所有组合。 (如果您知道“动态编程”这个术语,那正是我所描述的方法。)

    在 C# 术语中显而易见的数据结构是一个 Hashtable,其键是组合相加的总数,其值是列出最后一个元素位置的数组,这些元素可以在组合中使用,加起来可以那个特别的总数。

    您如何构建该数据结构?

    首先,您从一个包含总 0 作为键和一个空数组作为值的 Hashtable 开始。然后,对于数组的每个元素,您创建一个新总数列表,您可以从以前的总数中获得,并将元素的位置附加到它们的每个值(如果需要,插入一个新的)。浏览完所有元素后,您就有了数据结构。

    现在您可以在该数据结构中搜索所需范围内的总数。对于每个这样的总数,您可以编写一个递归程序,该程序将遍历您的数据结构以产生组合。这一步确实可以产生组合爆炸,但好的是产生的 EVERY 组合实际上是您最终答案中的组合。所以如果这个阶段需要很长时间,那是因为你有很多最终的答案!

    【讨论】:

    • 这似乎会使用大量内存来存储该数据结构。 Craig 的解决方案使用的内存非常少,而且速度非常快。另外,我计划将此算法用于浮点值,这会产生糟糕的哈希表键。
    • @Moop 这是内存/性能的权衡。实践中的内存使用并没有您想象的那么糟糕,但它并非微不足道。浮点舍入可以处理,但很复杂。当您有大量未达到阈值的组合时,速度会更快。克雷格的解决方案必须通过这些。这不会。
    【解决方案3】:

    试试这个算法

    int arr[] = {1,1,1,12,12,16}
    for(int i = 0;i<2^arr.Length;i++)
    {
    int[] arrBin = BinaryFormat(i); // binary format i
    for(int j = 0;j<arrBin.Length;j++)
      if (arrBin[j] == 1)
         Console.Write("{0} ", arr[j]);
    Console.WriteLine();
    }
    

    【讨论】:

    • 这不是蛮力吗?想象一下 int[] arr = 500 项。那是 2^500 次迭代。
    • 这是对的,但前提是数组的大小很小,因为使用您的解决方案的数组的大小只能达到 31(如果使用整数作为掩码)和 62(使用 long) .
    • 是的,你说得对。但是我们可以将 BigInteger 用于大数据。这个解决方案在大数据上速度很慢,但是对于这个问题是适用的
    • 此外,这永远不会检查解决方案是否在范围内。生成所有组合很容易,但要有效且有界地进行操作是困难的部分。
    • ok ok,是NP任务类,尝试高效有界
    【解决方案4】:

    这与subset sum problem 非常相似,恰好是NP-complete

    维基百科对 NP 完全问题的描述如下:

    尽管可以快速验证此类问题的任何给定解决方案, 首先没有已知的有效方法来找到解决方案 地方;确实,NP完全问题的最显着特征 是没有已知的快速解决方案。也就是需要的时间 使用任何当前已知的算法来解决问题会增加 随着问题规模的扩大,很快。这意味着 解决其中许多问题的中等大小版本所需的时间 问题很容易达到数十亿或数万亿年, 使用当今可用的任何数量的计算能力。作为结果, 确定是否有可能解决这些问题 很快,称为 P 与 NP 问题,是主要问题之一 当今计算机科学中未解决的问题。

    如果确实有一种方法可以解决这个问题,除了暴力破解 powerset 并找到在给定范围内总和为一个值的所有子集,那么我会非常有兴趣听到它。

    【讨论】:

      【解决方案5】:

      另一个实现的想法:

      从数字列表中创建一个堆栈列表,每个堆栈代表一个出现在列表中的数字,并且这个数字被压入堆栈的次数与他出现在数字列表中的次数一样多。更重要的是,此列表已排序。

      这个想法是您遍历堆栈列表,在每个堆栈中,如果它不超过最大值,则一次弹出一个数字并调用函数,并执行跳过当前堆栈的附加调用。

      该算法减少了许多冗余计算,例如当添加该值超过最大值时尝试添加具有相同值的不同元素。

      我能够用这个算法解决相当大的问题(50 个数字或更多),具体取决于最小值和最大值,显然当间隔非常大时,组合的数量可能会很大。

      代码如下:

      static void GenerateLimitedCombinations(List<int> intList, int minValue, int maxValue)
      {
          intList.Sort();
          List<Stack<int>> StackList = new List<Stack<int>>();
          Stack<int> NewStack = new Stack<int>();
          NewStack.Push(intList[0]);
          StackList.Add(NewStack);
      
          for (int i = 1; i < intList.count; i++)
          {
              if (intList[i - 1] == intList[i])
                  StackList[StackList.count - 1].Push(intList[i]);
              else
              {
                  NewStack = new Stack<int>();
                  NewStack.Push(intList[i]);
                  StackList.Add(NewStack);
              }
          }
      
          GenerateLimitedCombinations(StackList, minValue, maxValue, 0, new List<int>(), 0);
      }
      
      static void GenerateLimitedCombinations(List<Stack<int>> stackList, int minValue, int maxValue, int currentStack, List<int> currentCombination, int currentSum)
      {
          if (currentStack == stackList.count)
          {
              if (currentSum >= minValue)
              {
                  foreach (int tempInt in CurrentCombination)
                  {
                      Console.Write(tempInt + " ");
                  }
                  Console.WriteLine(;
              }
          }
      
          else
          {
              int TempSum = currentSum;
              List<int> NewCombination = new List<int>(currentCombination);
              Stack<int> UndoStack = new Stack<int>();
      
              while (stackList[currentStack].Count != 0 && stackList[currentStack].Peek() + TempSum <= maxValue)
              {
                  int AddedValue = stackList[currentStack].Pop();
                  UndoStack.Push(AddedValue);
                  NewCombination.Add(AddedValue);
                  TempSum += AddedValue;
                  GenerateLimitedCombinations(stackList, minValue, maxValue, currentStack + 1, new List<int>(NewCombination), TempSum);
              }
      
              while (UndoStack.Count != 0)
              {
                  stackList[currentStack].Push(UndoStack.Pop());
              }
      
              GenerateLimitedCombinations(stackList, minValue, maxValue, currentStack + 1, currentCombination, currentSum);
          }
      }
      

      这是一个测试程序:

      static void Main(string[] args)
      {
          Random Rnd = new Random();
          List<int> IntList = new List<int>();
          int NumberOfInts = 10, MinValue = 19, MaxValue 21;
      
          for (int i = 0; i < NumberOfInts; i++) { IntList.Add(Rnd.Next(1, 10));
          for (int i = 0; i < NumberOfInts; i++) { Console.Write(IntList[i] + " "); } Console.WriteLine(); Console.WriteLine();
      
          GenerateLimitedCombinations(IntList, MinValue, MaxValue);
          Console.ReadKey();
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-01-04
        • 2017-07-10
        • 2011-09-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-11-20
        相关资源
        最近更新 更多