All possible combinations of 1 to 9 in the same cells without repetition

Viewed 350

As a way to improve my VBA understanding, I'm attempting to build a cross sum solver. A cross sum, for those who don't know being the below. Each blank cell can contain a number 1 - 9, but the number can only be in the grid once and all sums must reconcile.

Cross Sum Example

I've got some code with nested for and if statements that does put in all possible variations in the cells but it take forever and I'm sure it's a highly inefficient way of doing it.

Sub Test()
 Dim StartTime As Double
 Dim SecondsElapsed As Double

 StartTime = Timer

 Dim wb As Workbook
 Dim ws As Worksheet

 Set wb = ThisWorkbook
 Set ws = wb.Worksheets("Sheet1")

 Application.ScreenUpdating = False

 Dim i, j, k, l, m, n, o, p, q As Integer

 For i = 1 To 9
  ws.Range("A1").Value = i

  For j = 1 To 9
   If j <> ws.Range("A1").Value Then
    ws.Range("C1").Value = j
   End If

   For k = 1 To 9
    If k <> ws.Range("A1").Value Then
     If k <> ws.Range("C1").Value Then
      ws.Range("E1").Value = k
     End If
    End If

    For l = 1 To 9
     If l <> ws.Range("A1").Value Then
      If l <> ws.Range("C1").Value Then
       If l <> ws.Range("E1").Value Then
        ws.Range("A3").Value = l
       End If
      End If
     End If

     For m = 1 To 9
      If m <> ws.Range("A1").Value Then
       If m <> ws.Range("C1").Value Then
        If m <> ws.Range("E1").Value Then
         If m <> ws.Range("A3").Value Then
          ws.Range("B3").Value = m
         End If
        End If
       End If
      End If

      For n = 1 To 9
       If n <> ws.Range("A1").Value Then
        If n <> ws.Range("C1").Value Then
         If n <> ws.Range("E1").Value Then
          If n <> ws.Range("A3").Value Then
           If n <> ws.Range("C3").Value Then
            ws.Range("E3").Value = n
           End If
          End If
         End If
        End If
       End If

       For o = 1 To 9
        If o <> ws.Range("A1").Value Then
         If o <> ws.Range("C1").Value Then
          If o <> ws.Range("E1").Value Then
           If o <> ws.Range("A3").Value Then
            If o <> ws.Range("C3").Value Then
             If o <> ws.Range("E3").Value Then
              ws.Range("A5").Value = o
             End If
            End If
           End If
          End If
         End If
        End If

        For p = 1 To 9
         If p <> ws.Range("A1").Value Then
          If p <> ws.Range("C1").Value Then
           If p <> ws.Range("E1").Value Then
            If p <> ws.Range("A3").Value Then
             If p <> ws.Range("C3").Value Then
              If p <> ws.Range("E3").Value Then
               If p <> ws.Range("A3").Value Then
                ws.Range("C5").Value = p
               End If
              End If
             End If
            End If
           End If
          End If
         End If

         For q = 1 To 9
          If q <> ws.Range("A1").Value Then
           If q <> ws.Range("C1").Value Then
            If q <> ws.Range("E1").Value Then
             If q <> ws.Range("A3").Value Then
              If q <> ws.Range("C3").Value Then
               If q <> ws.Range("E3").Value Then
                If q <> ws.Range("A5").Value Then
                 If q <> ws.Range("C5").Value Then
                  ws.Range("E5").Value = q
                 End If
                End If
               End If
              End If
             End If
            End If
           End If
          End If
         Next q
        Next p
       Next o
      Next n
     Next m
    Next l
   Next k
  Next j
 Next i

 Application.ScreenUpdating = True

 SecondsElapsed = Round(Timer - StartTime, 2)

 MsgBox "This code ran successfully in " & SecondsElapsed & " seconds", vbInformation
End Sub

Is there a more sensible way to achieve the putting the numbers in the cells? I've got the evaluate part to do the sums dependent upon the operators and the answer already working, so once I've got this working I'd not put it in the cell every time, but just pass to a variable. I'm just putting the value in a cell for the sake of testing.

Many thanks

4 Answers

You could put the numbers into an Array - working with Arrays is faster than working with Ranges, and you can use IsError(Application.Match(Value,Array,0)) to test if the number Value has been used anywhere in Array.

Once you find a "valid" solution, you can stop the loops (unless you want to check how many valid solutions there are) - I may get shouted down and vilified by some for this, but GoTo is a quick, dirty and simple solution

Beyond that, I'm going to use several CodeGolf tricks to make the code visually shorter, such as using Type Characters to simplify the Dim statements, or a chained Next statement - and instead of checking if the output is still valid after generating each number, I will do it once after generating all 9.

