Zoznam úloh

8. Akútna výstavba

Zadanie

Koho by už len zaujímala diaľnica, aj tak všetci vieme, že to hlavné, čo sa v okolí Kokavy nad Rimavicou bude stavať je predsa masivácky mrakodrap. Poďme sa teda za ľubozvučných zvukov výstavby pozrieť na aktuálnu situáciu na stavenisku.

Asi si viete presne predstaviť, ako to na takej stavbe vyzerá. Väčšina robotníkov len tak postáva okolo a len zopár vyvolených pracuje. A aby to nebolo málo, tak ešte chodia aj na pravidelné prestávky. Stavbyvedúceho Dušana už samozrejme nebaví toto všetko sledovať. To ale nemení nič na tom, že stále potrebuje svojich nadriadených informovať o postupe stavby. Na to ale potrebuje vždy vedieť, koľko chlapov aktuálne pracuje.

Presne preto povolal vás, lacnopracujúcich brigádnikov, aby ste mu tieto dáta pomohli získať a on si naďalej mohol v kľude užívať túto malebnú scenériu.

Úloha

Na vstupe dostanete popis jednotlivých Dušanových robotníkov. Kým je $i$-ty robotník prítomný na stavenisku, najprv prvých $a_i$ hodín obdivuje prácu ostatných, a následne ďalších $b_i$ hodín maká. Potom zase $a_i$ hodín obdivuje a $b_i$ hodín maká, a takto sa to pravidelne strieda, kým zas neodíde zo staveniska preč dať si pauzu.

Každú hodinu nastane jedna z dvoch situácií. Buď nejaký robotník príde z pauzy na stavenisko (a začne hneď svoj pracovný cyklus tým, že bude obdivovať ostatných) alebo sa naopak jeden robotník na stavenisku rozhodne, že už toho má dosť, a ide pauzovať.

Dušana by po každej takejto zmene zaujímalo, koľko robotníkov počas nasledujúcej hodiny na výstavbe aktívne pracuje.

Na začiatku sú všetci robotníci na pauze.

Formát vstupu

Na prvom riadku vstupu sa nachádzajú čísla $n$ a $m$ ($1 \le n,m \le 2 \cdot 10^5$)1, reprezentujúce počet robotníkov a počet hodín, ktoré prebieha stavba.

Nasleduje $n$ riadkov, na každom z nich sú dve čísla $a_i$ a $b_i$ ($1 \le a_i, b_i \le 2 \cdot 10^5$), ktoré popisujú pracovný cyklus $i$-teho robotníka. Tento robotník najprv $a_i$ hodín obdivuje prácu ostatných a potom $b_i$ hodín aktívne pracuje. Toto sa opakuje, až kým znova nepôjde pauzovať.

Na ďalších $m$ riadkoch sa nachádzajú dve čísla $op$ a $k$ ($0 \le k < n$), ktoré hovoria o tom, čo sa v danú hodinu stalo. Ak $op = 1$, tak sa robotník číslo $k$ vrátil z pauzy. Ak $op = -1$, tak naopak robotník číslo $k$ práve odchádza na pauzu. Ak má nejaký robotník prísť z pauzy, tak doteraz určite bol na pauze a ak má naopak odísť na pauzu, tak doteraz určite bol na stavenisku. Robotníkov číslujeme od $0$.

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

Sada 1 2 3, 4
$1 \leq n,m \leq$ $10\,000$ $200\,000$ $200\,000$
$1 \leq a_i, b_i \leq$ $250$ $250$ $200\,000$

Formát výstupu

Pre každú hodinu (teda tesne po tom, čo nejaký robotník odíde alebo príde) vypíšte na samostatný riadok jedno číslo, počet robotníkov, ktorí v túto hodinu aktívne pracujú.

Upozornenie

Odporúčame použiť rýchle načítavanie vstupu v jazyku C++. Teda namiesto std::endl použiť \n a vypnúť sychronizáciu s C-čkovým IO pomocou cin.tie(0)->sync_with_stdio(0).

Príklad

Input:

3 6
1 1
6 3
2 1
1 0
-1 0
1 0
1 2
1 1
-1 1

Output:

0
0
0
1
0
2
  • Prvá hodina začne príchodom robotníka 0, ktorý ju strávi obdivovaním rozostavaného mrakodrapu a osamelou meditáciou o význame stavby pre rozvoj a blahobyt dediny.

  • V druhej hodine bude po jeho odchode stavenisko opustené, teda aj teraz bude aktívne pracovať 0 robotníkov.

  • V tretej hodine sa vráti, ale aj keď už predtým hodinu obdivoval, začne zase od začiatku obdivovaním.

  • Vo štvrtej hodine konečne prvýkrát začne robotník 0 aktívne pracovať a robotník 2 obdivuje, ako mu to ide.

  • V piatej hodine ale znova prestane a robotníci 1 a 2 tiež len obdivujú, čo všetko už stihol postaviť.

  • Nakoniec v šiestej hodine budú aktívne pracovať robotníci 0 aj 2. Aký úspešný deň!


  1. Vedúcemu odborov Fipovi by sa takéto neľudsky dlhé pracovné hodiny asi nepáčili, ale mrakodrap treba dostavať čo najrýchlejšie… 

