Sieć światłowodowa
Tym razem omówimy zadanie Fiber-optic Network, które pojawiło się na zeszłorocznych zawodach ACM-ICPC Asia Mudanjiang Regional Contest.

Dla liczby w pozostałych trzech wierzchołkach możemy wybrać dowolnie, co daje 6 przyporządkowań
Dla
mamy jedno przyporządkowanie
a dla
dwa
Odpowiedzi dla kolejnych wierzchołków to 22, 14, 63 i 44.
W zadaniu tym mamy dane drzewo o wierzchołkach, a dla każdego wierzchołka
zadane liczby naturalne
(ograniczenia to
). Dobrym przyporządkowaniem będziemy nazywać przyporządkowanie każdemu wierzchołkowi
liczby całkowitej z odpowiadającego mu przedziału
w taki sposób, aby liczby przyporządkowane wierzchołkom połączonym krawędzią były względnie pierwsze. Dla przejrzystości oznaczmy, że dla danego przyporządkowania
liczba przypisana wierzchołkowi
to
W zadaniu proszą nas, aby dla każdego wierzchołka obliczyć sumę liczb przyporządkowanych mu we wszystkich dobrych przyporządkowaniach. Wyniki należy podać modulo
zatem wszystkie działania będziemy wykonywać modulo ta liczba.
Chwila myślenia i odrobina algorytmicznego rozsądku doprowadzają nas do wniosku, że w zadaniu nie ma na tyle "regularności" (cokolwiek by to znaczyło), aby dało się rozwiązać je inaczej niż poprzez wyznaczenie dla każdego wierzchołka oraz każdej możliwej wartości
dla ilu dobrych przyporządkowań zachodzi
To właśnie będzie naszym celem.
Jak można się spodziewać, zapewne sporo nam pomoże, jeżeli najpierw ukorzenimy nasze drzewo (w dowolnym wierzchołku). Rozwiązanie będzie bazowało na programowaniu dynamicznym. Oznaczymy przez poddrzewo ukorzenione w wierzchołku
a przez
liczbę dobrych przyporządkowań, dla których zachodzi
gdy ograniczymy się jedynie do wierzchołków z poddrzewa
Niewątpliwie dla
będących liśćmi zachodzi
gdy
i
dla pozostałych wartości
Załóżmy teraz, że chcemy wyznaczyć odpowiedź dla wierzchołka
który ma
synów
i mamy już wyznaczone tablice
Liczba dobrych przyporządkowań dla poddrzewa
spełniających
to iloczyn liczb dobrych przyporządkowań dla poddrzew
dla których
Zatem jeśli wprowadzimy oznaczenie
![]() |
(zauważmy, że tak zapisana suma jest dobrze określona, gdyż tylko skończona liczba składników jest niezerowa), to dostaniemy wzór
![]() |
(1) |
Moglibyśmy po prostu przeiterować po wszystkich możliwych wartościach przyporządkowanych każdemu wierzchołkowi
ale wtedy otrzymalibyśmy rozwiązanie co najmniej kwadratowe w zależności od
co byłoby zdecydowanie zbyt wolne. Dlatego stajemy przed kluczowym problemem szybkiego wyznaczenia
dla ustalonego syna
Skoro mowa o względnej pierwszości, to z pewnością warto poznać zbiór (różnych) dzielników pierwszych liczby
niech będą to liczby
Liczby względnie pierwsze z
to wszystkie liczby poza tymi, które są podzielne przez którąkolwiek z liczb
Dla
mamy
co umożliwia szybkie obliczenie żądanej wartości, o ile znalibyśmy wcześniej
Życie nie jest jednak takie proste i w ogólnym przypadku nie możemy po prostu odjąć
dla
gdyż wiele ze składników postaci
odjęlibyśmy więcej niż raz (konkretnie tyle razy, ile
ma dzielników wśród liczb
).
Jeśli wprowadzimy oznaczenia oraz
na zbiór liczb podzielnych przez liczbę
to dostaniemy zależność
![]() |
(2) |
Na ratunek w obliczeniu (2) przychodzi nam zasada włączeń i wyłączeń, a właściwie jej minimalne uogólnienie:
![]() |
Pomimo przerażającego wyglądu wzór ten jest całkiem prosty, ale jeżeli ktoś nie spotkał się z nim wcześniej, warto się zatrzymać i pokontemplować go przez chwilę, np. przekonując się o jego poprawności dla (Oczywiście, w ogólnej wersji wzoru nie ma znaczenia, czym są konkretnie zbiory
i pozostaje on prawdziwy dla dowolnych zbiorów.)
Zatem kolejną rzeczą, którą musimy zbadać, są zbiory postaci Wszystkie liczby
są pierwsze, więc jeżeli pewna liczba ma być podzielna przez wszystkie z nich, to musi być podzielna przez ich iloczyn (stwierdzenie odwrotne oczywiście też jest prawdziwe), co prowadzi nas do zależności

