Zadanie
Ako isto viete, nie každá kukurica je rovnaká. Niektoré odrody sú šťavnaté, iné majú zas obrovské obilky. Podaktoré sú dokonca aj dúhové!!! A povedzte mi, kto by už na svojom poli nechcel mať takúto biodiverzitu? Presne takto každoročne rozmýšľa aj Adam. Doteraz to mal pomerne jednoduché, na dedinskej burze vedel za výhodnú cenu jedného Trojkenu vykonať ľubovoľnú z barterových ponúk. Tento rok sa ale Americkým vedcom podarilo nevídané. Dokážu upravovať genetický kód kukurice, skoro úplne, ako sa im zachce1! Dokonca si otvorili aj pobočku v Adamovej rodnej dedine, Kombajnovských Klasoviciach. Za svoje služby si neúčtujú nič menej ako jeden Trojken. Pomôžte Adamovi zistiť, koľko najmenej Trojkenov musí zaplatiť za to, aby z jeho nudnej odrody kukurice získal nejakú zaujímavejšiu.
Úloha
Každá odroda kukurice sa dá jednoznačne určiť jej genetickým kódom
ako reťazec, ktorý sa skladá len z písmen L
a
R
(môže byť aj prázdny). Za jeden Trojken dokáže genetické
laboratórium v Kombajnovských Klasoviciach odstrániť gén na konci
genetického kódu nejakej odrody alebo pridať na koniec ľubovoľný z génov
L
a R
. Na burze taktiež existuje \(n\) barterových ponúk, ktoré sa vzťahujú na
odrody \(a_i\) a \(b_i\). Za vykonanie jednej výmeny zaplatí
Adam jednej Trojken a môže vymeniť odrodu \(a_i\) za odrodu \(b_i\) alebo naopak. Adama by zaujímalo
\(q\) scenárov, v ktorých začína s
odrodou s genómom \(z_i\) a chcel by
skončiť s genómom \(k_i\). Zistite,
koľko najmenej Trojkenov ho to môže stáť.
Formát vstupu
V prvom riadku vstupu sú čísla \(n\) a \(q\) \((0 \leq n \leq 10^3, 1 \leq q \leq 10^4)\) udávajúce počet barterových ponúk na burze a počet scenárov, na ktoré Adama zaujíma odpoveď. V ďalších \(n\) riadkoch sa na každom nachádzajú \(2\) reťazce \(a_i\) a \(b_i\), ktoré hovoria, že odroda s genómom \(a_i\) sa dá vymeniť za odrodu s genómom \(b_i\) za zaplatenie \(1\) Trojkenu a naopak. Nasleduje \(q\) riadkov, na každom z nich sa nachádzajú \(2\) reťazce \(z_i\) a \(k_i\). Pre každý riadok vypíšte, koľko najmenej Trojkenov treba zaplatiť na to, aby Adam z odrody \(z_i\) dostal odrodu \(k_i\).
Všetky reťazce genómov na vstupe sa skladajú len z písmen
L
a R
a obsahujú aspoň jedno písmeno. Existuje
ale aj kukurica s prázdnym genómom.
Obmedzenia
Označme si \(D\) najväčšiu dĺžku reťazca na vstupe. Potom pre všetky sady platí, že \((n+q) \cdot D \leq 2 \cdot 10^6\).
V jednotlivých sadách navyše platia nasledujúce obmedzenia:
Sada | 1 | 2 | 3 | 4 |
---|---|---|---|---|
\(0 \leq n \leq\) | \(10\) | \(100\) | \(1000\) | \(1000\) |
\(1 \leq q \leq\) | \(10\) | \(100\) | \(500\) | \(10\,000\) |
\(1 \leq D \leq\) | \(10\) | \(100\) | \(1000\) | \(1000\) |
Keďže je vstup veľmi veľký, odporúčame v jazyku C++
použiť rýchle načítavanie vstupu
cin.tie(0)->sync_with_stdio(0)
a namiesto
endl
používať '\n'
.
Formát výstupu
Vypíšte \(q\) riadkov, na \(i\)-tom z nich, koľko najmenej Trojkenov treba zaplatiť na to, aby Adam získal zo začiatočnej odrody koncovú.
Príklady
Input:
2 2
R LL
RR RLR
LLL RLR
RRL RLR
Output:
4
2
V prvom scenári jedna z najlacnejších možností môže vyzerať
napríklad takto. Najprv odstránime posledný gén, čím dostaneme odrodu
LL
. Tú dokážeme za jeden Trojken zmeniť za odrodu
R
. Potom už len stačí pridať postupne gény L
a
R
. Celkovo zaplatíme 4 Trojkeny. V druhom scenári vyzerá
najlacnejšia možnosť takto. Najprv odstránime posledný gén a potom
vymeníme odrodu RR
za odrodu RLR
. Takto
zaplatíme 2 Trojkeny.
Input:
0 1
LLR LR
Output:
3
Je zbytočné na začiatku pridávať nejaké gény na koniec, keďže
neexistujú žiadne barterové ponuky. Preto je optimálne 2-krát odstrániť
posledný gén a raz pridať gén R
na koniec, čo stojí spolu 3
Trojkeny.
Keďže sa na vstupe dokopy nachádza \(O((n+q)D)\) znakov, tak časová zložitosť načítavania vstupu bude pri všetkých riešeniach \(O((n+q)D)\), preto ju už ďalej pri odhade časovej zložitosti nebudeme spomínať.
Ako si to vôbec vieme predstaviť?
Celú úlohy si vieme reprezentovať ako hľadanie najkratšej cesty v nejakom neorientovanom grafe. Vrcholy tohto grafu budú jednotlivé odrody a hrany budú medzi odrodami \(a\) a \(b\) práve vtedy, ak sa dá zmeniť odroda \(a\) za odrodu \(b\) zaplatením \(1\) trojkenu. Hrany v tomto grafe si vieme rozdeliť na \(2\) skupiny: hrany, ktoré vznikli kvôli barterovým ponukám a hrany, ktoré vznikli kvôli laboratóriu.
Hrany, ktoré vznikli kvôli laboratóriu (označme si ich
stromové) sa v rôznych vstupoch nelíšia. A čo viac, keď sa
pozrieme na graf, ktorý obsahuje len tieto hrany, tak dostaneme binárny
strom, ktorý je zakorenený vo vrchole reprezentujúcom prázdny genóm.
Ľavý syn nejakého vrchola reprezentuje genóm, ktorý vznikol pridaním
L
na koniec aktuálneho genómu a pravý syn reprezentuje
genóm, ktorý vznikol pridaním R
.
BFS
Najprv si môžeme všimnúť, že sa nikdy neoplatí mať odrodu, ktorej genóm má dĺžku väčšiu ako \(D\). Teda nám stačí vygenerovať graf obsahujúci všetkých \(O(2^D)\) vrcholov. Keďže je najdlhšia dĺžka reťazca v prvej podúlohe pomerne malá, tak nám stačí si ho celý zapamätať a pre každú otázku pomocou prehľadávania do šírky nájsť najkratšiu cestu.
Náš graf má \(O(2^D)\) vrcholov a dokopy \(O(2^D + n)\) hrán, teda celková časová zložitosť pre jednu otázku bude \(O(2^D + n)\).
Časová zložitosť predpočitania bude \(O(nD \log{n})\) alebo \(O(2^D)\) v závislosti od implementácie.
Zbytočné a zaujímavé vrcholy
Ak by sa v úlohe nenachádzali žiadne barterové ponuky, tak cesta medzi ľubovoľnými vrcholmi \(a\) a \(b\) by vyzerala pomerne jednoducho. Najprv pôjdeme z vrcholu \(a\) smerom hore do ich najnižšieho spoločného predka (alebo aj v skratke LCA) a potom z tohto predka smerom dole do vrcholu \(b\).
Ako bude vyzerať nejaká cesta z vrcholu \(a\) do vrcholu \(b\) v grafe, kde sú aj hrany reprezentujúce barterové ponuky? Povedzme, že postupne použijeme barterové hrany \(b_1, b_2, ..., b_k\). Potom medzi koncom jednej a začiatkom ďalšej hrany bude cesta prechádzať len cez stromové hrany, teda pôjde hore do ich najnižšieho spoločného predka a potom dole.
Označme si teda vrcholy, ktoré sú konce nejakej barterovej hrany alebo sú súčasťou nejakej otázky ako zaujímavé. Ako sme si vyššie ukázali, tak nám stačí medzi každou dvojicou zaujímavých vrcholov vedieť zistiť najkratšiu cestu, ktorá prechádza len cez stromové hrany. Táto cesta pôjde cez ich najmenšieho spoločného predka. Keďže najnižší spoločný predok dvoch zaujímavých vrcholov sa nachádza niekde na ceste z jedného vrchola do koreňa, tak optimálna cesta medzi zaujímavými vrcholmi bude určite prechádzať len cez vrcholy, ktoré sú predkami aspoň jedného z nich.
Tu sa nám črtá trošku lepšie riešenie. Budeme sa pozerať na graf, ktorý obsahuje len zaujímavé vrcholy a vrcholy, ktoré sú predkami nejakého zaujímavého vrchola. Koľko ich bude? Každý vrchol je v hĺbke najviac \(D\), teda má nad sebou najviac \(D\) vrcholov. Takýto graf teda bude mať \(O((n + q)D)\) vrcholov a \(O((n + q)D)\) hrán. Ak v ňom budeme zas hľadať cesty pomocou prehľadávania do šírky, tak na jednu otázku vieme odpovedať v čase \(O((n + q)D)\) s predpočítaním v čase \(O(nD)\).
Ešte ich vieme nejaké odstrániť
Povedzme, že si medzi zaujímavé vrcholy pridáme LCA všetkých dvojíc na začiatku zaujímavých vrcholov. Keďže najkratšie cesty po stromových hranách idú len hore po LCA, tak pre každý zaujímavý vrchol nám stačí len pridať hranu medzi ním a prvým zaujímavým vrcholom, ktorý je nad ním. Takto sa vieme dostať z jedného zaujímavého vrcholu do druhého po stromových hranách rovnakou cestou, ako predtým, teda sme týmto nič nepokazili.
Koľko vrcholov ale musíme pridať? Predstavme si, že máme na začiatku v poli \(k\) zaujímavých vrcholov. Zoberme si teraz \(2\) vrcholy \(a\) a \(b\), ktorých LCA (označme si ho \(L\)) sa nachádza najhlbšie v strome. V podstrome vrcholu \(L\) sa nemôže nachádzať žiaden iný vrchol (okrem \(a\) a $b) z nášho poľa, lebo by to bolo v spore s tým, že \(L\) je najhlbšie LCA vrcholov z poľa. Preto pre ľubovoľný iný vrchol \(c\) bude platiť, že \(LCA(c,a) = LCA(c,b) = LCA(c,L)\). Preto môžeme \(a\) a \(b\) z poľa vymazať a vložiť tam \(L\). V každom takomto kroku sa zníži počet prvkov v poli o \(1\), teda nových zaujímavých vrcholov môžeme pridať najviac \(O(k)\). V asymptotickej časovej zložitosti sa nám teda nič nezhorší tým, keď tieto vrcholy pridáme medzi zaujímavé!
Vieme teda, že zaujímavých vrcholov bude spolu najviac \(O(n+q)\) a hrán medzi nimi tiež \(O(n+q)\). Keď sa nám už nejak podarí vytvoriť tento graf, tak pre každú otázku len stačí pomocou Dijkstry (lebo hrany teraz nemajú všetky dĺžku \(1\)) zistiť vzdialenosť konca a začiatku v čase \(O((n+q) \log{(n+q)}\).
Predpočítanie
Vrcholy budeme vkladať do písmenkového stromu (trie). Začneme so všetkými genómami v koreni. Keď sme v nejakom vrchole trie s nejakou množinou genómov, tak si ju vieme rozdeliť v lineárnom čase od ich počtu na tie, ktoré končia v tomto vrchole, ktoré idú do ľavého podstromu a ktoré do pravého. Kedy je vrchol trie zaujímavý? Ak sa vo vrchole končí nejaký genóm, tak určite je zaujímavý, lebo je to koniec nejakej hrany alebo súvisí z nejakou otázkou. Ak nejaké vrcholy patria do jeho ľavého aj pravého podstromu, tak bude tiež zaujímavý, keďže bude LCA nejakej dvojice vrcholov. Inak je nezaujímavý. Keď si takto rekurzívne spočítame všetky zaujímavé vrcholy, tak sa dá aj jednoducho zistiť, ako vyzerajú hrany medzi nimi. Vždy si len stačí do rekurzívneho volania posielať aj rodiča aktuálneho vrchola a jeho hĺbku.
Toto celé vieme predpočítať v čase \(O((n+q)D)\), lebo v každej z \(D\) vrstiev trie sa nachádza v súčte najviac \(n+q\) prvkov a v každej úrovni vykonáme prácu priamo úmernú počtu prvkov v nej.
Rýchlejšie otázky
V predošlom riešení sme si medzi zaujímavé vrcholy pridali všetkých \(O(q)\) vrcholov, ktoré sa vzťahujú na otázky. Každý z nich ale využijeme len v tej jednej otázke, ku ktorej sa vzťahujú a inak sú zbytočné. Skúsme teda vrcholy \(z_i\) a \(k_i\) pridať medzi zaujímavé len počas danej otázky a potom ich odstrániť.
Ako teraz zistiť odpovede na jednotlivé otázky? Keď budeme hľadať nejaký genóm \(x\) v našej trie zaujímavých vrcholov, tak niekedy narazíme na hranu, na ktorej sa prestane v nejakom znaku zhodovať (ak taká neexistuje, tak to znamená, že \(x\) je v trii a netreba riešiť žiadne špeciálne prípady). Nech táto hrana ide z vrcholu \(a\) do vrcholu \(b\). Teraz nám stačí pridať hrany z \(x\) do \(a\) a z \(x\) do \(b\) so správnou dĺžkou a máme graf, ktorý správne reprezentuje aktuálnu situáciu. Keď toto spravíme pre \(z_i\) a \(k_i\), tak len znova stačí pomocou Dijkstry zistiť ich vzdialenosť.
Keďže graf zaujímavých vrcholov teraz bude mať len \(O(n)\) vrcholov, tak odpovedať na otázku vieme v čase \(O(n \log{n})\). Predpočítanie sa nám nijak nezhoršilo. Finálna časová zložitosť je teda \(O(nD)\) na predpočítanie a \(O(n \log{n})\) na otázku.
#include <bits/stdc++.h>
using namespace std;
#define f first
#define s second
<string> zaujimave; // zoznam povodnych zaujimavych genomov
vector<vector<pair<int, int>>> sus; // zoznam susedov vrchola v tvare dvojica {id, dlzka}
vector// nove vrcholy dostanu id postupne pocas behu programu
struct node {
*L, *R;
node = "L", R_edge = "R";
string L_edge int id;
(int ID = -1) {
node= R = nullptr;
L = ID;
id }
};
// Funkcia, ktora spocita vzdialenost genomov A a B
// |A| + |B| - 2 LCP(A,B)
int tree_dist(string& a, string& b) {
int ans = a.size() + b.size();
int i = 0;
while (i < a.size() && i < b.size() && a[i] == b[i++])
-= 2;
ans return ans;
}
// Bucket sort
// Roztriedi vrcholy z pola cur podla toho, ci maju na dalsom mieste 'L', 'R' alebo uz koncia
void b_sort(vector<int>& cur, int d, vector<int>& L, vector<int>& R, vector<int>& koniec) {
for (int i : cur) {
if (zaujimave[i].size() <= d)
.push_back(i);
koniecelse if (zaujimave[i][d] == 'L')
.push_back(i);
Lelse
.push_back(i);
R}
}
// Zisti, ktore treba pridat navyse a vytvory z nich ohodnoteny graf
// cur su indexy aktualnych zaujimavych genomov, ktore sa nachadzaju v podstrome aktualneho vrchola
// edge je postupnost L/R, ktore su na hrane z aktualneho vrchola do jeho otca
// d je aktualna hlbka
// par je index otca
// d_par je hlbka otca
* generate_graph(vector<int>& cur, string& edge, int d, int par, int d_par) {
node// ak sa v podstrome aktualneho vrchola nenachadzaju ziadne zaujimave genomy,
// tak tento vrchol nepotrebujeme
if (cur.size() == 0) {
= "";
edge return nullptr;
}
* ret = new node();
node
// Budeme prechadzat po strome az kym nenarazime na zaujimavy vrchol
while (1) {
<int> L, R, koniec;
vector
// Rozdelime si zaujimave vrcholy podla toho, do ktoreho podstromu patria
(cur, d, L, R, koniec);
b_sort
// Pridame nulove hrany medzi rovnake koncove vrcholy
for (int i = 1; i < koniec.size(); i++) {
[koniec[i - 1]].push_back({koniec[i], 0});
sus[koniec[i]].push_back({koniec[i - 1], 0});
sus}
// Ak sa niektory zo zaujimavych vrcholov konci v tomto vrchole alebo je tento
// vrchol LCA nejakej dvojice, tak je zaujimavy
if ((L.size() && R.size()) || koniec.size()) {
// spocitame si index aktualneho vrchola
int moj_ind = koniec.size() ? koniec[0] : sus.size();
->id = moj_ind;
ret
// Ak pridavame novy vrchol
if (koniec.size() == 0)
.push_back({});
sus
// Pridame hranu medzi aktualnym vrcholom a otcom s dlzkou d - d_par
[moj_ind].push_back({par, d - d_par});
sus[par].push_back({moj_ind, d - d_par});
sus
// Rekurzivne vytvorime pravy a lavy podstrom aktualneho vrchola
->L = generate_graph(L, ret->L_edge, d + 1, moj_ind, d);
ret->R = generate_graph(R, ret->R_edge, d + 1, moj_ind, d);
retreturn ret;
}
// Aktualny vrchol nie je zaujimavy, teda treba zvysit hlbku a pridat na koniec hrany do
// otca spravny znak
if (L.size())
+= 'L';
edge else
+= 'R';
edge ++;
d}
}
// Funkcia, ktora najde hranu, na ktorej sa genom s oddeluje z trie. Vracia
// dvojicu {{id vrchneho, vzdialenost k vrchnemu}, {id spodneho, vzdialenost k spodnemu}}
<pair<int, int>, pair<int, int>> find(string& s, node* cur, int d = 0) {
pair;
string match* next;
node
// Zistime, do ktoreho podstromu patri genom s
if (s[d] == 'L') {
= cur->L;
next = cur->L_edge;
match } else {
= cur->R;
next = cur->R_edge;
match }
// Postupne skontrolujeme znaky na hrane do tohto podstromu
for (int i = 0; i < match.size(); i++) {
// Genom s moze skoncit niekde v strede tejto hrany
if (s.size() <= d + i) {
// Vzdialenost k vrchnemu vrcholu je pocet prejdenych znakov (i)
// Vzdialenost k spodnemu vrcholu je dlzka hrany - pocet prejdenych znakov
return {{cur->id, i}, {next->id, match.size() - i}};
}
// Genom sa moze prestat zhodovat niekde v strede hrany
if (match[i] != s[d + i]) {
// Vzdialenost od konca genomu s k vrcholu, v ktorom sa prestane zhodovat s triou
int dist = s.size() - d - i;
// Vzdialenosti k vrchnemu a spodnemu vrcholu spocitame rovnako, len pridame
// vzdialenost k vrcholu, kde sa s oddeluje od trie
return {{cur->id, dist + i}, {next->id, dist + match.size() - i}};
}
}
// Neexistuje dalsi vrchol, genom s sa teda prestal zhodovat s triou v nejakom liste
if (next == nullptr)
return {{cur->id, s.size() - d - match.size()}, {0, 1 << 30}};
// Rekurzivne sa zavolame na dalsi vrchol
return find(s, next, d + match.size());
}
int main() {
.tie(0)->sync_with_stdio(0);
cinint n, q;
>> n >> q;
cin .resize(2 * n);
susfor (int i = 0; i < n; i++) {
, b;
string a>> a >> b;
cin .push_back(a);
zaujimave.push_back(b);
zaujimave
// Pridame barterove hrany
[2 * i].push_back({2 * i + 1, 1});
sus[2 * i + 1].push_back({2 * i, 1});
sus}
<int> cur(2 * n);
vector// iota naplni vektor cur postupne cislami 0, 1, 2, ...
(cur.begin(), cur.end(), 0);
iota
// Vytvorime si koren trie
* tr = new node();
node
<int> R, L, koniec;
vector(cur, 0, L, R, koniec);
b_sorttr->id = sus.size();
.push_back({});
sus
// Vytvorime lavy a pravy podstrom korena
tr->L = generate_graph(L, tr->L_edge, 1, tr->id, 0);
tr->R = generate_graph(R, tr->R_edge, 1, tr->id, 0);
// Pre kazdu otazku spustime Dijkstru na vytvorenom grafe
for (int i = 0; i < q; i++) {
, b;
string a>> a >> b;
cin // Najdeme hranu, kde hrany, kde sa a a b v trii prestavaju zhodovat
auto a_edge = find(a, tr), b_edge = find(b, tr);
// Pole vzdialenosti, -1 znamena, ze sme este vrchol nepreskumali
<int> dist(sus.size(), -1);
vector
<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;
priority_queue
// Do prioritnej fronty pridame horny aj spodny vrchol hrany, v ktorom
// sa vrchol a prestava zhodovat
.push({a_edge.f.s, a_edge.f.f});
que.push({a_edge.s.s, a_edge.s.f});
que
// Obycajny Dijkstrov algoritmus
while (!que.empty()) {
int d, a;
(d, a) = que.top();
tie.pop();
queif (dist[a] != -1)
continue;
[a] = d;
distfor (auto u : sus[a]) {
if (dist[u.f] == -1)
.push({u.s + d, u.f});
que}
}
// Odpoved je minimum zo vzdialenosti a, b a najkratsej cestu v strome
int ans = tree_dist(a, b);
// Skusime najprv horny vrchol hrany, kde sa odpaja b a potom spodny vrchol tejto hrany
= min(ans, dist[b_edge.f.f] + b_edge.f.s);
ans = min(ans, dist[b_edge.s.f] + b_edge.s.s);
ans
<< ans << "\n";
cout }
}
Ešte rýchlejšie otázky
K zrýchleniu otázok nám stačí jednoduché pozorovanie. Keďže náš strom genómov má hĺbku najviac \(D\), tak maximálna možná vzdialenosť dvoch vrcholov bude \(2D\). Pri Dijkstre teda nemusíme používať prioritnú frontu, stačí nám pole obyčajných front veľkosti \(2D\). Vrcholy budeme vyberať zo začiatku prvej neprázdnej fronty a keď vkladáme nový vrchol vzdialený od počiatočného \(i\), tak ho vložíme do \(i\)-tej fronty. Takýmto spôsobom prejdeme vrcholy od najbližších po najvzdialenejšie (teda rovnako, ako pri použití prioritnej fronty). Navyše vkladať aj vyberať1 z tejto dátovej štruktúry vieme v konštantnom čase.
Pomocou tejto optimalizácie vieme odpovedať na otázky v čase \(O(n)\) a časová zložitosť predpočítania sa nijak nezhoršila. Výsledná časová zložitosť je teda \(O((n + q)D + qn)\).
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ť.