Zadanie

Anička a Bláčik sú dve rozkošné mačiatka, a ako typické mačiatka, ich obľúbená hračka je špagát. Zvykli ho najmä naháňať, ale odkedy zaspali na knižke o teórii hier, osmózou1 sa k nim dostali aj nejaké vedomosti, a ich hry začali byť elaborátnejšie.

Konkrétne, keď sa prebudili po výdatnom matematickom spánku, Bláčik a Anička si schmatli dva špagáty, jeden s dĺžkou \(a\) a druhý s dĺžkou \(b\) mačkometrov, a postupne sa striedajú na ťahu. Mačiatko si vyberie špagát a skráti ho o celočíselný pozitívny násobok dĺžky druhého špagátu2. Samozrejme, mačiatko bez špagátu je smutné mačiatko, a preto ani jedno z nich nechce špagát ťahom úplne zmiznúť.

A keďže mačiatka sú naozaj hyperaktívne, raz ako sa naučili hrať hru, budú ju hrať viackrát. Menovite, klbko z ktorého špagáty berú vystačí na to, aby si vyskúšali hru zo všetkými možnými dĺžkami prvého špagátu v rozmedzí \(a_0 \leq a\leq a_1\), a druhého špagátu v rozmedzí \(b_0\leq b\leq b_1\).

Po niekoľkých ďalších šlofíkoch na knihe sa už Anička a Bláčik naučili hrať optimálne a zaujímalo by ich, koľko z týchto hier výhrá ktoré z nich. Na vylúštenie tejto záhady ešte dosť nespali3 – hrajú sa predsa so špagátmi! – tak im budete s touto otázkou musieť pomôcť vy!

Úloha

Anička a Bláčik sa hrajú nasledovnú hru. Majú dva špagáty, s dĺžkami \(a\) a \(b\) mačkometrov. Striedajú sa v ťahoch, mačiatko na ťahu si vždy vyberie celé číslo \(k \geq 1\), a buď skráti prvý špagát o \(k\cdot b\) mačkometrov, alebo druhý špagát o \(k\cdot a\) mačkometrov. Prehráva mačiatko, ktoré jeden zo špagátov úplne zmizne. Samozrejme, špagát nemožno skrátiť na negatívnu dĺžku.

Hru sa hrajú viackrát, konrkétne pre každú kombináciu \(a\) a \(b\) v rozsahu \(a_0\leq a\leq a_1\) a \(b_0\leq b\leq b_1\) vrátane (hrajú tak \((a_1 - a_0 + 1)(b_1 - b_0 + 1)\) hier).

Anička vždy začína. Koľko z hier vyhrá, ak obe mačiatka hrajú optimálne?

Formát vstupu

V jednom vstupe sa bude nachádzať viacero testovacích vstupov. Prvý riadok vstupu obsahuje celé číslo \(1\leq t\leq 10^5\) – počet testovacích vstupov.

Na ďalších \(t\) riadkoch sa na každom nachádzajú štyri celé čísla oddelené medzerou: \(a_0, a_1, b_0\) a \(b_1\). Je zaručené, že \(1\leq a_0, a_1,b_0,b_1\leq 10^9\), \(0\leq b_1 - b_0, a_1 - a_0 \leq 10^6\).

Úloha má štyri sady. V prvej sade platí, že \(a_0,a_1,b_0,b_1\leq 500\). V druhej sade platí, že \(a_0=a_1\) a \(b_0=b_1\). V tretej sade platí, že \(a_1-a_0, b_1-b_0\leq 10^5\).

Je garantované, že súčet \(a_1-a_0 + 1\) (resp \(b_1-b_0 + 1\)) v prvých troch sadách neprekročí \(10^5\), a v štvrtej sade neprekročí \(5\cdot 10^6\).

Formát výstupu

Pre každý z \(t\) vstupov vypíšte jeden riadok a v ňom jedno celé číslo: počet hier v ktorých Anička vyhrá, ak obe mačiatka hrajú optimálne.

