VBA with 400000+ Combinations. Can it run faster?

andrew_milonavic

Board Regular
Joined
Nov 16, 2016
Messages
98
Hi everyone,

I'm using a VBA that calculates unique combinations from multiple columns. It works great! but in some cases there are 400000+ combinations before sorting and it runs really slow. I'm far from a VBA expert, any way to speed it up?

VBA Code:
Option Explicit

Sub NameCombos()
    'https://www.mrexcel.com/forum/excel-questions/1106189-all-combinations-multiple-columns-without-duplicates.html
    
    Dim lLastColumn As Long
    Dim lLastUsedColumn As Long
    Dim aryNames As Variant
    Dim lColumnIndex As Long
    Dim lWriteRow As Long
    Dim bCarry As Boolean
    Dim lWriteColumn As Long
    Dim rngWrite As Range
    Dim lFirstWriteColumn As Long
    Dim lLastWriteColumn As Long
    Dim oFound As Object
    Dim lRefColumn As Long
    Dim lInUseRow As Long
    Dim lCarryColumn As Long
    Dim lPrint As Long
    Dim lLastIteration As Long
    Dim lIterationCount As Long
    Dim sErrorMsg As String
    Dim bShowError As Boolean
    Dim lLastRow As Long
    Dim lLastRowDeDuped As Long
    Dim aryDeDupe As Variant

    Dim sName As String
    Dim bDupeName As Boolean
    
    Dim oSD As Object
    Dim rngCell As Range
    Dim varK As Variant, varI As Variant, varTemp As Variant, lIndex As Long
    Dim lRowIndex As Long
    Dim rngSortRange As Range
    Dim dteStart As Date
    Dim sOutput As String
    Dim lFirstHSortColumn As Long
    Dim lLastHSortColumn As Long
 
    Dim rngReplace As Range

    
    
    
    
    sErrorMsg = "Ensure a Worksheet is active with a header row starting in A1" & _
        "and names under each header entry."
    
    If TypeName(ActiveSheet) <> "Worksheet" Then
        bShowError = True
    End If
    
    If bShowError Then
        MsgBox sErrorMsg, , "Problems Found in Data"
        GoTo End_Sub
    End If
    
    lLastColumn = Range("A1").CurrentRegion.Columns.Count
    lLastUsedColumn = ActiveSheet.UsedRange.Columns.Count
    ReDim aryNames(1 To 2, 1 To lLastColumn)    '1 holds the in-use entry row
                                                
    'How many combinations? (Order does not matter)
    lLastIteration = 1
    For lColumnIndex = 1 To lLastColumn
        aryNames(1, lColumnIndex) = 2
        aryNames(2, lColumnIndex) = Cells(Rows.Count, lColumnIndex).End(xlUp).Row
        lLastIteration = lLastIteration * (aryNames(2, lColumnIndex) - 1)
    Next
    
    lRefColumn = lLastColumn + 1
    lFirstWriteColumn = lLastColumn + 2
    lLastWriteColumn = (2 * lLastColumn) + 1
    
    Select Case MsgBox("Process a " & lLastColumn & " column table with " & _
        lLastIteration & " possible combinations?" & vbLf & vbLf & _
        "WARNING: Columns right of the input range will be erased before continuing.", vbOKCancel + vbCritical + _
        vbDefaultButton2, "Process table?")
    Case vbCancel
        GoTo End_Sub
    End Select
    
    dteStart = Now()
    
    'Clear all columns right of input range
    If lLastUsedColumn > lLastColumn Then
        Range(Cells(1, lLastColumn + 1), Cells(1, lLastUsedColumn)).EntireColumn.ClearContents
    End If
    Cells(1, lLastWriteColumn + 1).Value = "ComboID"
    
    'Add Output Header
    Range(Cells(1, 1), Cells(1, lLastColumn)).Copy Destination:=Cells(1, lFirstWriteColumn)
    
    'Start checking combinations
    lWriteRow = 2
    For lIterationCount = 1 To lLastIteration
        If lIterationCount / 1000 = lIterationCount \ 1000 Then Application.StatusBar = _
            lIterationCount & " / " & lLastIteration
            
        'Reset the Dupe Name flag
        bDupeName = False
        
        'Check Active Combo for Dupe Names
        'Initialize the scripting dictionary
        Set oSD = CreateObject("Scripting.Dictionary")
        oSD.CompareMode = vbTextCompare
        
        'Load names into scripting dictionary
        For lColumnIndex = lLastColumn To 1 Step -1
            sName = Cells(aryNames(1, lColumnIndex), lColumnIndex).Value
            oSD.Item(sName) = oSD.Item(sName) + 1
        Next
        
        'If there are names, and at least one duplicate, set the bDupeName flag
        If oSD.Count > 0 Then
            varK = oSD.keys
            varI = oSD.Items
            For lIndex = 1 To oSD.Count
                If varI(lIndex - 1) > 1 Then
                    bDupeName = True: Exit For
                End If
            Next
        End If
        
        
        If Not bDupeName Then
            'The current row had names and no duplicates
            'Print Active Combo to the lWriteRow row
            For lColumnIndex = lLastColumn To 1 Step -1
                lWriteColumn = lColumnIndex + lLastColumn + 2
                Set rngWrite = Range(Cells(lWriteRow, lFirstWriteColumn), Cells(lWriteRow, lLastWriteColumn))
                Cells(lWriteRow, lRefColumn + lColumnIndex).Value = Cells(aryNames(1, lColumnIndex), lColumnIndex).Value
            Next
            
            'Uncomment next row to see the lIterationCount for the printed row
            Cells(lWriteRow, lLastWriteColumn + 1).Value = lIterationCount
            
            'Point to the next blank row
            lWriteRow = lWriteRow + 1
            
        End If
    
        'Increment Counters
        'Whether the line had duplicates or not, move to the next name in the
        '  rightmost column, if it was ag the last name, go to the first name in that column and
        '  move the name in the column to the left down to the next name (recursive check if THAT
        '  column was already using the last name for remaining columns to the left)
        aryNames(1, lLastColumn) = aryNames(1, lLastColumn) + 1
        If aryNames(1, lLastColumn) > aryNames(2, lLastColumn) Then
            bCarry = True
            lCarryColumn = lLastColumn
            Do While bCarry = True And lCarryColumn > 0
                aryNames(1, lCarryColumn) = 2
                bCarry = False
                lCarryColumn = lCarryColumn - 1
                If lCarryColumn = 0 Then Exit Do
                aryNames(1, lCarryColumn) = aryNames(1, lCarryColumn) + 1
                If aryNames(1, lCarryColumn) > aryNames(2, lCarryColumn) Then bCarry = True
            Loop
        End If
        
        'Check counter values (for debug)
