Școala Româno-Britanică
Modulul 3 Clasa a X-a | C++

Ora 5: Căutarea binară - aplicații

Ce vei învăța astăzi: Prima și ultima apariție, numărarea elementelor mai mici decât x

Rezultat practic: Adaptezi căutarea binară pentru a găsi prima și ultima apariție și pentru a număra elemente într-un tablou sortat (Competența 2.2).

Competența 2.2 Săptămâna 14

Înainte de a începe

De ce contează? Prima și ultima apariție sau numărul elementelor mai mici decât x sunt întrebările practice pe liste sortate - le vei întâlni atât la evaluarea modulului, cât și la proiectul cu clasamente de elevi.

Ce știi deja?

La ora 4 ai căutat o singură valoare într-un tablou sortat. Acum adaptezi același algoritm ca să răspunzi la întrebări mai complicate - prima apariție (prima poziție din tablou în care apare valoarea), ultima apariție (ultima poziție în care apare valoarea), numărarea elementelor - păstrând complexitatea O(log2 n).

Ideea-cheie: Pentru prima apariție, după ce găsești valoarea continui spre stânga (condiția a[mij] >= x); pentru ultima apariție continui spre dreapta (condiția a[mij] <= x) - ambele rămân în O(log2 n).
SINTEZĂ TEORETICĂ ȘI EXEMPLE GHIDATE

Ce vei învăța astăzi?

După această lecție vei ști să adaptezi căutarea binară pentru aplicații practice: prima și ultima apariție a unei valori și numărarea elementelor mai mici decât o valoare.

Prima apariție a unei valori
Ultima apariție a unei valori
Câte elemente sunt mai mici decât x
Numărarea aparițiilor unei valori

Prima apariție a unei valori

Într-un tablou sortat, valorile egale cu x sunt alăturate (așezate una lângă alta, fără alte valori între ele). Pentru a găsi prima apariție, căutăm cea mai mică poziție cu a[poz] >= x: când întâlnim un element egal cu x, reținem poziția și continuăm spre stânga.

Algoritmul PrimaApariție(a, n, x): // pasul 1: căutăm prima poziție cu a[poz] >= x st <- 1, dr <- n, poz <- 0 cât timp st <= dr execută mij <- (st + dr) / 2 dacă a[mij] >= x atunci poz <- mij dr <- mij - 1 // continuăm spre stânga altfel st <- mij + 1 sfârșit dacă sfârșit cât timp // pasul 2: verificăm dacă pe poziția găsită există chiar x dacă poz != 0 și a[poz] = x atunci scrie poz altfel scrie -1 sfârșit dacă
Program complet: prima apariție a lui x
#include <iostream>
using namespace std;

int main() {
    int n, a[100005], x, i, st, dr, mij, poz = 0;
    cin >> n;
    for (i = 1; i <= n; i++)
        cin >> a[i];
    cin >> x;

    st = 1;
    dr = n;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] >= x) {
            poz = mij;
            dr = mij - 1;
        } else {
            st = mij + 1;
        }
    }

    if (poz != 0 && a[poz] == x)
        cout << poz;
    else
        cout << -1;
    return 0;
}

Observă: condiția a[mij] >= x ne duce mereu spre stânga, astfel încât poz reține prima poziție cu valoare >= x. Verificăm apoi dacă pe acea poziție chiar se află x.

Ultima apariție a unei valori

Pentru ultima apariție, căutăm cea mai mare poziție cu a[poz] <= x: când întâlnim un element egal cu x, reținem poziția și continuăm spre dreapta.

Program complet: ultima apariție a lui x
#include <iostream>
using namespace std;

int main() {
    int n, a[100005], x, i, st, dr, mij, poz = 0;
    cin >> n;
    for (i = 1; i <= n; i++)
        cin >> a[i];
    cin >> x;

    st = 1;
    dr = n;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] <= x) {
            poz = mij;
            st = mij + 1;
        } else {
            dr = mij - 1;
        }
    }

    if (poz != 0 && a[poz] == x)
        cout << poz;
    else
        cout << -1;
    return 0;
}

Observă: condiția a[mij] <= x ne duce mereu spre dreapta, astfel încât poz reține ultima poziție cu valoare <= x.

Câte elemente sunt mai mici decât x

Dacă poz este prima poziție cu valoarea >= x, atunci toate elementele de la 1 la poz - 1 sunt strict mai mici decât x. Numărul lor este poz - 1.

Program complet: numărul de elemente mai mici decât x
#include <iostream>
using namespace std;