Dávajte si pozor, že výstup môže byť pomerne veľký. Ak programujete v pythone, odporúčame vám vypisovať celý výstup naraz pomocou jediného použitia print. V prípade, ak používate C++, odporúčame vám používať \n namiesto endl.

Príklad

Input:

2
11 11 2 2
1 6 1 6

Output:

1
20

*V prvom prípade hrajú mačiatka jedinú hru: s \(a=11\) a \(b=2\). Anička v prvom ťahu skráti špagát dĺžky \(11\) na dĺžku \(3\) (druhý špagát ostáva dlhý dva mačkometre). Bláčik nemá na výber, skráti špagát dĺžky \(3\) o dva mačkometre, a Aničke zostanú špagáty s dĺžkami \(1\) a \(2\). Keď z dlhšieho odhryzne jeden mačkometer, Bláčikovi ostanú dva špagáty dĺžky jedna. Nemá na výber, akýmkoľvek ťahom prehráva.


  1. Osmóza je presun hmoty (v tomto prípade znalostí) cez polopriepustnú membránu (v tomto prípade strany knihy). V prípade, že chcete aj vy skúsiť učenie osmózou, odporúčame zdriemnuť na niektorej zo zbierok KSP.↩︎

  2. Odhryzne zvyšok a stratí ho pod gaučom.↩︎

  3. Hovorí sa, že mačiatka spia 22 hodín denne, ale povedal to niekto tým mačiatkam?↩︎

Víťazné pozície

V tejto úlohe sme sa pohrali s teóriou hier, kde sme chceli spočítať koľko zo začiatočných pozícií je víťazných pre Aničku – teda ak začnú hrať hru hru s dĺžkami špagátov \(a\) a \(b\), či vie Anička vyhrať ak vie optimálne.

Keďže hra je symetrická (možné ťahy sú rovnaké bez ohľadu na to, kto je na ťahu), vieme všeobecne hovoriť o každej dvojici dĺžok špagátov či je víťazná pre mačiatko na ťahu. Stav hry (alebo pozíciu) si označíme ako \((a, b)\) – dĺžky špagátov.

Ako zistíme ktoré pozície sú víťazné? Zjavne, ak sa mačiatko dostalo na ťah a ostal mu jeden špagát nenulovaj dĺžky, a druhý špagát došiel, znamená to že mačiatko na prechádzajúcom ťahu prehralo. Preto sú pozície \((a, 0)\) a \((0,b)\) vyhrávajúce pre všetky celé čísla \(a, b > 0\).

Poďme sa pozrieť na pozíciu \((a, b)\), pričom \(a \leq b > 0\) (prípad, že \(b\leq a > 0\) je symetrický). Mačiatko na ťahu vie skrátiť prvý špagát na dĺžky \(a-b, a - 2b,\dots, a-kb \geq 0\) (pre nejaké celé číslo \(k\)).

Predstavme si, že vieme pre každú z pozícií \((a - b, b), \dots, (a\mod b, b)\) či je vyhrávajúca alebo nie. Ak medzi nimi je aspoň jedna prehrávajúca, mačiatko na ťahu spraví tento ťah a zrazu sa dostane na ťah druhé mačiatko, ale je v prehrávajúcej pozícii. A keďže prehrávajúca pozícia nie je vyhrávajúca, znamená to, že nech robí, čo robí, ak jeho oponent hrá optimálne, prehrá. V tomto prípade je tak pozícia \((a, b)\) vyhrávajúca.

Na druhú stranu, ak sú pozície \((a - b, b), \dots, (a - kb, b)\) všetky vyhrávajúce, mačiatko na ťahu nemá na výber, nech robí, čo robí, každý jeho ťah dôjde do pozície odkiaľ vie vyhrať súper. Vtedy teda musí byť pozícia \((a, b)\) prehrávajúca.