Označme si najprv pre každého robotníka jeho periódu $P_i = a_i + b_i$.

Jednoduché riešenie

Jedno z priamočiarych riešení by mohlo vyzerať napríklad takto. Vytvoríme si pole W dĺžky $N$, kde hodnota W[i] bude počet robotníkov, ktorý pracujú v $i$-tej hodine. Keď nejaký robotník príde, tak zvýšime počet pracujúcich robotníkov pre každú z hodín, v ktorých bude pracovať. Naopak, ak odíde, tak znížime počet pracujúcich robotníkov v týchto hodinách.

Toto riešenie bude mať časovú zložitosť $O(N^2)$, lebo kvôli každému príchodu alebo odchodu môžeme zmeniť až $O(N)$ prvkov poľa W.

Ľahký robotníci

Poďme sa najprv zamerať na riešenie prvých dvoch sád. Hneď si môžeme všimnúť, že zásadný rozdiel medzi týmito sadami a ostatnými je, že $\max{P_i}$ je najviac $500$. Rozdeľme si teda všetkých robotníkov do skupín podľa ich periódy. Teraz pre každú z týchto skupín môžeme robiť to isté, čo pri predošlom riešení. Pre každú zo skupín už ale nepotrebujeme pole veľkosti až $N$. Pre skupinu s periódou $P$ nám stačí pole veľkosti $P$ (lebo počet pracujúcich robotníkov tejto skupiny v čase $t$ bude rovnaký, ako počet pracujúcich robotníkov v čase $t + P$).

Pre každú skupinu robotníkov si teda spravíme pole dĺžky $P$, kde na $i$-tom indexe bude počet robotníkov, ktorí pracujú v hodine $i$ modulo $P$. Pri príchode alebo odchode robotníka nám stačí prejsť celé toto pole a ku každému zvyšku, v ktorom by tento robotník pracoval, pripočítame (respektíve odčitame) $1$. Keď chceme zistiť, koľko robotníkov pracuje v čase $t$, tak sa stačí pre každú skupinu pozrieť na hodinu so správnym zvyškom ($t % P$) a tieto hodnoty sčítať.

Pridanie alebo odobratie jedného robotníka teda zaberie $O(\max{P_i})$ času a zisťovanie odpovede zaberie taktiež $O(\max{P_i})$ času. Toto riešenie má teda časovú zložitosť $O(N \max{P_i})$.

Ľahkí a ťažkí robotníci

Už vieme pomerne efektívne riešiť situáciu, v ktorej majú všetci robotníci malú periódu. Poďme teda využiť jeden z klasických trikov, ktorý sa dá v takejto situácií využiť: rozdelíme si robotníkov na “ľahkých” a “ťažkých” a každú z týchto skupín budeme riešiť separátne. Ľahký robotníci budú tý, ktorých perióda je menšia ako $\sqrt{N}$. Pre nich už úlohu vieme riešiť efektívne v čase $O(N \sqrt{N})$.

Ťažký robotníci

Čo ale spraviť s ťažkými robotníkmi? Môžeme využiť to, že počet periód, ktoré stihnú do konca šichty spraviť, bude malý. Presnejšie to bude najviac $\frac{N}{\sqrt{N}} = \sqrt{N}$ periód.

Pri každom príchode alebo odchode ťažkého robotníka teda potrebujeme zmeniť hodnoty $O(\sqrt{N})$ intervalov. Stačí už len vymyslieť, ako toto robiť efektívne. Mohli robiť pomocou intervalového stromu, ale dá sa to aj rýchlejšie pekným trikom s prefixovými súčtami.

Budeme si pamätať pole A dĺžky $N$. Keď chceme k intervalu $[a,b]$ pripočítať $1$, tak k A[a] pripočítame $1$ a od A[b+1] odčítame $1$. Týmto sme dosiahli to, že sme v poli prefixových súčtov poľa A na intervale $[a,b]$ pripočítali $1$. Ak chceme odčítavať, tak to bude fungovať rovnako, len treba zmenit znamienka.

Počet aktuálne pracujúcich ťažkých robotníkov teda môžeme počítať nasledovne. Keď príde alebo odíde nejaký ťažký robotník, tak si do poľa A na $O(\sqrt{N})$ pozícií poznačíme $\pm 1$. Tiež si postupne v pomocnej premennej budeme počítať prefixový súčet tohto poľa, čo bude vlastne hľadaný počet pracujúcich ťažkých robotníkov.

Odchody a príchody ťažkých robotníkov teda vieme riešiť v čase $O(\sqrt{N})$ a dopočítať, koľko ich pracuje v aktuálnom čase vieme v konštantnom čase.

Optimálne riešenie

