Uproszczenie
Dla ułatwienia najpierw będziemy rozważać sytuację, gdy mamy tylko miesiąc.
Już dla niej optymalne rozwiązanie jest nieoczywiste, dlatego skupiamy się najpierw na tym przypadku, aby dowiedzieć się czegoś więcej o strukturze całego zadania.
Dla przypomnienia, oznaczało koszt bajtometra sześciennego wody na -tym liczniku, a początkową ilość zarejestrowanej wody. Oznaczmy przez ciąg (multizbiór) odczytów po pierwszym miesiącu.
Sortowanie
Możemy sobie wyobrazić taki schemat rozwiązania, w którym w jakiejś kolejności bierzemy liczniki i przydzielamy im jeszcze niewykorzystane wartości z . Warto zadać sobie w takim momencie pytanie: W jakiej kolejności?
Często ustalenie dobrej kolejności danych jest kluczowe do rozwiązania problemu i bardzo często możemy to zrobić, ponieważ jest to czynność po naszej stronie. Gdy szukamy takiej kolejności, trzeba się zawsze zastanowić jakich własności mniej więcej oczekujemy i jakie własności mają kolejności, które rozważamy.
Możemy posortować nasze dane na wiele sposobów, między innymi, według rosnąco/malejąco lub rosnąco/malejąco lub rosnąco/malejąco. Każda z nich ma swoje własności i czasem trzeba przyjrzeć się każdej kolejności, aby podjąć dobrą decyzję.
Intuicja
W przypadku tego zadania, intuicyjne jest takie podejście, że chcemy licznikom z dużym przydzielić jak najmniejsze wartości liczników w kolejnym miesiącu. Zgodnie z tą intuicją możemy chcieć posortować liczniki po wartości .
Wspomnimy tutaj, że sortując po niemalejąco, jesteśmy w stanie dociągnąć rozwiązanie do końca, ale jest to bardziej wymagające. Zachęcamy do zastanowienia się nad tym, ale nie jest to konieczne do zrozumienia rozwiązania wzorcowego.
Zatem, wracając na trop rozwiązania wzorcowego, będziemy sortować nierosnąco po . Przyjmiemy od tego momentu, że przenumerowujemy liczniki tak, aby spełniały . Zbadajmy własności takiego sortowania. Załóżmy, że przydzieliliśmy już wartości wszystkim licznikom w pierwszym miesiącu i oznaczyliśmy te wartości ciągiem . Weźmy -ty licznik. Mamy teraz kluczową własność, że dla każdego , jeżeli oraz próbujemy skonstruować najtańsze rozwiązanie, to możemy założyć, że . Możemy to udowodnić przez sprzeczność:
- W początkowym przypisaniu mieliśmy i . Załóżmy, że oraz . Wtedy (jako że ) i (gdyż ), więc możemy zamienić oraz ze sobą. Wynikiem z tych dwóch liczników w pierwszym przypadku było , a po zamianie będzie . Z posiadanych założeń (koniecznym jest ) wynika, że . Faktycznie, możemy najpierw wszystko wymnożyć:
,
następnie usunąć te same składniki:
,
poprzenosić składniki:
,
i wyciągnąć przed nawias:
. I już.
Rozwiązanie
Na podstawie powyższego argumentu z zamianą możemy wysnuć takie podejście, że będziemy przeglądać liczniki nierosnąco po oraz przydzielać im najmniejsze spełniające . To rozwiązanie jest optymalne, ponieważ każde inne przypisanie możemy sprowadzić takimi zamianami do przypisania, które wyprodukuje ten algorytm (nie pogarszając po drodze wyniku). To jest bardzo silna własność, dzięki której stwierdzamy też, że jeżeli jakiekolwiek przypisanie istnieje, to nasz algorytm je znajdzie.
Do implementacji tego rozwiązania w C++ możemy użyć struktury z STL oraz operacji na niej.
Dowolne
Rozwiązanie dla dowolnego jest uogólnieniem rozwiązania dla . Poprawnie jest zastosować algorytm dla po prostu razy tak, jakby każdy kolejny miesiąc był tym ostatnim miesiącem.
Dowód poprawności jest tak na prawdę uogólnieniem argumentu, który użyliśmy wcześniej. Weźmy jakieś przypisanie liczników, które oznaczymy jako , gdzie to numer miesiąca, a to numer licznika. Teraz bierzemy dowolne dwie pozycje i szukamy miesiąca takiego, że . Możemy ustawić dowolnie odczyty na tych licznikach w -tym miesiącu, więc ustawimy je tak, aby spełnione było . W kolejnych miesiącach możemy dalej zachować taką nierówność, więc finalnie otrzymamy . Jeśli takie nie istniało, to nic nie robimy. Wykonując taką zamianę, nie mogliśmy pogorszyć wyniku. Za pomocą tej zamiany możemy sprowadzić każde przypisanie do tego, które oblicza nasz algorytm, więc nasz algorytm jest optymalny.