kz-maxx

Heap

Куча: массив арқылы сақтау, push, pop, top, up, down

Түсіндіру

Heap, яғни куча — бұл структура данных. Ол ең кіші немесе ең үлкен элементті тез табуға көмектеседі.

Кучаның екі негізгі түрі бар:

  • min-heap — ең кіші элемент жоғарыда тұрады
  • max-heap — ең үлкен элемент жоғарыда тұрады
Бұл бетте біз min-heap жазамыз. Яғни h[1] әрқашан ең кіші элемент болады.
min-heap:
h[1] = ең кіші элемент

Куча binary tree сияқты көрінеді, бірақ кодта ол жай массивте сақталады.

Куча массивте қалай сақталады?

Біз екі айнымалы қолданамыз:

int h[N];
int sz;
  • h — куча сақталатын массив
  • sz — кучада қанша элемент бар

Индексация 1-ден басталады. Себебі онда parent және child табу оңай болады.

Не табамыз Формула
i вершинасының parent-і i / 2
i вершинасының сол child-ы i * 2
i вершинасының оң child-ы i * 2 + 1

Мысал

индекстер: 1 2 3 4 5
h: 2 5 7 9 8

Бұл кучаны дерево сияқты былай елестетуге болады:

2
5 7
9 8

Min-heap үшін правило:

parent <= children

Мысалы, 2 өзінің балалары 5 және 7-ден кіші. Сондықтан ең кіші элемент жоғарыда тұр.

Негізгі операциялар

Операция Не істейді
push(x) x санын кучаға қосады
top() ең кіші элементті қайтарады
pop() ең кіші элементті өшіреді
up(i) элементті жоғары көтереді
down(i) элементті төмен түсіреді

Есеп мысалы

Min-heap үшін бірнеше команда орындау керек.

Команда Не істейді
push x x санын қосады
top ең кіші элементті шығарады
pop ең кіші элементті өшіреді
size кучаның размерін шығарады

Input

8
push 5
push 2
push 7
top
pop
top
push 1
top

Output

2
5
1

Толық реализация

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int h[N];
int sz;

void up(int u) {
    while (u / 2 > 0 && h[u] < h[u / 2]) {
        swap(h[u], h[u / 2]);
        u /= 2;
    }
}

void down(int u) {
    int t = u;

    if (u * 2 <= sz && h[u * 2] < h[t]) {
        t = u * 2;
    }

    if (u * 2 + 1 <= sz && h[u * 2 + 1] < h[t]) {
        t = u * 2 + 1;
    }

    if (t != u) {
        swap(h[u], h[t]);
        down(t);
    }
}

void push(int x) {
    sz++;
    h[sz] = x;
    up(sz);
}

int top() {
    return h[1];
}

void pop() {
    h[1] = h[sz];
    sz--;
    down(1);
}

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

    int q;
    cin >> q;

    while (q--) {
        string op;
        cin >> op;

        if (op == "push") {
            int x;
            cin >> x;

            push(x);
        } else if (op == "top") {
            cout << top() << '\n';
        } else if (op == "pop") {
            pop();
        } else if (op == "size") {
            cout << sz << '\n';
        }
    }

    return 0;
}

Функциялар түсіндірмесі

1. up

void up(int u) {
    while (u / 2 > 0 && h[u] < h[u / 2]) {
        swap(h[u], h[u / 2]);
        u /= 2;
    }
}

up жаңа элемент қосылғаннан кейін керек. Жаңа элемент алдымен массивтің соңына қосылады. Егер ол parent-тен кіші болса, жоғары көтеріледі.

Кішкентай мысал

бұрын: 2 5 7
push(1)
up-қа дейін: 2 5 7 1

Жаңа элемент 1 позиция 4-те тұр. Оның parent-і позиция 2, онда 5 тұр. 1 < 5, сондықтан орындарын ауыстырамыз.

2 1 7 5

Енді 1 позиция 2-де тұр. Оның parent-і позиция 1, онда 2 тұр. 1 < 2, тағы ауыстырамыз.

1 2 7 5

Енді 1 жоғарыда тұр. Куча дұрыс болды.

2. down

void down(int u) {
    int t = u;

    if (u * 2 <= sz && h[u * 2] < h[t]) {
        t = u * 2;
    }

    if (u * 2 + 1 <= sz && h[u * 2 + 1] < h[t]) {
        t = u * 2 + 1;
    }

    if (t != u) {
        swap(h[u], h[t]);
        down(t);
    }
}

down ең жоғарғы элемент өшірілгеннен кейін керек. Біз соңғы элементті жоғарыға қоямыз. Кейін ол өз орнын тапқанша төмен түседі.

Кішкентай мысал

бұрын: 1 3 2 8 5
pop()

1 өшіріледі. Соңғы элемент 5 жоғарыға қойылады.

down-ға дейін: 5 3 2 8

5-тің балалары 3 және 2. Ең кіші бала — 2. 2 < 5, сондықтан ауыстырамыз.

кейін: 2 3 5 8

Куча қайтадан дұрыс болды.

3. push

void push(int x) {
    sz++;
    h[sz] = x;
    up(sz);
}

push жаңа элементті соңына қосады. Кейін up шақырып, оны дұрыс орынға көтереді.

бұрын: 2 5 7
push(1)
алдымен: 2 5 7 1
up кейін: 1 2 7 5

4. top

int top() {
    return h[1];
}

Min-heap-та ең кіші элемент әрқашан h[1]-де тұрады. Сондықтан top жай ғана h[1] қайтарады.

h = 2 5 7 9
top() = h[1] = 2

5. pop

void pop() {
    h[1] = h[sz];
    sz--;
    down(1);
}

pop ең кіші элементті өшіреді. Ал ең кіші элемент h[1]-де тұрады.

Өшіру жолы:

  • соңғы элементті бірінші орынға қоямыз
  • sz мәнін азайтамыз
  • down(1) шақырамыз
бұрын: 1 3 2 8 5
pop()
5 жоғарыға келеді: 5 3 2 8
down кейін: 2 3 5 8

priority_queue в C++

C++ ішінде дайын куча бар — priority_queue. По умолчанию ол max-heap болып жұмыс істейді.

Max-heap

priority_queue<int> q;

q.push(5);
q.push(2);
q.push(7);

cout << q.top();

Мұнда top() 7 қайтарады, себебі ол ең үлкен элемент.

Min-heap

priority_queue<int, vector<int>, greater<int>> q;

q.push(5);
q.push(2);
q.push(7);

cout << q.top();

Мұнда top() 2 қайтарады, себебі ол ең кіші элемент.

Олимпиадада және экзаменде ручная реализацияны түсіну маңызды. Бірақ практикада көбіне priority_queue қолдануға болады.

Асимптотика

Операция Уақыт
push O(log n)
pop O(log n)
top O(1)
size O(1)