'        Debug.Print lWriteRow,
'        For lPrint = 1 To lLastColumn
'            Debug.Print aryNames(1, lPrint) & ", ";
'        Next
'        Debug.Print
        DoEvents
    Next
    
    Application.StatusBar = "Sorting"
    Application.ScreenUpdating = False
    
    'Copy row names to right so that each copied row can be sorted alphabetically left to right
    '  this will allow the Excel remove duplicate fuction to remove rows that have identical names
    '  in all of their sorted columns.
    lLastRow = Cells(Rows.Count, lFirstWriteColumn).End(xlUp).Row
    Range(Cells(1, lFirstWriteColumn), Cells(lLastRow, lLastWriteColumn)).Copy Destination:=Cells(1, lLastWriteColumn + 2)
    lFirstHSortColumn = lLastWriteColumn + 2
    lLastHSortColumn = Cells(1, Columns.Count).End(xlToLeft).Column
    
    'Sort each row
    Application.ScreenUpdating = False
    lLastRow = Cells(Rows.Count, lFirstWriteColumn).End(xlUp).Row
    For lRowIndex = 2 To lLastRow
        Set rngSortRange = Range(Cells(lRowIndex, lFirstHSortColumn), Cells(lRowIndex, lLastHSortColumn))
        ActiveSheet.Sort.SortFields.Clear
        ActiveSheet.Sort.SortFields.Add Key:=rngSortRange, _
            SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
        With ActiveSheet.Sort
            .SetRange rngSortRange
            .Header = xlNo
            .MatchCase = False
            .Orientation = xlLeftToRight
            .SortMethod = xlPinYin
            .Apply
        End With
    Next
    
    'Check for duplicate rows in HSort Columns
    '  Can only happen if names are duplicated within an input column
    '  Build aryDeDupe  -- Array(1, 2, 3,...n)  -- to exclude iteration # column

    lLastRow = Cells(Rows.Count, lFirstWriteColumn).End(xlUp).Row
    ReDim aryDeDupe(0 To lLastHSortColumn - lFirstHSortColumn)
    lIndex = 0
    For lColumnIndex = lFirstHSortColumn To lLastHSortColumn
        aryDeDupe(lIndex) = CInt(lColumnIndex - lFirstWriteColumn + 1)
        lIndex = lIndex + 1
    Next
    ActiveSheet.Cells(1, lFirstWriteColumn).CurrentRegion.RemoveDuplicates Columns:=(aryDeDupe), Header:=xlYes
    'Above line won't work unless there are parens around the Columns argument ?????
    
    lLastRowDeDuped = Cells(Rows.Count, lFirstWriteColumn).End(xlUp).Row
    
 
    
    sOutput = lLastIteration & vbTab & " possible combinations" & vbLf & _
        lLastRow - 1 & vbTab & " unique name combinations" & vbLf & _
        IIf(lLastRowDeDuped <> lLastRow, lLastRow - lLastRowDeDuped & vbTab & " duplicate rows removed." & vbLf, "") & _
        lLastRowDeDuped - 1 & vbTab & " printed." & vbLf & vbLf & _
        Format(Now() - dteStart, "hh:mm:ss") & " to process."
    
    ActiveSheet.UsedRange.Columns.AutoFit
    MsgBox sOutput, , "Output Report"
    Debug.Print sOutput
        
