用LINQ /.NET4解决组合问题

时间:2010-05-17 18:29:44

标签: linq performance .net-4.0 parallel-processing

我在MSDN RSS Feed中看到this article弹出窗口,在阅读完之后,and the sourced article here我开始怀疑解决方案。

规则很简单:

找到一个由9位数组成的数字,其中1到9中的每个数字只出现一次。这个数字还必须满足这些可分性要求:

  1. 该数字应该可被9整除。
  2. 如果删除了最右边的数字,则剩余的数字应该可被8整除。
  3. 如果删除新号码的最右边数字,则剩余号码应该可被7整除。
  4. 依此类推,直到只有一位数字(必须能被1整除)。
  5. 这是他提出的怪物LINQ查询:

    // C# and LINQ solution to the numeric problem presented in:
    // http://software.intel.com/en-us/blogs/2009/12/07/intel-parallel-studio-great-for-serial-code-too-episode-1/
    
    int[] oneToNine = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    
    // the query
    var query = 
        from i1 in oneToNine
       from i2 in oneToNine
        where i2 != i1
           && (i1 * 10 + i2) % 2 == 0
        from i3 in oneToNine
        where i3 != i2 && i3 != i1
           && (i1 * 100 + i2 * 10 + i3) % 3 == 0
        from i4 in oneToNine
        where i4 != i3 && i4 != i2 && i4 != i1
           && (i1 * 1000 + i2 * 100 + i3 * 10 + i4) % 4 == 0
        from i5 in oneToNine
        where i5 != i4 && i5 != i3 && i5 != i2 && i5 != i1
           && (i1 * 10000 + i2 * 1000 + i3 * 100 + i4 * 10 + i5) % 5 == 0
        from i6 in oneToNine
        where i6 != i5 && i6 != i4 && i6 != i3 && i6 != i2 && i6 != i1
           && (i1 * 100000 + i2 * 10000 + i3 * 1000 + i4 * 100 + i5 * 10 + i6) % 6 == 0
        from i7 in oneToNine
        where i7 != i6 && i7 != i5 && i7 != i4 && i7 != i3 && i7 != i2 && i7 != i1
           && (i1 * 1000000 + i2 * 100000 + i3 * 10000 + i4 * 1000 + i5 * 100 + i6 * 10 + i7) % 7 == 0
        from i8 in oneToNine
        where i8 != i7 && i8 != i6 && i8 != i5 && i8 != i4 && i8 != i3 && i8 != i2 && i8 != i1
           && (i1 * 10000000 + i2 * 1000000 + i3 * 100000 + i4 * 10000 + 
               i5 * 1000 + i6 * 100 + i7 * 10 + i8) % 8 == 0
        from i9 in oneToNine
        where i9 != i8 && i9 != i7 && i9 != i6 && i9 != i5 && i9 != i4 && i9 != i3 && i9 != i2 && i9 != i1
        let number = i1 * 100000000 +
                     i2 * 10000000 +
                     i3 * 1000000 +
                     i4 * 100000 +
                     i5 * 10000 +
                     i6 * 1000 +
                     i7 * 100 +
                     i8 * 10 +
                     i9 * 1
        where number % 9 == 0
        select number;
    
    // run it!
    foreach (int n in query)
        Console.WriteLine(n);
    

    Octavio声明“请注意,根本没有尝试优化代码”,我想知道的是如果我们DID尝试优化此代码的话。这个代码真的是最好的吗?我想知道如何使用.NET4做到最好,特别是尽可能多地并行执行。我不一定在纯LINQ中寻找答案,假设任何形式的.NET4(托管c ++,c#等都可以接受)。

3 个答案:

答案 0 :(得分:3)

如果您有权访问ImmutableList类,它可以提供一个非常简短的解决方案。不是在每个阶段尝试每一个,而是将剩余的可能性传递给下一个状态。此外,通过保持每个阶段的总数来减少计算次数。

Dim query = From i1 In Tuple.Create(0L, allNums).ChooseNextNumber(1)
            From i2 In i1.ChooseNextNumber(2) _
            From i3 In i2.ChooseNextNumber(3) _
            From i4 In i3.ChooseNextNumber(4) _
            From i5 In i4.ChooseNextNumber(5) _
            From i6 In i5.ChooseNextNumber(6) _
            From i7 In i6.ChooseNextNumber(7) _
            From i8 In i7.ChooseNextNumber(8) _
            From i9 In i8.ChooseNextNumber(9)
            Select i9.Item1

<System.Runtime.CompilerServices.Extension()> _
Private Function ChooseNextNumber(
      ByVal previous As Tuple(Of Integer, ImmutableList(Of Integer)),
      ByVal modulusBase As Integer) _
    As IEnumerable(Of Tuple(Of Integer, ImmutableList(Of Integer)))
    Return From i In previous.Item2
           Let newTotal = previous.Item1 * 10 + i
           Where newTotal Mod modulusBase = 0
           Select Tuple.Create(newTotal, previous.Item2.Remove(i))
End Function

答案 1 :(得分:1)

一方面,最后一点(关于i9)是没有必要的,因为 1-9的所有排列都可以被9整除..​​.

答案 2 :(得分:1)

我认为你不能大大改进这个查询......它实际上已经很有效了,因为每一步的组合比上一步少得多。

您可以轻松地将一些代码分解,以使查询更具可读性。例如,使用一个谓词来检查每个步骤中的算法不变量,并使用一个帮助器来构建数字中的数字(而不是“内联”乘法和加法)。

让我们在 N 位置调用 Dn ,并将Xn数字设为D1 ... Dn。在每个步骤N,以下陈述应该是真的:

  • Dn不在[D1 ... D(n-1)]
  • Xn可被N
  • 整除

在以下代码中,此不变量由isValid委托实现:

// Delegate with variable number of arguments
delegate TResult FuncWithParams<TArg, TResult>(params TArg[] args);

void Main()
{

    var oneToNine = Enumerable.Range(1, 9).ToArray();

    // Creates a number from its digits
    FuncWithParams<int, int> makeNumber =
        digits => digits.Aggregate(0, (acc, d) => acc * 10 + d);

    // Checks the invariant against a sequence of digits
    FuncWithParams<int, bool> isValid =
        digits => !digits.Take(digits.Length - 1).Contains(digits.Last())
                && makeNumber(digits) % digits.Length == 0;

    var query = 
        from d1 in oneToNine
        from d2 in oneToNine
        where isValid(d1, d2)
        from d3 in oneToNine
        where isValid(d1, d2, d3)
        from d4 in oneToNine
        where isValid(d1, d2, d3, d4)
        from d5 in oneToNine
        where isValid(d1, d2, d3, d4, d5)
        from d6 in oneToNine
        where isValid(d1, d2, d3, d4, d5, d6)
        from d7 in oneToNine
        where isValid(d1, d2, d3, d4, d5, d6, d7)
        from d8 in oneToNine
        where isValid(d1, d2, d3, d4, d5, d6, d7, d8)
        from d9 in oneToNine
        where isValid(d1, d2, d3, d4, d5, d6, d7, d8, d9)
        select makeNumber(d1, d2, d3, d4, d5, d6, d7, d8, d9);

    query.Dump();
}

仍然很大,但比原版更具可读性......