Zadanie

Je to opäť tu!!! Sebik sa nevie dočkať. O dva mesiace nastane totiž jeho obľúbená časť roka - finále galakticky známej ligy Hviezdne Bitky (inde známe aj ako Stars Brawl), a on sa nevie dočkať, kedy znova uvidí svojich idolov si to opäť rozdať. Každá hviezda pošle elitný tím zložený iba z tých najlepších z jej planét, a tieto všetky vzrušujúce zápasy sa odohrajú pred očami fanúšikov neďaleko Sebikovej planéty. Má našporenú kopu peňazí, a už sa teší ako bude fandiť naživo - avšak má jeden problém. Sebik je pravý fanúšik Hviezdnych líg, a veľmi rád by si pozrel obrovské množstvo tímov - respektíve, nerád by zmeškal nejaké hry svojich obľúbencov, a lístky sa rýchlo míňajú! Pomôžte mu!

Úloha

V Hviezdnych Bitkách súťaží \(2^r\) tímov očíslovaných od \(0\) do \(2^r-1\). Hrajú vo forme štandardného pavúka s \(r\) kolami: prvé kolo hrá tím \(0\) proti tímu \(1\), tím \(2\) proti tímu \(3\), a v druhom kole sa stretnú výherci týchto dvoch zápasov, a tak ďalej až do finále. Sebik každému tímu fandí v inej miere, teda pre tím \(i\) si určil číslo \(a_i\). Sebik je ochotný vymeškať maximálne \(a_i\) zápasov odohraných tímom \(i\) v celom turnaji. Žiadne dva zápasy nebežia paralelne, teda ak by chcel, vedel by si kúpiť lístky na všetky zápasy, avšak to by bolo príliš drahé. Každý lístok na zápas má totiž svoju cenu \(c_{xj}\). Vašou úlohou je zistiť, ako najlacnejšie vie Sebik kúpiť lístky tak, aby pre nejaký tím nevymeškal viac zápasov ako chcel.

Formát vstupu

V prvom riadku vstupu je číslo \(r\), počet kôl v turnaji.

V druhom riadku je \(2^r\) čísel, pričom \(i\)-té z nich udáva, koľko zápasov je Sebik ochotný vymeškať pre tím \(i\). Tieto čísla sú iba v rozsahu od 0 do \(r\).

Nasleduje \(r\) riadkov. \(x\)-tý1 riadok obsahuje \(2^{r-x}\) čísel, pričom \(j\)-té z nich udáva \(c_{xj}\), teda cenu lístka \(j\)-tého zápasu \(x\)-tom kole. Teda v prvom riadku je \(2^{r-1}\) čísel, v druhom \(2^{r-2}\)

V jednotlivých sadách platia nasledujúce obmedzenia:

Sada 1 2 3 4
\(1 \leq r \leq\) \(18\) \(4\) \(10\) \(18\)
\(1 \leq \max c_{xj} \leq\) \(10^9\) \(10^9\) \(1000\) \(10^9\)

Navyše, v prvej sade platí, že všetky zápasy stoja rovnako veľa peňazí.

Formát výstupu

Vypíš jeden riadok a v ňom jedno celé číslo, a to najmenšiu možnú cenu, ktorú musí Sebik zaplatiť, aby pre žiadny tím nevymeškal viac zápasov ako chcel.

Príklad

Input:

2
1 1 0 1
300 300
300

Output:

600

Keďže nemôžeme zmeškať ani jeden zápas tímu \(2\), tak musíme kúpiť všetky zápasy čo by teoreticky mohli hrať, teda druhé semifinále a finále. Pre ostatné tímy stačí, ak kúpime iba finále, lebo \(1\) zápas zmeškať môžeme.

Input:

3
1 2 3 2 1 0 1 3
100 150 50 90
500 400
800

Output:

1350

Pre tím \(5\) nemôžeme zmeškať jediný zápas, a tak musíme kúpiť všetko, čo by teoreticky mohli hrať, teda zápasy za \(50\), \(400\) a \(800\). To nám vyrieši všetky tímy okrem tímu \(0\), pre ktorý ak by sa dostal do semifinále, zmeškali by sme dva zápasy, a tak musíme kúpiť ešte jeden lístok. Optimálne je kúpiť lístok za \(100\), a teda riešením bude \(100 + 50 + 400 + 800 = 1350\).

Input:

4
1 2 4 1 1 4 0 1 1 1 0 4 3 2 4 0
1 2 4 8 3 4 5 8
4 5 6 7
9 8
15

Output:

73

  1. indexované od jednotky↩︎

Štandardný pavúk je v informatických pojmoch vlastne úplný binárny strom, čo je taký strom, pre ktorý má každý vrchol okrem listov (tých naspodu) práve dvoch synov. V našom prípade má každý zápas, teda vrchol, cenu, a v listoch (tímy) máme uložené, koľko najviac zápasov si vieme dovoliť vymeškať pre tento tím. Hľadáme teda nejakú množinu vrcholov, ktorá by spĺňala, že pre každý tím (list) je na jeho ceste do koreňa (finále) najviac \(a_i\) nekúpených zápasov.