Sub CrossSumSolver()

    Dim StartTime As Double
    Dim SecondsElapsed As Double

    StartTime = Timer

    Dim wb As Workbook
    Dim ws As Worksheet

    Set wb = ThisWorkbook
    Set ws = wb.Worksheets("Sheet1")

    Application.Calculation = xlCalculationManual
    Application.ScreenUpdating = False

    Dim OutputArray(1 To 9) As Long, IsValid As Boolean, CheckLoop As Long
    Dim a&, b&, c&, d&, e&, f&, g&, h&, i& 'All "As Long"

    For a = 1 To 9
        OutputArray(1) = a
        For b = 1 To 9
            OutputArray(2) = b
            For c = 1 To 9
                OutputArray(3) = c
                For d = 1 To 9
                    OutputArray(4) = d
                    For e = 1 To 9
                        OutputArray(5) = e
                        For f = 1 To 9
                            OutputArray(6) = f
                            For g = 1 To 9
                                OutputArray(7) = g
                                For h = 1 To 9
                                    OutputArray(8) = h
                                    For i = 1 To 9
                                        OutputArray(9) = i
                                        'Array is populated - is it valid?
                                        IsValid = True
                                        'Are all 9 numbers used once?
                                        For CheckLoop = 1 To 9
                                            If IsError(Application.Match(CheckLoop, OutputArray, 0)) Then
                                                IsValid = False 'A number is missing!
                                                Exit For 'Only need to find 1 error
                                            End If
                                        Next CheckLoop
                                        If IsValid Then
                                            'Populate sheet
                                            ws.Range("A1").Value = OutputArray(1)
                                            ws.Range("C1").Value = OutputArray(2)
                                            ws.Range("E1").Value = OutputArray(3)
                                            ws.Range("A3").Value = OutputArray(4)
                                            ws.Range("C3").Value = OutputArray(5)
                                            ws.Range("E3").Value = OutputArray(6)
                                            ws.Range("A5").Value = OutputArray(7)
                                            ws.Range("C5").Value = OutputArray(8)
                                            ws.Range("E5").Value = OutputArray(9)
                                            'Calculate sheet
                                            ws.Calculate
                                            'Check if your output is correct
                                            If (False) Then GoTo QuickExit 'Replace (False) with your check!
                                        End If
    Next i, h, g, f, e, d, c, b, a 'No need for a Wall of "Next"

QuickExit:
    Application.Calculation = xlCalculationAutomatic
    Application.ScreenUpdating = True

    SecondsElapsed = Round(Timer - StartTime, 2)

    MsgBox "This code ran successfully in " & SecondsElapsed & " seconds", vbInformation
End Sub

Alternatively, you could use the evils of a recursive subroutine (i.e. a subroutine that calls itself) to loop through the numbers for each item in an array in turn. (Powerful when used right, but get it wrong and you will end up with your computer locked in a permanent loop and Excel/VBA eating more and more memory)

Option Explicit

Private ValueArray(1 To 9) As Long
Private wb As Workbook
Private ws As Worksheet

Public Sub ControlLoop()
    Dim StartTime As Double
    Dim SecondsElapsed As Double

    StartTime = Timer

    Set wb = ThisWorkbook
    Set ws = wb.Worksheets("Sheet1")

    Application.Calculation = xlCalculationManual
    Application.ScreenUpdating = False

    Dim OutermostLoop As Long

    For OutermostLoop = 1 To 9
        ClearArrayAbove 1
        RecursiveArrayLoop 1, OutermostLoop
    Next OutermostLoop

    Application.Calculation = xlCalculationAutomatic
    Application.ScreenUpdating = True

    SecondsElapsed = Round(Timer - StartTime, 2)

    MsgBox "This code ran successfully in " & SecondsElapsed & " seconds", vbInformation
End Sub

Private Sub ClearArrayAbove(ArrayItem As Long)
    If ArrayItem >= 9 Then Exit Sub 'Safety check
    Dim InnerLoop As Long

    For InnerLoop = ArrayItem To 9
        ValueArray(InnerLoop) = 0
    Next InnerLoop
End Sub

Private Sub RecursiveArrayLoop(ArrayItem As Long, NewValue As Long)
    Dim InnerLoop As Long

    'Number is not already in the array
    If IsError(Application.Match(NewValue, ValueArray, 0)) Then
        'Add number to array
        ValueArray(ArrayItem) = NewValue
        If ArrayItem < 9 Then
            'Go up a level, and loop again
            For InnerLoop = 1 To 9
                ClearArrayAbove ArrayItem
                RecursiveArrayLoop ArrayItem + 1, InnerLoop
            Next InnerLoop
        Else
            'All numbers filled!
            TestValidNumbers
        End If
    End If
End Sub

Private Sub TestValidNumbers()
    'Populate sheet
    ws.Range("A1").Value = ValueArray(1)
    ws.Range("C1").Value = ValueArray(2)
    ws.Range("E1").Value = ValueArray(3)
    ws.Range("A3").Value = ValueArray(4)
    ws.Range("C3").Value = ValueArray(5)
    ws.Range("E3").Value = ValueArray(6)
    ws.Range("A5").Value = ValueArray(7)
    ws.Range("C5").Value = ValueArray(8)
    ws.Range("E5").Value = ValueArray(9)
    'Calculate sheet
    ws.Calculate
    'Check if your output is correct
    'Do stuff here?
