I'm unable to compare two lists in VB.Net - arrays

I have a project to make the dice game Yahtzee. Out of Five Dice, I have to sort the dice in ascending order, and then check if they are in a certain order. Thus the small straight score in Yahtzee. So for example, if I get a [1,2,3,4,6] Then I have a small straight. But if I had a [1,2,4,6,5], I would not.
This is my code(Dice list are the randomized list of dice, this scoring function determines if it matches the small straight criteria)
Dim Sorted_List() As Integer
Sorted_List(0) = Dice_List(0)
Sorted_List(1) = Dice_List(1)
Sorted_List(2) = Dice_List(2)
Sorted_List(3) = Dice_List(3)
Sorted_List(4) = Dice_List(4)
Array.Sort(Sorted_List)
Dim Fours_List(3) As Integer
Fours_List(0) = Sorted_List(0)
Fours_List(1) = Sorted_List(1)
Fours_List(2) = Sorted_List(2)
Fours_List(3) = Sorted_List(3)
Dim smlStr1() As Integer = {1, 2, 3, 4}
If (Fours_List Is smlStr1) Then
lblSmallStraight.Text = "55"
End If

So you've got an array called DiceList and it holds the result of rolling the dice. We can sort it pretty easily using LINQ:
Dim rolls = DiceList.OrderBy(Function(x) x)
But we should also remove duplicates as it makes the problem easier to deal with:
Dim rolls = DiceList.OrderBy(Function(x) x).Distinct().ToArray()
Now a simple way to look at it might be just to collect the differences of the rolls into a string:
Dim consec = ""
For i = 1 to Ubound(rolls)
sum += (rolls(i) - rolls(i-1)).ToString()
Next i
And then ask:
If consec.Contains("1111") Then 'its a large straight
Else If consec.Contains("111") Then 'there's a small straight
Of course, you might think it simpler to just list out some combos:
Dim smallStraights = {"1234","2345","3456"}
Dim largeStraights = {"12345","23456"}
Then turn your rolls into a string:
Dim rollsStr = string.Join("", rolls.Select(Function(x) x.ToString()))
And ask if the string contains any of the straights:
If largeStraights.Any(Function(ls) rollsStr.Contains(ls)) Then '...
Else If smallStraights.Any(Function(ss) rollsStr.Contains(ss)) Then '...
Note that this last syntax is a bit odd; because our straights are in an array and we're querying to see if the rolls string contains any of the straights, we can't start out with rollsStr.Contains(...).
Instead we actually need to ask "for all these straights in this array, is there any array element such that the rolls string contains the array element" ?
With loops it would look like:
'smallStraights.Any(Function(ss) rollsStr.Contains(ss))
For Each ss In smallStraights
If rollsStr.Contains(ss) Return True 'stop as soon as one is found
Next ss
Return False 'none found
So how does this all work?
We have a set of rolls:
{2, 1, 4, 3, 6}
As a human, we could look for consecutives in this either by
counting up and jumping back and forth (find the 1, go left to the 2, right three places to the 3, left to the 4, right to the 6, work out that we had 4 in a row, but not 5, call it a straight small)
rearranging the dice so they are shown in order and see how many of them are "one more than the one to their left"
The latter approach is what I start with - I sort the array and look at the differences. Because duplicates would ruin our approach (A set of 1,2,2,3,4 is actually a small straight if you throw one of the 2s away, but if you kept it you'd have a difference chain of 1,0,1,1 and the 0 would upset things if we were looking for a difference chain of 1,1,1) I also take them out as part of the "sort the dice" step
After we implemented the approach of sorting the dice and then going through them one by one working out the difference to the previous die, we built a string up that described the differences.
Building a string makes our life easier because there are built in methods that ask if one string contains another. If our difference string was e.g. "0111", or "1112" then it does contain a "111" which means a small straight is present (remember that there are 5 dice, but only 4 differences because the algorithm is "this_dice minus previous_dice" i.e. for 5 dice A,B,C,D,E we do B-A, C-B, D-C, and E-D - 4 differences for 5 dice)
Then we might realise that it's actually easier to not do the differneces thing, but just to order the dice, remove the duplicates and look for the small combination of dice that mean a straight is present.. This means we're literally taking our {1,2,3,4,6} rolls, turning them into a string of "12346" and then looking in it to see if we can find "1234", or "2345", or "3456" - the small straights. If we do this after we look for the big straights, and only if we didn't find a big straight, then for a roll set of "12345" we wouldn't accidentally declare it a small straight (because "12345" contains "1234") when its really a big straight
Why choose one over the other? Well, looking for the limited number of small/large straights (there are only 5) is viable because there are only 5. If Yahtzee had 100 sided dice, and a straight could be 1-2-3-4, 2-3-4-5, 3-4-5-6, 4-5-6-7, 5-6-7-8 .. all the way to 97-98-99-100 then it would make sense to do the differences method, because instead of listing 98 combinations of small straight, the differences method always reduces the variety of what we're looking for to "111"; a small straight of 1-2-3-4 or 97-98-99-100 both become 1-1-1-1 if we do differences
So all that remains is for your code to turn your list of numbers into a single string and then use Contains. This is a lot easier to do than ask "does this list of numbers contain this other list of numbers" so we (ab)use string into being our data container for the numbers because it means they're no longer separate things that have to be cross coordinated; they're a single string that contains a pattern and over the time we've developed lots of ways in programming languages, of looking for patterns inside strings
You certainly could have one bunch of numbers, {1,2,3,4,6} and ask "does this set of numbers contain this other set of numbers {1,2,3,4}" but it would look a bit more like this (in programming 101 terms, not using LINQ or Sets etc)
Dim rolls = {1,2,3,4,6} 'note: these values must be unique
Dim straight = {1,2,3,4} 'note: these values must be unique
Dim numFound = 0
For Each r in rolls
For Each s in straight
If r == s Then
numFound += 1
End If
Next s
Next r
If numFound = straight.Length Then 'if we found all the numbers in the straight
Console.Write("All the numbers of the straight exist in the rolls")
End If
We still have the problem that duplicates defeat this method so we need to dedupe our rolls. We could do that by adding a bit on:
Dim numFound = 0
Dim prevR = -1
For Each r in rolls
If r = prevR Then Continue 'skip this one, it's a duplicate of the previous roll
prevR = r ' remember the current roll for next time
For Each s in straight
If r == s Then
numFound += 1
End If
Next s
Next r
We also still have the problem that the rolls need to be sorted, because we only check the previous one roll. If we are going to be working with unsorted rolls, then we need to check all the previous ones to see if the current roll occurred already:
For i = LBound(rolls) to UBound(rolls)
Dim r = rolls(i)
'Check ALL the previous rolls
Dim seenBefore = False
For p = i - 1 To LBound(rolls) Step -1
Dim prevR = rolls(p)
If prevR = r Then seenBefore = True
Next p
If seenBefore Then Continue 'skip this one, it's a duplicate of a previous roll
For Each s in straight
If r == s Then
numFound += 1
End If
Next s
Next r
You can see how the problem starts growing every time we think about it/try to solve another problem/bug with the previous iteration. All in we now have a mechanism for checking if one set of numbers exists in another set of numbers but its quite lengthy compared to our previous steps:
Dim rolls = {1,3,2,5,4}
Dim rollsString = string.Join("", rolls.OrderBy(...).Distinct()) 'turn the rolls into a string like "12345"
If rollsString.Contains("1234") Then 'rolls contains the small straight 1234
Note, these use LINQ extensively and you might not have been taught LINQ.. You might need an approach that implements the algorithm "sort, unique, look for straights" using what you know already, but equally you can probably do your own learning (getting help learning on SO is possible, though a lot of people want to just give you the answer in the shortest time possible to try and win the points) and justify your algorithm and your solution to the teacher.
If you don't want to use LINQ, you can take a look at some of the other things discussed and assemble a solution. Deduping a List can be done by something like:
Dim uniqueList as New List(Of Integer) 'must be a list, not an array
For Each i in listWithDuplicates
If Not uniqueList.Contains(i) Then uniqueList.Add(i)
Next i

Related

Is there a way to transfer all values from one array to another, then erase the original array?

I'm running into a problem with a block of code I'm trying to develop at my job. Essentially, I'm creating a userform in excel where folks will enter data for railcars as they get loaded at a certain location (we'll call these "spot 1, spot 2, spot 3, etc.").
Sometimes they'll have to move that car to a different spot, in which case I want them to be able to keep all the information on the railcar from the first/original entry, and then erase the data from the original spot once that's done.
To accomplish this in a more streamlined fashion, I've established arrays for each of the 5 spots that reference all the cells they're entering data into on the userform:
Dim spot1information(14)
spot1information(0) = UserForm.ProductType1.Value
spot1information(1) = UserForm.ProductID1.Value
spot1information(2) = UserForm.BatchID1.Value
etc....
Dim spot2information(14)
spot2information(0) = UserForm.ProductType2.Value
spot2information(1) = UserForm.ProductID2.Value
spot2information(2) = UserForm.BatchID2.Value
etc....
And so forth for all five spots. I don't know if this makes things more difficult, but note that these array values aren't all of the same type. For instance, index (0) will be a string, but index (10) is a DATETIME and index (12) is defined as Long.
So say that they are moving a car from spot 1 to spot 2. In short, I want the code to do the following:
Replace the values of indices 0 - 6 in spot2information (which is currently empty) with the values of indices 0 - 6 in spot1information (which the user has filled on the userform).
I'm only interested in carrying over indices 0-6 because they contain the pertinent railcar information
Empty every value of spot1information to ""
To accomplish this, I tried the following code and a few variations thereof:
If OriginalSpot.Value = 1 Then
If DestinationSpot.Value = 2 Then
For i = 0 to 6
spot2information(i) = spot1information(i)
Next
For Each i in spot1information
spot1information(i) = ""
Next
End If
End If
However, this keeps coming up with a type mismatch. I figure because the data in the spot2information array is empty, and the data in the spot1information array is not, but I'm not entirely sure of a way around this.
Update: I did what was suggested below and replaced: spot1information(i) = "" with Erase spot1information
The code now essentially works! The values of array "spot2information" are now the former values of "spot1information", with "spot1information" now empty.
The 2D array suggested below also works like a charm. New problem I've been facing is that array values are updating, but the userform isn't. (note: in the future I'll be posting this type of thing as a separate question, my apologies!)
Easier to manage this as a 2D array:
Sub Tester()
Dim spots(1 To 5, 0 To 14), n As Long, i As Long
'fill your spot arrays from the form....
For n = 1 To 5
spots(n, 0) = UserForm.Controls("ProductType" & n).Value
spots(n, 1) = UserForm.Controls("ProductID" & n).Value
spots(n, 2) = UserForm.Controls("BatchID" & n).Value
'etc etc
Next n
'swap a spot with another
Debug.Print spots(2, 1), spots(3, 1)
SwapSpots spots:=spots, fromSpot:=2, toSpot:=3
Debug.Print spots(2, 1), spots(3, 1)
End Sub
Sub SwapSpots(spots, fromSpot As Long, toSpot As Long)
Dim n As Long
For n = 0 To 6
spots(toSpot, n) = spots(fromSpot, n)
spots(fromSpot, n) = Empty 'empty the source slot value
Next n
End Sub
Assuming the DataType of the arrays is the same by Index i.e. index(0) is string for all spots, Index(2) is long for all spots, and so on.
If that is the case then this part should not produce any error:
For i = 0 to 6
spot2information(i) = spot1information(i)
Next
The error should be happening in this part more precisely in the line marked with #
For Each i in spot1information
spot1information(i) = "" '#
Next
and the reason for the error it seems to be that trying to assign a string value "" to a numeric type, given the "mismatch" error.
Using For Each i in spot1information indicates that you want to "Initiate" or Erase the entire array, therefore I suggest to use this line instead of the For…Next method.
Erase spot1information
In regards this:
But I've now run into a new problem, where the values on the userform haven't updated to reflect the new values stored in the array. Do I need to somehow "refresh" the userform?
You just updated the arrays, then you need to run the procedures used to update the values of the objects affected by both arrays in the UserForm.

Fast way to count duplicates in 30000 rows (Libreoffice Calc)

Actually, I already have a partial answer!!! Conditional formatting with "Cell value is" -> "duplicate" !!!
This way a check is performed for each user's new entry in "real time".
I need to check if duplicate entries exist in 30000 rows of a column (any value, but not blanks!) . I would like to keep track of how many duplicates during the filling process.
Ok, conditional formatting is a very effective visual indication and fast anough for my needs, but as I am not able to perform a loop to check the color of the cells (found some people against this approach!! Would be so easy! ) I need to find an alternative way to count the duplicates (as a whole, no need to identify how many for each case!).
I tryed the formula:
=SUMPRODUCT((COUNTIF(F2:F30001;$F$2:$F$30001)>1))
It works, but it takes two minutes to finish.
If you want to replicate my case. My 30000 entries are formatted as: letter "A" and numbers between 100000 and 999999, e.g., A354125, A214547, etc. Copy as text the result of "=CONCATENATE("A";RANDBETWEEN(100000;999999))" to save time.
Thanks!
PS: Does anybody know the algorithm used to find the duplicates in conditional formatting (it is fast)?
A macro solution is not the best, but is acceptable! ;)
The =SUMPRODUCT((COUNTIF(F2:F30001;$F$2:$F$30001)>1)) must do following: Count if $F$2 is in F2:F30001, then count if $F$3 is in F2:F30001, ..., then count if $F$30001 is in F2:F30001. So it must fully loop over the array F2:F30001 with each single item.
The fastest way counting duplicates in an array is avoiding fully loop over the array with each single item. One way is sorting first. There are very fast quick sort methods. Or using collections which per definition can only have unique items.
The following code uses the second way. The keys of a Collection must be unique. Adding an item having a duplicate key fails.
Public Function countDuplicates(vArray As Variant, Optional inclusive As Boolean ) As Variant
On Error Goto wrong
If IsMissing(inclusive) Then inclusive = False
oDuplicatesCollection = new Collection
oUniqueCollection = new Collection
lCountAll = 0
For Each vValue In vArray
If contains(oUniqueCollection, CStr(vValue)) Then
On Error Resume Next
oDuplicatesCollection.Add 42, CStr(vValue)
On Error Goto 0
Else
oUniqueCollection.Add 42, CStr(vValue)
End If
lCountAll = lCountAll + 1
Next
countDuplicates = lCountAll - oUniqueCollection.Count + IIF(inclusive, oDuplicatesCollection.Count, 0)
Exit Function
wrong:
'xray vArray
countDuplicates = CVErr(123)
End Function
Function contains(oCollection As Collection, sKey As String)
On Error Goto notContains
oCollection.Item(sKey)
contains = True
Exit Function
notContains:
contains = False
End Function
The function can be called:
=COUNTDUPLICATES(F2:F30001, TRUE())
This should return the same result as your
=SUMPRODUCT((COUNTIF(F2:F30001,$F$2:$F$30001)>1))
The optional second parameter inclusive means the count includes all the values which are present multiple times. For example {A1, A2, A2, A2, A3} contains 3 times A2. Counting inclusive means the count result will be 3. Counting not inclusive means the count result will be 2. There is 2 times A2 as a duplicate.
As you see, the function contains much more information than only the count of the duplicates. The oDuplicatesCollection contains each duplicate item. The oUniqueCollection contains each unique item. So this code could also be used for getting all unique items or all duplicate items.

