Two pointers

Fundamente și cunoștințe necesare

Tehnica Two pointers este o tehnică pe care o putem folosi în foarte multe tipuri de probleme în care avem de căutat subsecvențe cu diverse proprietăți, condiția principală fiind aceea că vrem să găsim o pereche de valori sau de indici ce respectă anumite reguli, fără să depășim o anumită valoare sau o anumită condiție. Această tehnică apare în foarte multe tipuri de probleme ce se dau la concursurile de informatică, de foarte multe ori reprezentând o optimizare la posibile soluții cu căutare binară sau alte structuri de date ce ar adăuga un factor de complexitate în plus la soluție.

În articol voi începe prin a explica tipurile de probleme unde putem folosi Two pointers, urmând ca apoi să prezint câteva probleme de diverse dificultăți, explicând principalele strategii de abordare a acestor tipuri de probleme și punând accentul și pe implementări clare, care au drept scop evitarea greșelilor tipice când vine vorba de implementarea acestei metode.

Pentru a folosi această metodă, e nevoie să stăpânim lucrul cu secvențe și ideal și căutarea binară, deoarece pentru multe dintre exemplele ce vor fi menționate, există soluții și folosind acest algoritm. Nu în ultimul rând, pentru anumite probleme e posibil să fie nevoie de structuri de date adiționale, cum ar fi map sau set.

În ceea ce privește modul în care începem implementările, avem două tipuri majore de implementări, în funcție de algoritm. Merită menționat faptul că acești pointeri sunt de fapt variabile corespunzătoare indicilor din vector la care ne aflăm la momentul respectiv.

În primul rând, vorbim de problemele în care vrem să plecăm de la prima poziție și să procesăm secvențele care respectă o anumită proprietate monotonă (crescătoare sau descrescătoare). În acest caz, vom avea ambii pointeri cu indexul de început de la 1 și vom avansa cu pointerul din dreapta atâta timp cât încă se mai respectă condiția cerută din enunț.

De asemenea, mai există probleme în care plecăm cu pointerul stâng de la prima poziție și cu pointerul drept de la ultima poziție și vrem să mergem cu acești pointeri în direcții opuse, deoarece căutăm o proprietate ce are o variație descrescătoare față de scopul problemei.

Problema 1 - Subarray Sums I

CSES

View Problem

CSES
Deschide

Dat fiind un vector cu nn elemente numere naturale, determinați numărul de subsecvențe din vector pentru care suma elementelor este egală cu xx.

Pentru a rezolva această problemă, trebuie să ne folosim de faptul că toate numerele din șirul dat sunt pozitive, ceea ce face rezolvarea mult mai ușoară. Astfel, vom putea afla pentru fiecare poziție de început a șirului, care este poziția cea mai din dreapta astfel încât suma valorilor din acel interval să fie mai mică sau egală cu xx. Dacă acea sumă este egală cu xx, vom incrementa răspunsul. Vom avea grijă la fiecare pas să incrementăm variabila stst, având grijă să scădem valoarea curentă din sumă mai întâi.

#include <iostream>
using namespace std;

int n, v[200002], x;
int st, dr, sum, ans;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> x;
    for (int i = 1; i <= n; i++) {
        cin >> v[i];
    }

    st = dr = 1;
    sum = ans = 0;
    while (st <= n) {
        while (dr <= n && sum < x) {
            sum += v[dr];
            dr++;
        }
        if (sum == x) {
            ans++;
        }
        sum -= v[st];
        st++;
    }
    cout << ans;
    return 0;
}

Problema 2 - Sum of Two Values

CSES

View Problem

CSES
Deschide

Se dă un vector cu nn valori pozitive și o valoare xx. Scrieți un program care să determine două valori aflate pe poziții distincte care adunate să dea suma xx.

Pentru a rezolva această problemă, trebuie să folosim o variantă diferită a tehnicii celor doi pointeri. Astfel, de data asta vom începe cu un pointer la poziția 1, iar cu celălalt la poziția nn. Pe parcurs, vom avea trei cazuri în funcție de suma a_p1+a_p2a\_{p_1} + a\_{p_2}, iar dacă găsim două poziții cu suma valorilor egală cu xx, afișăm pozițiile corespunzătoare, altfel modificăm p_1p\_1 sau p_2p\_2 după caz. Dacă nu găsim nicio pereche, afișăm IMPOSSIBLE.

#include <algorithm>
#include <iostream>

using namespace std;

int n, x, a[200002], b[200002];

int main() {
    cin >> n >> x;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        b[i] = a[i];
    }

    sort(a + 1, a + n + 1);
    int p1 = 1, p2 = n;

    while (p1 < p2) {
        if (a[p1] + a[p2] == x) {
            int valA = a[p1];
            int valB = a[p2];

            for (int i = 1; i <= n; ++i) {
                if (b[i] == valA) {
                    cout << i << " ";
                    valA = 0;
                } else if (b[i] == valB) {
                    cout << i << " ";
                    valB = 0;
                }
            }
            return 0;
        } else {
            if (a[p1] + a[p2] > x) {
                --p2;
            } else {
                ++p1;
            }
        }
    }

    cout << "IMPOSSIBLE";
    return 0;
}

