【问题标题】:Generate All Unique Combinations of Elements of a IEnumerable(Of T)生成 IEnumerable(Of T) 元素的所有唯一组合
【发布时间】:2017-12-11 20:49:51
【问题描述】:

这个问题与this SO post 几乎相同,只是我在寻找VB.NET (.NET 4) 的解决方案。我已经旋转了足够长的时间,试图想出一个通用的解决方案来解决这个“电源组”问题。

给定:

Dim choices As IEnumerable(Of String) = {"Coffee", "Tea", "Milk", "Cookies"}
Dim choiceSets = choices.CombineAll()

我正在寻找 choiceSets 成为 IEnumerable(Of IEnumerable(Of T)) 以便我可以执行以下操作:

For each choiceSet in choiceSets
    Console.WriteLine(String.Join(", ", choiceSet))
Next

并获得如下所示的结果:

Coffee
Tea
Milk
Cookies
Coffee, Tea
Coffee, Milk
Coffee, Cookies
Tea, Milk
Tea, Cookies
Milk, Cookies
Coffee, Tea, Milk
Coffee, Tea, Cookies
Coffee, Milk, Cookies
Tea, Milk, Cookies
Coffee, Tea, Milk, Cookies

如您所见,这是来自源 IEnumerable(Of T) 的每个 非重复 组合(其中可能有 1 到多个项目 - 本示例只有 4 个),它基于源 IEnumerable(Of T) 中的项目的顺序,列表中的每个项目都 >= 内部 IEnumerable(Of T) 中的项目数的前一个项目。

不管怎样,这不是功课;虽然确实有这种感觉。

编辑:更新了示例,使其看起来不像是按字母顺序排序的结果,以强调使用源 IEnumerable(Of T) 的现有顺序并添加了第 4 个选项以阐明每组中的排序要求。

【问题讨论】:

  • 请注意,IEnumerable 不保证顺序一致,因此“尊重”它可能因调用而异。
  • @dlev 这很值得注意,谢谢。我认为这个问题仍然可以与 IEnumerable 确实保证一致排序的(错误)假设有关。为了简单起见,让我们假装它确实如此:)
  • 你可以试试这里的代码示例:codeproject.com/KB/recipes/Combinatorics.aspx
  • @mellamokb 感谢您的链接。当我开始研究这个时,我确实看到了那篇文章的代码。在我看来,这似乎是一个过于复杂的解决方案,尤其是当我看到在自包含扩展中完成的类似组合问题的示例时。我对这个问题的后备解决方案确实是那篇文章的解决方案,如果没有其他人能想出一些不那么笨重的东西。
  • 请注意,您想要的称为集合的“幂集”;所有子集的集合。 (幂集还包括空子集,您将其省略。)如果您正在寻找此问题的解决方案,知道它的正确名称会有所帮助。

标签: .net vb.net algorithm combinatorics


【解决方案1】:

这是一个纯 Linq 解决方案,灵感来自 Eric Lippert 的 blog post 关于计算笛卡尔积。我稍微修改了CartesianProduct 方法,使其返回组合:

public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<IEnumerable<T>> sequences)
{
    IEnumerable<IEnumerable<T>> emptyProduct = new[] { Enumerable.Empty<T>() };
    return sequences.Aggregate(
        emptyProduct,
        (accumulator, sequence) => 
        from accseq in accumulator 
        // Exclude items that were already picked
        from item in sequence.Except(accseq)
        // Enforce ascending order to avoid same sequence in different order
        where !accseq.Any() || Comparer<T>.Default.Compare(item, accseq.Last()) > 0
        select accseq.Concat(new[] {item})).ToArray();
}

基于这种扩展方法,您可以产生如下期望的结果:

IEnumerable<string> items = new[] {"Coffee", "Tea", "Milk"};
IEnumerable<IEnumerable<string>> result =
    Enumerable.Range(1, items.Count())
        .Aggregate(
            Enumerable.Empty<IEnumerable<string>>(),
            (acc, i) =>
                acc.Concat(Enumerable.Repeat(items, i).Combinations()));

(它连接 1、2...N 项的所有组合)

请注意,这可能不是一个非常有效的解决方案,但我认为这是 Linq 的一个有趣用途......