End Sub

I'd advise you the following way to work for such a kind of problem:
Give the variables some clear names, like a1, a2, ..., as follows:

first row  :  a1  a2  a3
second row :  b1  b2  b3
third row  :  c1  c2  c3

Your algorithm then might look like the following (pseudo-code):

for a1 = 0 to 9:
  for a2 = 0 to 9:
    if (a1 <> a2) // all have to be different
    then:
      for a3 = 0 to 9:
      if ((a1 <> a3) and (a2 <> a3)) and // all have to be different
         (a1 - a2 / a3 = 1)              // start checking if the first row is correct,
                                         // otherwise it makes no sense to continue.
      then:
      ...

Good luck

To generate a random permutation of the digits 1 through 9 without repeats, pick a cell, say G1 and enter:

=RANDBETWEEN(1,9)

then in G2 enter:

=LARGE(IF(ISNA(MATCH({1;2;3;4;5;6;7;8;9},G$1:G1,0)),{1;2;3;4;5;6;7;8;9}),RANDBETWEEN(1,9-ROWS(G$2:G2)))

and copy downwards.

enter image description here

Each time the worksheet is re-calculated, a new permutation will be calculated.

Once the column has been filled, it can be mapped into any rectangular array with formulas like:

=G1

The brute force method that processes the puzzle in memory takes 588.03 Seconds(s) to process your puzzle and 212.79 Seconds(s) for a this puzzle. My gaming computer would probably process in less than half the time.

Sub SolveCrossSum()
    Dim t As Double: t = Timer

    Dim n1 As Long, n2 As Long, n3 As Long, n4 As Long, n5 As Long, n6 As Long, n7 As Long, n8 As Long, n9 As Long
    Dim Data() As Variant
    Dim result As String

    With Worksheets("Sheet3")
        Data = .Range("A1:G7").Value
        For n1 = 1 To 9
            For n2 = 1 To 9
                For n3 = 1 To 9
                    For n4 = 1 To 9
                        For n5 = 1 To 9
                            For n6 = 1 To 9
                                For n7 = 1 To 9
                                    For n8 = 1 To 9
                                        For n9 = 1 To 9
                                            If Solved(Data, t, n1, n2, n3, n4, n5, n6, n7, n8, n9) Then
                                                .Range("A1:E5").Value = Data
                                                Debug.Print "Cross Sum was solved in: "; Round((Timer - t), 2); " Seconds(s)"
                                                Exit Sub
                                            End If
                                        Next
                                    Next
                                Next
                            Next
                        Next
                    Next
                Next
            Next
        Next
    End With
    Debug.Print "No Answer Found for Cross Sum.  Execution Time: "; Round((Timer - t) / 60, 2); " Minutes(s)"
    Debug.Print n1, n2, n3, n4, n5, n6, n7, n8, n9
End Sub


Function Solved(ByRef Data() As Variant, t As Double, n1 As Long, n2 As Long, n3 As Long, n4 As Long, n5 As Long, n6 As Long, n7 As Long, n8 As Long, n9 As Long) As Boolean
    If hasDuplicates(n1, n2, n3, n4, n5, n6, n7, n8, n9) Then Exit Function

    If ev(ev(n1, n2, Data(1, 2)), n3, Data(1, 4)) <> Data(1, 7) Then Exit Function
    If ev(ev(n4, n5, Data(3, 2)), n6, Data(3, 4)) <> Data(3, 7) Then Exit Function
    If ev(ev(n7, n8, Data(5, 2)), n9, Data(5, 4)) <> Data(5, 7) Then Exit Function

    If ev(ev(n1, n4, Data(2, 1)), n7, Data(4, 1)) <> Data(7, 1) Then Exit Function
    If ev(ev(n2, n5, Data(2, 3)), n8, Data(4, 3)) <> Data(7, 3) Then Exit Function
    If ev(ev(n3, n6, Data(2, 5)), n9, Data(4, 5)) <> Data(7, 5) Then Exit Function

    Data(1, 1) = n1
    Data(1, 3) = n2
    Data(1, 5) = n3
    Data(3, 1) = n4
    Data(3, 3) = n5
    Data(3, 5) = n6
    Data(5, 1) = n7
    Data(5, 3) = n8
    Data(5, 5) = n9
    Solved = True
End Function

Function ev(v1 As Long, v2 As Long, operator As Variant) As Long
    Select Case operator
        Case "+"
            ev = v1 + v2
        Case "-"
            ev = v1 - v2
        Case "/"
            ev = v1 / v2
        Case "*"
            ev = v1 * v2
    End Select
End Function

Function hasDuplicates(ParamArray Args() As Variant) As Boolean
    Dim n1 As Long, n2 As Long
    For n1 = 0 To UBound(Args)
        If Args(n1) = 10 Then Exit Function
        For n2 = 0 To UBound(Args)
            If n1 <> n2 Then
                If Args(n1) = Args(n2) Then
                    hasDuplicates = True
                    Exit Function
                End If
            End If
        Next
    Next
End Function
Related