Bruteforce

Najprv sa pozrime na to, čo sa dalo robiť s druhou sadou, kde sme mali zápasov najviac \(16\). Tu sme vedeli vyskúšať každú kombináciu zápasov a skontrolovať, či nám pokryje tímy tak ako chceme, a potom z tých fungujúcich vybrať tú najlacnejšiu. Na to by sme potrebovali vygenerovať každú kombináciu. To vieme spraviť napríklad tak, že sa pre každý vrchol rozhodneme, či do množiny ktorú kupujeme patrí alebo nie, a teda máme dve možnosti pre \(2^r\) zápasov, teda totálne musíme vyskúšať \(2^{2^r}\) možností. Pre každú možnosť potrebujeme však skontrolovať, či naozaj pokrýva tímy podľa zadania. To vieme spraviť DFSkom v čase \(O(2^r)\), kedy si budeme jednoducho počítať, koľko zápasov sme na ceste od finále k tímu kúpili. Celková časová zložitosť je teda \(O(2^{2^r})\), čo je naozaj pomalé, ale pre túto sadu to stačí.

Zaujímavá myšlienka

Pozrime sa teraz na prvú sadu, kde majú všetky zápasy rovnakú cenu. Určite sa nám teda vždy oplatí pre každý tím kupovať tie zápasy, čo by mohli hrať najneskôr, lebo týmto pokryjeme najväčšie množstvo tímov. Teda ak pre nejaký tím potrebujeme mať kúpený aspoň jeden zápas, tak sa nám určite oplatí kúpiť finále, pretože to stojí rovnako ako iné zápasy a ešte nám to pokryje najviac iných tímov. Teda náš výsledný strom bude vyzerať asi tak, že si pre každý tím necháme prvých \(a_i\) zápasov nekúpených, a potom zvyšné až do finále kúpime. Toto nám pekne pokryje všetky tímy, a vieme tento algoritmus pekne spraviť DFSkom v \(O(r2^r)\), čo úplne stačí.

Niečo všeobecnejšie

(Tu už referujem na zápasy ako na vrcholy) Čo by sme však museli spraviť, ak by sa nám zrazu tie ceny lístkov všelijako pomenili? Povedzme, že sme si už vyplnili strom tak ako v prvej sade, a zmenili sa nám ceny. Ako vieme zistiť, čo sa nám už teraz neoplatí? No a tu prichádzame k zaujímavej myšlienke: Riešenie nie je optimálne, ak kupujeme vrchol A, a v jeho podstrome existujú také nekúpené vrcholy s cenou v súčte menšou ako A, že ak by sme nekúpili A a kúpili tieto vrcholy, riešenie je validné. Hľadáme teda, či nevieme nejaký vrchol jednoducho nahradiť nejakými inými z jeho podstromu, ktoré sú v súčte lacnejšie. Napríklad, namiesto finále by sa nám možno mohlo oplatiť kúpiť oboje semifinále. Ako by sme to mohli spraviť?

Vieme pustiť nejaké DFS z našeho vrcholu A. Pre každý vrchol, ktorý navštívime, sa ho opýtame: nájdi mi, ako najlacnejšie by si mohol pokryť tímy vo svojom podstrome ešte jedným zápasom (to je ten, ktorý odstraňujeme vyššie vo vrchole A). Potom keď obaja synovia pošlú túto hodnotu, máme dve možnosti. Buď je optimálne ten zápas v A v tomto podstrome nahradiť týmto vrcholom, alebo nejakými z jeho podstromov. A tak súčet tých hodnôt od synov porovnáme s cenou vrcholu, v ktorom sme (ak už nie je kúpený). Tú lepšiu cenu pošlem svojmu rodičovi. Potom keď prídem do A, už viem, ako najlacnejšie by bolo možné “nahradiť” to, že odstránime tento vrchol A.

Takže odhora si pre každý aktuálne kúpený vrchol skontrolujem, či ho neviem nejako lacnejšie nahradiť vrcholmi v jeho podstrome. Je dôležité, aby som to robil odhora, pretože takto keď už prejdem tie horné vrcholy mám garantované, že sú v optimálnom stave. Musím si však dať pozor na to, aby som nejaký tím nepokryl viac ako potrebujem. To vyriešim napríklad tak, že v každom vrchole si pamätám, o koľko viac zápasov ako aktuálne potrebuje má nad sebou. Toto vieme tiež updatovať DFSkom po každej iterácii. Kadžé spustené DFS nám potrvá \(O(2^r)\), tak zložitosť celého algoritmu bude \(O({2^r}*{2^r})\). Takéto riešenie by nám stačilo na získanie 6tich bodov, za predpokladu, že v prvej sade robíme algoritmus popísaný vyššie. Čo sa však dá robiť na plný počet bodov? No, zamyslime sa (najťažšia čast príkladu).