int main() {
    int n, a[100005], x, i, st, dr, mij, poz;
    cin >> n;
    for (i = 1; i <= n; i++)
        cin >> a[i];
    cin >> x;

    st = 1;
    dr = n;
    poz = n + 1;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] >= x) {
            poz = mij;
            dr = mij - 1;
        } else {
            st = mij + 1;
        }
    }

    cout << poz - 1;
    return 0;
}

Observă: dacă toate elementele sunt mai mici decât x, poz rămâne n + 1, iar poz - 1 = n, ceea ce este corect.

Exercițiu de completare

Completează enunțul cu cuvintele potrivite:

Pentru a găsi prima apariție a lui x, când găsim valoarea continuăm căutarea în partea ______, mutând dr pe ______.

Bancă de cuvinte:
stângă mij - 1 dreaptă mij + 1

Exercițiu de completare 2

Completează afirmațiile despre ultima apariție și numărare:

Pentru ultima apariție continuăm în dreapta, punând st pe ______. Dacă poz este prima poziție cu valoarea >= x, atunci numărul de elemente strict mai mici decât x este ______.

Bancă de cuvinte:
mij + 1 poz - 1 mij - 1 poz + 1

Test de înțelegere

Test rapid - aplicațiile căutării binare:

1. Când căutăm prima apariție și am găsit un element egal cu x:

2. Când căutăm ultima apariție și am găsit un element egal cu x:

3. Dacă poz este prima poziție cu valoarea >= x, atunci numărul de elemente strict mai mici decât x este:

Ai grijă la: oprirea buclei imediat după ce ai găsit valoarea: la prima apariție trebuie să continui căutarea spre stânga, altfel răspunsul poate fi orice poziție cu valoarea x, nu neapărat prima.

Aprofundare

De ce valorile egale cu x sunt alăturate într-un tablou sortat? Această proprietate este esențială: dacă a[i] == a[j] == x cu i < j, atunci orice element dintre ele este tot x (altfel tabloul nu ar fi sortat). Așadar, toate aparițiile lui x formează un bloc continuu (un grup de valori egale, așezate una lângă alta). Numărul de apariții se calculează deci ca diferența dintre ultima și prima poziție plus unu: ultima - prima + 1.

Inițializarea lui poz pentru numărarea elementelor mai mici: dacă toate elementele sunt mai mici decât x, prima poziție cu a[poz] >= x nu există, deci poz trebuie inițializat cu n + 1 pentru ca poz - 1 să dea răspunsul corect (n). Această inițializare „la marginea din dreapta" este un truc standard în problemele de tip „câte elemente sunt mai mici decât x".

Generalizarea celor patru tipuri de căutare: cu ajutorul a două funcții, prima >= x și prima > x, putem răspunde la toate întrebările: elemente < x, elemente <= x, prima apariție, ultima apariție, numărul de apariții. De exemplu, prima poziție cu valoarea > x se obține căutând prima poziție cu a[poz] > x (condiția a[mij] > x). Această idee apare frecvent la olimpiadă și în problemele de tip „punct de frontieră" (probleme în care se caută exact poziția la care datele își schimbă o proprietate).

Aplicație reală - granița dintre promovat și nepromovat: într-un catalog cu notele ordonate crescător, prima poziție cu notă >= 5 ne spune instant de unde încep elevii promovați, fără să parcurgem tot catalogul. Același raționament se aplică la determinarea numărului de produse dintr-un magazin cu preț sub un buget dat.

APLICARE ȘI REZOLVARE DE PROBLEME

PbInfo - Prima și ultima apariție

Enunț:

Cerință: Se dă un tablou cu n numere naturale, sortat crescător, și o valoare x. Să se afișeze poziția primei apariții și poziția ultimei apariții a lui x, separate printr-un spațiu. Dacă x nu apare, să se afișeze -1.

Date de intrare:

Programul citește de la tastatură numărul n, apoi cele n valori sortate, apoi valoarea x.

Date de ieșire:

Programul va afișa prima și ultima poziție a lui x, separate printr-un spațiu, sau -1 dacă valoarea nu există.

Restricții și precizări:

1 <= n <= 100000, valorile din tablou sunt numere naturale sortate crescător.

Exemplu:

Intrare:

8
1 2 3 5 5 5 7 9
5

Ieșire:

4 6
Cum gândim soluția:
  1. Căutăm prima apariție, continuând spre stânga când găsim valoarea
  2. Căutăm ultima apariție, continuând spre dreapta când găsim valoarea
  3. Dacă valoarea nu există, afișăm -1