Z tohto pozorovania sa vieme dostať k jednoduchému dynamickému programovaniu ktoré nám vyrieši prvú sadu: iterujeme \(a\) od \(1\) po \(a_2\) a \(b\) od \(1\) po \(b_1\), a pre každú pozíciu si skontrolujeme, či existuje medzi ťahmi nejaký do prehrávajúcej pozície (pre všetky “menšie” pozície sme si už v dynamickom programovsaní spočítali odpoveď). Ak áno, zaznačíme si pozíciu ako vyhrávajúcu. Ak nie, tak ako prehrávajúcu.

Po vypočítaní odpovede pre všetky pozície jednoducho spočítame počet vyhrávajúcich pozícií v intervale hier ktoré Bláčik a Anička budú hrať.

Pre každú z \(t\) otázok vieme použiť tú istú tabuľku – netreba ju navyše prepočítavať ak ju spravíme dosť veľkú.

Toto riešenie funguje v časovej zložitosti približne \(O(\max (a_2, b_2)^2 \log (\max(a_1, b_2)))\) (pomocou zložitejšej matematiky si vieme odhadnúť že priemerný počet možných ťahov na pozíciu je v skutočnosti približne logaritmický od možného počtu pozícií, a nie lineárny).

#include<bits/stdc++.h>

using namespace std;

#define FOR(i,n)	for(int i=0;i<(int)n;i++)
#define lli long long int

bool memo(lli a, lli b, vector<vector<int> > &W) {
    if (a == 0 || b == 0) return true;
    if (W[a][b] != -1) return W[a][b];
    
    bool seen_l = false;
    if (a < b) {
        int k = 1;
        while (k * a <= b) {
            if (!memo(a, b - k * a, W)) seen_l = true;
            k ++;
        }
    }
    else {
        int k = 1;
        while (k * b <= a) {
            if (!memo(a - k * b, b, W)) seen_l = true;
            k ++;
        }
    }
    
    W[a][b] = seen_l;
    return seen_l;
}

int main() {
    cin.sync_with_stdio(false); cin.tie();
    cout.sync_with_stdio(false); cout.tie();
    int t; cin >> t;
    vector<vector<int> > winning(501, vector<int>(501, -1));
    
    FOR(i, t) {
        lli a1, a2, b1, b2;
        cin >> a1 >> a2 >> b1 >> b2;
        
        lli count = 0;
        for (int a = a1; a <= a2; a ++) {
            for (int b = b1; b <= b2; b ++)
                count += (int)(memo(a, b, winning));
        }
        cout << count << "\n";
    }
}

Prvé pozorovanie

Oplatí sa nám naozaj skúšať všetkých \(\max a_2 \max b_2\) pozícií?

Ak \(a \leq b < 2a\) (alebo \(b\leq a\leq 2b\)), vtedy nemá mačiatko na ťahu na výber – má presne jeden ťah ktorý môže spraviť. Zamyslime sa tak nad prípadom, že \(b \geq 2a\). Mačiatko na ťahu má aspoň dva možné ťahy: do pozície \((b \mod a, a)\), alebo do pozície \((a+(b\mod a), a)\). Všimite si, že v pozícii \((a+b\mod a, a)\) je možný len jediný ťah: do \((b\mod a, a)\).

Rozoberme si najskôr prípad, že pozícia \((b\mod a, a)\) je vyhrávajúca. V tom prípade, ak mačiatko na ťahu (povedzme že je to Anička) prejde do pozície \((a+(b\mod a), a)\), Bláčik musí následne ísť do pozície \((b\mod a, a)\), kde sa dostane na ťah Anička. Táto pozícia je však vyhrávajúca, teda v tomto prípade má Anička vyhrávajúcu stratégiu s pozície \((a, b)\).

No a v prípade, že je pozícia \((b\mod a, a)\) prehrávajúca? Potom vieme, že keď sa tam Anička pohne, nech Bláčik robí čo robí, určite prehrá ak Anička ďalej hrá optimálne.

