CSES - Binäärihaku

Binäärihaku on algoritmi, jolla voi löytää tietyn alkion nn alkion järjestetystä taulukosta ajassa O(logn)O(\log n). Tutustumme seuraavaksi binäärihaun toteuttamiseen sekä sen soveltamiseen algoritmiikassa.

Haun toteutus

Mukava tapa toteuttaa binäärihaku on aloittaa taulukon alusta ja kulkea sitä eteenpäin hyppäyksin. Hypyn pituus on ensin nn, sitten n/2n/2, sitten n/4n/4, jne., kunnes lopuksi pituus on 11. Jokaisen pituuden kohdalla hypitään eteenpäin, kunnes seuraava hyppy veisi taulukon ulkopuolelle tai alkioon, joka on liian suuri.

Seuraava esimerkki etsii alkion xx sijainnin järjestetyssä taulukossa tt, jossa on nn alkiota:

int k = 0;
for (int b = n; b >= 1; b /= 2) {
    while (k+b < n && t[k+b] <= x) k += b;
}
if (t[k] == x) // alkio löytyi

Muuttuja kk ilmaisee kohdan taulukossa, ja se on aluksi 00. Muuttuja bb määrittää hypyn pituuden. Jos taulukossa on alkio xx, sen kohta on lopuksi muuttujassa kk.

Algoritmi vie aikaa O(logn)O(\log n), koska hypyn pituuksia on yhteensä O(logn)O(\log n) ja jokaisen pituuden kohdalla while-silmukkaa suoritetaan enintään kaksi kierrosta.

Toistuvat alkiot

Äskeisessä binäärihaun toteutuksessa on etuna, että sitä on helppoa muokata eri tilanteisiin sopivaksi. Esimerkiksi seuraava koodi laskee, montako kertaa järjestetyssä taulukossa esiintyy alkio xx:

int k1 = -1, k2 = -1;
for (int b = n; b >= 1; b /= 2) {
    while (k1+b < n && t[k1+b] < x) k1 += b;
    while (k2+b < n && t[k2+b] <= x) k2 += b;
}
c = k2-k1; // toistuvien alkioiden määrä

Tässä ideana on, että muuttujaan k1k_1 etsitään viimeinen kohta, jossa taulukon alkio on alle xx, ja muuttujaan k2k_2 etsitään viimeinen kohta, jossa taulukon alkio on enintään xx. Niinpä c=k2k1c=k_2-k_1 on alkion xx toistokertojen määrä taulukossa.

Muutoskohdan etsiminen

Binäärihaun pääasiallinen käyttötarkoitus algoritmiikassa ei ole alkioiden etsiminen taulukosta vaan funktion muutoskohdan etsiminen.

Oletetaan, että meillä on funktio ff, jolle pätee f(k)=0f(k)=0, kun k<mk < m, ja f(k)=1f(k)=1, kun kmk \ge m. Binäärihaun avulla löydämme tehokkaasti muutoskohdan mm näin:

int k = -1;
for (int b = N; b >= 1; b /= 2) {
    while (f(k+b) == 0) k += b;
}
int m = k+1;

Tässä NN on sopiva yläraja, jossa varmasti f(N)=1f(N)=1, ja haku etsii viimeisen kohdan kk, jolle pätee f(k)=0f(k)=0. Niinpä haun päätteeksi m=k+1m=k+1. Haku vie aikaa O(logN)O(\log N).

Esimerkki: Tehdas

Tarkastellaan tehtävää (tämän viikon tehtävä 1), jossa tehtaassa on nn konetta, jotka on numeroitu 1,2,,n1,2,\dots,n. Koneella ii vie kik_i minuuttia aikaa valmistaa yksi tuote. Mikä on pienin aika, jossa voimme valmistaa tt tuotetta?

Hyödyllinen havainto on, että jos aikaa on aa minuuttia, voimme valmistaa koneella ii yhteensä a/ki\lfloor a/k_i \rfloor tuotetta. Vastaavasti käyttämällä kaikkia koneita voimme valmistaa aa minuutissa i=1na/ki\sum_{i=1}^n \lfloor a/k_i \rfloor tuotetta.

Voimme ratkaista tehtävän binäärihaun avulla muotoilemalla funktion ff niin, että f(a)=0f(a)=0, jos i=1na/ki<t\sum_{i=1}^n \lfloor a/k_i \rfloor < t, ja muuten f(a)=1f(a)=1. Nyt funktion ff muutoskohta mm kertoo, mikä on pienin määrä aikaa, jossa voimme valmistaa tt tuotetta.

Jotta voimme käyttää binäärihakua, meidän tulee valita jotenkin parametri NN. Mahdollinen valinta on N=k1tN=k_1 t. Tämä tarkoittaa, että valmistamme kaikki tuotteet koneen 11 avulla. Tämä ei selkeästi ole yleensä hyvä tapa, koska muita koneita ei käytetä lainkaan, mutta saamme tästä ylärajan vastaukselle.

Tuloksena oleva algoritmi vie aikaa O(nlogN)O(n \log N), koska binäärihaussa on O(logN)O(\log N) askelta ja jokaisen suorittaminen vie aikaa O(n)O(n).