Scrie soluția în C++:
Soluție corectă:
Soluția completă
#include <iostream>
using namespace std;

int main() {
    int n, a[100005], x, i, st, dr, mij, prima = 0, ultima = 0;
    cin >> n;
    for (i = 1; i <= n; i++)
        cin >> a[i];
    cin >> x;

    st = 1;
    dr = n;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] >= x) {
            prima = mij;
            dr = mij - 1;
        } else {
            st = mij + 1;
        }
    }

    st = 1;
    dr = n;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] <= x) {
            ultima = mij;
            st = mij + 1;
        } else {
            dr = mij - 1;
        }
    }

    if (prima != 0 && a[prima] == x)
        cout << prima << " " << ultima;
    else
        cout << -1;
    return 0;
}

Ce învățăm: prima și ultima apariție se găsesc cu două căutări binare distincte, una orientată spre stânga, cealaltă spre dreapta.

PbInfo - Elemente mai mici decât x

Enunț:

Cerință: Se dă un tablou cu n numere naturale, sortat crescător, și o valoare x. Să se afișeze câte elemente din tablou sunt strict mai mici decât x.

Date de intrare:

Programul citește de la tastatură numărul n, apoi cele n valori sortate, apoi valoarea x.

Date de ieșire:

Programul va afișa numărul de elemente strict mai mici decât x.

Restricții și precizări:

1 <= n <= 100000, valorile din tablou sunt numere naturale sortate crescător.

Exemplu:

Intrare:

6
1 3 5 7 9 11
7

Ieșire:

3
Cum gândim soluția:
  1. Căutăm prima poziție cu a[mij] >= x (lower bound)
  2. Inițializăm poz = n + 1
  3. Afișăm poz - 1, numărul de elemente mai mici decât x
Scrie soluția în C++:
Soluție corectă:
Soluția completă
#include <iostream>
using namespace std;

int main() {
    int n, a[100005], x, i, st, dr, mij, poz;
    cin >> n;
    for (i = 1; i <= n; i++)
        cin >> a[i];
    cin >> x;

    st = 1;
    dr = n;
    poz = n + 1;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] >= x) {
            poz = mij;
            dr = mij - 1;
        } else {
            st = mij + 1;
        }
    }

    cout << poz - 1;
    return 0;
}

Ce învățăm: poziția primului element >= x ne spune exact câte elemente sunt mai mici decât x, fără a parcurge tabloul.

EXERCIȚII PRACTICE

Exercițiul Practic: Aplicații ale căutării binare

Sarcini de lucru

Completează următoarele sarcini pentru a-ți consolida aplicațiile căutării binare

Sarcina 1: Numără aparițiile lui x

Cerință: Scrie un program C++ care citește un tablou sortat, valoarea x și afișează de câte ori apare x în tablou, folosind prima și ultima apariție.

Soluție pentru Sarcina 1:
#include <iostream>
using namespace std;

int main() {
    int n, a[100005], x, i, st, dr, mij, prima = 0, ultima = 0;
    cin >> n;
    for (i = 1; i <= n; i++)
        cin >> a[i];
    cin >> x;

    st = 1;
    dr = n;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] >= x) {
            prima = mij;
            dr = mij - 1;
        } else {
            st = mij + 1;
        }
    }

    st = 1;
    dr = n;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] <= x) {
            ultima = mij;
            st = mij + 1;
        } else {
            dr = mij - 1;
        }
    }

    if (prima != 0 && a[prima] == x)
        cout << ultima - prima + 1;
    else
        cout << 0;
    return 0;
}

Verifică: numărul de apariții este ultima - prima + 1, deoarece valorile egale sunt alăturate în tabloul sortat.

Sarcina 2: Prima poziție cu valoare strict mai mare decât x

Cerință: Scrie un program C++ care citește un tablou sortat, valoarea x și afișează poziția primului element cu valoarea strict mai mare decât x, sau -1 dacă nu există.

Soluție pentru Sarcina 2:
#include <iostream>
using namespace std;

int main() {
    int n, a[100005], x, i, st, dr, mij, poz;
    cin >> n;
    for (i = 1; i <= n; i++)
        cin >> a[i];
    cin >> x;

    st = 1;
    dr = n;
    poz = n + 1;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] > x) {
            poz = mij;
            dr = mij - 1;
        } else {
            st = mij + 1;
        }
    }

    if (poz == n + 1)
        cout << -1;
    else
        cout << poz;
    return 0;
}

