Zadanie
Bzučiakov zdanlivo jednoduchý plán sa nakoniec nečakane skomplikoval. Ó nie nie, celú krádež a únik zvládol triviálne! Hoci sa mu podarilo infiltrovať diskrétny banket, ukradnúť identitu a dokonca prejsť cez pasovú kontrolu na Vesmírnom Belize so zabudntým pasom, jeho fatálnou chybou bolo nechať si kúpu zvlhčovača vzduchu na poslednú chvíľu.
Na Vesmírnom Belize sa zvlhčovače vzduchu zháňajú ťažšie než identita samotnej Jednotky. A o jeho preferovanej luxusnej značke môže Bzučiak len snívať. Jediné, čo sa mu podarilo získať, bol zjavne použitý zvlhčovač od pofidérneho predajcu vedľa smetiakov za benzínkou – a to ešte za premrštenú cenu!
S ťažkým srdcom sa Bzučiak vrátil do hotelovej izby, odhodlaný tešiť sa aspoň z toho mála, čo má. Jeho tešba však trvala len krátko. Ešte predtým, ako stihol zvlhčovač zapnúť, si všimol, že s ním niečo nie je v poriadku. Po otvorení zistil, že vnútro prístroja je celé zelené a ochlpatené – vesmírna pleseň!
Bzučiak stál pred dilemou. Zohnať náhradný zvlhčovač by bolo takmer také náročné ako vykonať celú predchádzajúcu lúpež. Na druhej strane, používať plesnivý zvlhčovač by bolo mimoriadne zdraviu škodlivé. Po chvíli váhania sa rozhodol pre kompromis – postaví zvlhčovač na balkón, nechá naň svietiť žeravé slnká trojhviezdneho systému Belize a fúkať slaný morský vzduch a hádam tá pleseň skape. V takýchto nepriaznivých podmienkach predsa nemôže dlho prežiť…
Úloha
Povrch zvlhčovača vzduchu predstavuje (z pohľadu plesňe nekonečnú) mriežku. Na niektorých políčkach mriežky sa na začiatku nachádza pleseň. Zhora na mriežku praží horúce slnko, zľava fúka vysušujúci vietor. Kvôli týmto nepriaznivým podmienkam pleseň postupne vymiera, no predtým, než úplne zanikne, sa pokúsi o posledný akt sebazáchovy.
Kolónia plesne sa správa podľa nasledujúcich pravidiel:
- Ak na plesnivé políčko priamo pôsobí aj slnko aj vietor (t.j. nemá plesnivých susedov ani zhora, ani zľava), život je pre ňu príliš ťažký a pleseň na tomto políčku v nasledujúcej sekunde zahynie.
- Ak na prázdne políčko priamo nepôsobí ani slnko, ani vietor (t.j. má plesnivých susedov zhora aj zľava), vznikajú na ňom ideálne podmienky a v nasledujúcej sekunde sa tam pleseň rozšíri.
- Všetky ostatné políčka zostávajú v nasledujúcej sekunde nezmenené.
- Všetky zmeny v rámci jednej sekundy sa dejú súčasne.
Kolóniu plesne vieme reprezentovať ako množinu obdĺžnikových plôch. Plesnivá oblasť so súradnicami \((x_1, y_1, x_2, y_2)\) obsahuje všetky políčka súradníc \((x, y)\) takých, že \(x_1 \leq x \leq x_2\) a \(y_1 \leq y \leq y_2\). Plesnivé plochy sa môžu prekrývať, avšak v tom prípade je na danom políčku pleseň len raz.
Otázkou teda zostáva, pri počiatočnej konfigurácii plesne, koľko sekúnd potrvá kým všetka pleseň zahynie a Bzučiak bude môcť konečne začať používať svoj zvlhčovač?
Formát vstupu
Na prvom riadku vstupe je číslo \(T\) (\(1 \leq T \leq 100\)) udávajúce počet testovacích prípadov. Nasleduje \(T\) testovacích prípadov, pričom každý z nich je zadaný nasledovne:
Na prvom riadku je číslo \(N\) (\(1 \leq N \leq 50\,000\)) udávajúce počet plesnivých plôch. Nasleduje \(N\) riadkov, v každom z nich sú štyri celé čísla \(x_1\), \(y_1\), \(x_2\) a \(y_2\) (\(1 \leq x_1 \leq x_2 \leq 10^9\), \(1 \leq y_1 \leq y_2 \leq 10^9\)) udávajúce súradnice plesnivej obdĺžnikovej plochy.
Súradnice rastú smerom dole a doprava.
Nech \(\sum N\) je súčet \(N\) pre všetky testovacie prípady v jednom vstupe. Vstupy úlohy budú rozdelené do 5 sád, v ktorých platia nasledujúce obmedzenia:
- \(T \leq 20\), \(N \leq 10\), \(x_2, y_2 \leq 100\)
- \(\sum N \leq 1\,000\), \(x_2, y_2 \leq 10^6\), súčet plôch všetkých obdĺžnikov \(\leq 4*10^6\)
- \(N \leq 2\,500\), \(\sum N \leq 5\,000\)
- \(\sum N \leq 50\,000\), plochy sa neprekrývajú
- \(\sum N \leq 50\,000\)
Formát výstupu
Pre každý testovací prípad vypíšte jedno číslo – počet sekúnd, ktoré potrvá, kým všetka pleseň zahynie.
Príklady
Input:
3
1
1 1 5 1
1
1 1 1 5
1
1 1 3 3
Output:
5
5
5
Input:
1
3
4 1 4 1
2 2 3 2
1 2 1 4
Output:
6
Nech A
, B
, C
sú plesnivé
oblasti súradníc (4, 1, 4, 1), (2, 2, 3, 2) a (1, 2, 1, 4). Nech
P
je pleseň ktorá voči minulej sekunde prežila a
n
je pleseň ktorá sa narodila. V nasledujúcich sekundách
bude pleseň vyzerať takto:
...A 1s .... 2s .... 3s .... 4s .... 5s .... 6s ....
CBB. -\ .PPn -\ ..PP -\ ...P -\ .... -\ .... -\ ....
C... -/ Pn.. -/ .Pn. -/ ..Pn -/ ...P -/ .... -/ ....
C... P... Pn.. .Pn. ..Pn ...P ....
Input:
1
4
49 8 66 93
62 4 67 99
38 61 58 81
58 2 60 66
Output:
110
Keď sa pekne spýtate ChatGPT-4o, tak vám tento vstup nakreslí. Na takéto veci je AI veľmi fajn, avšak nepoužívajte ju priamo na riešenie a programovanie úloh.
Poznámka
Bol by som rád keby ste túto úlohu skúsili vyriešiť a naprogramovať (na čo najviac bodov). Aj keď je to sedmička, nie je mimoriadne ťažké získať viac ako polovicu bodov. Ak sa úlohu pokúšate riešiť na plný počet bodov, tak odporúčame programovať v C++ a popis napísať poriadne a zrozumiteľne.
Zadanie úlohy je podobné Game of Life, ale s inými pravidlami. Pravidlá sú nasledovné:
- Bunka \((x, y)\) je v novom kroku živá, ak aspoň dve z troch buniek \(\{(x, y), (x-1, y), (x, y-1)\}\) sú v aktuálnom kroku živé.
- Inak je bunka v novom kroku mŕtva.
Plocha bude mŕtva v konečnom čase
Najprv si skúsme uvedomiť, že v každej situácii všetky bunky zomrú v konečnom čase. Všimnime si, že ak sa pozrieme na minimálne a maximálne súradnice všetkých buniek, tak žiadna bunka mimo obdĺžnika \((x_{min}, y_{min}, x_{max}, y_{max})\) nebude nikdy živá. Pre ľubovoľný vstup je teda konečne veľa buniek, ktoré môžu byť potenciálne živé. Pozrime sa na najhorší možný prípad, kedy na začiatku sú všetky bunky v tomto obdĺžniku živé (toto je naozaj najhorší prípad, keďže na rozdiel od Game of Life bunky nezomierajú, ak ich je niekde veľa). V každom čase, kedy existuje aspoň jedna živá bunka existuje aj niekoľko buniek, ktorá majú najmenšie \(y\) a v rámci nich existuje práve jedna bunka, ktorá má zároveň najmenšie \(x\) spomedzi nich. Táto bunka určite zomrie a už nikdy neožije. Keďže v každom kroku zomrie aspoň jedna bunka, žiadne bunky nepribúdajú a buniek je konečne veľa, všetky bunky zomrú v konečnom čase.
Simulácia – sada 1/5
Vyriešme prvú sadu priamočiarou simuláciou. Pamätajme si všetky bunky v 2D mriežke. V každom kroku sa pozrieme na každú bunku a podľa pravidiel zo zadania určíme, či bude živá alebo nie.
Toto by malo časovú zložitosť \(O([\text{veľkosť mriežky}] \cdot [\text{odpoveď}])\). Nech rozmer plochy je \(S\). Najhorší vstup čo môžeme dostať je mriežka \(S \times S\) plná živých buniek. Dá sa ukázať, že na tejto ploche všetky bunky zomrú za \(2 \cdot S - 1\) krokov. Teda časová zložitosť je \(O(S^3)\) a pamäťová \(O(S^2)\).
Tento kód môžeme trochu zoptimalizovať tak, že si nám stačí pamätať
iba živé bunky (tých by malo byť zvyčajne oveľa menej ako tých mŕtvych)
– na to môžeme použiť množinu (Python set
,
C++ std::unordered_set
). V každom kroku sa potom
nemusíme pozerať na každé políčko, ale iba na tie, ktoré sú blízko
živých. Konkrétne nám stačí pre každú živú bunku skontrolovať, či
zostane žiť a či vďaka nej neožije jej pravý sused. Avšak pre hore
spomenutý najhorší vstup (\(S \times
S\)) bude mať toto riešenie rovnakú časovú zložitosť.
Záleží na komponentoch
Dve bunky alebo obdĺžniky nazveme susedné, ak zdieľajú hranu alebo pravohorný-lavodolný \(\nearrow\swarrow\) roh. Teda nasledujúce konfigurácie buniek/obdĺžnikov nazývame susedné:
Fig. 1
A. AB .A
B. .. B.
Ďalej budeme hovoriť, že ak sú dve bunky/obdĺžniky susedné, tak patria do toho istého komponentu. Táto vlastnosť je tranzitívna, teda ak \(A-B\) patrí do toho istého komponentu a \(B-C\) patria do toho istého komponentu, tak aj \(A-C\) patria do toho istého komponentu.
Tvrdíme, že komponenty sú vždy súvislé a navzájom nezávislé. Ak si skúsime vytvoriť vstupy pre túto úlohu, môžeme si všimnúť, že ak na začiatku existovali nejaké komponenty, tieto komponenty sa počas celej simulácie nikdy neovplyvnili, nespojili ani nerozdelili na viac komponentov.
Ak je toto pravda, úlohu vieme riešiť samostatne pre každý komponent a výsledok bude maximum z výsledkov pre jednotlivé komponenty.
Dôkaz uvedieme neskôr.
Záleží na diagonálach
Podobne ak sa pozeráme na simuláciu jedného komponentu tak si všimneme, že bunky zomierajú a vznikajú v skupinách na diagonálach. Pozrite sa ako sa správa tento vstup:
Fig. 2
OOOO .OOO ..OO ...O .... .... .... ....
O... Oo.. .Oo. ..Oo ...O .... .... ....
O... O... Oo.. .Oo. ..Oo ...O .... ....
O... O... O... Oo.. .Oo. ..Oo ...O ....
V každom kroku zomrie jedna diagonála a jedna nová ožije. Komplikovanejšie vstupy sa budú správať podobne.
Spomeňme si na argument, ktorý sme spravili na začiatku, že v každom kroku zomrie aspoň jedna bunka. Tento argument môžeme rozšíriť na celú diagonálu – v každom kroku zomrú všetky bunky na najľavohornejšej diagonále. Navyše, keďže išlo o najľavšiu diagonálu, tieto bunky už nikdy neožijú.
Odbočka – Simulácia s kladivom – sada 2/5
Pozrime sa na jeden komponent. Simulovať bunku po bunke je pomalé, ale už sme si všimli, že diagonály sú zaujímavé. Predstavme si, že na ploche sa nachádza jedna diagonála. Čo sa s ňou stane v ďalšom kroku? Táto jedna diagonála celá umrie, lebo žiadna bunka nemá naľavo a hore živé bunky. Ale vedľa tejto diagonály ožije nová diagonála, ktorá bude o jedno políčko kratšia.
Na diagonále naraz ožívajú, zostávajú žiť alebo umierajú veľké intervaly políčok. Vieme sa na tento proces pozrieť zametaním. Začneme v najľavejšej diagonále a zametáme smerom doprava dole. Implementovať krok vieme nasledovne:
- pre každý interval živých buniek sa tento interval v ďalšom kroku zmenší o jedna
- tým, že na ploche sú pôvodne nejaké obdĺžniky, medzi jednotlivými krokmi musíme manuálne pridať nejaké intervaly
- táto kolekcia intervalov a zmeny na nich sa dajú implementovať pomocou usporiadanej množiny alebo intervalového stromu.
Akú to bude mať časovú zložitosť? Časovú zložitosť budú dominovať operácie s intervalovým stromom. Jednak doňho pridávame nové diagonály a dvak v každom kroku zmenšujeme všetky intervaly. Nech počet buniek na začiatku je \(Z\). Ukážeme, že počet týchto operácií je \(O(Z)\), a teda časová zložitosť \(O(Z \log Z)\). Uvedomme si, že jediný spôsob akým pribúdajú políčka do intervalového stromu je tým, že pridávame diagonály. Pravidlá množenia sa plesne sme transformovali na pravidlo, že živý interval sa v ďalšom kroku presunie do susednej diagonály a skráti o jedna. Navyše jediný spôsob ako vie políčko zomrieť je tak, že zmizne skrátením intervalu o jedna. Počet týchto skrátení bude teda presne toľko, koľko políčok pridáme do intervalového stromu a to je nanajvýš \(Z\). Máme teda horný odhad. Zároveň je to presný odhad, keďže to vieme dosiahnuť napríklad vstupom:
Fig. 3
1 1 1 1000000
1 1 1000000 1
Pre obmedzenia druhej sady (obmedzenia na veľkosť súradníc a počet živých políčok) je najhorší vstup nasledovného tvaru (veľa vyššie spomenutých vstupov (Fig. 3) vložených do seba):
Fig. 4
AAAAAAAA...A
B
B CCCCCC...C
B D
B D EEEE...E
B D F
B D F GG...G
B D F H.
. . . . .
. . . . .
. . . . .
B D F H .
Dá sa ukázať, že časová zložitosť je \(O(D \log D)\), kde \(D\) je počet živých diagonálnych intervalov na vstupe, avšak v tomto najhoršom prípade sú \(D\) a \(Z\) skoro rovnaké. Znamená to ale, že na vstupe s približne štvorcovými počiatočnými plochami (hore uvedený vstup má mimoriadne neštvorcové plochy) by mal tento algoritmus zložitosť \(O(\sqrt Z \log Z)\).
Zatiaľ sme však nespomenuli, ako hľadať komponenty.
Zjednodušenie úlohy
Simuláciou sa veľmi ďalej nedostaneme, treba si úlohu zjednodušiť. Znova sa pozrime na jeden komponent. Ukázali sme, že ako prvá zomrie bunka v najľavejšej diagonále. Otázkou teda zostáva, kedy zomrie posledná bunka. Ak si trochu nasimulujeme ako sa hra správa, vieme si všimnúť, že ako posledná zomrie bunka, ktorej súradnice \((x, y)\) sú maximum \(x\)-ových a \(y\)-ových súradníc všetkých buniek. Úloha sa nám teda zjednoduší na hľadanie najspodnejšej a najpravejšej bunky komponentu (dôkaz neskôr). A to je triviálne, ak vieme, že máme na ploche iba jeden komponent. Zostáva nám teda pre každú bunku určiť, do ktorého komponentu patrí. A tu sa reálne začína úloha.
Zadanie: Na vstupe máme \(N\) potenciálne prekrývajúcich sa obdĺžnikov. Ak sa dva obdĺžniky prekrývajú, dotýkajú hranou alebo pravohornými-ľavodolnými \(\nearrow\swarrow\) rohmi, tak hovoríme, že obdĺžniky sú susedné. Susedné obdĺžniky patria do rovnakého komponentu. Nájdi komponenty.
Určenie komponentov na úrovni buniek – sada 2/5
Toto riešenie dostane rovnako veľa bodov, ako vyššie spomínaná simulácia kladivom, avšak je oveľa jednoduchšie na nakódenie.
V druhej sade máme zaručené, že súčet plôch všetkých obdĺžnikov je malý. Môžeme teda každú bunku považovať za vrchol, vytvoriť hrany medzi susednými bunkami a nájsť komponenty v tomto grafe. Pre každú bunku vieme, že je k nej iba konštantne veľa susedných buniek (6 smerov, navyše pre každú bunku stačí zohľadniť iba polovicu z nich a zvyšok zohľadní susedná bunka). Tento graf bude mať teda \(O(Z)\) vrcholov a hrán.
Určenie komponentov na úrovni obdĺžnikov – sada 3/5
Teraz už ideme konečne opísať riešenie, ktoré nám dá viac bodov a je dokonca stále veľmi jednoduché na naprogramovanie.
Podobne ako v predchádzajúcom riešení, snažíme sa zostrojiť graf a nájsť v ňom komponenty. Snažíme sa postaviť graf, kde každý obdĺžnik je vrchol a hrana vedie medzi obdĺžnikmi, ktoré sú susedné.
V tejto sade však vieme, že počet obdĺžnikov je taký malý, že si vieme dovoliť \(O(N^2)\) riešenie. Môžeme sa teda pozrieť na každú dvojicu obdĺžnikov, určiť v \(O(1)\) čase, či sú susedné, postaviť graf a nájsť komponenty. Dostaneme graf s \(O(N)\) vrcholmi a v najhoršom prípade \(O(N^2)\) hranami.
Určiť či sú obdĺžniky susedné je priamočiare a pozostáva iba z porovnania súradníc oboch obdĺžnikov a kontroly, či sa pretínajú, dotýkajú hranami alebo správnymi rohmi.
Hľadanie komponentov v postavenom grafe
Na hľadanie komponentov v grafe vieme použiť rôzne metódy – ľubovoľné prehľadávanie (DFS, BFS) alebo Union-Find.
Časová zložitosť DFS a BFS je \(O(V+E)\) a časová zložitosť Union-Find je \(O(E \alpha^{-1}(E))\). Čo je ale zaujímavé je, že Union-Find je nielen rýchlejšie naprogramovať, ale reálne aj beží rýchlejšie. Pre účely popisu však povieme, že použitím prehľadávania dostávame lineárnu zložitosť vzhľadom na počet hrán.
Bez ohľadu na to, aký prístup zvolíme, nie je nutné si graf explicitne konštruovať. Ak máme funkciu, ktorá nám pre dva vrcholy povie, či je medzi nimi hrana (teda napríklad funkciu susednosti dvoch obdĺžnikov), graf vieme používať iba implicitne.
Neexistuje \(O(N^2 \alpha^{-1}(N))\)
A dokonca ani \(O(N \cdot \log N \cdot \alpha^{-1}(N))\).
Čo tým myslím? Keď si pozrieme Wikipédiu, tá hovorí, že časová
zložitosť \(M\) operácií Union-Findu je
\(O(M \alpha^{-1}(N))\). Avšak ja
tvrdím, že ak zavoláme funkciu UF.find
alebo
UF.join
\(N \log N\) krát,
časová zložitosť bude iba \(O(N \log
N)\), nie \(O(N \cdot \log N \cdot
\alpha^{-1}(N))\). Väčšina týchto volaní sa dokázateľne vykoná v
\(O(1)\) čase.
Spomeňme si na analýzu Union-Find algoritmu, kde funkcia
UF.find
robí path compression a
UF.join
joinuje buď podľa ranku alebo
veľkosti komponentu.
Potom vieme, že najhlbší strom, ktorý vie vzniknúť, bude mať hĺbku
\(O(\log N)\) (kvôli rank
join). A z toho vidno, že pre každý vrchol sa funkcia
UF.find
rekurzívne zavolá tiež maximálne iba \(O(\log N)\) krát (kvôli path
compression). Ak sme pre nejaký vrchol \(\log N\) krát rekurzívne zavolali
UF.find
(teda že tento vrchol nebol priamo pod koreňom),
tak už určite bude navždy priamo pod koreňom a všetky ďalšie volania
nebudú rekurzívne.
Počet rekurzívnych UF.find
volaní vieme teda ohraničiť
ako \(O(N \log N)\). Pre \(M\) operácií bude časová zložitosť \(O(\max(M, \min(M, N) \log N))\), čo pre
\(M \in O(N \log N)\) znamená \(O(N \log N)\) a nie \(O(N \cdot \log N \cdot
\alpha^{-1}(N))\).
Zložitosť pre tretiu sadu je teda bez ohľadu na to, či použijeme BFS, DFS alebo Union-Find \(O(N^2)\).
Ďalšie zjednodušenia
Aby sa nám v ďalších sadách pracovalo ľahšie, zjednodušíme si vstup.
V prvom rade si súradnice skomprimujeme. Teda si pozbierame všetky \(x\)-ové súradnice, utríedime ich a prečíslujeme na hodnoty \(0\) až počet rôznych \(x\)-ových súradníc. Počet rôznych \(x\)-ových súradníc bude nanajvýš \(2N\).
Avšak, ak by sme to spravili takto, pokazili by sme si riešenie.
Totiž, ak sme mali napríklad obdĺžniky (10, 20, 13, 21)
a
(17, 20, 19, 21)
, po prečíslovaní by sa z nich stali
(0, 0, 1, 1)
a (2, 0, 3, 1)
. Zatiaľ čo
originálne dva obdĺžniky nie sú susedné, prečíslované obdĺžniky už sú.
Preto, ak chceme takto prečíslovávať, pre každú \(x_1\) súradnicu by sme mali do
prečíslovacieho zoznamu pridať aj \(x_1-1\). Potom budú dané obdĺžniky vyzerať
nasledovne – (1, 1, 2, 2)
a (4, 1, 5, 2)
.
Počet súradníc bude teda nanajvýš \(3N\).
V druhom rade prestaneme používať uzavreté intervaly na obdĺžniky. Nepopierateľne sa s tým ľahšie programuje. Ak by sme túto zmenu spravili už v riešení tretej sady, funkcia na susednosť by bola oveľa krajšia. Avšak v tomto prípade nám to pomôže aj pri kompresii. Ak prestaneme používať uzavreté intervaly, zmizne hore popísaný problém a znova budeme mať iba nanajvýš \(2N\) rôznych súradníc (čo nám prakticky zrýchli program o viac ako 33 %).
Rovnako upravíme aj \(y\)-ové súradnice.
Obdĺžnikov je veľa, ale neprekrývajú sa – sada 4/5
Čo nám pomáha, že sa obdĺžniky neprekrývajú? Čo je problém s predchádzajúcim riešením? Ak sa všetky obdĺžniky pretínajú, vznikne graf s \(O(N^2)\) hranami (a taký vstup vieme triviálne zostrojiť). Čo však v prípade, že sa obdĺžniky nepretínajú? Dá sa ukázať, že počet hrán je v tomto prípade \(O(N)\).
Dôkaz málo susedností
Pozrime sa na dva susedné neprekrývajúce sa obdĺžniky. Ako môžu vyzerať? Buď je hrana jedného podúsekom hrany toho druhého, alebo sa obe prekrývajú čiastočne, alebo sa dotýkajú rohom.
Fig. 5
AAA AAAAAAAAA AAAAAA AAAAAA AAA
BBBBBBBBB BBB BBBBBB BBBBBB BBB
Vidíme, že pre B
sa môže takmer každá z týchto situácií
stať iba nanajvýš raz (napríklad nemôžu sa dva obdĺžniky dotýkať jeho
pravého horného rohu). Jediné, čo sa môže stať viackrát je, že viacero
obdĺžnikov má hranu ako podúsek jeho hrany. V tejto situácii sa na to
však vieme pozrieť z opačnej strany a znova povedať, že pre tieto menšie
obdĺžniky môže existovať iba jeden nadobdĺžnik. Každá susednosť nám
“vyrieši” aspoň jednu hranu alebo roh a keďže tých je \(O(N)\), tak celkovo môže existovať iba
\(O(N)\) susedností.
Zametanie
Ako efektívne hľadať tieto susednosti? Celkom určite zametaním. Vďaka
vlastnosti, ktorú sme hore ukázali, by malo fungovať veľa rôznych
prístupov – či už riešenie používajúce std::map
alebo
intervalový strom. Dôležité je, že si obdĺžniky pozametáme a budeme
efektívne hľadať susednosti na okrajoch.
Najjednoduchšie je spraviť dve zametania, jedno vertikálne a jedno horizontálne. Zamerajme sa teraz na vertikálne zametanie. Potom sa v rámci tohto zametania stačí sústrediť iba na susednosti, ktoré sú spôsobené situáciami zobrazenými vyššie (Fig. 5).
Ak robíme zametanie, budeme mať nejakú štruktúru, v ktorej si budeme pre aktuálny riadok pamätať aktuálne aktívne obdĺžniky. Potom, keď ideme na ďalší riadok, budeme musieť pridať niektoré nové obdĺžniky a niektoré nové obdĺžniky musíme odobrať. Najprv vyhodnotíme všetky susednosti, potom odstránime všetky obdĺžniky, ktoré treba odstrániť, a nakoniec pridáme všetky, ktoré treba pridať. Čo je zaujímavé je, že keďže robíme dve zametania, netreba vyhodnocovať žiadne susednosti medzi obdĺžnikmi, ktoré práve pridávame. Tie sa vyhodnotia v kolmých zametaniach.
Ako nájsť všetky susednosti? Očakávame, že naša zvolená štruktúra
podporuje vrátenie všetkých prvkov, ktoré sa pretínajú s požadovaným
intervalom. Napríklad pre std::set
, kde kľúč je
pair<x_1, x_2>
vieme tieto prvky nájsť pomocou
funkcie lower_bound
– ak zavoláme
set.lower_bound({x_2, -1})
, dostaneme prvok, najľavejší
interval, ktorý je napravo od a určite nesusedí s intervalom \((x_1, x_2)\). Podobne
set.lower_bound({x_1, -1}) - 2
nám dá najpravější interval,
ktorý je naľavo a určite nesusedí s intervalom \((x_1, x_2)\) (zamyslite sa, prečo
-2
). Stačí nám zavolať jedno z týchto volaní a potom
iterovať cez všetky vedľajšie intervaly, až kým neprestaneme susediť.
Vyššie sme ukázali, že susedností je \(O(N)\), takže celkovo týchto iterácií
spravíme \(O(N)\).
Musíme implementovať nasledujúce operácie:
- pridať interval
- odobrať interval
- iterovať cez všetky intervaly, ktoré susedia s daným intervalom
Celkovo bude časová zložitosť \(O(N \log N)\), keďže triedime a používame nejakú stromovú štruktúru.
Obdĺžniky sa ale môžu prekrývať – sada 5/5
Pozrime sa, aké všelijaké problémy nám môžu vzniknúť, keď sa obdĺžniky prekrývajú:
Fig. 6
1 AAA CC D F
2 BBBabaBBB CCoDoooFoooo
3 BBBbabBBB ooDoEoFoGoHH
4 AAA Y ooooooFoGoo
5 X F G
Pridajme obdĺžnik do prázdneho lazy intervalového stromu. Keďže je to lazy strom, na pridanie stačí navštíviť iba \(O(\log N)\) vrcholov (až \(2 \log N\) bude obsahovať lazy informáciu a po ceste sme navštívili až \(2 \log N\) ďalších predkov). V predchádzajúcej sade, kde sa žiadne obdĺžniky neprekrývali, sme mali vlastne zaručené, že každý vrchol je pokrytý nanajvýš jedným obdĺžnikom. V tejto sade, žiaľ, už túto peknú vlastnosť nemáme. Pamätajme si teda v každom vrchole pole obdĺžnikov. Keď lenivo pridávame nový obdĺžnik do \(O(\log N)\) vrcholov, je možné, že v ňom už nejaký obdĺžnik je. V tomto prípade by sme radi tiež dali tieto dva obdĺžniky do jedného komponentu.
Spravíme v tomto lazy intervalovom strome jednu zvláštnu vec, a to že
si lazy informácie nebudeme propagovať ďalej. Ak štandardne máme vo
vrchole zapamätanú premennú hodnota
a lazy
,
tak v našom intervalovom strome nám stačí lazy
(ktorá je
pole identifikátorov obdĺžnikov). Prečo to robíme? Keďže chceme
podporovať aj odoberanie obdĺžnikov, ľahšie sa nám to bude robiť, ak sa
obdĺžniky nerozlezú až hlboko do listov. Ak by sme informácie
propagovali, explicitné upratanie obdĺžnikov by potenciálne trvalo až
\(O(N)\). Trochu sa nám týmto
komplikuje vyhodnocovanie pretnutí pri pridávaní obdĺžnikov, ale to
zvládneme vyriešiť.
Nazvime množinu vrcholov, ktoré pokrývajú interval prislúchajúci pridávanému obdĺžniku \(J\) (teda práve tie vrcholy, do ktorých by sme uložili lazy informáciu). Čo by sa zmenilo, keby sme každý obdĺžnik rozdelili na množinu nových obdĺžnikov tak, že každý pokrýva práve jeden vrchol z \(J\). Celý algoritmus by mal naďalej fungovať (keďže takýto vstup existuje), iba sa nám zložitosti zhoršia o logaritmus, pretože máme o logaritmus krát viac obdĺžnikov. Robiť to teda nebudeme, ale bez ujmy na všeobecnosti nad tým tak môžeme uvažovať.
Pozrime sa teda iba na jeden vrchol z \(J\). Pridaný obdĺžnik prejde v intervalovom strome nejakú cestu. Naším cieľom je ho spojiť so všetkými ostatnými obdĺžnikmi, ktoré sa s ním pretínajú. Teda nájsť všetky obdĺžniky, ktorých cesta je buď podmnožina (starý obdĺžnik pokrýva celý nový obdĺžnik) alebo nadmnožina (nový obdĺžnik pokrýva celý starý obdĺžnik). Každú dvojicu ciest stačí spracovať iba raz. Rozumné miesto kde to robiť je v koncovom vrchole kratšej cesty.
Fig. 7 1 2 3 4 5
1 AAAAAAAA A A A A .
2 AAAABBAA / \ / \ / \ / \ / \
3 AAAABBCC . . . . . C . C . C
4 AAAACCCC / \ / \ / \ / \ / \
5 CCCC . . B . B . . . . .
Na obrázku (Fig. 7) vidíme, ako by vyzeral intervalový strom v jednotlivých časových bodoch zametania. Vidíme, že cesta do vrcholu s obdĺžnikom \(B\) je nadmnožina cesty do vrcholu pre \(A\) (rovnako aj pre dvojice \(C-A\) a \(B-C\)). O spojenie \(A-B\) a \(A-C\) sa teda chceme postarať v \(A\) a o spojenie \(B-C\) v \(C\).
Pozrime sa čo sa deje v tomto koncovom vrchole \(V\) pre nový obdĺžnik. Chceme pridať obdĺžnik do \(V\) a celý ho pokryť. Pod \(V\) môže byť viacero doposiaľ disjunktných obdĺžnikov a týmto ich všetky dostaneme do jedného komponentu:
Fig. 8
A B
A B
XX...XXaXXbXXXX...XX
A B
A B
WW...WWaWWbWWgW...WW
A B G
A B
YY...YYaY B H
A B
A B
A ZbZZZZ...ZZ
A B
Povedzme, že v tomto prípade (Fig. 8) zametáme zhora nadol, \(X\) pokrýva nejaký veľký interval a tým, že ho pridáme, spojíme obdĺžniky \(A\), \(B\) a \(X\) do jedného komponentu. Keďže \(A\) a \(B\) sú veľmi úzke, v intervalovom strome je \(J_A\) a \(J_B\) iba jeden list. Ak by sme pri pridávaní \(X\) do intervalového stromu išli až po úroveň listov, vykonali by sme až \(O(N)\) operácií pre jeden obdĺžnik. Namiesto toho, aby sme teda chodili do všetkých potomkov \(J\), pamätajme si informáciu o prítomnosti obdĺžnika aj vo všetkých predkoch \(J\). Tých je iba nanajvýš \(O(\log N)\). Informáciu o prítomnosti obdĺžniku si teda pamätáme na celej spomínanej ceste a zjednotenie všetkých ciest pre \(J\) má veľkosť iba \(O(\log N)\).
Ak by sme sa však pokúsili spojiť nový obdĺžnik so všetkými obdĺžnikmi vo \(V\), mali by sme stále zlú časovú zložitosť. Obdĺžniky sa nám vo vrcholoch hromadia a každý môže obsahovať až \(O(N)\) obdĺžnikov (avšak všetky vrcholy dokopy majú stále iba \(O(N \log N)\)). Preto je dôležité, že nový obdĺžnik pokrýva celé intervaly prislúchajúce vrcholom z \(J\). Vieme si teda predstaviť, že aj keď máme vo vrchole \(V\) zapamätaných povedzme tisíc obdĺžnikov, pridaním tohoto nového obdĺžnika sa všetkých tisíc spojí do jedného komponentu. Pri pridávaní ďalšieho obdĺžnika by ho stačilo spojiť s týmto veľkým komponentom, namiesto spájania s tisíc obdĺžnikmi. Nový obdĺžnik vyrieši/vyčistí všetky vrcholy z \(J\), tak že v nich zostane iba jeden komponent. Tento prístup je teda ako keby sme si v každom vrchole pamätali nezávislú Union-Find štruktúru.
Problémy nastanú, keď sa pokúsime obdĺžnik odstrániť – ako na obrázku (Fig. 8). Ak odstránime obdĺžnik \(X\), nastanú dva problémy. Jednak sme potenciálne odstránili koreň nejakého Union-Find komponentu. Dvak, keďže \(X\) pokrýval celý interval od \(A\) po \(B\), ako zaručíme, že nebudeme znova vykonávať veľa práce pri pridávaní obdĺžnika \(W\)?
Prvý problém nie je problém, za koreň zvolíme ľubovoľného potomka. Pre druhý problém je dôležité to, že obdĺžnik \(W\) pokrýva celý interval daného vrcholu. Keďže odstránením vrcholu sa komponenty Union-Findu nerozpadli a \(W\) pokrýva celý interval, stačí \(W\) spojiť iba so všetkými koreňmi Union-Find komponentov, čím znova vyčistíme daný vrchol a amortizovane spotrebujeme \(O(1)\) času.
Týmto by sme mali celkom vyriešené spájanie s nadmnožinovými cestami. Čo tie podmnožinové? Teda ako sa zachovať keď pridávame obdĺžniky \(G\) a \(H\)? Stále ich chceme vyriešiť v koncovom vrchole tej kratšej cesty. To znamená, že po ceste dole intervaláčom sa pozeráme, či aktuálny vrchol nie je koncovým pre nejaký obdĺžnik. Môže byť dokonca koncovým pre viacero, ale znova si uvedomme, že v tom prípade budú určite všetky patriť do toho istého komponentu. A ak taký existuje, tak to musí znamenať, že všetky obdĺžniky v tomto vrchole patria do jedného komponentu. Pozor, ide o implikáciu, nie ekvivalenciu – to, že existuje iba jeden komponent neznamená, že existuje obdĺžnik čo pokrýva celý interval. Zamyslite sa, ako zistiť, či existuje pokrývajúci obdĺžnik. V tom prípade nový obdĺžnik tiež pridáme do tohoto veľkého komponentu, inak vytvoríme nový komponent obsahujúci iba nový obdĺžnik.
Všimnime si, že nám nevadí vytvárať veľa malých komponentov. Je možné, že vo vrchole pre interval dĺžky 16 budeme mať tisíc jednoprvkových komponentov. Očividne by sa počet komponentov dal zredukovať na nanajvýš 16. My však chceme byť efektívny, a teda lenivý. Túto redukciu si odložíme na moment, keď pridáme obdĺžnik, ktorý bude mať tento vrchol vo svojom \(J\). Tým, že by sme to spravili skôr si nič nepomôžeme, keďže všetky dôležité spojenia komponentov sa vyriešili na hlbších vrstvách intervalového stromu.
Teraz si môžeme všimnúť, že vo vrchole si vždy pamätáme jeden veľký
komponent a kopu jednoprvkových komponentov, ktoré sa doňho dostali z
jeho potomkov. Môžeme tomu teda prispôsobiť implementáciu a nemusíme si
vo vrchole pamätať Union-Find, ale iba nejaké dve množiny (napríklad
std::unordered_set
) – jednu pre veľký komponent a druhú pre
jednoprvkové komponenty. Množinu používame, aby sme vedeli rýchlo
odstraňovať prvky.
std::unordered_set
má síce \(O(1)\) operácie, nie je však taký rýchly
ako pole. Implementáciu vieme teda vylepšiť tým, že si budeme pamätať
dve polia a hodnoty z nich nemažme. Hodnoty v poliach totiž používame
iba ak ich chceme všetky spojiť do jedného komponentu, teda zobrať
všetky hodnoty z druhého pola a nasypať ich do prvého. Keď už toto
robíme, vieme si pre každý prvok skontrolovať, či je ešte živý
(rects[ri].y2 >= sweep_time
) a ak nie, jednoducho ho
odignorovať. Dajme si pozor, aby sme nehýbali s prvkami vo veľkom
komponente (to by nám pokazilo časovú zložitosť). Na to, či vo veľkom
komponente existuje ešte aspoň jeden aktívny obdĺžnik si budeme pamätať
maximálnu koncovú súradnicu zo všetkých obdĺžnikov v ňom (podobne
riešime zamyslenie nad existencie pokrývajúceho obdĺžnika). Celé toto
nás bude stáť zopár riadkov, ale kód by mal byť oveľa rýchlejší.
Týmto sme vyriešili pridávanie a odoberanie obdĺžnikov a spájanie tam, kde sa prekrývajú. Spájanie tam kde sa dotýkajú doriešime všeliako na kolene, keďže stačí skontrolovať iba \(O(\log N)\) vrcholov, ktorých intervaly sa dotýkajú pridávaného obdĺžnika.
Premeníme intervaly vstupe na polootvorené a skomprimujeme si súradnice. Zametáme, pričom si pamätáme globálnu Union-Find štruktúru. Máme lazy intervalový strom v ktorom nepropagujeme a vo vrcholoch si pamätáme dve polia a pomocné agregačné informácie o tom prvom. Obe polia obsahujú indexy nejakých obdĺžnikov, pričom v prvom poli sú všetky súčasťou jedného komponentu a v druhom sú všekty zatiaľ ako samostatný komponent. Agregačné informácie pre prvé pole sú: o aký komponent ide, dokedy je ešte niečo aktívne a dokedy je ešte nejaký plne pokrývajúci prvok aktívny. Keď pridávame obdĺžnik, po ceste kontrolujeme, či je vrchol pokrytý jedným obdĺžnikom a podľa toho pridáme nový obdĺžnik do prvého alebo druhého pola. Keď prídeme do vrcholu, ktorý nový obdĺžnik celý pokrýva, skončíme rekurziu. Presypeme druhé pole do prvého, pričom ignorujeme neaktívne prvky. Aktualizujeme agregačné informácie. Vždy keď dávame prvok do prvého pola, spojíme ho v globálnom Union-Finde s daným komponentom. Počas rekurzie vyriešime aj dotýkajúce sa obdĺžniky. Obdĺžniky pri zametaní teda explicitne nemažeme. Skončime s Union-Find štruktúrou, ktorá určuje rozdelenie obdĺžnikov do nezávislých komponentov.
No a toto je prakticky celé riešenie. Časová a pamäťová zložitosť sú \(O(N \log N)\).
Dá sa to aj lepšie
Predsa len, komu sa chce pamätať si pole alebo nebodaj
std::unordered_set
vo vrcholoch intervalového stromu. Keď
namiesto toho sa môžeme ďalej zamyslieť nad tým, čo sa ako deje a v
každom vrchole si pamätať iba \(O(1)\)
informácie.
Znova majme lazy intervalový strom, ale tentokrát budeme informácie
aj propagovať. To preto, lebo z neho nebudeme explicitne mazať veci. Pre
každý vrchol si budeme snažiť zapamätať, aký najdlhšie žijúci komponent
v ňom je, do akého času bude žiť a ktorý obdĺžnik ho reprezentuje —
(life, kompid, rectid)
. life
je v tomto
prípade najväčšia koncová súradnica spomedzi všetkých obdĺžnikov na
danom intervale (ľubovoľný čo doň zasahuje). Ak máme takúto informáciu
pre dva intervaly, tak informácia pre ich zjednotenie je jednoducho ich
maximum. Keď pridávame nový obdĺžnik, túto informáciu si vypýtame pre
celý jeho interval. Vďaka nej potom vieme povedať, či sa obdĺžnik s
niečím pretne alebo nie (ak life >= y_1
, tak áno, inak
nie).
Zistili sme, že sa pretneme s nejakým obĺžnikom \(R\). Predpokladáme, že on už je pospájaný do jedného komponentu so všetkými ostanými obdĺžnikmi, ktoré sú celé v ňom. Interval nového obdĺžnika teda môžeme rozdeliť na dva nezávislé kratšie úseky naľavo a napravo od \(R\) a rekurzívne sa pýtať intervalového stromu. Tu je problém, že odpovede naľavo a napravo vedia patriť do toho istého komponentu ako \(R\) a pokazíme si časovú zložitosť.
Pre každý komponent si pamätajme intervaly, ktoré pokrýva. Potom
nebudeme deliť interval nového obdĺžnika na dva menšie podúseky, ale na
veľa podúsekov, ktoré sú pomedzi intervaly komponentu \(komp[R]\). Na tieto podintervaly sa stále
vieme zavolať rekurzívne. Samozrejme, týchto podintervalov môže byť veľa
a môžu byť aj prázdne. To sa môže zdať ako strata času. Dôležité je, že
pridaním tohto nového obdĺžnika tieto intervaly zaplníme a odstránime zo
zoznamu intervalov pre \(komp[R]\). Ak
bol interval prázdny alebo sa nič nepretlo, tak nový obdĺžnik bude
určite žiť na tomto intervale v intervalovom strome dlhšie ako čokoľvek
čo tam bolo doteraz. Ak sa obdĺžnik pretne, počet komponentov klesne o
jedna. Amortizovane bude táto operácia trvať rovnako dlho, koľko
existuje intervalov a komponentov. Teraz si už iba stačí uvedomiť, že
pridaním nového obdĺžnika vieme vytvoriť iba konštantne veľa nových
intervalov. Na to, aby sme vytvárali, pridávali a odoberali nové
intervaly, si budeme pre každý komponent pamätať std::set
a
robiť tieto operácie efektívne na ňom. Intervalov a komponentov bude
nanajvýš \(O(N)\), a teda časová aj
pamäťová zložitosť budú \(O(N \log
N)\).
Zamyslite sa, ako bude tento algoritmus prebiehať na takomto vstupe pri zametaní zhora nadol:
Fig. 8
AAAAAAAAAAA
B C
B DDDDD C
B DDDDD C
B C
Aj ak sa nepretneme, musíme sa rekurzívne zavolať na podintervaly, aby sme správne modifikovali zoznamy intervalov pre komponenty.
Naozaj prosím žiadne sety
Ale komu sa chce spájať a rozpájať nejaké intervaly v sete. Vystačíme
si iba s intervalovým stromom. Problém bol, že prvým zavolaním sme
efektívne našli pretínajúci komponent, avšak ďalšie volania nám mohli
dávať ten istý komponent. Napríklad ak by hodnoty life
v
intervalovom strome boli 1 2 3 4 5 4 3 2 1
a všetky patrili
do toho istého komponentu, tak by sme vykonali \(O(N)\) volaní.
My sme však ukázali, že intervalov, ktoré patria do rôznych komponentov, je celkovo iba \(O(N)\). Rovnakú myšlienku sme použili na riešenie štvrtej sady! Tam sme ukázali, že existuje nanajvýš \(O(N)\) disjunktných intervalov rôznych obdĺžnikov. Tu sme ukázali, že existuje nanajvýš \(O(N)\) disjunktných intervalov rôznych komponentov. Na dosiahnutie dobrej časovej zložitosti nám stačí dané riešenie prispôsobiť na komponenty, teda efektívne implementovať 3. operáciu — iterovanie.
Pre ľubovoľný list intervalového stromu chceme efektívne (\(O(\log N)\)) nájsť najbližší list napravo od neho, ktorý patrí do iného komponentu (alebo žiadneho). So správnymi údajmi môžeme výsledok “binárne vyhľadávať”. Vo vrcholoch si budeme pamätať dodatočné informácie:
- či je celý interval pokrytý jedným komponentom alebo nie
- do akého komponentu patrí najľavejší list tohto intervalu
- do akého komponentu patrí najpravý list tohto intervalu
Ak máme túto informáciu pre dva susedné intervaly, vieme ju jednoducho zistiť aj pre ich zjednotenie (zmena je buď v ľavom, pravom alebo na hranici intervalu).
Treba sa zamyslieť, ako tieto informácie ovplyvnia lazy updaty. Lazy update robíme iba ak pokrývame celý interval. V tom prípade v danom intervale určite nebude žiadna zmena komponentu a ľavý aj pravý okraj budú mať hodnotu nového komponentu.
Ako teda binárne vyhľadávať? Sme v nejakom liste a chceme nájsť najbližšiu zmenu komponentu napravo od nás. Budeme bublať smerom dohora. Ak sme pravé dieťa, tak s tým nič nenarobíme. Ak sme ľavé dieťa, tak najbližšia zmena je potenciálne v našom súrodencovi alebo na hranici medzi nami. To vieme okamžite zistiť tak, že sa pozrieme na predpočítanú informáciu. Inak pokračujeme v bublaní dohora. Ak je nejaká zmena v našom súrodencovi, tak bubleme dodola. Ak je zmena v jeho ľavom synovi, tak ideme tam, inak je možno na rozmedzí a inak musí byť v pravom synovi. Potrvá nám \(O(\log N)\) času vybublať hore a dole a nájsť najbližší odlišný komponent. Počas tohoto si treba dať pozor na správne propagovanie lazy informácii.
Zmenili sme intervali na polootvorené, skomprimovali sme si
súradnice, vytvorili Union-Find štruktúru a lazy intervalový strom,
zametáme jedným smerom. Pridanie jedného obdĺžnika potrvá \(O(\log N)\) (Union-Find operácie sa
amortizujú na \(O(1)\)). Obdĺžniky
neodoberáme, iba ich začneme ignorovať, ak sú príliš staré (ich koncová
súradnica je malá). Nájdenie najbližšieho susedného obdĺžnika trvá \(O(\log N)\) a zavoláme to \(O(N)\) krát. Celková časová zložitosť je
\(O(N \log N)\), pamäťová \(O(N)\) a set
sme použili iba
pri kompresií súradníc.
Dôkazy
Diagonála je určená ako všetky bunky, ktorých \(x + y = C\).
Komponent s jednou živou bunkou je triviálny prípad, takže predpokladáme, že každý komponent má aspoň dve živé bunky.
Lema 1: Ak máme jeden komponent a diagonála \(x+y=C\) je najmenšia diagonála, ktorá obsahuje živé bunky, potom v ďalšom kroku žiadna bunka nebude na diagonále \(C\) a nejaká bunka bude na diagonále \(C+1\).
Dôkaz: Pozrime sa na bunku s najväčším \(y\) na diagonále \(C\). Keďže ide o jeden komponent s aspoň dvoma bunkami, musí existovať aspoň jedna bunka na pozíciách \(\leftarrow\rightarrow\uparrow\downarrow\nearrow\swarrow\). Pozície \(\uparrow\leftarrow\swarrow\) sú v rozpore s tým, že ide o bunku s najväčším \(y\) na diagonále \(C\). Pozície \(\downarrow\rightarrow\) sú bunky na diagonále \(C+1\), ktoré vďaka tejto bunke tento krok prežijú, čo spĺňa Lemu. Nakoniec pozícia \(\nearrow\) znamená, že ak na pozícii \(\rightarrow\) nie je živá bunka, v ďalšom kroku tam ožije, čo spĺňa Lemu. ∎
Lema 2: Súvislý komponent v ďalšom kroku zostane súvislý.
Dôkaz: Sporom. Predpokladajme, že sa komponent rozdelil.
Jedna možnosť je, že sa vytvoril nový komponent alebo komponenty, ktoré nie sú napojené navzájom alebo k pôvodnému. Na začiatok si uvedomme, že nemôžu vzniknúť dva rôzne komponenty a zároveň všetky bunky z predchádzajúceho komponentu zaniknúť. Stačí sa teraz sústrediť na to, či môže vzniknúť nový komponent, ktorý nie je napojený na na nejakú preživšiu bunku.
Prepokldajme, že sa také stalo. Tento komponent bude mať tvar iba jednej diagonály. Bez ujmy na všebecnosť zoberme najmenšiu možnú diagonálu, teda jednu bunku (obdobný dôkaz sa aplikuje na ľubovoľne dlhú diagonálu).
Keďže vznikla nová bunka, ktorá nie je na nič napojená, obe bunky \(\leftarrow\uparrow\) čo ju stvorili musia zomrieť a v jej okolí \(\leftarrow\rightarrow\uparrow\downarrow\nearrow\swarrow\) nesmú zostať žiť žiadne:
- > A = A
-† > †- = †a
-† > †x- = †x-
> B-- = Bb-
Kde †
reprezentujú \(\leftarrow\uparrow\) bunky, na
-
nesmie byť živá bunka a na A
alebo
B
musí byť aspoň jedna živá bunka (inak je toto celý
komponent a Lema platí). To ale spôsobí, že na a
alebo
b
vznikne bunka, čím dostávame spor s tým, že vznikla
diagonála dĺžky iba jedna.
Alternatívne sa komponent rozdelí tým, že niektorá bunka zomrie.
-A
-†B
DC
V tomto prípade †
je bunka čo v ďalšom kroku zomrie, na
-
nesmie byť bunka a na ABCD
musí byť aspoň
jedna živá bunka. Ak by na ABCD
bola práve jedna alebo
všetky štyri živé bunky, †
nespôsobí rozdelenie komponentu.
Ak na ABCD
budú tri živé bunky, tak je to iba jednoduchšia
situácia ako dve živé bunky. Ak na ABCD
budú práve dve živé
bunky, máme 6 možností. Vieme si overiť, že pre žiadnu z nich sa
komponent lokálne nerozpadne, pretože buď zostanú niektoré
ABCD
bunky žiť alebo vďaka †
vzniknú nové.
Prípady kedy AB
, BC
alebo CD
žijú
sú triviálne. Názorne si ukážeme prípad kedy AD
sú živé a
ostatné prípady by sa dali ukázať obdobne. V ďalšom kroku určite budú
bunky BC
žiť a niektoré z AD
možno zomrú. Ak
niektorá z buniek AD
prežije, na danej strane komponentu
ide o nezaujímavý prípad, keďže komponent sa tam nerozpadne. Ak však
povedzme A
zomrie, argument vieme posunúť o jedna ďalej,
prepísať †->D, A->†
a pozrieť sa rovnako na túto
situáciu. Takto to vieme posúvať ďalej a ďalej. Avšak buniek je konečne
veľa, takže musíme prísť do prípadu, ktorý je triviálny a spĺňa Lemu.
Následne spätne ukážeme, že aj všetky poposúvané situácie teda spĺňali
Lemu.
Nie je to úplne najelegantnejší, ale hádam to je valídny dôkaz… ∎
Lema 3: Dva rôzne komponenty sa nikdy nespoja.
Dôkaz: Keďže komponenty sú rôzne, musí vzniknúť nejaká nová bunka, ktorá ich spojí. Na to, aby vznikla bunka, musia existovať bunky \(\leftarrow\uparrow\). Keďže tieto dve bunky sú susedné v \(\nearrow\swarrow\) smere, musia byť z toho istého komponentu (inak spor). Nová bunka musí susediť s druhým komponentom. Ak by susedila v jednom z \(\nearrow\swarrow\) smerov, tak máme spor, že komponenty sú rôzne (susedia s \(\leftarrow\uparrow\) bunkami). Ak je druhý komponent v smeroch \(\rightarrow\downarrow\), tak nám to nepomôže, keďže v čase, keď bunka vznikne, tieto bunky zomrú. Dva rôzne komponenty sa teda nikdy nemôžu spojiť. Preto sa rôzne komponenty ani nikdy neovplyvnia. ∎
Lema 4: V súvislom komponente sa maximálna súradnica \(X\) a \(Y\) živých buniek v ďalšom kroku nezmení.
Dôkaz: Ukážeme dôkaz pre maximálne \(Y\) a dôkaz pre maximálne \(X\) je obdobný. Pozrime sa na bunku s maximálnym \(Y\). Keďže ide o súvislý komponent s aspoň dvoma bunkami, existuje aspoň jedna živá bunka v smeroch \(\leftarrow\rightarrow\uparrow\downarrow\nearrow\swarrow\). Smery \(\downarrow\swarrow\) sú v rozpore s tým, že táto bunka má maximálne \(Y\). Smery \(\leftarrow\uparrow\) spôsobia, že táto bunka v ďalšom kroku prežije, čo spĺňa Lemu. Smery \(\rightarrow\nearrow\) spôsobia, že bunka \(\rightarrow\) v ďalšom kroku bude žiť, čo spĺňa Lemu. ∎
Spojením Lemy 1 a 4 sme ukázali, že dĺžka prežitia komponentu je \(X+Y-C\), kde \(X\) je maximálna \(x\) súradnica komponentu, \(Y\) maximálna \(y\) súradnica komponentu a \(C\) je najmenšia diagonála, teda najmenšie \(x+y\) spomedzi všetkých buniek komponentu.
#include <bits/stdc++.h>
using namespace std;
struct UF {
<int> e;
vector(int n): e(n, -1) {}
UFint find(int x) { return e[x] < 0 ? x : e[x] = find(e[x]); }
bool join(int a, int b) {
= find(a), b = find(b);
a if (a == b) return false;
if (e[a] > e[b]) swap(a, b);
[a] += e[b];
e[b] = a;
ereturn true;
}
};
struct STval {
// x coordinate, color, contains color change
int maxi, c, firstc, lastc;
bool ch;
constexpr STval(): maxi(-1), c(-1), firstc(-1), lastc(-1), ch(false) {}
(int maxi, int c, int firstc, int lastc, bool ch):
STval(maxi), c(c), firstc(firstc), lastc(lastc), ch(ch) {}
maxibool isempty() const { return c == -1; }
static STval merge(const STval &a, const STval &b) {
bool ch = a.ch | b.ch | (a.lastc != b.firstc);
return a.maxi > b.maxi ?
(a.maxi, a.c, a.firstc, b.lastc, ch) :
STval(b.maxi, b.c, a.firstc, b.lastc, ch);
STval}
void update(const STval &b) {
if (b.maxi > maxi) {
= b.maxi;
maxi = b.c;
c }
= false;
ch = b.firstc;
firstc = b.lastc;
lastc }
};
// lazy ST with range set
struct ST {
using T = STval;
static constexpr T unit = STval();
int n;
<T> val, lazy;
vector<pair<int, int>> bound;
vector
(int _n) {
STfor (n = 1; n < _n; n *= 2);
.resize(2 * n, unit);
val.resize(2 * n);
lazy.resize(2 * n);
boundfor (int i = 0; i < n; ++i)
[n + i] = {i, i+1};
boundfor (int i = n - 1; i > 0; --i)
[i] = {bound[2 * i].first, bound[2 * i + 1].second};
bound}
void push(int i) {
if (lazy[i].isempty()) return;
[i].update(lazy[i]);
valif (i < n) {
[2 * i].update(lazy[i]);
lazy[2 * i + 1].update(lazy[i]);
lazy}
[i] = unit;
lazy}
void update(int cur, int qlo, int qhi, T v) {
if (qhi <= bound[cur].first || bound[cur].second <= qlo) return;
if (qlo <= bound[cur].first && bound[cur].second <= qhi) {
[cur].update(v);
lazyreturn;
}
(cur);
pushfor (int i: {0, 1}) update(2 * cur + i, qlo, qhi, v), push(2 * cur + i);
[cur] = STval::merge(val[2 * cur], val[2 * cur + 1]);
val}
(int cur, int qlo, int qhi) {
T query(cur);
pushif (qhi <= bound[cur].first || bound[cur].second <= qlo) return unit;
if (qlo <= bound[cur].first && bound[cur].second <= qhi) return val[cur];
return STval::merge(query(2 * cur, qlo, qhi), query(2 * cur + 1, qlo, qhi));
}
// find leftmost index ind in from qlo for which val[ind-1] != val[ind]
int find_next_change(int qlo) {
if (qlo >= n) return n + 1;
{
int path[30], len = 0;
for (int cur = n + qlo; cur > 0; cur /= 2)
[len++] = cur;
pathfor (int i = len - 1; i >= 0; --i)
(path[i]);
push}
int res = n + 1;
for (int cur = n + qlo; cur > 0; cur /= 2) {
if (!val[cur].ch) continue;
assert(cur < n);
(cur * 2); push(cur * 2 + 1);
pushif (val[cur * 2].lastc != val[cur * 2 + 1].firstc) {
int r = bound[cur * 2 + 1].first;
if (r > qlo) res = min(res, r);
}
}
int cur = n + qlo;
for (; cur > 0; cur /= 2) {
if (cur % 2 || !val[cur + 1].ch) // we are right child or there is no change
continue;
+= 1;
cur break;
}
if (cur == 0) return n + 1;
assert(cur < n);
assert(val[cur].ch);
while (cur < n) {
(cur * 2); push(cur * 2 + 1);
pushif (val[cur * 2].ch) cur = cur * 2;
else if (val[cur * 2].lastc != val[cur * 2 + 1].firstc) {
= bound[cur * 2 + 1].first;
cur break;
} else if (val[cur * 2 + 1].ch) cur = cur * 2 + 1;
else assert(false); // st is mangled up
}
assert(cur < n); // we should not end up in leaf
= min(res, cur);
res return res;
}
};
struct Rect {
int x1, y1, x2, y2, i;
};
(vector<Rect> Rs) {
UF find_components<int> xs, ys;
setfor (auto &r: Rs)
.insert(r.x1), xs.insert(r.x2), ys.insert(r.y1), ys.insert(r.y2);
xs
<int, int> xmap, ymap;
unordered_map{
int i = 0;
for (auto x: xs) xmap[x] = i++;
= 0;
i for (auto y: ys) ymap[y] = i++;
}
for (auto &r: Rs) {
.x1 = xmap[r.x1];
r.x2 = xmap[r.x2];
r.y1 = ymap[r.y1];
r.y2 = ymap[r.y2];
r}
(Rs.begin(), Rs.end(), [](const Rect &a, const Rect &b) {
sortreturn a.x1 < b.x1;
});
(ymap.size());
ST st(Rs.size());
UF uffor (auto &r: Rs) {
{ // topleft corner is special
auto qres = st.query(1, r.y1-1, r.y1);
if (qres.maxi > r.x1) uf.join(r.i, qres.c);
}
int qlo = r.y1, qhi = qlo + 1, maxq = r.y2;
while (qlo <= maxq) {
auto qres = st.query(1, qlo, min(qhi, maxq + 1));
if (qres.maxi >= r.x1) uf.join(r.i, qres.c);
= qhi;
qlo = st.find_next_change(qlo);
qhi }
int col = uf.find(r.i);
.update(1, r.y1, r.y2, {r.x2, col, col, col, false});
st}
return uf;
}
void solve() {
int Rn;
>> Rn;
cin
<Rect> Rs(Rn);
vectorfor (int i = 0; i < Rn; i++)
[i].i = i;
Rsfor (auto &r: Rs) {
>> r.x1 >> r.y1 >> r.x2 >> r.y2;
cin --r.x1, --r.y1;
}
auto uf = find_components(Rs);
<int> minxy(Rn, 2.1e9), maxx(Rn, -1), maxy(Rn, -1);
vectorfor (int i = 0; i < Rn; i++) {
int c = uf.find(i);
[c] = min(minxy[c], Rs[i].x1 + Rs[i].y1);
minxy[c] = max(maxx[c], Rs[i].x2 - 1);
maxx[c] = max(maxy[c], Rs[i].y2 - 1);
maxy}
int res = 0;
for (int i = 0; i < Rn; i++)
= max(res, 1 + maxx[i] + maxy[i] - minxy[i]);
res
<< res << '\n';
cout }
int main() {
.tie(0)->sync_with_stdio(0);
cin
int TC;
>> TC;
cin while (TC--) solve();
};
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ť.
-
Benjamín Skukálek
odoslané 18. november 2024 10:43erm what the sigma
Viliam Gottweis
👍
Tomáš Kodaj
Skibidi
Matúš Fülöp
E