Dzięki niej wzór (2) możemy wyrazić następująco:
![]() |
(3) |
Widać, że wielokrotnie spotykamy się tutaj z podobnymi sumami składników zatem wprowadzając oznaczenie
możemy przepisać wzór (3) w trochę milszej postaci:
![]() |
(4) |
Zauważmy, że jeżeli mamy tablicę to jesteśmy w stanie relatywnie szybko wyznaczyć tablicę
A konkretnie, aby obliczyć
potrzebujemy wysumować
składników. Zatem wykonanie tego dla
będzie miało złożoność czasową

Zastanówmy się teraz, jaką złożoność ma wyznaczanie za pomocą wzoru (4). Zauważmy, że występują w nim jedynie składniki postaci
gdzie
jest pewnym dzielnikiem
Ponadto nie wszystkie dzielniki uwzględniamy (tylko takie niepodzielne przez kwadraty liczb pierwszych), ale ta obserwacja nie jest nam potrzebna, aby uzyskać sensowne oszacowanie. Zatem, aby wyznaczyć wartości
dla
musimy wysumować co najwyżej tyle składników, ile jest łącznie dzielników liczb
ale ich liczba jest również rzędu
Aby szybko się dostać do tych składników, warto na samym początku programu dla każdej liczby od
do
spamiętać jej bezkwadratowe dzielniki.

Podsumowując, mając tablice dla wszystkich synów wierzchołka
możemy w czasie
kolejno wyznaczyć tablice
i
co pozwoli nam skorzystać ze wzoru (1) do wyznaczenia
Zatem w sumarycznym czasie
możemy tą metodą wyznaczyć odpowiedź dla korzenia drzewa. Uruchamiając powyższy algorytm dla każdej możliwości wyboru korzenia, możemy rozwiązać zadanie w czasie
Jednak autorów zadania taka złożoność nie satysfakcjonuje i wymagane jest rozwiązanie
Pomysł pozwalający obliczać nam kompletne informacje dla wszystkich wierzchołków drzewa (a nie tylko te uwzględniające wierzchołki z odpowiadających im poddrzew), polega na spychaniu informacji z korzenia w dół, przesyłając informacje z "naddrzewa". Niech będzie pewnym wierzchołkiem, a
jego ojcem w drzewie. Pamiętamy, że tablica
jest iloczynem odpowiednich wartości tablic
dla sąsiadów w poddrzewie, dlatego jeżeli mamy kompletną tablicę
to aby obliczyć wkład naddrzewa
o korzeniu w wierzchołku
powinniśmy podzielić wartości z tablicy
przez odpowiadające wartości z tablicy
przeliczyć jeszcze raz wartości tablic
i
na podstawie nowych wartości
(tablice te teraz lekko zmieniają swoje znaczenie, gdyż zmieniamy zbiór sąsiadów, którzy są uwzględniani w ich liczeniu), co pozwala nam wyliczyć kompletną tablicę
(tzn. taką, w której uwzględniony jest też ojciec). Aby wyznaczyć kompletne
na podstawie informacji z ojca, potrzebujemy wykonać stałą liczbę przeliczeń tablic, z których każde odbywa się w czasie
co daje algorytm o sumarycznej złożoności