Verifică: folosim condiția strictă a[mij] > x pentru a găsi primul element mai mare decât x, nu mai mare sau egal.

Scrie pe caiet - De reținut

Titlul lecției

Ora 5: Căutarea binară - aplicații

Definiția lecției

Într-un tablou sortat, toate valorile egale cu x sunt alăturate, formând un bloc continuu; de aceea putem găsi prima și ultima apariție tot cu căutare binară. Prima apariție se obține căutând prima poziție cu a[poz] >= x: când găsim un element egal, reținem poziția și continuăm spre stânga, mutând dr = mij - 1. Ultima apariție se obține simetric, căutând ultima poziție cu a[poz] <= x și continuând spre dreapta cu st = mij + 1. După ieșirea din buclă verificăm dacă pe poziția reținută chiar se află x, altfel afișăm -1. Dacă poz este prima poziție cu valoarea >= x, atunci numărul de elemente strict mai mici decât x este poz - 1, cu poz inițializat cu n + 1 pentru cazul în care toate elementele sunt mai mici. Numărul de apariții se calculează cu formula ultima - prima + 1.

Definiții esențiale

  • Într-un tablou sortat, valorile egale formează un bloc continuu de poziții alăturate.
  • Prima apariție: prima poziție cu a[poz] >= x; când găsim un element egal, continuăm spre stânga (dr = mij - 1).
  • Ultima apariție: ultima poziție cu a[poz] <= x; când găsim un element egal, continuăm spre dreapta (st = mij + 1).
  • Numărul de elemente mai mici decât x: poz - 1, unde poz este prima poziție cu a[poz] >= x (inițializat cu n + 1).

Algoritmul cheie - pseudocod

Algoritmul PrimaApariție(a, n, x): st <- 1, dr <- n, poz <- 0 cât timp st <= dr execută mij <- (st + dr) / 2 dacă a[mij] >= x atunci poz <- mij dr <- mij - 1 altfel st <- mij + 1 sfârșit dacă sfârșit cât timp dacă poz != 0 și a[poz] = x atunci scrie poz altfel scrie -1 sfârșit dacă

Algoritmul cheie - cod C++

Prima apariție a lui x
st = 1;
dr = n;
poz = 0;
while (st <= dr) {
    mij = (st + dr) / 2;
    if (a[mij] >= x) {
        poz = mij;
        dr = mij - 1;
    } else {
        st = mij + 1;
    }
}
if (poz != 0 && a[poz] == x)
    cout << poz;
else
    cout << -1;

Greșeli comune de evitare

  • Oprirea căutării la primul element egal: la prima apariție trebuie să continuăm spre stânga.
  • Inițializarea lui poz cu 0 la numărarea elementelor mai mici: dacă toate elementele sunt mai mici, răspunsul devine greșit; folosește n + 1.
  • Verificarea lipsă a[poz] == x: fără ea putem afișa o poziție pe care nu se află valoarea căutată.

Exemplu practic

Într-un catalog de note ordonate crescător, profesorul vrea să afle câți elevi au luat exact nota x la test. Valorile egale sunt alăturate, deci numărul lor este ultima - prima + 1. Programul de mai jos găsește prima și ultima apariție a notei x, apoi afișează numărul de apariții.

Numărarea aparițiilor notei x
#include <iostream>
using namespace std;

int main() {
    int n, a[100005], x, i, st, dr, mij, prima = 0, ultima = 0;
    cin >> n;
    for (i = 1; i <= n; i++)
        cin >> a[i];
    cin >> x;

    st = 1; dr = n;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] >= x) {
            prima = mij;
            dr = mij - 1;
        } else {
            st = mij + 1;
        }
    }

    st = 1; dr = n;
    while (st <= dr) {
        mij = (st + dr) / 2;
        if (a[mij] <= x) {
            ultima = mij;
            st = mij + 1;
        } else {
            dr = mij - 1;
        }
    }

    if (prima != 0 && a[prima] == x)
        cout << ultima - prima + 1;
    else
        cout << 0;
    return 0;
}

Pentru notele 4 5 5 5 7 9 și x = 5, programul afișează 3 - exact numărul de elevi care au luat nota 5.

Sfaturi practice și trucuri

  • Numărul de apariții: ultima - prima + 1 - două căutări binare, răspuns instant.
  • Pentru „elemente <= x", caută prima poziție cu valoarea > x și scade 1.
  • Testează întotdeauna cazul în care x nu apare deloc în tablou.