编辑:这是保持原始顺序的Combinations 方法的新版本:

public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<IEnumerable<T>> sequences)
{
    var indexedSequences = sequences.Select(seq => seq.Select((item, idx) => new IndexedItem<T>(item, idx)));
    IEnumerable<IEnumerable<IndexedItem<T>>> emptyProduct = new[] { Enumerable.Empty<IndexedItem<T>>() };
    var indexedResult =
        indexedSequences.Aggregate(
            emptyProduct,
            (accumulator, sequence) => 
            from accseq in accumulator 
            // Exclude items that were already picked
            from item in sequence.Except(accseq)
            // Enforce ascending order of indexes to avoid same sequence in different order
            where !accseq.Any() || item.Index > accseq.Last().Index
            select accseq.Concat(new[] {item})).ToArray();
    return indexedResult.Select(seq => seq.Select(i => i.Item));
}

class IndexedItem<T>
{
    public IndexedItem(T item, int index)
    {
        this.Item = item;
        this.Index = index;
    }

    public T Item { get; private set; }
    public int Index { get; set; }
}

可能比以前的版本效率更低,但它可以完成工作......

【讨论】:

  • 感谢您的意见。正确数量的结果(我喜欢它甚至不包括空集),但排序不满足问题陈述,但它非常接近(最接近的答案)。一旦将第 4 个项目添加到“项目”中,订购规则就会明显崩溃。结果从上到下正确排序,但在集合中未正确排序。我可以进行一些后处理排序,以使它们以正确的顺序返回。
  • @ckittel,请参阅我的更新答案。我为每个项目关联了一个索引,以便我可以保持原始顺序
  • A+。太感谢了!该解决方案非常适合我的需求。我可能会在今天晚些时候自己写一个答案,以展示 VB.NET 版本以供他人受益,但这个解决方案绝对是公认的答案。感谢您对解决方案如此勤奋。
【解决方案2】:

如果它对其他人有用,我已将原始 C# 扩展 Thomas Levesque 转换为发布到 VB.NET:

    <System.Runtime.CompilerServices.Extension()> _
    Public Function Combinations(Of T)(ByVal sequences As IEnumerable(Of IEnumerable(Of T))) As IEnumerable(Of IEnumerable(Of T))
        Dim seed As IEnumerable(Of IEnumerable(Of T)) = {  Enumerable.Empty(Of T) }
        Dim r = sequences.Aggregate(seed, 
             Function(ByVal accumulator, ByVal sequence) _
               From accseq In accumulator    _
               From item In sequence.Except(accseq) _
               Where (Not accseq.Any()) OrElse Comparer(Of T).Default.Compare(item, accseq.Last()) > 0  _
               Select accseq.Concat(  {item}  ) ).ToArray()
        Return r
    End Function

不得不调用 Repeat n 次来生成一个包含所有可能值的集合 n 次的重复 Enumerable 有点尴尬,其中 n 是 T 的每个唯一组合中的元素数,但它得到了工作完毕。结果的顺序对我来说并不重要,所以我没有转换后来发布的“索引”版本。

这是我对扩展的用法,它对整数数组而不是字符串进行操作,并获得其中没有元素的“空”集和“完整”(或原始)集

    Dim allRolesArray  = {1,4,5,2,0}
    Dim comboCountValues = Enumerable.Range(0, allRolesArray.Count()+1)
    Dim allRoleCombos = comboCountValues.Aggregate(
        Enumerable.Empty(Of IEnumerable(Of Integer))(),
        Function (acc, i) acc.Concat(Enumerable.Repeat(allRolesArray, i).Combinations() ) ).ToList