End_Sub:

    Application.ScreenUpdating = True
    Application.StatusBar = False
    
End Sub

Any help would be great!

Thanks

Andrew
 

Excel Facts

Get help while writing formula
Click the italics "fx" icon to the left of the formula bar to open the Functions Arguments dialog. Help is displayed for each argument.
I'm using a VBA that calculates unique combinations from multiple columns. It works great! but in some cases there are 400000+ combinations before sorting and it runs really slow. I'm far from a VBA expert, any way to speed it up?
In a word, yes.

The code you've posted does a lot of reading and writing to Excel, which will really slow down the code. To demonstrate:

VBA Code:
Sub Demo()

    Dim i As Long, N As Long, Output() As Long
    Dim t As Double
   
    N = 50000
    ReDim Output(1 To N, 1 To 1)
   
    t = Timer
    For i = 1 To N
        Range("A" & i).Value = i
    Next i
    MsgBox Format(Timer - t, "0.0") & " seconds"

    t = Timer
    For i = 1 To N
        Output(i, 1) = i
    Next i
    Range("B1").Resize(N).Value = Output
    MsgBox Format(Timer - t, "0.0") & " seconds"

End Sub

Based on the linked post, and you other post here: Excel combinations from multiple columns with no duplicates

I assume you're trying to do something like this:

Input
AACDB
BBDFJ
CFGIK
DHJ
E
Output
ABCDJ
ABCDK
ABCFJ
ABCFK
ABCIJ
ABCIK
ABCJK
ABDFJ
ABDFK
ABDIJ
ABDIK
ABDJK
ABGDJ
ABGDK
ABGFJ
ABGFK
ABGIJ
ABGIK
ABGJK
ABHDJ
ABHDK
ABHFJ
ABHFK
ABHIJ
ABHIK
ABHJK
AFCDB
AFCDJ
AFCDK
etc


I've generated this output using VBA code:

Code:
Sub Test()

    Dim i As Long, j As Long, Cycle() As Long, Total As Long, Count() As Long, N As Long, m As Long, r As Long, c As Long, Tally As Long, temp As Long, MyInput() As Long
    Dim MyOutput() As String, People() As String
    Dim Duplicate() As Boolean
           
    Total = Range("Total").Value
    r = Range("Input").Rows.Count
    c = Range("Input").Columns.Count
    N = Range("People").Rows.Count
    ReDim People(1 To N)
    ReDim MyInput(1 To r, 1 To c)
    ReDim MyOutput(1 To Total, 1 To c)
    ReDim Cycle(1 To c)
    ReDim Count(1 To c)
   
    For j = 1 To c
        Cycle(j) = Range("Cycle")(j).Value
        Count(j) = Range("Count")(j).Value
        On Error Resume Next
        For i = 1 To r
            MyInput(i, j) = Range("Input")(i, j).Value
        Next i
        On Error GoTo 0
    Next j
    For i = 1 To N
        People(i) = Range("People")(i).Value
    Next i
   
    On Error Resume Next
    Range("Myresults").ClearContents
    On Error GoTo 0
   
    For i = 1 To Total
        ReDim Duplicate(1 To N)
        For j = 1 To c
            m = MyInput(1 + Int((i - 1) / Cycle(j)) Mod Count(j), j)
            If Duplicate(m) Then
                Exit For
            Else
                MyOutput(Tally + 1, j) = People(m)
                Duplicate(m) = True
            End If
        Next j
        If j = c + 1 Then Tally = Tally + 1
    Next i

    With Range("C10").Resize(Tally, c)
        .Value = MyOutput
        .Name = "Myresults"
    End With
   
End Sub

... using a few helper cells to make it faster for me to code (these calcs could also be done in VBA) :

Duplicates.xlsm
ABCDEFGHIJKLMN
1PeopleRosterInput
2AAACDB11342
3BBBDFJ224610
4CCFGIK367911
5DDHJ4 810 
6EE5    
7FCount
8G53443
9HOutputTotalCycle
10IABCDJ720144481231
11JABCDK
12KABCFJ
13ABCFK
14ABCIJ
15ABCIK
16ABCJK
17ABDFJ
18ABDFK
19ABDIJ
20ABDIK
21ABDJK
22ABGDJ
23ABGDK
24ABGFJ
25ABGFK
26ABGIJ
27ABGIK
28ABGJK
29ABHDJ
30ABHDK
31ABHFJ
32ABHFK
33ABHIJ
34ABHIK
35ABHJK
36AFCDB
37AFCDJ
Sheet1
Cell Formulas
RangeFormula
J2:N6J2=IFERROR(MATCH(C2,People,),"")
J8:N8J8=COUNT(J2:J6)
I10:M10I10=J10*J8
Named Ranges
NameRefers ToCells
Count=Sheet1!$J$8:$N$8I10
Cycle=Sheet1!$J$10:$N$10I10
Input=Sheet1!$J$2:$N$6J8
People=Sheet1!$A$2:$A$12J2:N6
Roster=Sheet1!$C$2:$G$6J2

If you have access to the latest Excel functions like FILTER and SEQUENCE, we could also use a formula approach to generate the permutations and filter duplicates.

I suggest you update your Account details so we know what version of Excel you're using, as the best solution may vary by version.
 
Upvote 0

Forum statistics

Threads
1,224,812
Messages
6,181,105
Members
453,021
Latest member
Justyna P

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top