Losowe wyszukiwanie rozwiązań (VBA)
Transkrypt
Losowe wyszukiwanie rozwiązań (VBA)
Losowe wyszukiwanie rozwiązań (VBA) Dariusz Jabłoński (hurgadion) [email protected] W pierwszym moim artykule dotyczącym losowego wyszukiwania rozwiązań skoncentrowałem się na pokazywaniu problemów dotyczących zagadnień typowo matematycznych. W niniejszym artykule prezentuję przykłady poszukiwania rozwiązań dla problemów częściej spotykanych w działalności gospodarczej. Zaprezentowano ponadto, inaczej niż poprzednio, losową metodę wyszukiwania rozwiązań z wykorzystaniem języka programowania VBA a nie tylko formuł Excela. Podejście to, może być wykorzystywane czasem jako alternatywa dla Solvera, zdarza się przy tym, że daje ono również rozwiązania inne niż Solver. Zadanie 1.1 Załóżmy, że mamy kilkanaście magazynów umieszczonych w całym kraju, powiedzmy: 15. W każdym z tych magazynów jest jakiś towar, np w magazynie nr 1. jest towar a, b, c ,d. W magazynie 2 jest towar b, c, x, y, z... W kolejnych magazynach podobnie. W żadnym magazynie nie ma całego "alfabetu" towarów. Jak obliczyć optymalną (minimalną) liczbę magazynów, które zawierają cały alfabet towarów? Rozwiązanie: W załączniku (Arkusz magazyny) w tabeli mieszczącej się w zakresie C2:S30 można wpisać w których magazynach występują poszczególne artykuły. Jest to tablica pomocnicza, na podstawie której są wykonywane obliczenia. W komórce S4 jest wpisana formuła tablicowa: =SUMA(D4:R4*$D$31:$R$31) która zlicza w ilu magazynach wytypowanych występuje dany artykuł z alfabetu. W zakresie D31:R31 po uruchomieniu Solvera będą występować zera i jedynki. Jedynka oznacza, że dany magazyn jest wytypowany. W Solverze ponadto jest warunek dla zakresu D4:R4 gwarantujący, że dla każdego artykułu z alfabetu zostanie wytypowany co najmniej jeden magazyn po optymalizacji. Po odpaleniu Solvera należy uruchomić poniższe makro, aby wybrać unikalnie magazyn, z którego będziemy sprowadzać dany artykuł. 1 http://www.excelforum.pl/topics1/optymalizacja-ilosci-dostawcow-vt23792.htm#127612 Sub Art() Dim tbl1(1 To 26) Dim x&, i&, j&, s As String x = 1 For i = 1 To 15 s = "" If Cells(31, i + 5).Value = 1 Then For j = 1 To 26 If Cells(j + 3, i + 5).Value = 1 Then If tbl1(j) = 0 Then If s = "" Then s = Cells(j + 3, 5).Value Else s = s & ", " & Cells(j + 3, 5).Value End If tbl1(j) = 1 End If End If Next j End If Cells(i + 5, 4).Value = s Next i End Sub Makro wybiera pierwszy z kolei magazyn, z którego da się sprowadzić dany artykuł. Po kolei dla każdego magazynu następuje sprawdzenie czy dany artykuł został już wybrany, jeżeli tak, to przechodzimy dalej, jeżeli nie, to artykuł jest przypisywany do magazynu. Problem postawiony w tym zadaniu da się rozwiązać bez wykorzystania Solvera (dzięki temu da się także uzyskać rozwiązanie nieco innego od Solvera, o ile rozwiązanie nie jest jedyne). Sub Permutacja() Dim i&, j&, l&, x&, x1&, a& ReDim V(1 To 15) ReDim VK(1 To 15) ReDim VZ(1 To 26) ReDim V1(1 To 26, 1 To 15) ReDim VPom(1 To 26, 1 To 15) Range("F31:T31").ClearContents V1 = Range("F4:T29").Value x1 = 15 For l = 1 To 10000 V = RndInt() x = 0 For i = 1 To 15 a = V(i) For j = 1 To 26 VPom(j, i) = V1(j, a) If i = 1 Then VZ(j) = 0 VZ(j) = VZ(j) + VPom(j, i) Next j x = x + 1 If Application.Product(VZ) > 0 Then Exit For Next i If x < x1 Then VK = V x1 = x End If Next l For i = 1 To x1 Cells(31, VK(i) + 5).Value = 1 Next i End Sub Makro działa w następujący sposób: 1) Kluczowym elementem makra jest pętla iterowana przez i (ilość magazynów) oraz iterowana przez j (ilość artykułów), 2) W każdej iteracji pętli iterowanej przez l (każda pętla odpowiada innemu losowaniu) zostaje wylosowana jednowymiarowa tablica V o unikalnych elementach 1-15 (umożliwia to pomocnicza funkcja RndInt), 3) Do tablicy V1 zostają wpisane komórki zakresu F4:T29 (ten zakres jest kluczowy, odpowiada on informacji o rozmieszczeniu artykułów w magazynach) 4) Następnie zostają przepermutowane kolumny tablicy V1 w zależności od kolejności indukowanej przez elementy tablicy V. 5) W kluczowej pętli iterowanej przez i, j następuje wybór x magazynów, które posiadają wszystkie artykuły (ten warunek sprawdza funkcja Application.Product) 6) Główna pętla generowana przez l umożliwia znalezienie (a raczej zoptymalizowanie) liczby magazynów zawierającej wszystkie artykuły. 7) Dla znalezionej optymalnej liczby magazynów zostaje przypisana losowa tablica V. Funkcja pomocnicza losująca zestaw liczb od 1-15 (na podstawie pozycji [1]) Function RndInt() Dim V() As Variant, Val As Variant Dim i&, j&, r&, c&, a& Dim t1 As Variant, t2 As Variant Randomize a = 15 ReDim V(1 To a) ReDim Val(1 To 2, 1 To a) For i = 1 To a Val(1, i) = Rnd Val(2, i) = i Next i For i = 1 To a For j = i + 1 To a If Val(1, i) > Val(1, j) Then t1 = Val(1, j) t2 = Val(2, j) Val(1, j) = Val(1, i) Val(2, j) = Val(2, i) Val(1, i) = t1 Val(2, i) = t2 End If Next j Next i i = 0 For r = 1 To a i = i + 1 V(i) = Val(2, i) Next r RndInt = V End Function ▪ Zadanie 2. Mamy zbiór danych liczbowych (od dwóch do dwudziestu dodatnich składników). Znaleźć możliwe kombinacje sum pomiędzy dwoma z góry ustalonymi wartościami. Rozwiązanie: Unikalne (dzięki kolekcji) kombinacje wyławia losowo następujące makro (Arkusz losowanki): Sub Dim Dim Dim Dim Losowanko() tabl(), tablprop(), tabl1() NoDupes As New Collection x&, i&, j&, s As Double, m&, a As String, p&, w& Item As Variant p = Application.CountA(Range("A1:T1")) If p < 2 Then Exit Sub ReDim tabl(1 To p) ReDim tablprop(1 To p) ReDim tabl1(1 To p) tabl = Cells(1, 1).Resize(, p).Value Application.ScreenUpdating = False w = Application.Max(2, Cells(Rows.Count, "A").End(xlUp).Row) Range("A2:T" & w).ClearContents Randomize x = 1 On Error Resume Next For i = 1 To 100 * Sqr(2 ^ (p + 3)) For j = 1 To p tablprop(j) = Round(Rnd, 0) Next j s = Application.SumProduct(tabl, tablprop) If s <= Cells(3, "V").Value And s >= Cells(2, "V").Value Then m = Cells(Rows.Count, "A").End(xlUp).Row + 1 m = Application.Max(m, 3) Cells(m, 1).Resize(, p) = tablprop For j = 1 To p Cells(m, j).Value = Cells(m, j).Value * Cells(1, j).Value Next j Cells(m, 1).Value = Lacz_Teksty(Cells(m, 1).Resize(, p)) NoDupes.Add Cells(m, 1).Value, CStr(Cells(m, 1).Value) End If Next i On Error GoTo 0 x = 3 w = Application.Max(2, Cells(Rows.Count, "A").End(xlUp).Row) Range("A3:T" & w).ClearContents For Each Item In NoDupes Cells(x, 1).Value = Item x = x + 1 Next Item x = Cells(Rows.Count, "A").End(xlUp).Row For i = 3 To x a = Cells(i, 1).Value For j = 1 To p Cells(i, j).Value = Extra(a, j) Cells(i, j).Value = Cells(i, j).Value * 1 Next j Next i Application.ScreenUpdating = True End Sub Function Extra(txt, n) Dim All As Variant All = Split(txt, "_") Extra = All(n - 1) End Function Function Lacz_Teksty(x As Range) Dim s As String, kom As Range For Each kom In x If Len(s) = 0 Then s = kom.Value Else If Len(kom.Value) > 0 Then s = s & "_" & kom.Value End If Next kom Lacz_Teksty = s End Function Makro działa w następujący sposób: 1) Liczba p zlicza ile jest wypełnionych po kolei komórek w wierszu pierwszym (dla tylu komórek są wyszukiwane rozwiązania), 2) W komórkach V2, V3 są wpisane graniczne wartości, dla których będzie przeprowadzane wyszukiwanie rozwiązań, 3) Następnie do tablicy tabl są wpisane wartości niepustych komórek z wiersza pierwszego, 4) W głównej pętli iterowanej przez i następuje losowe wyszukiwanie odpowiednich rozwiązań, w każdym kroku zostaje utworzona losowa, zerojedynkowa tablica tablprop, 5) Następnie jest obliczona SUMA.ILOCZYNÓW (w wersji VBA) tablic tabl, tablprop 6) W kolejnym kroku jest sprawdzana czy obliczona SUMA.ILOCZYNÓW jest pomiędzy granicznymi wartościami z punktu 2), jeżeli warunek jest sprawdzony następuje dopisanie danego losowego rozwiązania, o ile wcześniej nie zostało już wpisane. Umożliwia to fragment kodu wyławiający unikaty za pomocą obiektu kolekcji, 7) Unikalne rozwiązania są po kolei wypisywane w odpowiednich, kolejnych wierszach W makrze zostały użyte standardowe, pomocnicze funkcje (Extra: na podstawie [1], Lacz_Teksty) umożliwiające operacje na łańcuchach tekstowych. Być może da się ich uniknąć i rozwiązać problem nieco łatwiej, np. za pomocą tablic (Homework ) ▪ Zadanie 3 Ogrodnikowi polecono zaprojektować ogród z drzewami i krzewami, przy czym właściciel ogrodu przeznaczył na zakup 1500zł i ponadto życzył sobie, aby: a) cisów było co najwyżej 6 sztuk, b) jałowców tyle samo co tuj i co najmniej po 20 sztuk c) jodły 2 razy więcej niż sosny d) kosodrzewiny więcej niż 6 sztuk, ale mniej niż 12 e) świerków srebrnych 4 sztuki, a świerków białych 6 sztuk f) każdego drzewa i krzewu musi być co najmniej po 2 sztuki Ile drzew i krzewów może kupić ogrodnik za tą sumę, spełniając jednocześnie wymagania właściciela ogrodu jeśli ceny drzew i krzewów są takie, jak podano wyżej w tabeli? TOWAR CENA cis 18,00 zł jałowiec 11,00 zł jodła 18,00 zł sosna 10,00 zł kosodrzewina 18,00 zł świerk biały 13,00 zł świerk srebrny 40,00 zł tuja 25,00 zł Rozwiązanie: Problem można rozwiązać w sposób standardowy wykorzystując Solvera, otrzymamy wtedy rozwiązanie jak na załączonej tabeli. CENA 18,00 zł 11,00 zł 18,00 zł 10,00 zł 18,00 zł 13,00 zł 40,00 zł 25,00 zł LICZBA 6 24 4 2 11 6 4 24 Można jednak spróbować znaleźć rozwiązanie losowo wyszukując optymalne wartości za pomocą poniższego makra, które wyszukuje także rozwiązania inne niż Solver (Arkusz solveration). Sub Solveration() Dim table(), tablelos() Dim i& ReDim table(1 To 8) ReDim tablelos(1 To 8) Range("C2:C9").ClearContents table = Application.Transpose(Range("B2:B9")) Randomize For i = 1 To 1000000 tablelos(1) tablelos(2) tablelos(4) tablelos(3) tablelos(5) tablelos(6) tablelos(7) tablelos(8) = = = = = = = = Int(2 + 4 * Rnd, 0) Int(20 + 50 * Rnd, 0) Int(2 + 50 * Rnd(), 0) 2 * tablelos(4) Int(7 + 5 * Rnd, 0) 6 4 tablelos(2) If Application.SumProduct(table, tablelos) = 1500 Then Range("C2:C9").Value = Application.Transpose(tablelos) Exit Sub End If Next i MsgBox “Nie udało się znaleźć właściwego rozwiązania” End Sub Makro działa w następujący sposób: 1) Główną pętlą jest pętla iterowana przez i. Makro odnajduje rozwiązanie, o ile istnieje (można makro przerobić, aby wyszukiwało rozwiązanie optymalne zamiast dokładnego: Homework ). W tym przypadku rozwiązanie nie jest jednoznaczne, istnieje parę różnych rozwiązań problemu. Główna pętla jest ustawiona na milion kroków, jednak po znalezieniu optymalnego rozwiązania zostaje przerwane działanie głównej pętli, 2) Każdy krok głównej pętli polega na wylosowaniu optymalnego rozwiązania (tablica tablelos, poszczególne elementy tej tablicy odpowiadają ilości danego gatunku drzewa), które jest dobierane w taki sposób, aby spełniało warunki postawione w zadaniu (jest to możliwe dzięki funkcji zaokrąglającej Int oraz podstawowym działaniom matematycznym). Do tablicy table są przypisane ceny poszczególnych drzew. Następnie potencjalne rozwiązanie zostaje zweryfikowane (zostaje obliczona SUMA.ILOCZYNÓW tablic table oraz tablelos i porównana z oczekiwaną wartością). Po pozytywnej weryfikacji potencjalnego rozwiązania następuje przypisanie tablicy tablelos do właściwego zakresu. ▪ Ważnym zagadnieniem optymalizacyjnym jest tzw. problem plecakowy, poniżej prezentujemy rozwiązanie oparte o losową metodę wyszukiwania rozwiązań. Zadanie 4. Mamy m plecaków oraz n przedmiotów o wadze w i wartości c oraz ograniczenia pojemności plecaków a_i, i = 1, …, m. Należy upakować te przedmioty do tych plecaków tak, aby suma wartości przedmiotów w plecakach była jak największa. Rozwiązanie: Problem można rozwiązać za pomocą poniższego makro (Arkusz plecak): Sub Plecaki() Dim tabil() As Long, tabcost() As Long, tablos() As Long, tab1() As Long, tabplecak() As Long, tab2() As Long Dim c1&, a&, b&, c&, d&, x&, i&, j&, k&, m&, n& m = Application.CountA(Range("1:1")) n = Application.CountA(Range("4:4")) Application.ScreenUpdating = False ReDim tabplecak(1 To m) For i = 1 To m tabplecak(i) = Cells(1, i).Value Next i ReDim ReDim ReDim ReDim tabil(1 To n) tabcost(1 To n) tablos(1 To n) tab1(1 To n) For i = 1 To n tabil(i) = Cells(4, i).Value tabcost(i) = Cells(5, i).Value d = d + tabcost(i) Next i Randomize For i = 1 To Application.Min(1000000, 50 * m ^ UBound(tablos)) ReDim tab2(1 To m) c = 0 For j = 1 To n tablos(j) = Round(-0.5 + (m + 1) * Rnd, 0) For k = 1 To m If tablos(j) = k Then tab2(k) = tab2(k) + tabil(j) c = c + tabcost(j) End If Next k Next j x = 0 For j = 1 To m If tab2(j) <= tabplecak(j) Then x = x + 1 Next j If c > c1 And x = m Then c1 = c tab1 = tablos End If If c1 = d Then Exit For Next i Cells(6, 1).Resize(, n) = tab1 Range("C7").Value = c1 Application.ScreenUpdating = True End Sub Podobnie jak w poprzednich wypadkach rozwiązanie polega na losowej optymalizacji rozwiązania. W rozwiązaniu są wykorzystane tablice na których są wykonywane odpowiednie działania. Osoby zainteresowane dokładniejszym opisem proszę o kontakt. ▪ Na koniec rozwiążemy (bardziej jako ciekawostkę) w nieco nietypowy sposób klasyczny problem dotyczący znajdowania unikatów, szerzej o tym problemie można poczytać w jednym z wątków FAQ www.excelforum.pl2. Rozwiązanie wykorzystuje funkcję Join (Arkusz unikaty). Zadanie 5. Znaleźć w określonym jednokolumnowym zbiorze danych unikaty. Rozwiązanie: Losowe Unikaty kolejno wyławia z zakresu z kolumny A makro (Arkusz unikaty): Sub LosoweUnikaty() Dim tbl(), x As Range Dim a&, i&, s&, m As String a = WorksheetFunction.CountA(Range("A:A")) Set x = Range("A2:A" & a) s = 1 For i = 1 To 10 * x.Count If i = 1 Then ReDim tbl(1 To s) tbl(i) = Application.Index(x, Round(0.5 + x.Count * Rnd, 0)) Else m = Application.Index(x, Round(0.5 + x.Count * Rnd, 0)) 2 http://www.excelforum.pl/topics7/41-unikaty-vt22640.htm If Join(tbl) Like "*" & m & "*" Then GoTo dalej: Else s = s + 1 ReDim Preserve tbl(1 To s) tbl(s) = m End If End If dalej: Next i Range("B2:B" & a).ClearContents Cells(2, 2).Resize(UBound(tbl)) = Application.Transpose(tbl) End Sub Rozwiązanie polega na losowym wyławianiu Unikatów z ustalonego zakresu w kolumnie A. 1) Za pomocą funkcji Rnd oraz Arkuszowej funkcji Indeks następuje wylosowanie pewnego elementu z zakresu kolumny A. 2) Następnie zostaje sprawdzone za pomocą funkcji Join czy wylosowany element został wylosowany już wcześniej. Jeżeli wcześniej nie został wylosowany, to element zostaje dołączony do tablicy tbl przechowującej wylosowane kolejno unikaty. 3) Na koniec unikaty są wpisane do odpowiedniego zakresu kolumny B. 4) Może się zdarzyć, że nie wszystkie unikaty są wytypowane przez makro, ale jest to bardzo mało prawdopodobne. 5) Aby się zabezpieczyć przed wylosowaniem niewłaściwej liczby unikatów można wyliczyć tą liczbę unikatów i wykonywać makro do uzyskania odpowiedniej ilości elementów. W poprawie niniejszego artykułu pomógł mi Artik: www.excelforum.pl. Bibliografia: [1] Walkenbach J., „Excel 2010 PL Programowanie w VBA”, Helion, Gliwice, 2011 [2] Jabłoński D., „Losowe wyszukiwanie rozwiązań”, www.excelperfect.pl, 2011 Załącznik: losowe.wyszukiwanie.VBA.xls ▪