【问题标题】:Get all possible distinct triples using LINQ使用 LINQ 获取所有可能的不同三元组
【发布时间】:2014-10-11 15:27:17
【问题描述】:

我有一个包含以下值的列表:{1、2、3、4、5、6、7}。我希望能够检索三个的唯一组合。结果应该是这样的:

{1,2,3}
{1,2,4}
{1,2,5}
{1,2,6}
{1,2,7}
{2,3,4}
{2,3,5}
{2,3,6}
{2,3,7}
{3,4,5}
{3,4,6}
{3,4,7}
{3,4,1}
{4,5,6}
{4,5,7}
{4,5,1}
{4,5,2}
{5,6,7}
{5,6,1}
{5,6,2}
{5,6,3}

我已经有 2 个 for 循环可以做到这一点:

for (int first = 0; first < test.Count - 2; first++)
{
  int second = first + 1;
  for (int offset = 1; offset < test.Count; offset++)
  {
    int third = (second + offset)%test.Count;
    if(Math.Abs(first - third) < 2)
      continue;
    List<int> temp = new  List<int>();
    temp .Add(test[first]);
    temp .Add(test[second]);
    temp .Add(test[third]);
    result.Add(temp );
  }
}

但是由于我正在学习 LINQ,我想知道是否有更聪明的方法可以做到这一点?

【问题讨论】:

  • smarter 是错误的形容词,我想。
  • 我的意思是在性能方面更好。 LINQ 看起来也比 for 循环酷得多 :)
  • @Sajeetharan:这是一个不同的问题。您链接到的问题是找到多个序列的笛卡尔积;这个问题是找到特定序列的排列。
  • @EricLippert 是的,我现在明白了!
  • Tony Dinh,我很困惑是否要订购排列。我注意到 {3, 4, 1} 在列表中,但不在 {1, 3, 4} 上。这是故意的,顺序是否重要,还是 {1, 3, 4} 和 {3, 4, 1} 一样好?

标签: c# linq unique


【解决方案1】:

更新:我用这个问题作为从here开始的系列文章的主题;我将在该系列中介绍两种略有不同的算法。谢谢你的好问题!


到目前为止发布的两个解决方案是正确的,但对于数字变大的情况效率低下。到目前为止发布的解决方案使用算法:首先枚举所有可能性:

{1, 1, 1 }
{1, 1, 2 }, 
{1, 1, 3 }, 
...
{7, 7, 7}   

在这样做的同时,过滤掉第二个不大于第一个,第三个不大于第二个的任何地方。这执行 7 x 7 x 7 过滤操作,数量不多,但如果您试图从 30 中获得 10 个元素的排列,那就是 30 x 30 x 30 x 30 x 30 x 30 x 30 x 30 x 30 x 30,这是相当多的。你可以做得更好。

我会按如下方式解决这个问题。首先,生成一个有效的不可变集的数据结构。让我非常清楚不可变集是什么,因为您可能不熟悉它们。您通常将集合视为添加项目和从中删除项目的东西。不可变集合有一个Add 操作,但它不会改变集合;它会返回一个 new 集合,其中包含添加的项目。删除也一样。

这是一个不可变集合的实现,其中元素是从 0 到 31 的整数:

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System;

// A super-cheap immutable set of integers from 0 to 31 ; 
// just a convenient wrapper around bit operations on an int.
internal struct BitSet : IEnumerable<int>
{
  public static BitSet Empty { get { return default(BitSet); } }
  private readonly int bits;
  private BitSet(int bits) { this.bits = bits; }
  public bool Contains(int item) 
  {
    Debug.Assert(0 <= item && item <= 31); 
    return (bits & (1 << item)) != 0; 
  }
  public BitSet Add(int item) 
  { 
    Debug.Assert(0 <= item && item <= 31); 
    return new BitSet(this.bits | (1 << item)); 
  }
  public BitSet Remove(int item) 
  { 
    Debug.Assert(0 <= item && item <= 31); 
    return new BitSet(this.bits & ~(1 << item)); 
  }
  IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
  public IEnumerator<int> GetEnumerator()
  {
    for(int item = 0; item < 32; ++item)
      if (this.Contains(item))
        yield return item;
  }
  public override string ToString() 
  {
    return string.Join(",", this);
  }
}

仔细阅读此代码以了解其工作原理。同样,请始终记住向该集合添加元素不会更改该集合。它会生成一个新集合,其中包含添加的项目。

好的,既然我们已经知道了,让我们考虑一种更有效的算法来生成您的排列。

我们将递归地解决问题。递归解决方案始终具有相同的结构:

  • 我们能解决一个小问题吗?如果是,请解决。
  • 如果不是,请将问题分解为多个较小的问题并逐一解决。

