Денис Сапожников
Денис Сапожников Contact author via telegram ВШЭ Прикладная математика и информатика 23', ШАД Data Science 22'

Сканирующая прямая

Идея для метода - очень простая и понятная. Пусть мы живём в мире, в котором случаются некоторые события в определенный момент времени ti. Соответственно, в зависимости от задачи нам нужно понять:

  1. Какие события бывают. То есть придумать типы событий
  2. Понять в какой момент времени у нас случается i-тое событие, чтобы определиться с порядком обработки событий
  3. Научиться корректно обрабатывать все события

Звучит слишком обще, давайте разберем пару примеров:

Про рыбок

У вас есть n аквариумов, которые стоят в ряд. В i-том аквариуме ai рыбок. Следующая рыбка в нём родится через f(ai) = max(1, 1000 - ai) секунд, после чего ai увеличивается на 1 и таймер i-го аквариума обнуляется.
Петя изначально стоит возле первого аквариума и хочет находиться возле любого аквариума в момент рождения рыбки в нём. Перемещение между соседними аквариумами занимает 1 секунду. Ваша задача понять, сколько секунд Петя сможет следить за рождением каждой рыбки.

Решение за O(быстро)

Придумывать события в этой задаче очень просто: событие - рождение рыбки. А время события - АБСОЛЮТНОЕ время события, прошедшее с начала эксперимента.

Такие события нам надо обрабатывать в порядке возрастания времени, при этом события динамические, то есть события добавляются и удаляются. Поэтому для хранения событий будем использовать set.

Обработка событий тривиальная, смотри реализацию.

Код
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
#include <fstream>
#include <map>

using namespace std;

int get_time(int f) {
    return max(1000 - f, 1);
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;
    vector<int> a(n);
    set<pair<int, int>> s; // { time, ind }
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        s.insert({ get_time(a[i]), i });
    }


    int last_ti = 0;
    int last_i = 0;
    while (true) {
        auto [ti, i] = *s.begin();
        s.erase(s.begin());

        if (ti - last_ti < abs(i - last_i)) {
            cout << ti;
            return 0;
        }
        else {
            ++a[i];
            s.insert({ ti + get_time(a[i]), i });
        }

        last_ti = ti;
        last_i = i;
    }
}

Про жесткий диск

Васин жесткий диск состоит из M секторов. Вася последовательно устанавливал на него различные операционные системы следующим методом: он создавал новый раздел диска из последовательных секторов, начиная с сектора номер ai и до сектора bi включительно, и устанавливал на него очередную систему. При этом если очередной раздел хотя бы по одному сектору пересекается с каким-то ранее созданным разделом, то ранее созданный раздел «затирается», и операционная система, которая на него была установлена, больше не может быть загружена.
Напишите программу, которая по информации о том, какие разделы на диске создавал Вася, определит, сколько в итоге работающих операционных систем установлено и в настоящий момент работает на Васином компьютере.

Решение за O(n log n)

Эта задача сильно сложнее предыдущей. Тут придётся додуматься, что это задача на scanline.

Пусть событие - это начало или конец отрезка, в который мы записали что-то на жестком диске. То есть у нас есть два типа событий - открытие отрезка и закрытие отрезка.

Обрабатывать события будем в порядке увеличения координаты этого события. События статичны, поэтому можно их отсортировать один раз без сложных структур (типа set).

В любой момент времени мы будем поддерживать множество из текущих открытых отрезков. Нам важно, что в этом множестве любой диск будет поврежден, кроме самого верхнего.

Обсудим обработку двух типов событий:

  1. Начало отрезка. Если есть отрезок, который выше данного (в нашем set), то помечаем текущий отрезок как поломанный. Если такого отрезка нет, то помечаем поломанным самый верхний отрезок из текущих (потому что наш отрезок выше него и повреждает его).
  2. Конец отрезка - удаляем отрезок из текущих

Собственно, это всё. Самое сложное в задаче, как вы могли догадаться - это придумать, что какую структуру нужно поддерживать, чтобы обрабатывать все события.

Код
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
#include <numeric>

using namespace std;

struct Event {
    int c, type, id; // type == -1, если открывается, type == 1, если закрывается

    bool operator<(Event other) const {
        return c < other.c || c == other.c && type < other.type;
    }
};

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int m;
    cin >> m;
    int n;
    cin >> n;
    vector<int> l(n), r(n);
    vector<Event> events(2 * n);

    for (int i = 0; i < n; ++i) {
        cin >> l[i] >> r[i];
        events[2 * i] = { l[i], -1, i };
        events[2 * i + 1] = { r[i], 1, i };
    }

    sort(events.begin(), events.end());
    set<int> pr; // { pr } pr = id
    vector<bool> bad(n, false);

    for (auto [c, type, id] : events) {
        if (type == -1) {
            if (!pr.empty() && id < *prev(pr.end())) {
                bad[id] = true;
            }
            else if (!pr.empty()) {
                bad[*prev(pr.end())] = true;
            }

            pr.insert(id);
        }
        else { // type == 1
            pr.erase(id);
        }
    }

    cout << n - accumulate(bad.begin(), bad.end(), 0);
}

Часто, чтобы обрабатывать события, необходимо использовать set или дерево отрезков и его модификации.