Tequila

Firma este, de fapt, un arbore cu radacina. Vom calcula  level_X pentru fiecare nod  X , nivelul sau in arborele (radacina va avea nivelul  1 ). Astfel, solutia va fi  \sum\limits_{X = 1}^{N} \frac{val_X}{level_X} .
Pentru a intelege formula mai bine, sa consideram un nod  X si sa incercam sa calculam probabilitatea ca acesta sa fie ales de catre "Zetul". Cand Zetul va alege la intamplare un nod  Y , vom avea 2 cazuri:

  1.  Y nu se afla pe drumul de la radacina la  X . In acest caz, vom sterge subarborele lui  Y si Zetul va alege alt  Y .
  2.  Y se afla pe drumul de la radacina la  X . In acest caz, probabilitatea ca nodul  Y sa fie chiar nodul  X este  \frac{1}{level_X} (Exista  level_X noduri pe drumul  radacina - X ), deci Zetul va bea  val_X shot-uri de tequila cu probabilitatea  \frac{1}{level_X} .

O operatie de update se va rezolva in  O(1) , deoarece la raspuns vom adauga  \frac{newVal_X-val_X}{level_X} , iar mai apoi vom actualiza  val_X cu  newVal_X .

Complexitate finala:  O(n + m)

Ultimul Cartus

  • Vom considera, in primul rand, ca cele  N dosare sunt numerotate de la  0 la  N - 1 in locul variantei preferate in enunt pentru simplitate in explicarea solutiei. Sa notam cu  P_N permutarea solutie corespunzatoare unui  N .

Se observa printr-o cautare de tip brute-force a rapunsurilor pentru primele cateva cazuri (i.e.  N = 1, 2, 4, 8 ) ca:

  1. Solutia este unica (deci conditia de minim lexicografic nu mai reprezinta o dificultate);
  2.  P_N se obtine din  P_{N / 2} .

 P_1 = (0)
 P_2 = (1, 0)
 P_4 = (3, 1, 2, 0)
 P_8 = (7, 3, 5, 1, 6, 2, 4, 0)

  • Analizand cele doua jumatati ale lui  P_8 , adica  (7, 3, 5, 1) si, respectiv,  (6, 2, 4, 0) observam ca ordinea relativa a elementelor este aceasi si, mai mult, este data de  P_4 . Mult mai usor de observat, prima jumatate contine toate elementele impare, iar a doua toate elementele pare.
  • Intuim astfel ca  P_{16} = Impar(P_8) + Par(P_8)  =  Impar((7, 3, 5, 1, 6, 2, 4, 0))  +  Par((7, 3, 5, 1, 6, 2, 4, 0))  =  (15, 7, 11, 3, 13, 5, 9, 1)  +  (14, 6, 10, 2, 12, 4, 8, 0)  =  (15, 7, 11, 3, 13, 5, 9, 1, 14, 6, 10, 2, 12, 4, 8, 0) , lucru pe care il putem verifica foarte usor trimitand o sursa pentru subtaskul  5 .
  • Intr-adevar, este adevarat pe cazul general prin inductie ca  P_N = Impar(P_{N / 2}) + Par(P_{N / 2}) .
  • Generarea efectiv recursiva a permutarii dupa regula de mai sus si rularea functiei  BubbleSort() asupra ei pentru a-i calcula costul are complexitatea timp  O(N^2) (desi la prima vedere ar putea parea  O(N^3) ) si este suficienta pentru obtinerea a  30\% din punctaj.

Calcularea variabilei  ops poate fi simplificata daca ne punem mai intai problema numarului de swap-uri facut de fiecare apel al functiei  Bubble() in parte. Pentru exemplificare vom lua cazul  N = 32 :

 gap = 16 \Rightarrow ops = 16 operatii

 gap = 8 \Rightarrow ops = 24 operatii

 gap = 4 \Rightarrow ops = 40 operatii

 gap = 2 \Rightarrow ops = 72 operatii

 gap = 1 \Rightarrow ops = 136 opearatii

 Total = 16 + 24 + 40 + 72 + 136 = 288 operatii

Sirul diferentelor de ordin  1 al sirului  16, 24, 40, 72, 136 este  8, 16, 32, 64 , care este chiar sirul puterilor de  2 .

  • Variindu-l pe  N , observam ca in general primul termen al sirului valorilor  ops este intotdeauna  \frac{N}{2} , iar sirul diferentelor de ordin  1 este dat de sirul puterilor de  2 , incepand cu  \frac{N}{4} (a nu se uita ca  N este putere de  2 ). 
  • Aceaste observatii ne sunt suficiente pentru a scrie o functie de calculare a costului pentru un  N dat ce ruleaza in timp  O(log(N)) :