Visual Basic 2D Array Issue

I currently am having some trouble trying to get my program to work with a 2D array. I had it working earlier with a 1D array but I am totally lost now that I have to make these changes.
Below is what I currently have as my 2D array and the code that I thought would work for spitting out a letter grade but does not give me anything. Would anyone be able to tell me what I'm doing wrong?
Private strGrades(,) As String = {{"900", "A"},
{"815", "B"},
{"750", "C"},
{"700", "D"},
{"0", "F"}}
Dim strGradeSearch As String
Dim intRow As Integer
strGradeSearch = txtGrade.Text
For intRow = 0 To 4
If intRow > strGrades.GetUpperBound(0) Then
strGrades(0, intRow) = strGradeSearch
intRow += 1
End If
Next intRow
If intRow <= strGrades.GetUpperBound(0) Then
lblLetter.Text = strGrades(intRow, 0)
End If
Please take all the following as positive comments :-)
OK. looking at your code, there are tbh several issues. You're trying to treat strings as numbers. While a string can contain what looks like a number, it only contains a string of characters that happen to be numbers. They make sense to use, but to a computer, they aren't. There is often stuff that VB does in the background to try and make life easier, but to be honest, it can be a pain.
When comparing something like grades, you need to compare actual numbers, not strings that contain numbers. You'll potentially get unexpected results. You need to get the computer to convert the string to a number. See below.
Your loop wont actually do anything because the If statement will never execute the code inside it because intRow will never be greater than the last element of the array. Anyhow.. Onwards.
A way to convert strings to numbers is to user the Val function, though this "old" VB. The current way is to use Integer.Parse. Have a look at this link for some basic information about it.
Lets walk through what you want to do.
Get the string in the textbox.
Convert the string to a number.
Loop through the array and for each element, get the number stored as a string and convert it to a number and then compare it to the grade number.
If the grade is greater than any of the values, make a note of the
letter linked to the grade and stop searching through the loop.
Assign the letter that was found to the label.
The following code should do this
Dim strGrades(,) As String = {{"900", "A"},
{"815", "B"},
{"750", "C"},
{"700", "D"},
{"0", "F"}}
Dim intGradeSearch As Integer
Dim strGradeLetter As String = ""
intGradeSearch = Integer.Parse(TxtGrade.Text)
For i As Integer = 0 To 4
If intGradeSearch >= Integer.Parse(strGrades(i, 0)) Then
strGradeLetter = strGrades(i, 1)
Exit For
End If
Next
LblLetter.Text = strGradeLetter
End Sub
You dont need to check intRow after the loop has finished, because in this case, at some point in the loop, a grade letter will always be found if the number in the textbox is greater than or equal to a number in the array.
If you have any questions, please don't hesitate to ask.

