views:

103

answers:

3

I saw this article pop-up in my MSDN RSS feed, and after reading through it, and the sourced article here I began to wonder about the solution.

The rules are simple:

Find a number consisting of 9 digits in which each of the digits from 1 to 9 appears only once. This number must also satisfy these divisibility requirements:

  1. The number should be divisible by 9.
  2. If the rightmost digit is removed, the remaining number should be divisible by 8.
  3. If the rightmost digit of the new number is removed, the remaining number should be divisible by 7.
  4. And so on, until there's only one digit (which will necessarily be divisible by 1).

This is his proposed monster LINQ query:

// 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 states "Note that no attempt at all has been made to optimize the code", what I'd like to know is what if we DID attempt to optimize this code. Is this really the best this code can get? I'd like to know how we can do this best with .NET4, in particular doing as much in parallel as we possibly can. I'm not necessarily looking for an answer in pure LINQ, assume .NET4 in any form (managed c++, c#, etc all acceptable).

+1  A: 

Well for one thing, the last bit (concerning i9) is not necessary, since all permutations of 1-9 are divisible by 9...

BlueRaja - Danny Pflughoeft
+1  A: 

I don't think you can dramatically improve this query... It's actually pretty efficient already, because each step has much less possible combinations than the previous step.

What you could easily do is to factorize some code to make the query more readable. For instance, use a predicate that checks the algorithm invariant at each step, and a helper to build the numbers from the digits (rather than "inline" multiplications and additions).

Let's call Dn the digit at position N, and Xn the number made of D1...Dn. At each step N, the following statements should be true :

  • Dn isn't in [D1...D(n-1)]
  • Xn is divisible by N

In the following code, this invariant is implemented by the isValid delegate :

// 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();
}

Still pretty big, but much more readable than the original...

Thomas Levesque
this is much easier on the eyes, but is is actually more performant?
slf
No, it isn't. Actually, I think it's *slightly* slower than the original implementation, because of the overhead of delegate calls, but the difference is probably not big enough to notice
Thomas Levesque
+2  A: 

If you have access to an ImmutableList class, it can make a pretty short solution. Instead of trying each one at every stage, you pass only the remaining possibilities to the next state. Also, the number of calculations is reduced by keeping the totals at each stage.

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
Gideon Engelberth
Even though I don't like the VB syntax, this is very elegant solution... +1 !
Thomas Levesque