//Returneaza log2(N) 
int GetBits(int N) {
    int bits = -1;
    while (N) {
        ++ bits;
        N /= 2
    }

    return bits;
}

//Returneaza costul permutarii optime de lungime N
long long int GetCost(int N) {
    int K = GetBits(N); //N = 2 ^ K

    long long int sum = N / 2; //Rapunsul curent
    long long int ops = N / 2; //Termenul curent din sirul ops
    long long int dif = N / 4; //Termenul curent din sirul diferentelor de ordin 1
    
    //Generam sirul ops si ii calculam suma elementelor
    for (int i = 2; i <= K; ++ i) {
        ops += dif;
        sum += ops;
        dif *= 2;
    }

    return sum;
}
  • Combinand constructia recursiva a sirului  P_N cu functia anterior prezentata obtinem o solutie de complexitate totala  O(N) , suficienta pentru obinerea a  65 \% din punctaj.
  • Pentru punctaj MAXIM ( 100 de puncte) trebuie sa calculam doar cele  M pozitii cerute din  P_N . Putem scrie o functie recursiva  FindElement(N, pos) ce returneaza elementul  P_N[pos] , bazandu-ne pe definitia recursiva a lui  P_N data mai sus, in timp  O(log(N)) .
  • BONUS! Fie  rev_X = numarul obtinut prin inversarea ordinii bitilor numarului  X , atunci  P[X] = rev(N - X) .

Complexitatea totala:  O(M \cdot log(N))

Prea Simplu!

  • Aceasta problema se voia a fi cea mai grea din concurs si se pare ca si-a facut bine datoria in aceasta calitate.
  • Primele doua subtaskuri se rezolva prin abordari de backtracking si, respectiv, dinamica pe stari. Nu vom insista foarte mult asupra acestor abordari.
  • Pentru ultimele trei subtaskuri avem nevoie un algoritm de rezolvare polinomial. Mai exact, cautam o formula pentru rezultat.
  • Incepem rezolvarea solutia propriu-zisa printr-o prin cea mai importanta observatie pentru rezolvarea problemei:

