Programare Competitivă

Căutare binară pe răspuns

Căutarea binară pe răspuns este o tehnică care transformă o problemă de optimizare (“găsește valoarea maximă/minimă X pentru care…”) într-o problemă de decizie (“este posibil cu valoarea X?”), pe care o rezolvă apoi prin căutare binară.

E una dintre cele mai puternice tehnici în programare competitivă - o vei întâlni la olimpiade, concursuri și probleme clasice.


Ideea

De obicei, căutarea binară caută o valoare într-un vector sortat. Dar uneori “vectorul” nu există fizic - îl “imaginăm”: e mulțimea răspunsurilor posibile.

Condiția cheie: monotonie

Pentru a aplica căutarea binară pe răspuns, trebuie să existe un prag critic k*:

  • Pentru orice x >= k* → răspunsul e “DA, posibil”
  • Pentru orice x < k* → răspunsul e “NU, imposibil”

(sau invers, în funcție de problemă)

x:     1  2  3  4  5  6  7  8  9  10
check: N  N  N  N  D  D  D  D  D  D
                   ↑
                  k* = 5 (cel mai mic x care merge)

Dacă funcția de check e monotonă (NU… NU… DA… DA…), putem găsi k* în O(log(rang)) apeluri la check.


Problema clasică: Bookshelves

Enunțul

Avem n cărți cu lățimile w[1..n]. Le așezăm în ordine pe k rafturi (raft după raft, pe o singură linie). Vrem să găsim lățimea minimă a unui raft, astfel încât toate cărțile să încapă pe cele k rafturi.

Cărți: [3, 5, 2, 8, 4, 1], k = 3

Cu lățimea raftului = 10:
  Raft 1: 3 + 5 + 2 = 10
  Raft 2: 8
  Raft 3: 4 + 1 = 5
  OK

Cu lățimea raftului = 9:
  Nu merge (nu poti pune cartea de 8 cu 3+5=8)

Lățime minimă = 10

Strategia

Fixăm x = lățimea raftului. Verificăm în O(n) dacă încap toate cărțile în k rafturi. Dacă da, x e posibil. Dacă nu, trebuie mai mare.

Funcția check:

bool check(int x, int k) {
    int rafturi = 1, curent = 0;
    for (int i = 0; i < n; i++) {
        if (w[i] > x) return false; // o carte e mai lata decat raftul
        if (curent + w[i] > x) {
            rafturi++;
            curent = w[i];
        } else {
            curent += w[i];
        }
    }
    return rafturi <= k;
}

Căutarea binară cu st <= dr

  • Minimul posibil al lățimii: max(w[i]) (cartea cea mai largă trebuie să încapă)
  • Maximul posibil: suma tuturor (toate cărțile pe un raft)

Păstrăm o variabilă rasp cu cel mai bun răspuns găsit până acum:

int st = *max_element(w, w + n);
int dr = accumulate(w, w + n, 0);
int rasp = dr; // un raspuns valid initial (maximul sigur merge)

while (st <= dr) {
    int mij = st + (dr - st) / 2;
    if (check(mij)) {
        rasp = mij;      // salvam - mij e un raspuns valid
        dr = mij - 1;    // cautam unul mai mic
    } else {
        st = mij + 1;    // mij e prea mic
    }
}

cout << rasp; // latimea minima

Complexitate: O(n log S), unde S = suma lățimilor.


Cod complet

#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");

int w[100001];
int n, k;

bool check(int x)
{
    int rafturi = 1, curent = 0;
    for (int i = 0; i < n; i++) {
        if (w[i] > x) return false;
        if (curent + w[i] > x) {
            rafturi++;
            curent = w[i];
        } else {
            curent += w[i];
        }
    }
    return rafturi <= k;
}

int main()
{
    fin >> n >> k;

    int st = 0, dr = 0;
    for (int i = 0; i < n; i++) {
        fin >> w[i];
        if (w[i] > st) st = w[i];
        dr += w[i];
    }

    int rasp = dr;

    while (st <= dr) {
        int mij = st + (dr - st) / 2;
        if (check(mij)) {
            rasp = mij;
            dr = mij - 1;
        } else {
            st = mij + 1;
        }
    }

    fout << rasp;

    return 0;
}

date.in:

6 3
3 5 2 8 4 1

date.out:

10

Șablonul general

