Zadanie
Je to tak, už ste všetko zjedli. Je načase ísť
pracovať. Dnes sa na programovaní naučíte pracovať s Unixovým
terminálom. Ako prvý sa naučíte príkaz echo s veľmi
príznačným menom, ktorý ako ozvena vypíše svoje parametre na štandardný
výstup.
Potom však prídete k príkazom, ktoré začnú mať príliš krátke a
nezrozumiteľné mená. Začne to úplne nebadane, príkazmi cp,
mv a rm na kopírovanie, presúvanie a mazanie
súborov, ktorých mená vznikli jednoducho z CoPy, MoVe a ReMove. Neskôr
sa to začne zhoršovať (aj si hovoríte že ste mali jest pomalšie ale už
je neskoro). Potom prídete napríklad k príkazom chmod
(change mode), tar (tape archiver), tr
(transform) či chgrp (change group). Vy sa snažíte
sústrediť ale jediné čo vám chodí po rozume je jedlo a to že kedy bude
dalšie.
Nič si nezapamätáte, ale zato večeru už máte premyslenú. Idete radšej
programovať v jazyku C. Vôbec si však nepomôžete, práve
naopak. Tu sú mená funkcií nielen prikrátke, ale priam až kryptické.
Napríklad strrchr. Po dlhých minútach zamyslenia
(rozptyľuje vás predstava blížiacej sa večere) zistíte že to
str znamená string, r znamená
reverse a chr znamená character a
funkcia teda hľadá výskyt znaku v reťazci odzadu. Keďže vás to
premýšľanie začalo bolieť, rozhodli ste sa už druhýkrát zmeniť predmet
svojho záujmu. Vybrali ste si jazyk SNOBOL. Ale toto vám už nedalo.
Prečo práve SNOBOL? Ako k tomu došli? Vraj StriNg Oriented symBOlic
Language. To nijak nedáva zmysel. Napokon ste došli k tomu, že vo
všeobecnosti skratka vzniká tak, že len jednoducho vyberieme niektoré
znaky názvu, pričom zachováme ich poradie a zapíšeme ich za seba (kiežby
ste aj k tej večery už konečne došli). Z každého slova musíte vybrať
aspoň jedno písmeno. A žiadne ďalšie pravidlá zrejme neplatia. Vy by ste
si chceli vedieť overovať, či je daná skratka platná ale chcete to
automatizovať aby sa pri tom dalo jesť. Tak šup šup, už začína večera
ale vy nemôžte ísť kým nevylúštite zvyšok.
Úloha
Vašou úlohou je spočítať počet možností, ako mohla vzniknúť skratka \(S\) z názvu \(T\).
Aby skratka \(S\) mohla vzniknúť z textu \(T\), musí platiť, že písmenám v \(S\) vieme priradiť písmená v \(T\), pričom musíme zachovať poradie (teda ak priradíme písmenko z \(S\) nejakému písmenku z \(T\), ďalšiemu môžeme priradiť len nejaké napravo v \(T\)) a z každého slova \(T\) musíme použiť aspoň jedno písmeno.
Keďže počet možností je v niektorých prípadoch naozaj veľký, vypíšte len jeho zvyšok po delení \(10^9+7\).
Formát vstupu
Na prvom riadku sa nachádza číslo \(w\) – počet slov textu, z ktorého má byť utvorená skratka.
Na druhom riadku sa nachádza skratka - reťazec \(S\) a na treťom riadku sa nachádza text \(T\), ktorého skratka to má byť. Oba reťazce na vstupe pozostávajú len z malých písmen anglickej abecedy a medzier. Jednotlivé slová \(T\) sú oddelené práve jednou medzerou (medzier je teda \(w-1\)). Skratka žiadne medzery neobsahuje.
Formát výstupu
Vypíšte jedno číslo – počet možností, ako možno priradiť písmenám skratky \(S\) písmená textu \(T\) tak, aby tvorili platnú skratku, modulo \(10^9+7\). Ak to nie je možné, vypíšte \(0\).
Hodnotenie
Nech \(M\) je dĺžka skratky a \(N\) dĺžka textu. Sú \(4\) sady vstupov. V jednotlivých sadách platia nasledovné obmedzenia:
- \(M \leq 5\), \(N \leq 12\)
- \(M \leq 12\), \(N \leq 50\)
- \(M \leq 120\), \(N \leq 200\)
- \(M \leq 200\), \(N \leq 10000\)
Príklady
Input:
2
tar
tape archiver
Output:
4
Možnosti sú TApe aRchiver, TApe archiveR, Tape ARchiver a Tape ArchiveR.
Input:
4
snobol
string oriented symbolic language
Output:
1
Input:
3
strrchr
string reverse char
Output:
3
Input:
4
radar
radio detection and ranging
Output:
1
Jediná možnosť je RAdio Detection And Ranging. RADio detection And Ranging nie je platná možnosť, pretože zo slova detection sme nepoužili žiadne písmeno.
Input:
4
acm
academy of computer makers
Output:
0
Skratka je jednak prikrátka, a jednak neobsahuje žiadne z písmen slova of.
Zabudnime zatiaľ na obmedzujúcu podmienku v zadaní, že z každého slova musíme do skratky vybrať aspoň jedno písmeno. Keďže medzery sa v skratke nenachádzajú, bez tohto obmedzenia sa nás úloha pýta, koľkými spôsobmi možno dostať skratku – reťazec \(S\) ako podreťazec reťazca \(T\). Táto úloha sa dá riešiť jednoducho dynamickým programovaním.
Dynamické programovanie označuje techniku, kedy si problém vieme rozdeliť na menšie, výsledky pre menšie problémy si zapamätať a potom ich rovno používať, nepočítať ich znova. Naše podproblémy budú zodpovedať tú istú otázku, ale len pre menšie prefixy \(S\) a \(T\). Konkrétne: označíme si \(dp[i][j]\) počet spôsobov, ako vybrať z prvých \(i\) znakov \(T\) podpostupnosť zhodnú s prvými \(j\) znakmi \(S\). Tieto hodnoty budeme počítať od menších hodnôt \(i\) a \(j\) k väčším.
Ako vypočítať hodnotu \(dp[i][j]\), ak poznáme hodnoty pre menšie \(i\) a \(j\) Chceme teda získať prefix \(S\) dĺžky \(j\) ako podpostupnosť prefixu \(T\) dĺžky \(i\).
Ak je \(j=0\), potom zrejme existuje práve jedna možnosť, ako vybrať prázdnu podpostupnosť z niečoho, a to práve tá, že nevyberiem žiadny znak.
Ak je \(i=0\) a \(j \neq 0\), potom nie je možné z prázdneho prefixu \(T\) neprázdnu podpostupnosť.
Ak sa \(j\)-ty znak \(S\) zhoduje s \(i\)-tym znakom \(T\) , potom tento znak mohol byť z \(T\) vybraný a ostáva nám vybrať prvých \(j-1\) znakov \(S\) z o 1 kratšieho prefixu \(T\). Už vieme, koľkými spôsobmi to ide: \(dp[i-1][j-1]\). Inou možnosťou je, že sme tento znak z \(T\) nevybrali (nejaký rovnaký sme v \(T\) vybrať museli, ale skôr). To znamená, že sa pokúšame vybrať prvých \(j\) znakov \(S\) ako podpostupnosť z o 1 kratšieho prefixu \(T\), čo ide \(dp[i-1][j]\) spôsobmi. Každá možnosť konštrukcie podpostupnosti spadá do práve jedného z týchto prípadov, a teda bude započítaná práve raz.
Ak sa \(j\)-ty znak \(S\) nezhoduje s \(i\)-tym znakom \(T\), musia všetky spôsoby, ako vyrobiť žiadaný prefix, vyzerať tak, že používajú iba prvých \(i-1\) znakov \(T\).
Vďaka pamätaniu si predchádzajúcich hodnôt \(dp\) vieme každú ďalšiu hodnotu \(dp\) spočítať v konštantnom čase, teda celková časová zložitosť je úmerná počtu dvojíc \(i\) a \(j\), teda \(O(|S| \cdot |T|)\).
Ako by sme vedeli upraviť tento algoritmus, aby počítal len možnosti, kedy z každého slova zoberieme aspoň jedno písmeno?
Parametre \(i\), \(j\) dynamického programovania vlastne zodpovedajú akýmsi stavom. Stav je napríklad, že mám prefix \(S\) dĺžky \(j\) a prefix \(T\) dĺžky \(i\). V našej pôvodnej úlohe tiež môžeme nájsť nejaké stavy. Konkrétne je stavom to, že máme prefix \(S\) dĺžky \(j\), prefix \(T\) dĺžky \(i\) a buď sme už z aktuálneho slova (z toho, ktorému patrí \(i\)-ty znak \(T\)) vybrali nejaké písmeno, alebo nie. Týmto sa stavový priestor zväčší iba dvojnásobne, takže to časovú zložitosť nepokazí.
Nech teda \(dp[i][j][N]\) značí stav, kedy máme prefix \(S\) dĺžky \(j\) a prefix \(T\) dĺžky \(i\), pričom z aktuálneho slova sme ešte nepoužili žiaden znak a \(dp[i][j][P]\) značí, že sme už nejaký znak slova použili. \(N\) a \(P\) sú len symboly pre lepšie pochopenie. V implementácii môžeme použiť hodnoty \(0\) a \(1\) alebo mať dve polia, \(dp_N\) a \(dp_P\).
Ak je \(i\)-ty znak \(T\) písmeno, do stavu \(dp[i][j][N]\) sa dá dostať jedine zo stavu \(dp[i-1][j][N]\). Ak by sme totiž použili daný znak, dostali by sme sa do stavu s \(P\). To však nenastalo, a teda máme o 1 kratší prefix.
Do stavu \(dp[i][j][P]\) sme sa mohli dostať viacerými spôsobmi:
\(i\)-ty znak \(T\) sme nevybrali, museli sme vybrať nejaký znak predtým, čiže sme prišli zo stavu \(dp[i-1][j][P]\), \(i\)-ty znak \(T\) sme vybrali (samozrejme, len ak je zhodný s \(j\)-tym znakom \(S\)) a už predtým sme vybrali nejaký iný znak slova, teda sme prišli zo stavu \(dp[i-1][j-1][P]\), \(i\)-ty znak \(T\) sme vybrali a bol to prvý vybraný znak slova, žiaden predošlý sme nevybrali, prišli sme zo stavu \(dp[i-1][j-1][N]\). Ostáva poriešiť hranice medzi slovami. Tie sa jednoducho riešia vtedy, ak je aktuálny (\(i\)-ty) znak \(T\) medzera. Vtedy začína nové slovo, takže počet spôsobov, ako mať vybraný nejaký znak nového (aktuálneho) slova (\(dp[i][j][P]\)) je nula. Počet spôsobov, ako nemať vybraný znak nového slova (\(dp[i][j][N]\)) je rovný \(dp[i-1][j][P]\), teda hodnota pre o 1 kratší prefix \(T\) s použitím nejakého znaku posledného slova. Všimnime si, že nezapočítavame \(dp[i-1][j][N]\), pretože to zodpovedá situácii, kedy sme z predošlého slova nič nevybrali.
Počet možností pre skúmaný stav získame ako súčet počtov možností pre stavy, z ktorých sa do aktuálneho vieme dostať.
Odpoveďou je potom odpoveď pre celý reťazec \(S\) a celý reťazec \(T\), pričom aj z posledného slova sme museli nejaké písmeno vybrať. Je to teda hodnota \(dp[|T|][|S|][P]\).
Časová zložitosť riešenia je stále \(O(|S| \cdot |T|)\), pretože máme len dvakrát viac hodnôt ako v zjednodušenom prípade a každú hodnotu vieme vypočítať v konštantnom čase.
Pamäťová zložitosť je rovnaká, ale vieme dosiahnuť aj zložitosť \(O(|S|)\), pretože pri výpočte hodnôt \(dp[i]\) nám stačí poznať hodnoty \(dp[i-1]\), teda stačí si pamätať dva stĺpce, predošlý a aktuálny, tabuľky \(dp\).
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ť.