让我们从琐碎的问题开始。

假设您有一个集合,并且您希望从中选择 个项目。答案很明确:只有一个可能的排列为零元素,那就是空集。

假设你有一个包含 n 个元素的集合,并且你想选择多于 n 个元素。显然没有解,甚至空集也没有。

我们现在已经处理了集合为空或选择的元素数量大于元素总数的情况,因此我们必须从至少有一个东西的集合中选择至少一个东西。

在可能的排列中,其中一些包含第一个元素,而另一些则没有。找到所有包含第一个元素的元素并生成它们。为此,我们通过递归选择 少一个 元素来缺少第一个元素。

那些没有的有第一个元素,我们通过枚举集合的排列没有第一个元素来找到。

static class Extensions
{
  public static IEnumerable<BitSet> Choose(this BitSet b, int choose)
  {
    if (choose < 0) throw new InvalidOperationException();
    if (choose == 0) 
    { 
      // Choosing zero elements from any set gives the empty set.
      yield return BitSet.Empty; 
    }
    else if (b.Count() >= choose) 
    {
      // We are choosing at least one element from a set that has 
      // a first element. Get the first element, and the set
      // lacking the first element.

      int first = b.First();
      BitSet rest = b.Remove(first);
      // These are the permutations that contain the first element:
      foreach(BitSet r in rest.Choose(choose-1))
        yield return r.Add(first);
      // These are the permutations that do not contain the first element:
      foreach(BitSet r in rest.Choose(choose))
        yield return r;
    }
  }
}

现在我们可以提出您需要回答的问题:

class Program
{
  static void Main()
  {
    BitSet b = BitSet.Empty.Add(1).Add(2).Add(3).Add(4).Add(5).Add(6).Add(7);
    foreach(BitSet result in b.Choose(3))
      Console.WriteLine(result);
  }
}

我们完成了。我们只生成了我们实际需要的序列。 (虽然我们已经做了很多集合操作来达到这个目的,但是集合操作很便宜。)这里的重点是理解这个算法是如何工作的非常有指导意义。对不可变结构进行递归编程是许多专业程序员工具箱中没有的强大工具。

【讨论】:

  • 您花时间编写了一个响应操作问题的解决方案 + 可以很好地扩展 + 可以在更广泛的环境中使用,这一点给我留下了深刻的印象
  • 感谢@Eric 的精彩回答。我绝对可以使用它来优化我项目的其他部分。我想澄清的一件事 ToString() 不是为我编译的。所以我必须编写一个单独的函数来打印出所有正位索引。我在这里错过了什么吗?
  • @TonyDinh:不客气。也许您使用的是旧版本的 BCL,它没有 String.Join 的重载;我似乎记得它是相对较新的。写你自己的ToString
【解决方案2】:

你可以这样做:

var data = Enumerable.Range(1, 7);
var r = from a in data
        from b in data
        from c in data
        where a < b && b < c
        select new {a, b, c};
foreach (var x in r) {
    Console.WriteLine("{0} {1} {2}", x.a, x.b, x.c);
}

Demo.

编辑:感谢 Eric Lippert 简化了答案!

【讨论】:

  • 在始终为真的条件下加入自己与简单地进行多选查询有何不同?
  • @EricLippert 它有一个对称的外观。
  • 我对您的评论感到困惑。 from a in data from b in data from c in data where a &lt; b where b &lt; c ... 如何不那么“对称”?您的解决方案是在内存中构建数据结构来处理不必要的连接。
  • @EricLippert 谢谢!我想到了另一种语法,它不是对称的。
  • 不客气。我注意到这个查询效率很低——请参阅我的回答以了解有关此问题的一些想法。您可以通过向上移动 where a &gt; b 子句来提高效率。 from a in d from b in d where a &gt; b from c in d where c &gt; b select ...
【解决方案3】:
var ints = new int[] { 1, 2, 3, 4, 5, 6, 7 };

var permutations = ints.SelectMany(a => ints.Where(b => (b > a)).
                        SelectMany(b => ints.Where(c => (c > b)).
                        Select(c => new { a = a, b = b, c = c })));

【讨论】:

  • 这会产生 {1, 2, 3} 和 {2, 1, 3},这不是原始发布者想要的。
  • 没错,我虽然操作员正在寻找所有的组合
  • 我注意到您的支票c &gt; ac &gt; b 是多余的。你已经确定了b &gt; a在第一个Where中,所以你知道如果c &gt; b那么它也必须大于a
  • @EricLippert 你没记错,这是我之前代码的剩余部分
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-15
  • 2018-01-28
  • 2013-10-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多