MATLAB solve array

I've got multiple arrays that you can't quite fit a curve/equation to, but i do need to solve them for a lot of values. Simplified it looks like this when i plot it, but the real ones have a lot more points:
So say i would like to solve for y=22,how would i do that? As you can see there'd be three solutions to this, but i only need the most left one.
Linear is okay, but i'd rather us a non-linear method.
The only way i found is to fit an equation to a set of points and solve that equation, but an equation can't approximate the array accurately enough.
This implementation uses a first-order interpolation- if you're looking for higher accuracy and it feels appropriate, you can use a similar strategy for another order estimator.
Assuming data is the name of your array containing data with x values in the first column and y values in the second, that the columns are sorted by increasing or decreasing x values, and you wanted to find all data at the value y = 22;
searchPoint = 22; %search for all solutions where y = 22
matchPoints = []; %matrix containing all values of x
for ii = 1:length(data)-1
if (data(ii,2)>searchPoint)&&(data(ii+1,2)<searchPoint)
xMatch = data(ii,1)+(searchPoint-data(ii,2))*(data(ii+1,1)-data(ii,1))/(data(ii+1,2)-data(ii,2)); %Linear interpolation to solve for xMatch
matchPoints = [matchPoints xMatch];
elseif (data(ii,2)<searchPoint)&&(data(ii+1,2)>searchPoint)
xMatch = data(ii,1)+(searchPoint-data(ii,2))*(data(ii+1,1)-data(ii,1))/(data(ii+1,2)-data(ii,2)); %Linear interpolation to solve for xMatch
matchPoints = [matchPoints xMatch];
elseif (data(ii,2)==searchPoint) %check if data(ii,2) is equal
matchPoints = [matchPoints data(ii,1)];
end
end
if(data(end,2)==searchPoint) %Since ii only goes to the rest of the data
matchPoints = [matchPoints data(end,1)];
end
This was written sans-compiler, but the logic was tested in octave (in other words, sorry if there's a slight typo in variable names, but the math should be correct)

Help with a special case of permutations algorithm (not the usual)

I have always been interested in algorithms, sort, crypto, binary trees, data compression, memory operations, etc.
I read Mark Nelson's article about permutations in C++ with the STL function next_perm(), very interesting and useful, after that I wrote one class method to get the next permutation in Delphi, since that is the tool I presently use most. This function works on lexographic order, I got the algo idea from a answer in another topic here on stackoverflow, but now I have a big problem. I'm working with permutations with repeated elements in a vector and there are lot of permutations that I don't need. For example, I have this first permutation for 7 elements in lexographic order:
6667778 (6 = 3 times consecutively, 7 = 3 times consecutively)
For my work I consider valid perm only those with at most 2 elements repeated consecutively, like this:
6676778 (6 = 2 times consecutively, 7 = 2 times consecutively)
In short, I need a function that returns only permutations that have at most N consecutive repetitions, according to the parameter received.
Does anyone know if there is some algorithm that already does this?
Sorry for any mistakes in the text, I still don't speak English very well.
Thank you so much,
Carlos
My approach is a recursive generator that doesn't follow branches that contain illegal sequences.
Here's the python 3 code:
def perm_maxlen(elements, prefix = "", maxlen = 2):
if not elements:
yield prefix + elements
return
used = set()
for i in range(len(elements)):
element = elements[i]
if element in used:
#already searched this path
continue
used.add(element)
suffix = prefix[-maxlen:] + element
if len(suffix) > maxlen and len(set(suffix)) == 1:
#would exceed maximum run length
continue
sub_elements = elements[:i] + elements[i+1:]
for perm in perm_maxlen(sub_elements, prefix + element, maxlen):
yield perm
for perm in perm_maxlen("6667778"):
print(perm)
The implentation is written for readability, not speed, but the algorithm should be much faster than naively filtering all permutations.
print(len(perm_maxlen("a"*100 + "b"*100, "", 1)))
For example, it runs this in milliseconds, where the naive filtering solution would take millenia or something.
So, in the homework-assistance kind of way, I can think of two approaches.
Work out all permutations that contain 3 or more consecutive repetitions (which you can do by treating the three-in-a-row as just one psuedo-digit and feeding it to a normal permutation generation algorithm). Make a lookup table of all of these. Now generate all permutations of your original string, and look them up in lookup table before adding them to the result.
Use a recursive permutation generating algorthm (select each possibility for the first digit in turn, recurse to generate permutations of the remaining digits), but in each recursion pass along the last two digits generated so far. Then in the recursively called function, if the two values passed in are the same, don't allow the first digit to be the same as those.
Why not just make a wrapper around the normal permutation function that skips values that have N consecutive repetitions? something like:
(pseudocode)
funciton custom_perm(int max_rep)
do
p := next_perm()
while count_max_rerps(p) < max_rep
return p
Krusty, I'm already doing that at the end of function, but not solves the problem, because is need to generate all permutations and check them each one.
consecutive := 1;
IsValid := True;
for n := 0 to len - 2 do
begin
if anyVector[n] = anyVector[n + 1] then
consecutive := consecutive + 1
else
consecutive := 1;
if consecutive > MaxConsecutiveRepeats then
begin
IsValid := False;
Break;
end;
end;
Since I do get started with the first in lexographic order, ends up being necessary by this way generate a lot of unnecessary perms.
This is easy to make, but rather hard to make efficient.
If you need to build a single piece of code that only considers valid outputs, and thus doesn't bother walking over the entire combination space, then you're going to have some thinking to do.
On the other hand, if you can live with the code internally producing all combinations, valid or not, then it should be simple.
Make a new enumerator, one which you can call that next_perm method on, and have this internally use the other enumerator, the one that produces every combination.
Then simply make the outer enumerator run in a while loop asking the inner one for more permutations until you find one that is valid, then produce that.
Pseudo-code for this:
generator1:
when called, yield the next combination
generator2:
internally keep a generator1 object
when called, keep asking generator1 for a new combination
check the combination
if valid, then yield it

Resources