Rzucamy n kostkami o k ścianach. Ile jest możliwych przypadków, że na kostkach suma oczek wyniesie x? Zakładamy, że ściany kostki są ponumerowane od 1 do k. Rozwiąż zadanie stosując jak najbardziej optymalną metodę wyznaczenia rozwiązania.
Przypuśćmy, że mamy n = 3 kostki sześcienne k = 6 (tj. po sześć ścian) i chcemy osiagnąć sumę x = 5. Można to zrobić na następujące sposoby:
Rzut 1 | Rzut 2 | Rzut 3 |
---|---|---|
3 | 1 | 1 |
2 | 2 | 1 |
2 | 1 | 2 |
1 | 3 | 1 |
1 | 2 | 2 |
1 | 1 | 3 |
Jak można zauważyć rzucając trzema sześciennymi kostkami sumę x = 5 można osiągnąć na 6 różnych sposobów.
Szukanie wszystkich możliwych rozwiązań ze sprawdzaniem na sam koniec czy suma oczek jest odpowiednia rośnie potęgowo kn. Jak nietrudno się domyśleć nawet dla małych wartości n i k wartość ta zaczyna bardzo szybko rosnąć. Większość przypadków jest jednak identyczna, więc nie ma potrzeby wyznaczania tych samych wartości w kółko.
W przykładzie wyszło, że jest 6 różnych rozwiązań. Jednak jesli dobrze się przyjrzeć w rzeczywistości unikalnych zestawów oczek na kostkach są tylko dwa. Są to np. {2, 1, 2} oraz {1, 1, 3}. Generując wszystkie możliwe przestawienia elementów otrzymamy wszystkie 6 rozwiązań. W zadaniu nie interesuje nas jak wyglądają rozwiązanie, a tylko ile ich jest. Jak jednak najprościej wyznaczyć wszystkie takie grupy?
Najprostszą metodą jest umieszczenie w kodzie warunku, że każda kolejna liczba oczek jest taka sama lub mniejsza. To gwarantuje, że zostanie znalezione rozwiązanie {3, 1, 1}, ale nie {1, 3, 1} czy {1, 1, 3}. To znacząco ogranicza liczbę potrzebnych obliczeń. Ponadto warto pamiętać, że podczas losowanie każdej kolejnej kostki można sprawdzić czy jest możliwe osiągnięcie żądanej sumy. Jeśli nie to obliczenia można przerwać i przejść do następnego przypadku.
W rozwiązaniu siłowym potrzebaby obliczyć sumę, aż 63 = 216 rozwiązań. Spróbujmy jednak ten problem ugryźć mając na uwadze podane wcześniej założenia.
Rzut 1 | Rzut 2 | Rzut 3 | Zostało |
---|---|---|---|
3 | - | - | 2, ok |
3 | 3 | - | -1, przerywamy |
3 | 2 | - | 0, ale nie w trzech rzutach, przerywamy |
3 | 1 | - | ok |
3 | 1 | 1 | 0, ok, doliczamy |
2 | 2 | 1 | 0, ok, doliczamy |
2 | 1 | 1 | 1, za mało, przerywamy |
1 | - | - | niemożliwe, bo maksymalna suma pozostała 2 |
W zależności od implementacji rzczywista liczba wywołań może się różnić, ale jak można zauważyć sprawdzanych rozwiązań jest znacząco mniej, bo tylko 3 (chodzi o sprawdzenia tylko dla 3 wykonanych rzutów) zamiast 216.
Jak wprowadzanie do pełnego rozwiązania rozpatrzmy prostszą funkcję, która policzy ile jest unikalnych rozwiązań. Funkcja liczRozwiazaniaUnikalne() optymalizuje proces wybierania kolejnych wyrzuconych oczek. Funkcja działa rekurencyjnie.
(2.) Jeśli została tylko jedna kostka do wyrzucenia to (3.) zwróć jeden jeśli pozostała suma x jest możliwa do wyrzucenia, a w przeciwnym 0. Dalsze obliczenia (4.) ograniczamy poprzez: sprawdzenie czy x jest nieosiągalny, bo jest ujemny, albo nie jest możliwe osiagnięcie sumy kontynuując rzuty wartości 1 lub wartości maksymalnych. W przypadku, gdy cel może zostać osiągnięty to: (6. - 8.) należy sprawdzić możliwe dalsze rzuty oczek i (9.) zwrócić ile zostało znalezionych rozwiązań. Warto tutaj zwrócić uwagę na to, że w każdym kolejnym wywołaniu zmniejszamy liczbę kostek n o 1, liczbę ścian ustawiamy na wartość ostatnio wyrzuconych oczek tj. i, a sumę x zmniejszamy o i oczek.
Funkcja liczRozwiazania() będzie teraz liczyć ile jest możliwych rozwiązań licząc przekształcenia znalezionych unikalnych rozwiązań. Wykorzystane zostaną tutaj wzory z kombinatoryki. Jednak, aby móc z tego skorzystać trzeba pamiętać ile zostało wyrzuconych jakich oczek. Ze względu na to, że może być wyrzucone k różnych wartości to można zadeklarować tablicę liczników dla każdej z wartości.
Ze względu na to, że potrzebna jest tablica liczników to główna funkcja ją deklaruje, zarządza nią i rozpoczyna wywołanie właściwej części funkcji liczRozwiazania(). W tej części została wykorzystana funkcja silnia() wyjaśniona tutaj.
(2.) Podobnie jak w poprzednim przypadku jeśli mamy ostatni rzut to sprawdzamy czy istnieje możliwe rozwiązanie. Jeśli tak to (3. - 7.) liczymy ile jest permutacji bez powtórzeń wylosowanych oczek i (8.) zwracamy obliczoną wartość. Jeśli jednak warunek nie jest spełniony to (9.) sprawdzamy czy dalsze obliczenia mają sens. Jeśli tak to (10. - 14.) wywołujemy kolejne rzuty kostek, ale pamiętając, że należy odpowiednio zwiększać liczniki. Na koniec wystarczy zwrócić wartość zmiennej ile.
W celu wczytania od użytkownika danych można posłużyć się poniższym fragmentem kodu: