I'm looking for code in VBA to generate all subsets of the items in a passed array.
Below is simple code to select all N choose 2 subsets of array size N.
Looking to augment this for N choose (N-1)... all the way down to N choose 1.
Option Base 1
Sub nchoose2()
iarray = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
n = UBound(iarray)
x = 1
t = 0
r = 0
Do While (n - x) >= 1
For i = 1 To (n - x)
Cells((i + t), 1) = iarray(x)
Cells((i + t), 2) = iarray(i + x)
Next i
x = x + 1
t = t + (n - (1 + r))
r = r + 1
Loop
End Sub
In addition to the Gray-code algorithm, you can also exploit the correspondence between subsets of an n-element set and binary vectors of length n. The following code illustrates this approach:
Sub AddOne(binaryVector As Variant)
'adds one to an array consisting of 0s and 1s
'thought of as a binary number in little-endian
'the vector is modified in place
'all 1's wraps around to all 0's
Dim bit As Long, carry As Long, i As Long, n As Long
carry = 1
n = UBound(binaryVector)
i = LBound(binaryVector)
Do While carry = 1 And i <= n
bit = (binaryVector(i) + carry) Mod 2
binaryVector(i) = bit
i = i + 1
carry = IIf(bit = 0, 1, 0)
Loop
End Sub
Function listSubsets(items As Variant) As Variant
'returns a variant array of collections
Dim lb As Long, ub As Long, i As Long, j As Long, numSets As Long
Dim vect As Variant 'binary vector
Dim subsets As Variant
lb = LBound(items)
ub = UBound(items)
ReDim vect(lb To ub)
numSets = 2 ^ (1 + ub - lb)
ReDim subsets(1 To numSets)
For i = 1 To numSets
Set subsets(i) = New Collection
For j = lb To ub
If vect(j) = 1 Then subsets(i).Add items(j)
Next j
AddOne vect
Next i
listSubsets = subsets
End Function
Function showCollection(c As Variant) As String
Dim v As Variant
Dim i As Long, n As Long
n = c.Count
If n = 0 Then
showCollection = "{}"
Exit Function
End If
ReDim v(1 To n)
For i = 1 To n
v(i) = c(i)
Next i
showCollection = "{" & Join(v, ", ") & "}"
End Function
Sub test()
Dim stooges As Variant
Dim stoogeSets As Variant
Dim i As Long
stooges = Array("Larry", "Curly", "Moe")
stoogeSets = listSubsets(stooges)
For i = LBound(stoogeSets) To UBound(stoogeSets)
Debug.Print showCollection(stoogeSets(i))
Next i
End Sub
Running the code results in the following output:
{}
{Larry}
{Curly}
{Larry, Curly}
{Moe}
{Larry, Moe}
{Curly, Moe}
{Larry, Curly, Moe}
I asked a similar question a while back (2005) and received this excellent code from John Coleman:
Sub MAIN()
Dim i As Long, st As String
Dim a(1 To 12) As Integer
Dim ary
For i = 1 To 12
a(i) = i
Next i
st = ListSubsets(a)
ary = Split(st, vbCrLf)
For i = LBound(ary) To UBound(ary)
Cells(i + 1, 1) = ary(i)
Next i
End Sub
Function ListSubsets(Items As Variant) As String
Dim CodeVector() As Integer
Dim i As Integer
Dim lower As Integer, upper As Integer
Dim SubList As String
Dim NewSub As String
Dim done As Boolean
Dim OddStep As Boolean
OddStep = True
lower = LBound(Items)
upper = UBound(Items)
ReDim CodeVector(lower To upper) 'it starts all 0
Do Until done
'Add a new subset according to current contents
'of CodeVector
NewSub = ""
For i = lower To upper
If CodeVector(i) = 1 Then
If NewSub = "" Then
NewSub = Items(i)
Else
NewSub = NewSub & ", " & Items(i)
End If
End If
Next i
If NewSub = "" Then NewSub = "{}" 'empty set
SubList = SubList & vbCrLf & NewSub
'now update code vector
If OddStep Then
'just flip first bit
CodeVector(lower) = 1 - CodeVector(lower)
Else
'first locate first 1
i = lower
Do While CodeVector(i) <> 1
i = i + 1
Loop
'done if i = upper:
If i = upper Then
done = True
Else
'if not done then flip the *next* bit:
i = i + 1
CodeVector(i) = 1 - CodeVector(i)
End If
End If
OddStep = Not OddStep 'toggles between even and odd steps
Loop
ListSubsets = SubList
End Function
The original question and answer:
John Coleman
Related
I am trying to figure out a loop logic to get all possible permutations where I add a set value to each item in a set array iLoop number of times. I'm gonna try my best to explain what I am looking for.
I have a set value "StrokeValue" and a set array "DistanceMatesArray"
Dim StrokeValue as single
Dim DistanceMatesArray as variant
StrokeValue = 300
DistanceMatesArray = Array(300, 300, 300, 300)
Now I need to loop through each possible result where I add StrokeValue to each Item which in the first loop would result in possible DistanceMatesArrays:
The tricky part is when I want to add StrokeValue more than once and get every outcome where I added StrokeValue iLoop number of time "AllowedActions" resulting in a list such as:
I kind of suspect that I need a 2D array to store all the results from previous loop., that's why in the example the rows are coloured to indicate which one row was taken as a starting point to add the StrokeValue once
What I got so far looks like this:
Public StrokeValue As Single
Public DistanceMatesArray As Variant
Public iError As Long
Public NumberOfCombinations As Long
Public x As Long
Public y As Long
Public i As Long
Option Explicit
Sub Test()
'Declare variables
Dim PreviousLoopResultsArray As Variant
Dim NextLoopResultsArray As Variant
Dim iresults As Long
Dim iLoop As Long
Dim iPreviousResult As Long
'Set variables
StrokeValue = 300
'Array
DistanceMatesArray = Array(300, 300, 300, 300)
ReDim NextLoopResultsArray(0, UBound(DistanceMatesArray))
For i = LBound(DistanceMatesArray) To UBound(DistanceMatesArray)
NextLoopResultsArray(0, i) = DistanceMatesArray(i)
Next i
'------------------------------------------------------
'Loop
Do While iError = NumberOfCombinations
'Try DistanceMatesArray
For i = 0 To iresults
For x = 0 To UBound(DistanceMatesArray) 'Loop horizontally
DistanceMatesArray(x) = NextLoopResultsArray(i, x)
Next x
Debug.Print Join(DistanceMatesArray)
'TRY HERE
Next i
'Array
PreviousLoopResultsArray = NextLoopResultsArray
'Array
If iLoop <> 0 Then
For x = 0 To UBound(DistanceMatesArray) 'Loop horizontally
DistanceMatesArray(x) = PreviousLoopResultsArray(iPreviousResult, x)
Next x
End If
'Set variables
iLoop = iLoop + 1
iPreviousResult = 1
iresults = ((UBound(DistanceMatesArray) + 1) ^ iLoop) - 1
ReDim NextLoopResultsArray(iresults, UBound(DistanceMatesArray))
'Populate NextLoopResultsArray
For y = 0 To iresults 'Loop vertically
If y Mod (UBound(DistanceMatesArray) + 1) = 0 And y <> iresults And y <> 0 Then
For x = 0 To UBound(DistanceMatesArray) 'Loop horizontally
DistanceMatesArray(x) = PreviousLoopResultsArray(iPreviousResult, x)
Next x
iPreviousResult = iPreviousResult + 1
End If
For x = 0 To UBound(DistanceMatesArray) 'Loop horizontally
NextLoopResultsArray(y, x) = DistanceMatesArray(x)
With ThisWorkbook.Worksheets(1).Cells(y + 1, x + 1)
.Value = NextLoopResultsArray(y, x)
End With
Next x
Next y
'Modify NextLoopResultsArray
x = 0
For y = 0 To iresults 'Loop vertically
NextLoopResultsArray(y, x) = NextLoopResultsArray(y, x) + StrokeValue
With ThisWorkbook.Worksheets(1).Cells(y + 1, x + 1)
.Value = NextLoopResultsArray(y, x)
.Interior.Color = vbYellow
End With
If x + 1 > UBound(DistanceMatesArray) Then
x = 0
Else
x = x + 1
End If
Next y
'Set variables
iPreviousResult = 0
'Excel reset
For i = 1 To (UBound(DistanceMatesArray) + 1)
Columns(i).Clear
Next i
Loop
End Sub
At the end of the loop I am expecting to have each one row as DistanceMatesArray i.e. one of them would now be
DistanceMatesArray = array(300,600,600,300)
Where it added StrokeValue twice.
Would someone, please, help me figure out a shorter and simpler logic behind this?
EDIT:
Results expected after running it up to 3 loops looks like this:
And without duplicate outcomes
Continuing to try and figure out the logic of it, maybe now someone get's a betetr idea for what I am lookign for and can help
No need to mention that it's an infinite loop - I know that and That's the point, it needs to go on untill I validate the right array in which case iError <> NumberOfCombinations.
Been able to learn more about arrays, so I consider this a big win.
The code took in account the duplicates but for now your iterations are hardset (could easily ask how many iterations with an inputbox), not in the endless loop you had set up, hope that rework won't be too much.
Some variables are reworked, I tried to keep most of your original ones though.
Public StrokeValue As Single
Public DistanceMatesArray As Variant
Public iError As Long
Public iTerations As Long
Public i As Long
Public j As Long
Public k As Long
Option Explicit
Sub TestArrayfill()
Dim pArray As Variant, nArray As Variant, cArray As Variant 'pArray = previous array, nArray = next array, cArray = check array
Dim iresults As Long, iLoop As Long, nb As Long, actB As Long, addCounter As Long, Lastrow As Long
'Set variables
StrokeValue = 300
addCounter = 1
iTerations = 4
'Array
DistanceMatesArray = Array(300, 300, 300, 300)
nb = UBound(DistanceMatesArray) + 1
ReDim Preserve DistanceMatesArray(1 To nb)
cArray = DistanceMatesArray
ReDim pArray(1 To nb, 1 To nb)
For i = 1 To nb
pArray(1, i) = DistanceMatesArray(i)
Next i
actB = nb
For iLoop = 1 To iTerations 'I can't figure out the limitations with permutations so we'll just bruteforce it with nb*actB (maximum possibilities)
ReDim nArray(1 To nb * actB, 1 To nb) '(re)setting nArray
If iLoop = 1 Then actB = 1 'workaround to have pArray as a 2D-array
For i = 1 To actB 'loop through every row in pArray except for when iLoop = 1
For j = 1 To nb 'loop through every cell in pArray(i)
For k = 1 To nb 'setting the extra StrokeValue
If j = k Then
cArray(k) = pArray(i, k) + StrokeValue
Else
cArray(k) = pArray(i, k)
End If
Next k
If Not arrElemInArray(cArray, nArray) Then
For k = 1 To nb
nArray(addCounter, k) = cArray(k) 'add the "row" to our nArray
Next k
addCounter = addCounter + 1
End If
Next j
Next i
actB = addCounter - 1
ReDim pArray(1 To actB, 1 To nb) 'ReDim is possible on both dimensions, Redim Preserve is not so we use this to our advantage
For i = 1 To actB 'another loop is necessary however
For j = 1 To nb
pArray(i, j) = nArray(i, j)
Next j
Next i
' nArray = Application.Transpose(nArray)
' ReDim Preserve nArray(1 To nb, 1 To actB)
' nArray = Application.Transpose(nArray)
' pArray = Application.Transpose(pArray)
' ReDim pArray(1 To UBound(nArray, 2), UBound(nArray, 1))
' pArray = Application.Transpose(pArray)
' pArray = nArray
addCounter = 1
Lastrow = Range("A" & Rows.Count).End(xlUp).Row
If Lastrow = 1 Then
Cells(Lastrow, 1).Value = "Loop" & iLoop
Else
Cells(Lastrow + 1, 1).Value = "Loop " & iLoop
Lastrow = Lastrow + 1
End If
Cells(Lastrow + 1, 1).Resize(UBound(nArray, 1), UBound(nArray, 2)) = nArray
Next iLoop
End Sub
Function arrElemInArray(arr As Variant, arrX As Variant) As Boolean 'this is from one of your previous questions btw, just a bit modified to fit our needs
Dim i As Long, j As Long, boolFound As Boolean, mtch
If Not IsArray(arrX) Then
For j = LBound(arr) To UBound(arr)
If arr(j) = arrX Then arrElemInArray = True: Exit For
Next j
Exit Function
End If
For i = LBound(arrX) To UBound(arrX)
boolFound = True 'True at beginning so if any cells deviates from the corresponding check, it gets set to False, ergo it doesn't exist yet.
For j = LBound(arr) To UBound(arr)
If arr(j) <> arrX(i, j) Then
boolFound = False
End If
Next j
If boolFound Then arrElemInArray = True: Exit Function
Next i
arrElemInArray = False
End Function
Hope it's all clear and works for you :)
I understood your logic for the first table
but for the following ones I find it difficult to understand what you want especially in relation to the capture that you put in your message
for the first
Sub testing()
Dim StrokeValue As Single
Dim DistanceMatesArray As Variant
Dim i As Long 'variable row iteration
Dim c As Long 'variable column itération
Dim Table As Variant 'variable variant no dimention in the first
StrokeValue = 300
DistanceMatesArray = Array(300, 300, 300, 300) 'is an array in base 0
nb = UBound(DistanceMatesArray) + 1 'convert a ubound of DistanceMatesArray in count (in base 1)
ReDim Table(1 To nb, 1 To nb) 'table dimensioning (variant) in base 1
'loop for row
For i = 1 To UBound(Table) 'start at index 1
'loop for column
For c = 1 To UBound(Table, 2) 'start at index 1
'if index row and index column then item has multipled by (2)
If c <> i Then Table(i, c) = StrokeValue Else Table(i, c) = StrokeValue + StrokeValue
Next c
Next i
'just for see on sheet
Cells.Resize(UBound(Table), UBound(Table)) = Table
End Sub
I'm a bit new with Visual Basic and I'm trying to make a program that would generate random numbers from 1 to 9 without repetition using arrays. I've also read that instead of using GoTo statements (as they are generally frowned upon), Do While Loop or While End While Statements could be used. I've tried using these loops but have not made it work. Here is the code:
Dim x As Integer = 0, y As Integer = 0, num As Integer = 0, arr(8) As Integer
lstLoop.Items.Clear()
For x = 0 To 8
Start:
Randomize()
num = Fix(Rnd() * 9) + 1
For y = 0 To 8
If num = arr(y) Then
GoTo Start
End If
Next
arr(x) = num
lstLoop.Items.Add(arr(x))
Next
The idea is to loop while you have found the number in the array:
Sub GenerateRandom()
Dim x As Integer
Dim num As Integer
Dim arr(8) As Integer
Randomize Timer
For x = 0 To 8
Do
num = Fix(Rnd() * 9) + 1
Loop While FindInArray(arr, x - 1, num)
arr(x) = num
Next x
End Sub
Function FindInArray(arr() As Integer, maxIndex As Integer, num As Integer) As Boolean
Dim i As Integer
FindInArray = False
For i = 0 To maxIndex
If arr(i) = num Then
FindInArray = True
Exit Function
End If
Next
End Function
use dictionary.
Sub test()
Dim dic As Object
Dim n As Integer
Dim Num As Integer
Set dic = CreateObject("Scripting.Dictionary")
Do Until n = 9
num = WorksheetFunction.RandBetween(1, 9)
If dic.Exists(num) Then
Else
n = n + 1
dic.Add num, num
End If
Loop
Range("a1").Resize(1, 9) = dic.Keys
End Sub
or
Sub test()
Dim dic As Object
Dim Num As Integer
Set dic = CreateObject("Scripting.Dictionary")
Do Until dic.Count = 9
num = WorksheetFunction.RandBetween(1, 9)
If dic.Exists(num) Then
Else
dic.Add num, num
End If
Loop
Range("a1").Resize(1, 9) = dic.Keys
End Sub
So the problem is more in depth than a simple comparison. Essentially im trying to model this dice roll known as the roll and keep system. Example would be 5k3. Where I would roll 5 dice and keep the 3 highest then add them together.
I've gotten my little macro program to roll the dice. Then I put them in an array in my example that would be an array with 5 indices. Now I need to take those 5 dice, and only keep the largest 3 of them.
The code is here A2 gives me the number of sides on the dice, B2 gives me how many I roll, and C2 gives me how many I keep. This rolls 10 dice, but then I transfer 5 of them into my actual dicepool. I know I could probably skip that, but I can deal with that later.
Private Sub CommandButton1_Click()
Dim i As Integer
Dim j As Integer
Dim k As Integer
Dim RandNum As Integer
Dim RollArray() As Integer
Dim KeptArray() As Integer
Dim RollArrayDummy() As Integer
Dim NumRoll As Integer
Dim Kept As Integer
Dim Largest As Integer
NumRoll = Range("B2").Value
ReDim RollArray(NumRoll)
Kept = Range("C2").Value
ReDim KeptArray(Kept)
For i = 5 To 15
Randomize
RandNum = 1 + Rnd() * (Range("A2").Value - 1)
Cells(i, 1).Value = RandNum
Next i
For j = 1 To NumRoll
RollArray(j) = Cells(4 + j, 1).Value
Cells(4 + j, 2).Value = RollArray(j)
Next j
k = 1
i = 1
m = 1
Largest = 1
For k = 1 To Kept
m = 1
KeptArray(k) = Largest
If m <= NumRoll Then
If Largest >= RollArray(m) And Largest >= KeptArray(k) Then
Largest = KeptArray(k)
Else
KeptArray(k) = Largest
Largest = RollArray(m)
End If
m = m + 1
End If
Cells(4 + k, 3).Value = KeptArray(k)
Next k
End Sub
I've tried so many things, like creating a dummy array, and comparing the variable Largest with it. And a ton of other things. My big problem is that I can't reuse any of the numbers.
If I roll 5 and keep 3. Say I roll [4,2,3,3,6] . I keep the [6,4,3]. Im sure this is incredibly simple and im overlooking it but its driving me absolutely insane.
Today I was watching some MonteCarlo simulations, so I have decided to do the whole question from the beginning. Thus, imagine that this is the input:
After the first roll, this is what you get:
The values in yellow are the top 3, which are kept. This is the result from the second roll:
And here is the whole code:
Public Sub RollMe()
Dim numberOfSides As Long: numberOfSides = Range("A2")
Dim timesToRoll As Long: timesToRoll = Range("B2")
Dim howManyToKeep As Long: howManyToKeep = Range("C2")
Dim cnt As Long
Dim rngCurrent As Range
Cells.Interior.Color = vbWhite
Set rngCurrent = Range(Cells(1, 6), Cells(1, 6 + timesToRoll - 1))
For cnt = 1 To timesToRoll
rngCurrent.Cells(1, cnt) = makeRandom(1, numberOfSides)
Next cnt
Dim myArr As Variant
With Application
myArr = .Transpose(.Transpose(rngCurrent))
End With
WriteTopN howManyToKeep, myArr, Cells(2, lastCol(rowToCheck:=2))
End Sub
Public Sub WriteTopN(N As Long, myArr As Variant, lastCell As Range)
Dim cnt As Long
For cnt = 1 To N
Set lastCell = lastCell.Offset(0, 1)
lastCell = WorksheetFunction.Large(myArr, cnt)
lastCell.Interior.Color = vbYellow
Next cnt
End Sub
The makeRandom and lastCol functions are some functions that I use for other projects as well:
Public Function makeRandom(down As Long, up As Long) As Long
makeRandom = CLng((up - down + 1) * Rnd + down)
If makeRandom > up Then makeRandom = up
If makeRandom < down Then makeRandom = down
End Function
Function lastCol(Optional strSheet As String, Optional rowToCheck As Long = 1) As Long
Dim shSheet As Worksheet
If strSheet = vbNullString Then
Set shSheet = ActiveSheet
Else
Set shSheet = Worksheets(strSheet)
End If
lastCol = shSheet.Cells(rowToCheck, shSheet.Columns.Count).End(xlToLeft).Column
End Function
Instead of looping through the array "manually", the WorksheetFunction.Large() nicely returns the Nth-largest value.
And if you are willing to color the "dice", which were used to take the top score, you may add this piece:
Public Sub ColorTopCells(howManyToKeep As Long, rngCurrent As Range, myArr As Variant)
Dim colorCell As Range
Dim myCell As Range
Dim cnt As Long
Dim lookForValue As Long
Dim cellFound As Boolean
For cnt = 1 To howManyToKeep
lookForValue = WorksheetFunction.Large(myArr, cnt)
cellFound = False
For Each myCell In rngCurrent
If Not cellFound And myCell = lookForValue Then
cellFound = True
myCell.Interior.Color = vbMagenta
End If
Next myCell
Next cnt
End Sub
It produces this, coloring the top cells in Magenta:
Edit: I have even wrote an article using the code above in my blog here:
vitoshacademy.com/vba-simulation-of-rolling-dices
Try this, changed a few things:
Edited the random bit too
Private Sub CommandButton1_Click()
Dim i As Long, j As Long, k As Long
Dim RandNum As Long
Dim RollArray() As Long
Dim KeptArray() As Long
Dim NumRoll As Long
Dim Kept As Long
NumRoll = Range("B2").Value
ReDim RollArray(1 To NumRoll)
Kept = Range("C2").Value
ReDim KeptArray(1 To Kept)
For i = 5 To 15
Randomize
'RandNum = 1 + Rnd() * (Range("A2").Value - 1)
RandNum = 1 + Int(Rnd() * Range("A2").Value)
Cells(i, 1).Value = RandNum
Next i
For j = 1 To NumRoll
RollArray(j) = Cells(4 + j, 1).Value
Cells(4 + j, 2).Value = RollArray(j)
Next j
For k = 1 To Kept
KeptArray(k) = Application.WorksheetFunction.Large(RollArray, k)
Cells(4 + k, 3).Value = KeptArray(k)
Next k
End Sub
Makes use of the Excel large function
Here is my attempt to fix this problem. I left the reading cell values and writing results to the OP as I am focused on the logic of the process.
There are three main functions. DiceRollSim(), RollDie() and GetNLargestIndex() as well as a function to test the code, named Test().
DiceRollSim() runs the particular simulation given the number of sides, and number of die and the number to keep. It prints the results in the output window. DollDie() fills in an array of random values simulating the rolling of the die. Caution is needed to make sure the interval probabilities are maintained as VBA does round values when converting the result of Rnd() into integers. Finally, GetNLargestIndex() is the meat of the answer, as it takes the die roll results, creates an array of index values (the 1st, 2nd, 3rd .. ) and then sorts the array based on the values of the die rolls.
Option Explicit
Public Sub Test()
DiceRollSim 6, 15, 3
' Example, 15k3:
' Rolling 15 die.
' x(1) = 5 *
' x(2) = 4
' x(3) = 4
' x(4) = 2
' x(5) = 4
' x(6) = 5 **
' x(7) = 6 ***
' x(8) = 1
' x(9) = 4
' x(10) = 3
' x(11) = 1
' x(12) = 3
' x(13) = 5
' x(14) = 3
' x(15) = 3
' Sorting die values.
' x(7) = 6
' x(6) = 5
' x(1) = 5
' Sum of 3 largest=16
End Sub
Public Sub DiceRollSim(ByVal n_sides As Long, ByVal n_dice As Long, ByVal n_keep As Long)
Dim die() As Long, i As Long
ReDim die(1 To n_dice)
Debug.Print "Rolling " & n_dice & " die."
Call RollDie(n_sides, n_dice, die)
For i = 1 To n_dice
Debug.Print "x(" & i & ")=" & die(i)
Next i
Dim largest() As Long
Debug.Print "Sorting die values."
Call GetNLargestIndex(die, n_keep, largest)
Dim x_sum As Long
x_sum = 0
For i = 1 To n_keep
x_sum = x_sum + die(largest(i))
Debug.Print "x(" & largest(i) & ")=" & die(largest(i))
Next i
Debug.Print "Sum of " & n_keep & " largest=" & x_sum
End Sub
Public Sub RollDie(ByVal n_sides As Long, ByVal n_dice As Long, ByRef result() As Long)
ReDim result(1 To n_dice)
Dim i As Long
For i = 1 To n_dice
' Rnd() resurns a number [0..1)
' So `Rnd()*n_sides` returns a floating point number zero or greater, but less then n_sides.
' The integer conversion `CLng(x)` rounds the number `x`, and thus will not keep equal
' probabilities for each side of the die.
' Use `CLng(Floor(x))` to return an integer between 0 and n_sides-1
result(i) = 1 + CLng(WorksheetFunction.Floor_Math(Rnd() * n_sides))
Next i
End Sub
Public Sub GetNLargestIndex(ByRef die() As Long, ByVal n_keep As Long, ByRef index() As Long)
Dim n_dice As Long, i As Long, j As Long, t As Long
n_dice = UBound(die, 1)
' Instead of sorting the die roll results `die`, we sort
' an array of index values, starting from 1..n
ReDim index(1 To n_dice)
For i = 1 To n_dice
index(i) = i
Next i
' Bubble sort the results and keep the top 'n' values
For i = 1 To n_dice - 1
For j = i + 1 To n_dice
' If a later value is larger than the current then
' swap positions to place the largest values early in the list
If die(index(j)) > die(index(i)) Then
'Swap index(i) and index(j)
t = index(i)
index(i) = index(j)
index(j) = t
End If
Next j
Next i
'Trim sorted index list to n_keep
ReDim Preserve index(1 To n_keep)
End Sub
I want to make a sub, which determines if the cells in the 12th column starts with 262015. If it does start with this, it should create a new random 8-digit number starting with "18" and then 6 randomly created unique digits.
My code does not seem to figure out if the cell starts with 262015, and I have not been able to find help on creating the 8-digit number with these requirements.
Hope you can help me!
Sub Opgave8()
For i = 2 To 18288
If Left(Worksheets("arab").Cells(i, 12), 6) = "262015" Then
Worksheets("arab").Cells(i, 3) = "18" & studyid(6)
End If
Next i
Function UniqueRandDigits(x As Long) As String
Dim i As Long
Dim n As Integer
Dim s As String
Do
n = Int(Rnd() * 10)
If InStr(s, n) = 0 Then
s = s & n
i = i + 1
End If
Loop Until i = x + 1
UniqueRandDigits = s
End Function
End Sub
For i = 2 To 18288
If Left(Worksheets("Base").Cells(i, 12), 6) = "262015" Then
Worksheets("Base").Cells(i, 3) = "18" & Randdigits(6)
End If
Next i
Function RandDigits(x As Long) As String
Dim i As Long
Dim s As String
For i = 1 To x
s = s & Int(Rnd() * 10)
Next i
RandDigits = s
End Function
EDIT: here's one where all digits are different
Function UniqueRandDigits(x As Long) As String
Dim i As Long
Dim n As Integer
Dim s As String
Do
n = Int(Rnd() * 10)
If InStr(s, n) = 0 Then
s = s & n
i = i + 1
End If
Loop Until i = x + 1
UniqueRandDigits = s
End Function
EDIT2: And here is one that forces all numbers to be different
dim n as string
dim ok as boolean
For i = 2 To 18288
If Left(Worksheets("Base").Cells(i, 12), 6) = "262015" Then
ok = false
do
n = UniqueRandDigits(6)
If Application.WorksheetFunction.CountIf(Worksheets("Base").Range("L2:L18288"), n) = 0 Then
Worksheets("Base").Cells(i, 3) = "18" & n
ok = true
end if
loop until ok
End If
Next i
Using Left function, you need to specify the String, then the number of characters from the left, and then you check if it's equal to "262015".
Try the code below:
For i = 2 To 18288
If Left(Worksheets("Base").Cells(i, 12), 6) = "262015" Then
Worksheets("Base").Cells(i, 3) = "XXX"
End If
Next i
This is my current implementation using bits:
Function Array_PowerSet(Self)
Array_PowerSet = Array()
PowerSetUpperBound = -1
For Combination = 1 To 2 ^ (UBound(Self) - LBound(Self)) ' I don't want the null set
Subset = Array()
SubsetUpperBound = -1
For NthBit = 0 To Int(WorksheetFunction.Log(Combination, 2))
If Combination And 2 ^ NthBit Then
SubsetUpperBound = SubsetUpperBound + 1
ReDim Preserve Self(0 To SubsetUpperBound)
Subset(SubsetUpperBound) = Self(NthBit)
End If
Next
PowerSetUpperBound = PowerSetUpperBound + 1
ReDim Preserve Array_PowerSet(0 To PowerSetUpperBound)
Array_PowerSet(PowerSetUpperBound) = Subset
Next
End Function
Please ignore the abuse of Variants. Array_Push and Array_Size should be self-explanatory.
Previously, I was generating a binary string for each combination, but that involved calling another function which wasn't very efficient.
Aside from using less Variants and moving external function calls inside, is there any way I can make this more efficient?
EDIT: Here's a fully independent version.
Function Array_PowerSet(Self As Variant) As Variant
Dim PowerSet() As Variant, PowerSetIndex As Long, Size As Long, Combination As Long, NthBit As Long
PowerSetIndex = -1: Size = UBound(Self) - LBound(Self) + 1
ReDim PowerSet(0 To 2 ^ Size - 2) ' Don't want null set
For Combination = 1 To 2 ^ Size - 1
Dim Subset() As Variant, SubsetIndex As Long: SubsetIndex = -1
For NthBit = 0 To Int(WorksheetFunction.Log(Combination, 2))
If Combination And 2 ^ NthBit Then
SubsetIndex = SubsetIndex + 1
ReDim Preserve Subset(0 To SubsetIndex)
Subset(SubsetIndex) = Self(NthBit)
End If
Next
PowerSetIndex = PowerSetIndex + 1
PowerSet(PowerSetIndex) = Subset
Next
Array_PowerSet = PowerSet
End Function
And a test:
Dim Input_() As Variant, Output_() As Variant, Subset As Variant, Value As Variant
Input_ = Array(1, 2, 3)
Output_ = Array_PowerSet(Input_)
For Each Subset In Output_
Dim StringRep As String: StringRep = "{"
For Each Value In Subset
StringRep = StringRep & Value & ", "
Next
Debug.Print Left$(StringRep, Len(StringRep) - 2) & "}"
Next
Since the number of subsets grows exponentially, no algorithm is truly efficient, although there is room for improvement in what you are doing:
ReDim Preserve, when used to extend an array by a single item, is inefficient since it involves creating a new array with 1 more space and then copying the old elements to the new array. It is better to pre-allocate enough space and then trim it down to size:
Function PowerSet(Items As Variant) As Variant
'assumes that Items is a 0-based array
'returns a 0-based jagged array of subsets of Items
'where each subset is a 0-based array
Dim PS As Variant
Dim i As Long, j As Long, k As Long, n As Long
Dim subset As Variant
n = 1 + UBound(Items) 'cardinality of the base set
ReDim PS(0 To 2 ^ n - 2)
For i = 1 To 2 ^ n - 1
subset = Array()
ReDim subset(0 To n - 1)
k = -1 'will be highest used index of the subset
For j = 0 To n - 1
If i And 2 ^ j Then
k = k + 1
subset(k) = Items(j)
End If
Next j
ReDim Preserve subset(0 To k)
PS(i - 1) = subset
Next i
PowerSet = PS
End Function
A test function:
Sub test()
Dim stuff As Variant, subsets As Variant
Dim i As Long
stuff = Array("a", "b", "c", "d")
subsets = PowerSet(stuff)
For i = LBound(subsets) To UBound(subsets)
Cells(i + 1, 1).Value = "{" & Join(subsets(i), ",") & "}"
Next i
End Sub
Using collections to build your sets is an option...
Function Generator()
Dim Arr() As Variant: Arr = Array(1, 2, 3, 4)
Dim PSCol As Collection: Set PSCol = PowerSetCol(Arr)
Dim SubSet As Collection, SubSetStr As String
For i = 1 To PSCol.Count
Set SubSet = PSCol.Item(i)
SubSetStr = "{"
For j = 1 To SubSet.Count
SubSetStr = SubSetStr & SubSet.Item(j) & IIf(j = SubSet.Count, "", ", ")
Next j
SubSetStr = SubSetStr & "}"
Debug.Print SubSetStr
Next i
End Function
Function PowerSetCol(Arr As Variant) As Collection
Dim n As Long, i As Long
Dim Temp As New Collection, SubSet As Collection
For i = 1 To 2 ^ (UBound(Arr) + 1) - 1
Set SubSet = New Collection
For n = 0 To UBound(Arr)
If i And 2 ^ n Then SubSet.Add Arr(n)
Next n
Temp.Add SubSet
Next i
Set PowerSetCol = Temp
End Function
******* EDIT ********
Apparently accessing collections through index is more intensive than enumerating through the items. Also; you can't use join directly as stated by #John Coleman but a single line function can be used in it's place.
Hopefully the code below is a more optimal solution
Function Generator()
Dim Arr() As Variant: Arr = Array(1, 2, 3, 4)
Dim PSColl As Collection: Set PSColl = PowerSetColl(Arr)
Dim Str As String, Coll As Collection, Item As Variant
For Each Coll In PSColl
Str = ""
For Each Item In Coll
Str = strJoin(", ", Str, CStr(Item))
Next Item
Debug.Print "{" & Str & "}"
Next Coll
End Function
Function PowerSetColl(Arr As Variant) As Collection
Dim Temp As New Collection, SubSet As Collection
Dim n As Long, i As Long
For i = 1 To 2 ^ (UBound(Arr) + 1) - 1
Set SubSet = New Collection
For n = 0 To UBound(Arr)
If i And 2 ^ n Then SubSet.Add Arr(n)
Next n
Temp.Add SubSet
Next i
Set PowerSetColl = Temp
End Function
Function strJoin(Delimiter As String, Optional Str1 As String, Optional Str2 As String) As String
strJoin = IIf(IsMissing(Str1) Or Str1 = "", Str2, IIf(IsMissing(Str2) Or Str2 = "", Str1, Str1 & Delimiter & Str2))
End Function