【讨论】:

    【解决方案3】:

    我找到了另一个approach here(在那里寻找 C# 代码)。

        Public Function GetPowerSet(Of T)(items As IEnumerable(Of T)) As IEnumerable(Of IEnumerable(Of T))
    
             Dim result = From m In Enumerable.Range(0, 1 << items.Count)
                     Select
                        From i In Enumerable.Range(0, items.Count)
                        Where (m And (1 << i)) <> 0
                            Select items(i)
             Return result
    
    End Function
    

    【讨论】:

      【解决方案4】:

      一个简单的递归解决方案(大量的列表创建开销):

          static List<IEnumerable<string>> GetChoiceSets(IEnumerable<string> choices)
          {
              if (choices == null || !choices.Any())
                  return null;
              else
              {
                  var first = choices.Take(1);
                  var inner = GetChoiceSets(choices.Skip(1));
      
                  if (inner == null)
                      return new List<IEnumerable<string>> { first, new List<string> { } };
                  else
                      return inner.Select(lst => first.Union(lst)).Union(inner).ToList();
              }
          }
      

      使用链接 SO 算法的稍微不那么天真的迭代解决方案:

          static List<List<string>> GetChoiceSets2(List<string> choices)
          {
              int capacity = (int)Math.Pow(2, choices.Count());
              int bit = 1;
              List<List<string>> choiceSets = new List<List<string>>(capacity);
              for (int i = 0; i < capacity; i++)
                  choiceSets.Add(new List<String>());
      
              for (int i = 0; i < choices.Count(); i++)
              {
                  for (int n = 0; n < capacity; n++)
                  {
                      if ((n & bit) == bit)
                          choiceSets[n].Add(choices[i]);
                  }
                  bit *= 2;
              }
      
              return choiceSets;
          }
      

      这些可能都可以改进,但取决于所使用的集合的大小,一个或另一个应该足够有效。

      【讨论】:

      • 谢谢,好的开始。第一个解决方案不是按指定的顺序返回它们,其中集合的大小增加(或大小保持不变)并保持顺序。我得到[咖啡,茶,牛奶],[咖啡,茶],[咖啡,牛奶],[咖啡],[茶,牛奶],[茶],[牛奶],[]。 (最后加上一个空白项?)我将尝试第二种解决方案,看看它是否符合必要的要求。
      • 看起来第二个解决方案存在相同的订购要求差距(并且最后还有空白项)。在这两种情况下,确实都找到了正确的值,只是排序不合规格。
      【解决方案5】:

      我没有在VB.NET中编程,这只是输入。所以可能存在严重的错误。但这种方法应该可行。

      static List<List<string>> GetChoiceSets(List<string> choices)
      {
          int capacity = (int) Math.Pow(2, choices.Count()) - 1;
          int bit = 1;
          List<List<string>> choiceSets = new List<List<string>>(capacity);
          for (int i = 0; i < capacity; i++)
              choiceSets.Add(new List<String>());
      
          n = 0;
          for (int size = 1; size <= choices.Count(); size++)
          {
              List<int> indexes = new List<int>(size);
              for (int i = 0; i < size; i++)
                  indexes.Add(i);
      
              // We break out after exhausting all sets of this size.
              for (;;) {
                  // Insert solution.
                  for (int i = 0; i < size; i++)
                      choiceSets[n].Add(choices[indexes[i]]);
                  n++;
      
                  // Figure out the first place we can advance indexes.
                  int j = 1;
                  for (; j <= size; j++) {
                      if (indexes[size - j] < choices.Count() - j) {
                          break;
                      }
                  }
                  threshold = choices.Count() - j
                  // Did we finish?
                  if (threshold < 0)
                      break;
      
                  // We will increment the index at threshold, and make following ones
                  // increment from there.
                  indexes[threshold]++;
                  for (int i = 1; i + threshold < choices.Count(); i++)
                      indexes[threshold + i] = indexes[threshold] + i;
              }
          }
      
          return choiceSets;
      }
      

      【讨论】:

        【解决方案6】:
        IEnumerable<IEnumerable<string>> seed = new[] { Enumerable.Empty<string>() };
        
        choices.Aggregate(
            seed,
            (acc, item) =>
                acc.SelectMany(a => new[] { a, a.Concat(new[] {item}) }))
        

        choices.Aggregate(
            seed,
            (acc, item) =>
                from a in acc
                from c in new[] { Enumerable.Empty<string>(), new[] { item } }
                select a.Concat(c))
        

        【讨论】:

        • 感谢您的意见。结果数量正确,但排序不满足问题陈述。事实上完全不同,尤其是当您开始在“选择”中添加三个以上的项目时。
        猜你喜欢
        • 2023-03-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-08-06
        • 1970-01-01
        • 2018-09-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多