Sirurile binare ce se pot obtine folosind maxim  K operatii de  flip sunt acelea si numai acelea ce au maxim  K intervale maximale de caractere  1 neintrerupte.

  • Demonstratie:
  1. "Sirul are maxim  K intervale neintrerupte de  1  \Rightarrow Sirul se poate obtine"
    • Putem folosi cate un  flip pentru fiecare dintre cele maxim  K intervale maximale de  1 .
  2. "Sirul are maxim  K intervale neintrerupte de  1  \Leftarrow Sirul se poate obtine"
    • Fie o succesiune arbitrara de operatii de  flip . Sa ne imaginam ca dupa fiecare operatie  flip(l, r) vom insera  2 separatori, reprezentati prin caracterul  | intre elementele  l - 1 si  l si, respectiv, intre elementele  r si  r + 1 . Sa luam un exemplu cu  N = 10 si  K = 4 :
      Sirul initial este: 0 0 0 0 0 0 0 0 0 0
      Dupa operatia  flip(2, 4) sirul este:  0|1 1 1|0 0 0 0 0 0
      Dupa operatia  flip(3, 10) sirul este: 0|1|0 0|1 1 1 1 1 1|
      Dupa operatia  flip(1, 4) sirul este: |1|0|1 1|1 1 1 1 1 1|
      Dupa operatia  flip(8, 8) sirul este: |1|0|1 1|1 1 1|0|1 1|
    • A se observa ca pot exista separatori atat intre elementele sirului, cat si la capetele sale (in total exista  N + 1 = 11 pozitii unde pot fi pusi separatori). Daca consideram toate intervalele de elemente ale sirului situate intre  2 separatori consecutivi, observam ca valoarea sirului este constanta pe fiecare interval in parte (o variatie in valoare ar implica existenta unui separator inauntru - acel separator ce creeaza discrepanta, deci cei  2 separatori nu ar mai fi consecutivi).
    • Cum dupa  K operatii de flip pot exista maxim  2 \cdot K separatori unici, inseamna ca numarul total de intervale cuprinse intre 2 separatori consecutivi poate fi maxim  2 \cdot K - 1 . Numarul maxim de intervale neintrerupte de  1 se obtine daca  K dintre intervalele dintre 2 separatori consecutivi incepand cu primul contin valoarea  1 , iar restul valorilor sirului sunt setate pe  0 (nu avem cum sa alegem  K + 1 dintre intervalele dintre  2 separatori consecutivi astfel incat nicicare 2 sa nu fie adiacente). A se preciza ca am omis tratarea cazului cand primul si / sau ultimul separator nu sunt la inceputul / sfarsitul sirului - in acest caz va exista un prefix si / sau sufix implicit plin cu valori de  0 , care nu prezinta interes deosebit.
  • Urmatorul pas este reducerea lui "fix  K operatii de  flip " la "cel mult  K operatii de  flip ". In acest sens sunt instrumentale  3 tipuri de secvente de operatii de "asteptare":
    1. Dublarea unei operatii de forma  flip(l, r) , efectiv anuland-o (astfel putem transforma problema intr-una de paritate)
    2. Transformarea unei operatii de forma  flip(l, r) , unde  1 < l in  flip(l - 1, r) \: flip(l - 1, l - 1)
    3. Transformarea unei operatii de forma  flip(l, r) , unde  r < N in  flip(l, r + 1) \: flip(r + 1, r + 1)
  • Cu toate acestea  N = 1 si  K = 1 sunt particulare si trebuie tratate ca atare!
  • Tot ce mai ramane de facut este calcularea numarului de siruri cu maxim  K intervale neintrerupte de  1 . Sa presupunem ca vom itera valoarea lui  K prin toate valorile naturale nenule mai mici sau egale cu  K - asa ca ne intereseaza numarul de siruri cu fix  K intervale de  1 . Pentru a le numara pe acestea, vom apela din nou la analogia cu separatorii (de data aceasta vom considera toti cei  N + 1 separatori)- un interval de valori  1 poate fi reprezentat bijectiv printr-un interval de separatori (pentru o mai buna intelegere, daca numerotam separatorii cu numere naturale consecutive de la  1 la  N + 1 , sirul 0001111001001101, este reprezentat de intervalele de separatori  (4, 8) ,  (10, 11) ,  (13, 15) ,  (16, 17) ).
  • Fie cele  K intervale de separatori  (l_i, r_i) si sirul  S \: = \: l_1, r_1, l_2, r_2, l_3, ..., l_N, r_N . Contitiile necesare si suficiente pentru ca S sa reprezinte un sir binar de interes sunt date de sirul de inecuatii  1 \leq l_1 < r_1 < l_2 < r_2 < l_3 < \: ... \: < l_N < r_N \leq N + 1 . Pentru a numara toate sirurile S trebuie sa numaram toate felurile de a alege  2 \cdot K numere naturale distincte cuprinse intre  1 si  N + 1 . Prin definitie, vorbim despre  \binom{N + 1}{2 \cdot K} .
  • Asadar, toata problema se reduce la a calcula  \sum_{k=1}^{K} \binom{N + 1}{2 \cdot k} .
  • Pentru  50 \% din punctaj putem calcula combinarile folosind Triunghiul lui Pascal, generat de identitatea  \binom{N}{K} = \binom{N - 1}{K - 1} + \binom{N - 1}{K} . Pentru a ne incadra in memorie si timp, va trebui sa alocam dinamic cate o matrice de  N \times K pentru fiecare test si sa-i calculam elementele in timp proportional cu marimea ei.
  • Pentru  75 \% din punctaj ne vom folosi de identitatea  \binom{N}{K + 1} = \binom{N}{K} * \frac{N-K}{K + 1} pentru a parcurge randul  N + 1 din Triunghiul lui Pascal element cu element si a aduna fiecare al doilea element la suma actuala, totul in timp  O(K) . Cea mai dificila parte a acestui procedeu este impartirea modulara prin  K + 1 - acest lucru se poate face calculand inversul multiplicativ al lui  K + 1 la fiecare pas folosindu-ne de faptul ca  10^9 + 7 este atat prim cat si foarte mare, deci mereu prim cu  K + 1 . Mai exact, din Mica Teorema a lui Fermat stim ca  a^{P - 1} \equiv 1 (mod \: 10 ^ 9 + 7) , deci  a^{P - 2} \cdot a \equiv 1 (mod \: 10 ^ 9 + 7) , adica  a^{P - 2} este, prin definitie, inversul modular al lui  a . In particular, pentru o valoare  v fixata, inversul ei modulo  10^9 + 7 este dat de  v^{10^9 + 5} \: mod \: 10^9 + 7 , expresie ce se poate calcula prin exeponentiere rapida in timp  O(log(MOD)) .
  • Partea cu adevarat grea a problemei se voia a fi trecerea de la  75 puncte la  100 puncte. Se va merge pe aceeasi idee ca la solutia de  75 , doar ca se va evita aducerea in discutie a numerelor care nu au invers modular (i.e. adica acele numere care nu sunt prime cu  MOD ). Astfel exista doua solutii:
    1. Mentinem numarul curent (adica  \binom{N}{K} ) intr-un arbore de intervale cu factorii sai primi, implementand operatorii * si / adaugand factori si, respectiv, eliminand factori prin operatii de update (fiecare nod mentine numarul modulo  MOD in conditiile in care consideram doar factorii primi din intervalul nodului curent). Nu vom insista foarte mult asupra acestei solutii deoarece este intrinsec mai lenta decat a doua. Se pot lua  100 de puncte cu aceasta abordare daca o optimizam foarte mult: implementam iterativ arborele de intervale, precalculam puterile factorilor primi si facem diverse optimizari de constanta.
    2. Vom incerca sa nu ne departam chiar atat de mult de ideea de invers modular. Stim ca un numar  X cu proprietatea ca  gcd(X, MOD) = 1 are invers modular; ceea ce ne deranjeaza pe noi sunt factorii primi comuni cu modulul - daca pe acestia i-am trata separat problema ar fi rezolvata. Prin urmare vom mentine un numar  X ca pe o pereche  (val, facts[]) , unde facts[] contine puterile factorilor primi din  X ce se gasesc in descompunerea lui  MOD , iar  val reprezinta valoarea modulo  MOD a celorlalti factori primi, combinati intr-un singur numar (acest numar are cu siguranta invers modular, deci nu ne intereseaza prea mult valorile fiecarui factor prim in parte, ci doar produsul lor). Noua implementare de invers modular se va face pe aceeasi idee ca pentru un modul prim, doar ca se va folosi Teorema lui Euler - astfel, inversul modular al unui numar  v este  v^{phi(MOD) - 1} \: mod \: MOD (alternativ se poate folosi si Algoritmul Extins al lui Euclid).
  • Prezentam mai jos o implementare a clasei Integer suficienta pentru punctaj MAXIM, ce mentine un numar sub forma explicata mai sus. Mentionam ca smenul este reutilizabil in multe cazuri in care se doreste impartirea cu numere in contextul aritmeticii modulare cu modul neprim.