Căutarea binară pe răspuns cu while (st <= dr) are o structură standard unică, indiferent dacă vrem minim sau maxim. Diferența e doar ce salvăm și în ce direcție restrângem.

Șablonul pentru MINIM (cel mai mic x care merge)

int st = MIN_RASPUNS;
int dr = MAX_RASPUNS;
int rasp = -1; // sau o valoare sentinela

while (st <= dr) {
    int mij = st + (dr - st) / 2;
    if (check(mij)) {
        rasp = mij;       // mij e valid - il salvam
        dr = mij - 1;     // incercam sa gasim unul mai mic
    } else {
        st = mij + 1;     // mij nu merge, incercam mai mare
    }
}

cout << rasp; // cel mai mic x care merge

Șablonul pentru MAXIM (cel mai mare x care merge)

int st = MIN_RASPUNS;
int dr = MAX_RASPUNS;
int rasp = -1;

while (st <= dr) {
    int mij = st + (dr - st) / 2;
    if (check(mij)) {
        rasp = mij;       // mij e valid - il salvam
        st = mij + 1;     // incercam sa gasim unul mai mare
    } else {
        dr = mij - 1;     // mij nu merge, incercam mai mic
    }
}

cout << rasp; // cel mai mare x care merge

Avantajul acestui șablon: e simetric, nu depinde de cum incrementezi mij, iar variabila rasp păstrează mereu cel mai bun răspuns valid găsit.


Problema 2: aggressive cows

Ai n boxe pe o linie (pozițiile x[1..n], sortate crescător) și vrei să plasezi c vaci astfel încât distanța minimă între oricare două vaci să fie maximă.

Boxe: 1, 2, 4, 8, 9, 10
c = 3

Distanța minimă maximă = 5 (pozițiile 1, 4, 10 - distanțele 3 și 6, minim 3)
Pentru pozitiile 1, 4, 10 distantele sunt 3, 6 - minim 3
Pentru pozitiile 1, 4, 9 distantele sunt 3, 5 - minim 3
...

Strategia

Fixăm distanța minimă d. Putem plasa cele c vaci astfel încât toate perechile să aibă distanță >= d?

Greedy: plasează prima vacă în prima boxă. Apoi, pentru fiecare boxă nouă, dacă distanța față de ultima vacă e >= d, plasează o vacă acolo.

bool check(int d) {
    int plasate = 1;
    int ultima = x[0];
    for (int i = 1; i < n; i++) {
        if (x[i] - ultima >= d) {
            plasate++;
            ultima = x[i];
        }
    }
    return plasate >= c;
}

Căutăm cel mai mare d care merge (deci folosim varianta pentru MAXIM):

int st = 1, dr = x[n-1] - x[0];
int rasp = -1;

while (st <= dr) {
    int mij = st + (dr - st) / 2;
    if (check(mij)) {
        rasp = mij;
        st = mij + 1;
    } else {
        dr = mij - 1;
    }
}

cout << rasp;

Când să aplici căutarea binară pe răspuns?

Semne că problema cere această tehnică:

1. “Cea mai mare/mică valoare astfel încât…”

  • “Cel mai mic raft astfel încât cărțile să încapă în k rafturi”
  • “Cea mai mare distanță minimă”
  • “Numărul minim de zile pentru a termina”

2. Funcția check e mai ușoară decât optimizarea directă

Uneori direct e greu să calculezi optimul, dar să verifici dacă o valoare fixă merge e simplu.

3. Răspunsul e într-un domeniu mărginit

Știi un minim și un maxim pentru răspuns (rang [st, dr]). Căutarea binară face O(log(dr - st)) iterații.


Problema 3: koko eating bananas

Există n grămezi de banane v[i]. Koko mănâncă cu viteza k banane/oră. Într-o oră, mănâncă din o singură grămadă - dacă grămada e mai mică decât k, termină grămada și așteaptă ora următoare.

Găsește viteza minimă k astfel încât Koko să termine toate grămezile în h ore.

Grămezi: [3, 6, 7, 11], h = 8

k = 4: 3/4=1h, 6/4=2h, 7/4=2h, 11/4=3h → 1+2+2+3 = 8h ✓
k = 3: 1+2+3+4 = 10h ✗
Răspuns: 4

Soluție