Ukázali sme si tak, že v oboch prípadoch má Anička vyhrávajúcu stratégiu, teda vždy keď \(b\geq 2a\) je pozícia \((b, a)\) vyhrávajúca. Všimnite si, že nemusíte počítať presnú Aničkinu stratégiu – stačí nám vedieť či z pozície vyhrá, alebo nie.

Takto vieme jednoducho overiť, či je nejaké pozícia \((a, b)\) vyhrávajúca (bez ujmy na všeobecnosti uvažujme \(a\leq b\), inak je situácia symetrická): ak \(b\geq 2a\) (v tomto prípade je zahrnutý prípad \(a=0\)), pozícia je vyhrávajúca. Inak sa posuňme na pozicíu \((b-a, a)\) a postupujeme rovnakým spôsobom, pričom pozícia \((a,b)\) je vyhrávajúca práve ak je pozícia \((b-a, a)\) prehrávajúca.

Koľko nám takýto výpočet bude trvať? Po dvoch ťahoch z \((a, b)\) dostaneme na \((2a - b, b - a)\), \(b-a < a\), takže sa nám najväčšie z čísel zmenšilo o polovicu. Teda určite na overenie výhernosti pozície \((a, b)\) (s \(a\leq b\)) nepotrebujeme viac ako \(2\log b\) krokov.

Takto si jednoducho vieme overiť kto vyhráva každú z hier ktorú mačiatka spolu hrajú, a algoritmus celkovo zaberie \(O((a_2-a_1+1)(b_2-b_1+1)\log\max(a_2,b_2))\) času, a dokonca ho vieme implementovať v konštantnej pamäti1. Toto riešenie stačí na štyri body.

[1] V prípade, že ho implementujete cez cykly. Ak používate rekurziu ako doleuvedený kód, tak zásobník v rekurzii zožerie \(O(\log\max(a_2,b_2))\) pamäti.

#include<bits/stdc++.h>

using namespace std;

#define FOR(i,n)	for(int i=0;i<(int)n;i++)
#define lli long long int

bool memo(lli a, lli b) {
    if (a == 0 || b == 0) return true;
    if (a > b) return memo(b, a);
    if (b > a * 2) return true;
    return (!memo(b - a, a));
}

void solve() {
    lli a1, a2, b1, b2;
    cin >> a1 >> a2 >> b1 >> b2;
    
    lli count = 0;
    for (lli a = a1; a <= a2; a ++) {
        for (lli b = b1; b <= b2; b ++) count += (int)(memo(a, b));
    }
    cout << count << "\n";
}

int main() {
    cin.sync_with_stdio(false); cin.tie();
    cout.sync_with_stdio(false); cout.tie();
    int t; cin >> t;
    FOR(i, t) solve();
}

Ako to zlepšiť?

V druhej polovice vstupov mačiatka hrajú priveľa hier aby sme každú kontrolovali separátne2. Musíme vymyslieť niečo lepšie.

[2] Pýtate sa kedy to stihnú? Mačiatka sú veľmi hyperaktívne

Jedna možnosť, ktorá podľa šikovnosti implementácie vie získať medzi \(6\) a \(8\) bodmi je pre každé \(a\) medzi \(a_1\) a \(a_2\) simulovať všetky \(b\) naraz, a v každom kroku počítať koľko z pozícií vyhráva okamžite (podľa horeuvedeného pravidla).

Implementácia tejto verzie nie je úplne triviálna, a nechávam ju ako zamyslenie sa na čitateľa (hoci je vzorák, naopak, implementačne veľmi jednoduchý, ako čoskoro uvidíte, skúsiť si nakódiť túto verziu je samo o sebe vcelku zaujímavá úloha na zamyslenie :)

Optimálne riešenie

