Omówienie zadania Liczniki (II etap XXXI OI)

Uproszczenie

Dla ułatwienia najpierw będziemy rozważać sytuację, gdy mamy tylko m=1 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, ci oznaczało koszt bajtometra sześciennego wody na i-tym liczniku, a si początkową ilość zarejestrowanej wody. Oznaczmy przez a 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 a. 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 a rosnąco/malejąco lub s rosnąco/malejąco lub c 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 ci przydzielić jak najmniejsze wartości liczników w kolejnym miesiącu. Zgodnie z tą intuicją możemy chcieć posortować liczniki po wartości c.

Wspomnimy tutaj, że sortując po c 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 c. Przyjmiemy od tego momentu, że przenumerowujemy liczniki tak, aby spełniały c1c2cn. 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 bi. Weźmy i-ty licznik. Mamy teraz kluczową własność, że dla każdego j<i, jeżeli sjsi oraz próbujemy skonstruować najtańsze rozwiązanie, to możemy założyć, że bjbi. Możemy to udowodnić przez sprzeczność:

  • W początkowym przypisaniu mieliśmy sibi i sjbj. Załóżmy, że sjsi oraz bi<bj. Wtedy si<bj (jako że sibi<bj) i sjbi (gdyż sjsibi), więc możemy zamienić bi oraz bj ze sobą. Wynikiem z tych dwóch liczników w pierwszym przypadku było (bisi)ci+(bjsj)cj, a po zamianie będzie (bjsi)ci+(bisj)cj. Z posiadanych założeń (koniecznym jest cjci) wynika, że (bjsi)ci+(bisj)cj(bisi)ci+(bjsj)cj. Faktycznie, możemy najpierw wszystko wymnożyć:
    bjcisici+bicjsjcjbicisici+bjcjsjcj,
    następnie usunąć te same składniki:
    bjci+bicjbici+bjcj,
    poprzenosić składniki:
    bicjbjcjbicibjci,
    i wyciągnąć przed nawias:
    (bibj)cj(bibj)ci. 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 ci oraz przydzielać im najmniejsze bi spełniające sibi. 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 std::multiset z STL oraz operacji lower_bound na niej.

Dowolne m

Rozwiązanie dla dowolnego m jest uogólnieniem rozwiązania dla m=1. Poprawnie jest zastosować algorytm dla m=1 po prostu m 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 bi,j, gdzie i to numer miesiąca, a j to numer licznika. Teraz bierzemy dowolne dwie pozycje i<j i szukamy miesiąca k takiego, że max(bk1,i,bk1,j)min(bk,i,bk,j). Możemy ustawić dowolnie odczyty na tych licznikach w k-tym miesiącu, więc ustawimy je tak, aby spełnione było bk,ibk,j. W kolejnych miesiącach możemy dalej zachować taką nierówność, więc finalnie otrzymamy bm,ibm,j. Jeśli takie k 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.