bool check(int k) {
    long long ore = 0;
    for (int i = 0; i < n; i++)
        ore += (v[i] + k - 1) / k; // ceil(v[i] / k)
    return ore <= h;
}

int st = 1, dr = *max_element(v, v + n);
int rasp = dr;

while (st <= dr) {
    int mij = st + (dr - st) / 2;
    if (check(mij)) {
        rasp = mij;
        dr = mij - 1;
    } else {
        st = mij + 1;
    }
}

cout << rasp;

Pas cu pas: Bookshelves

Cărți [3, 5, 2, 8, 4, 1], k = 3.

  • Minim: max = 8
  • Maxim: suma = 23
  • rasp inițial = 23

Iterație 1: st=8, dr=23, mij = 15

Check(15): 3+5+2=10 (raft 1), 8+4+1=13 (raft 2) → 2 rafturi <= 3 ✓ → rasp = 15, dr = 14

Iterație 2: st=8, dr=14, mij = 11

Check(11): 3+5+2=10 (raft 1), 8=8 (raft 2), 4+1=5 (raft 3) → 3 rafturi <= 3 ✓ → rasp = 11, dr = 10

Iterație 3: st=8, dr=10, mij = 9

Check(9): 3+5=8 (raft 1), 2+8 nu >9, 2=2 (raft 2), 8=8 (raft 3), 4+1=5 (raft 4) → 4 > 3 ✗ → st = 10

Iterație 4: st=10, dr=10, mij = 10

Check(10): 3+5+2=10, 8, 4+1=5 → 3 rafturi ✓ → rasp = 10, dr = 9

st=10 > dr=9, ieșim

Răspuns: rasp = 10


Greșeli frecvente

1. Funcția check nu e monotonă

Dacă valorile “DA/NU” sunt amestecate (NU, DA, NU, DA), nu poți aplica căutarea binară.

Verifică mereu: dacă x merge, toate valorile mai mari/mici (după caz) merg?


2. Limitele inițiale greșite

int st = 0;   // GRESIT pentru bookshelves - cartea de 8 nu incape pe raft de 0
int st = max(w); // CORECT

Gândește-te la minimul și maximul posibil al răspunsului.


3. Uitarea actualizării lui rasp

// GRESIT - uitam rasp = mij
while (st <= dr) {
    int mij = st + (dr - st) / 2;
    if (check(mij)) dr = mij - 1;
    else st = mij + 1;
}
// rasp e necunoscut!

// CORECT - salvam in rasp valoarea valida
while (st <= dr) {
    int mij = st + (dr - st) / 2;
    if (check(mij)) { rasp = mij; dr = mij - 1; }
    else st = mij + 1;
}

4. Overflow la mij

int mij = (st + dr) / 2; // poate depasi int daca st, dr sunt mari
int mij = st + (dr - st) / 2; // mai sigur - nu overflow-eaza

5. Confundarea x cu indicele

Nu confunda valoarea căutată (răspunsul) cu indicele într-un vector. La căutare binară pe răspuns, mij nu e un indice într-un vector, ci o valoare candidată.


Tabel exemple

Problema Ce căutăm Funcția check
Bookshelves min lățime raft cărțile încap în k rafturi?
Aggressive cows max distanță minimă putem plasa c vaci la distanța >= d?
Koko bananas min viteză mâncăm totul în <= h ore?
Split array min suma maximă putem împărți în <= k părți?
Rope cutting max lungime bucată putem tăia >= c bucăți de lungime x?

Complexitate

Pentru un răspuns în intervalul [1, R]:

  • Căutarea binară: O(log R) iterații
  • Fiecare iterație cheamă check în O(f(n))
  • Total: O(f(n) * log R)

De obicei, check e liniar O(n), deci total O(n log R).


Ce să reții

  • Căutarea binară pe răspuns transformă optimizare în decizie + căutare binară.
  • Condiție: funcția check(x) trebuie să fie monotonă (toate DA-urile împreună, toate NU-urile împreună).
  • Determină st și dr din contextul problemei.
  • Folosim șablonul while (st <= dr) și păstrăm cel mai bun răspuns în rasp.
    • Pentru minim: dacă check reușește, rasp = mij; dr = mij - 1
    • Pentru maxim: dacă check reușește, rasp = mij; st = mij + 1
  • Exemple clasice: bookshelves, aggressive cows, koko bananas, split array.
  • Complexitate tipică: O(n log R).