Na vyriešenie celej úlohy nám už len stačí všetko skonbinovať dokopy. Na začiatku hodiny vyriešime príchod alebo odchod robotníka (podľa toho, či je ľahký alebo ťažký). Potom už len stačí zvlášť dopočíť počet pracujúcich ľahkých robotníkov a dopočítať aktuálny prefixový súčet pre ťažkých robotníkov, čím dostaneme finálnu odpoveď pre túto hodinu.

Celková časová zložitosť je $O(N \sqrt{N})$, lebo vyriešiť príchod a odchod robotníka vieme v čase $O(\sqrt{N})$ (bez ohľadu na to, čí je ľahký alebo ťažký) a dopočítať počet pracujúcich ľahkých robotníkov vieme tiež v čase $O(\sqrt{N})$. Počet pracujúcich ťažkých robotníkov počitame postupne pomocou prefixových súčtov v konštantom čase.

Pamäťová zložitosť je $O(N)$, lebo pre ťažkých robotníkov si pamätáme pole dĺžky $N$ a pre ľahkých robotníkov $\sqrt{N}$ polí dĺžky $O(\sqrt{N})$.

#include<bits/stdc++.h>
using namespace std;
#define a first
#define b second

void tazky_prichod(int cas, vector<int>& tazky, pair<int,int> robotnik){
    int perioda = robotnik.a + robotnik.b;

    // zmeny z pracovania na obdivovanie
    for(int i = cas + perioda;i < tazky.size();i += perioda)
        tazky[i] -= 1;

    // zmeny z obdivovania na pracovanie
    for(int i = cas + robotnik.a;i < tazky.size();i += perioda)
        tazky[i] += 1;
}

void tazky_odchod(int zaciatok, int cas, vector<int>& tazky, pair<int,int> robotnik){
    int perioda = robotnik.a + robotnik.b;

    // ak aktualne pracuje, tak ho treba hned odstranit
    if((cas - zaciatok)%perioda >= robotnik.a){
        tazky[cas] -= 1;
    }

    // zmeny z pracovania na obdivovanie
    for(int i = zaciatok + perioda;i < tazky.size();i += perioda)if(i > cas)
        tazky[i] += 1;

    // zmeny z obdivovania na pracovanie
    for(int i = zaciatok + robotnik.a;i < tazky.size();i += perioda)if(i > cas)
        tazky[i] -= 1;
}

void lahky_prichod(int cas, vector<vector<int>>& lahky, pair<int,int> robotnik){
    int perioda = robotnik.a + robotnik.b;

    // pripocitam 1 ku vsetkym zvyskom, v ktorych tento robotnik pracuje
    for(int i = robotnik.a;i < perioda;i++)
        lahky[perioda][(cas + i)%perioda] += 1;
}

void lahky_odchod(int zaciatok, vector<vector<int>>& lahky, pair<int,int> robotnik){
    int perioda = robotnik.a + robotnik.b;

    // odcitam 1 od vsetkych zvyskov, v ktorych tento robotnik pracoval
    for(int i = robotnik.a;i < perioda;i++)
        lahky[perioda][(zaciatok + i)%perioda] -= 1;
}

// pocet lahkych robotnikov pracujucich v danom case
int pocet_lahkych(int cas, vector<vector<int>>& lahky){
    int vysledok = 0;

    // staci sa pre kazdu periodu pozriet na spravny zvysok a vsetko scitat
    for(int period = 1;period < lahky.size();period++)
        vysledok += lahky[period][cas%period];

    return vysledok;
}

const int SQRT = 450;

int main(){
    cin.tie(0)->sync_with_stdio(0);
    int n,m; cin >> n >> m;

    vector<pair<int,int>> robotnici(n); // zoznam dvojic a_i, b_i
    vector<int> posledne_pridanie(n, -1); // cas, v ktorom dany robotnik posledne prisiel
    vector<vector<int>> lahky(SQRT, vector<int>(SQRT, 0));
    vector<int> tazky(m, 0);

    for(auto &robotnik : robotnici)
        cin >> robotnik.a >> robotnik.b;

    int prefix_sucet = 0; // pocet aktualne pracujucich tazkych robotnikov
    for(int cas = 0;cas < m;cas++){
        int op, index;
        cin >> op >> index;

        if(op == 1){
            if(robotnici[index].a + robotnici[index].b >= SQRT)
                tazky_prichod(cas, tazky, robotnici[index]);
            else
                lahky_prichod(cas, lahky, robotnici[index]);
            posledne_pridanie[index] = cas;
        }else{
            if(robotnici[index].a + robotnici[index].b >= SQRT)
                tazky_odchod(posledne_pridanie[index], cas, tazky, robotnici[index]);
            else
                lahky_odchod(posledne_pridanie[index], lahky, robotnici[index]);
        }

        prefix_sucet += tazky[cas];

        // vysledok je pocet pracujucich tazkych + lahkych robotnikov
        cout << prefix_sucet + pocet_lahkych(cas, lahky) << "\n";
    }
}
Pre odovzdávanie sa musíš prihlásiť.
Trojsten

Korešpondenčný seminár z programovania zastrešuje občianske združenie Trojsten.

Kontakt
Ďalšie projekty