Poďme sa ďalej zamyslieť nad našou úvahou, že pozícia \((a, b)\) s \(a\leq b\) je vyhrávajúca ak \(b\geq 2a\). Ak nie je, potom je vyhrávajúca ak je \((b-a, a)\) prehrávajúca. Teraz máme dva prípady. Ak \(a\leq 2(b-a)\), potom je táto pozícia vyhrávajúca, podľa predchádzajúceho pravidla. Toto je ekvivalentné prípadu, že \(b\geq \frac32 a\).

V druhom prípade platí \(a < 2(b-a)\), a hra pokračuje do stavu \((2a - b, b - a)\). Zas máme dva prípady: ak \(b-a \geq 2(2a - b)\), čiže \(b \geq \frac 53 a\), vtedy je pozícia víťazná, inak…

$\(\frac21, \frac 32, \frac 53,\dots\), to znie skoro povedome! A veru, ak takto pokračujeme ďalej, dostaneme sériu zlomkov susediacich fibonacciho čísel. Indukciou si vieme dokázať, že po \(2i+1\) iteráciach z \((a, b)\) takto dostaneme \((b\cdot F_{2i+1} - a\cdot F_{2i+2}, a\cdot F_{2i+1} - b\cdot F_{2i})\), a po \(2(i+1)\) iteráciach z \((a, b)\) dostaneme \((a\cdot F_{2i+3} - b\cdot F_{2i+2}, b\cdot F_{2i+1} - a\cdot F_{2i+2})\), pričom \(F_n\) zaznačuje \(n\)-té Fibonacciho číslo (\(F_0=0, F_1=1, F_{n+2}=F_n + F_{n+1}\)).

Z tohto dostaneme, že na to, aby bola pozícia \((a, b)\) vyhrávajúca, musí platiť \(b\geq a F_{2k+4}/F_{2k+3}\) a zároveň \(b\leq a F_{2k+5}/F_{2k+4}\) (ako sa ukáže, pre všetky \(k\))

Ak ste niekedy počuli z zlatom reze, tak viete že pomer vedľajších Fibonacciho čísel konverguje ku \(\phi\), a teda \((a, b)\) je vyhrávajúca pozícia práve vtedy ak \(b\geq \phi a\) (alebo \(a\geq \phi b\)).

Takto vieme počet vyhrávajúcich pozícií pre Aničku spočítať v konštantnom čase: jednoducho spočítame pre každé možné \(a\), koľko \(b\) v intervale \([b_1, b_2]\) platí \(b\geq a\phi\), alebo \(b\leq a/\phi\).

\(\phi\) vieme jednoducho odhadnúť ako pomer dostatočne veľkých Fibbonacciho čísel – takto sa vieme vyhnúť problémom s zaokrúhľovaním necelých čísel pri overovaní koncov víťazných intervalov.

Celé riešenie tam má lineárnu časovú, a konštantnú pamäťovú zložitosť.

#include<bits/stdc++.h>

using namespace std;

#define FOR(i,n)	for(int i=0;i<(int)n;i++)
#define lli long long int

void solve(lli f1, lli f2) {
    lli a1, a2, b1, b2;
    cin >> a1 >> a2 >> b1 >> b2;
    
    if (a2 - a1 > b2 - b1) {
        swap(a1, b1); swap(a2, b2);
    }
    
    lli count = 0;
    for (lli a = a1; a <= a2; a ++) {
        lli lb = a * f1 / f2;
        lli ub = a * f2 / f1;
        if (ub * f1 < f2 * a) ub ++;
        
        count += max(0LL, min(lb, b2) + 1LL - b1) + max(0LL, b2 - max(ub, b1) + 1LL);
    }
    cout << count << "\n";
}

int main() {
    cin.sync_with_stdio(false); cin.tie();
    cout.sync_with_stdio(false); cout.tie();
    int t;
    cin >> t;
    
    lli f1 = 1, f2 = 1;
    FOR(i, 45) {
        swap(f1, f2);
        f2 += f1;
    }
    
    FOR(i, t) solve(f1, f2);
}

  1. 1↩︎

  2. 2↩︎

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ť.