Problema 3 - Nane

InfoArena

View Problem

InfoArena
Deschide

Nane de pe Jiu, mare algoritmician fiind, vă provoacă să rezolvați o problemă prea ușoară pentru el. Nane vă dă NN numere naturale și un număr KK. Numim subsecvență specială o subsecvență pentru care efectuând operația OR pe biți pentru elementele din subsecvență (să numim această operație sumă OR) obținem un rezultat care are, în reprezentare binară, cel mult KK biți de 1. Două subsecvențe sunt diferite dacă cel puțin o poziție din una nu se regăsește în cealaltă. Scrieți un program care să determine numărul de subsecvențe speciale.

Pentru a rezolva această problemă, vom folosi metoda celor doi pointeri pentru a afla numărul de secvențe care au suma OR cu cel mult kk de 1, actualizările fiind foarte similare cu cele de la celelalte probleme de acest tip. De asemenea, deoarece vorbim de suma OR, trebuie să folosim câte un vector de frecvență pentru fiecare bit pentru a evita calculele adiționale.

#include <fstream>
#define ll long long
using namespace std;

int n, k, v[100002], fr[32];

ll ans;

bool ok() {
    int cnt = 0;
    for (int i = 0; i <= 30; ++i) {
        if (fr[i]) {
            ++cnt;
        }
    }
    if (cnt <= k) {
        return 1;
    }
    return 0;
}

void add(int poz, int val) {
    for (int i = 0; i <= 30; ++i) {
        if ((v[poz] & (1 << i))) {
            fr[i] += val;
        }
    }
}
int main() {
    ifstream cin("nane.in");
    ofstream cout("nane.out");

    cin >> n >> k;
    for (int i = 1; i <= n; ++i) {
        cin >> v[i];
    }
    int st = 1;
    int dr = 1;
    while (st <= n) {
        while (dr > n || !ok()) {
            if (ok()) {
                ans += dr - st;
                add(st, -1);
                ++st;
            } else {
                add(st, -1);
                ++st;
                ans += dr - st;
            }
        }
        while (dr <= n && ok()) {
            add(dr, 1);
            ++dr;
        }
    }
    cout << ans << '\n';
    return 0;
}

Problema 4 - JJOOII 2

OJ.UZ

View Problem

OJ.UZ
Deschide

Se consideră un șir format din NN caractere din mulțimea J,O,I{J, O, I}. Se numește JOI-șir de nivel KK un șir format din KK litere J, KK litere O și KK litere I (în această ordine). De exemplu, JJOOII este un JOI-șir de nivel 2. Bitaro dorește să transforme șirul SS într-un JOI-șir de nivel KK, utilizând următoarele 3 operații, de oricâte ori și în orice ordine:

  • Operația 1: Bitaro șterge primul caracter din SS;
  • Operația 2: Bitaro șterge ultimul caracter din SS;
  • Operația 3: Bitaro șterge un caracter din interiorul lui SS (care nu este nici primul nici ultimul).

Deoarece operațiile de tip 3 necesită mult timp, Bitaro dorește să transforme șirul SS într-un JOI-șir de nivel KK folosind un număr minim de operații de tip 3. Scrieți un program care, cunoscând NN, SS și KK, determină numărul minim de operații de tip 3 necesare pentru a transforma șirul SS într-un JOI-șir de nivel KK. Dacă acest lucru nu este posibil, programul va afișa valoarea 1-1.

Pentru a rezolva această problemă, va trebui mai întâi să aflăm unde sunt situate literele J, O și I în șirul nostru de caractere. Ulterior, pe măsură ce fixăm secvențele de kk de J, avem pointeri care duc la secvențele corespunzătoare de O și I din celelalte două șiruri, calculele ulterioare devenind destul de ușoare.

#include <iostream>
using namespace std;

int vj[200001], vo[200001], vi[200001];

int lj, lo, li;

int main() {
    int n, k;
    cin >> n >> k;

    string s;
    cin >> s;

    for (int i = 0; i < n; i++) {
        if (s[i] == 'J') {
            vj[++lj] = i;
        }
        if (s[i] == 'O') {
            vo[++lo] = i;
        }
        if (s[i] == 'I') {
            vi[++li] = i;
        }
    }

    int pj = 1;
    int po = 1;
    int pi = 1;

    int ans = n + 1;

    for (int pj = 0; pj + k - 1 < lj; ++pj) {
        int po = 0;
        while (po + k - 1 < lo && vo[po] <= vj[pj + k - 1]) {
            po++;
        }

        if (po + k - 1 < lo) {
            int pi = 0;
            while (pi + k - 1 < li && vi[pi] <= vo[po + k - 1]) {
                pi++;
            }

            if (pi + k - 1 < li) {
                int start = vj[pj];
                int end = vi[pi + k - 1];
                int segment_length = (end - start + 1) - 3 * k;

                ans = min(ans, segment_length);
            }
        }
    }

    cout << (ans == n + 1 ? -1 : ans);
    return 0;
}

Probleme suplimentare

Probleme de la olimpiade

Probleme de pe Codeforces

Bibliografie și lectură suplimentară