class Integer {
public:
    //Pregateste clasa pentru interactiunea cu un modul - ii calculeaza phi-ul si descompunerea in factori primi
    static void prepare_MOD(int mod) {
        MOD = mod;
        phi = MOD;
        p_sz = 0;
        memset(p, 0, sizeof p);
        for (int i = 2; i * i <= mod; ++ i)
            if (mod % i == 0) {
                p[p_sz ++] = i;
                phi /= i;
                phi *= (i - 1);
 
                while (mod % i == 0)
                    mod /= i;
            }
 
        if (mod > 1) {
            p[p_sz ++] = mod;
            phi /= mod;
            phi *= (mod - 1);
            mod = 1;
        }
    }
 
    Integer() {
        memset(k, 0, sizeof k);
        val = 1;
    }
 
    Integer(const int &arg) {
        memset(k, 0, sizeof k);
        val = arg;
        for (int i = 0; i < p_sz; ++ i) {
            while (val % p[i] == 0) {
                val /= p[i];
                ++ k[i];
            }
        }
        val %= MOD;
    }
 
    Integer operator*(const Integer &arg) const {
        Integer ans;
        for (int i = 0; i < p_sz; ++ i)
            ans.k[i] = k[i] + arg.k[i];
        ans.val = (1LL * val * arg.val) % MOD;
        return ans;
    }
 
    Integer operator/(const Integer &arg) const {
        Integer ans;
        for (int i = 0; i < p_sz; ++ i)
            ans.k[i] = k[i] - arg.k[i];
        ans.val = (1LL * val * inv(arg.val)) % MOD;
        return ans;
    }
    
    //Returneaza numarul complet - cu tot cu factorii primi tinuti separat
    int to_int() {
        int ans = val;
        for (int i = 0; i < p_sz; ++ i)
            ans = (1LL * ans * expo(p[i], k[i])) % MOD;
        return ans;
    }
private:
    static const int FACTMAX = 9;
    static int MOD;
    static int phi;
    static int p[FACTMAX];
    static int p_sz;
    
    //Returneaza a ^ b prin exponentiere rapida
    static int expo(int a, int b) {
        if (!b)
            return 1 % MOD;
        else if (b & 1)
            return (1LL * expo(a, b - 1) * a) % MOD;
        else {
            int aux = expo(a, b >> 1);
            return (1LL * aux * aux) % MOD;
        }
    }
    
    //Returneaza inversul modular al lui x
    static int inv(int x) {
        return expo(x, phi - 1);
    }
 
    //X = (facts, val) 
    int k[FACTMAX]; // = facts
    int val;
};
 
int Integer :: MOD;
int Integer :: phi;
int Integer :: p[FACTMAX];
int Integer :: p_sz;
  • Optimizari posibile: precalcularea puterilor tuturor factorilor primi din descompunerea lui  MOD , precalcularea inverselor modulare in timp liniar. Cu aceste optimizari viteza aproape ca se dubleaza.

Complexitate totala:  O(N \cdot log(MOD))