Ako to zlepšiť?

Už teraz si v DFSku počítame, ako zlepšiť hodnoty pokrytia pre každý vrchol, tak nevieme pustiť jedno DFSko a spočítať to všetko naraz? DFSko musíme samozrejme upraviť, lebo teraz počíta najlacnejšie ceny pre aktuálny stav toho čo kupujeme, a my by sme ich menili počas toho počítania. Vyriešime to tak, že si pre každý vrchol vyrátame, koľko “navyše” by sme chceli v jeho v podstrome tých pokrytí kúpiť. Respektíve, pýtame sa niečo takéto: Odstránil som \(t\) vrcholov nad tebou, ako najlacnejšie vieš pokryť vo svojom podstrome to, čo hore ubudlo? Toto nám hovorí o všetkých úpravách zároveň.

Na túto otázku viem zodpovedať podobne ako pri prvom DFSku, avšak si proste pre každý vrchol pamätám každú hodnotu, na ktorú sa vrchný vrchol môže pýtať. Všimnime si, že týchto hodnôt môže byť len málo (iba \(r\)), takže ani časová zložitosť nebude taká zlá. V tom našom DFSku si potom pre vrchol prejdeme všetky požiadavky na pokrytie čo môže dostať (teda \(r\) hodnôt), a vyplníme ich najlacnejšie pokrytie pomocou najlacnejších pokrytí takých istých požiadaviek na pokrytie od jeho detí, prípadne zarátame aj seba. Triviálne.

A v tomto momente si možno chytáte hlavu, pretože nechápete o čom to točím, alebo s víziou implementačného pekla a debuggovacej bolesti pred očami. Avšak s 25 riadkami implementácie nás príde zachrániť dynamické programovanie.

Optimálne riešenie

Niektorí ste už možno robili dynamické programovanie na poli. Robili ste ho už na stromoch?

Ešte raz prečítame náš návrh na to nové, upravené DFSko. Parafrázujem: pre každý vrchol chceme vedieť pre každú hodnotu \(x\) od \(0\) do \(r\), ako najlacnejšie viem kúpiť zápasy tak, aby nad týmto vrcholom mohlo chýbať o \(x\) vrcholov viac. Všimnime si, ako nešikovné a ťažko spracovateľné je toto zadefinovanie. Doteraz sme ho používali, lebo viedlo od našej pôvodnej myšlienky kontrolovať každý vrchol zvlášť (teda \(x\) sa vtedy rovnalo 1), avšak to už teraz nechceme robiť. Zadefinujme si teda nový cieľ: Pre každý vrchol chceme vedieť, koľko nás najmenej bude stáť, aby sme ešte mohli na ceste do finále vymeškať \(x\) zápasov.

Takéto zadefinovanie je veľmi šikovné, lebo môžeme pekne prirodzene ísť od listov stromu. Pre každý vrchol si chceme vypočítať pole dĺžky \(r+1\). V \(i\) - tom políčku tohoto poľa (indexované od nuly) bude, koľko najmenej by nás stálo, aby sme mohli vo zvyšných zápasoch odtiaľto až do finále vymeškať ešte \(i\) zápasov. Potom postupujeme takto: Ako vypočítať \(i\) - tú hodnotu v tom poli pre vrchol? Napíšem tu taký dlhý vzorček a potom ho vysvetlím. \(DP[vrchol][i] = min(DP[syn1][i] + DP[syn2][i] + cena[vrchol], DP[syn1][i+1] + DP[syn2][i+1])\)

Poďme si to rozobrať. Na to, aby sme vedeli po tomto vrchole vymeškať \(i\) ďalších, môžeme buď zaplatiť: - Zobrať najlacnejší spôsob ako môžem vymeškať ešte \(i\) zápasov od mojich synov, a kúpiť samého seba. (\(DP[syn1][i] + DP[syn2][i] + cena[vrchol]\)) - Zobrať najlacnejší spôsob ako môžem vymeškať ešte \(i+1\) zápasov od mojich synov, a vymeškať samého seba (\(DP[syn1][i+1] + DP[syn2][i+1]\))

Vždy si vezmem lepšie riešenie z týchto dvoch, a to bude tým najlepším riešením. Takto viem ísť pekne od listov po kolách hore, alebo aj DFSkom. To je však náročnejšie. No a aká by bola časová zložitosť? V každom vrchole musíme zbehnúť ten vzorček napísaný vyššie pre každé \(i\). Teda to bude jednoducho \(O(r*{2^r})\), a toto nám teda zbehne vo všetkých sadách.

Diskusia

Tu môžte voľne diskutovať o riešení, deliť sa o svoje kusy kódu a podobne.

Pre pridávanie komentárov sa musíš prihlásiť.