Chương 3·Bài 39

Sweep Line

Nhiều bài về đoạn thẳng, khoảng thời gian, hình chữ nhật có lời giải chung: tưởng tượng một đường thẳng quét qua trục từ trái sang phải, chỉ dừng ở các điểm có sự kiện (một đoạn bắt đầu, một đoạn kết thúc, một câu hỏi), và giữ một cấu trúc dữ liệu mô tả những gì đường quét đang cắt. Giữa hai sự kiện liên tiếp, trạng thái không đổi, nên bài toán trên trục liên tục hay tọa độ tới 101810^{18} chỉ còn O(n)O(n) bước. Cái khó là chọn đúng sự kiện, đúng thứ tự xử lý khi trùng tọa độ, và đúng cấu trúc giữ trạng thái.

Sự kiện mở và đóng

Đoạn [l,r][l, r] trên tọa độ nguyên cho hai sự kiện: +1+1 tại ll, −1-1 tại r+1r + 1 (đoạn nửa mở [a,b)[a, b) thì −1-1 tại bb). Sắp mọi sự kiện theo tọa độ; quét qua và cộng dồn, giá trị hiện tại là mức phủ của đoạn từ sự kiện này tới sự kiện kế tiếp. Đây là mảng hiệu (bài 02) khi tọa độ quá lớn để lập mảng: chỉ lưu những điểm có thay đổi. Hai sự kiện cùng tọa độ thì xử lý hết rồi mới đọc mức phủ, hoặc xếp thứ tự theo đúng nghĩa đề (đóng trước mở khi đoạn nửa mở chạm nhau).

Vạch cam là sự kiện đang xét; dưới trục là mức phủ, không đổi giữa hai sự kiện liên tiếp.

Ví dụGiờ Cao Điểm

Đề. n≤2⋅105n \le 2 \cdot 10^5 khoảng nửa mở [ai,bi)[a_i, b_i), mọi đầu mút khác nhau. Tìm số khoảng cùng phủ một thời điểm nhiều nhất.

Lời giải. Mỗi khoảng sinh hai sự kiện: +1+1 tại aia_i (bắt đầu), −1-1 tại bib_i (kết thúc). Sắp theo thời điểm rồi cộng dồn: sau mỗi sự kiện, tổng đang có là số khoảng đang mở, và đáp án là giá trị lớn nhất của nó.

Bước 1: sinh sự kiện.

C++
ev.push_back({a, 1});
ev.push_back({b, -1});

Bước 2: sắp và quét.

C++
sort(ev.begin(), ev.end());
ll cur = 0, best = 0;
for (pair<ll, ll> e : ev) {
    cur += e.second;
    best = max(best, cur);
}

Khoảng nửa mở nên khi trùng thời điểm, sự kiện −1-1 phải đứng trước +1+1; pair sắp −1-1 trước 11 một cách tự nhiên. Ở bài này các đầu mút khác nhau nên chi tiết đó không phát sinh.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n;
vector<pair<ll, ll>> ev;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        ll a, b;
        cin >> a >> b;
        ev.push_back({a, 1});
        ev.push_back({b, -1});
    }
    sort(ev.begin(), ev.end());
    ll cur = 0, best = 0;
    for (pair<ll, ll> e : ev) {
        cur += e.second;
        best = max(best, cur);
    }
    cout << best << '\n';
}
4
1 4
2 5
6 8
3 7

O(nlog⁡n)O(n \log n) cho phần sắp.

Ví dụMức Phủ Trên Trục Số

Đề. n≤2⋅105n \le 2 \cdot 10^5 đoạn đóng, đầu mút nguyên tới 101810^{18}. Với mỗi kk, đếm số tọa độ nguyên được đúng kk đoạn phủ.

Lời giải. Đoạn đóng [l,r][l, r] trên tọa độ nguyên thành sự kiện +1+1 tại ll và −1-1 tại r+1r + 1, để làm việc như đoạn nửa mở. Sau khi xử lý mọi sự kiện tại xx, mức phủ cc giữ nguyên cho các tọa độ từ xx tới trước sự kiện kế tiếp x′x', nên cộng x′−xx' - x vào ô đáp án thứ cc. Không cần xử lý riêng sự kiện cùng tọa độ: khoảng giữa chúng dài 0, đóng góp 0 dù mức phủ trung gian là gì.

Bước 1: sự kiện. r+1r + 1 tới 1018+110^{18} + 1, vẫn vừa ll.

C++
ev.push_back({l, 1});
ev.push_back({r + 1, -1});

Bước 2: cộng độ dài từng khoảng. Sau khi cộng sự kiện thứ ii, mức phủ cur đúng cho mọi tọa độ từ ev[i] tới trước ev[i + 1].

C++
for (ll i = 0; i + 1 < (ll)ev.size(); i++) {
    cur += ev[i].second;
    ans[cur] += ev[i + 1].first - ev[i].first;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, ans[N];
vector<pair<ll, ll>> ev;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        ll l, r;
        cin >> l >> r;
        ev.push_back({l, 1});
        ev.push_back({r + 1, -1});
    }
    sort(ev.begin(), ev.end());
    ll cur = 0;
    for (ll i = 0; i + 1 < (ll)ev.size(); i++) {
        cur += ev[i].second;
        ans[cur] += ev[i + 1].first - ev[i].first;
    }
    for (ll k = 1; k <= n; k++) cout << ans[k] << ' ';
    cout << '\n';
}
4
0 2
2 4
4 4
1 3

O(nlog⁡n)O(n \log n).

Gán tài nguyên bằng hàng đợi ưu tiên

Muốn gán mỗi khoảng vào một phòng (máy, nhân viên) sao cho hai khoảng chồng nhau không cùng phòng, và dùng ít phòng nhất: quét các khoảng theo thời điểm bắt đầu, giữ hàng đợi ưu tiên các phòng đang dùng theo thời điểm rảnh. Khoảng mới bắt đầu sau khi phòng rảnh sớm nhất đã rảnh thì dùng lại phòng đó, không thì mở phòng mới. Số phòng mở ra bằng mức phủ lớn nhất, cận dưới hiển nhiên, nên là tối ưu.

Quét các khoảng theo thời điểm bắt đầu; mỗi hàng là một phòng.

Hỏi nhanh

Đang có hai phòng rảnh lúc 5 và lúc 9, khoảng mới bắt đầu lúc 7. Thuật toán làm gì?

Ví dụPhân Phòng Tối Thiểu

Đề. n≤2⋅105n \le 2 \cdot 10^5 lượt, lượt ii chiếm các ngày ai..bia_i..b_i; hai lượt chung một ngày phải khác phòng. In số phòng ít nhất và phòng của từng lượt.

Lời giải. Xét lượt theo ngày bắt đầu tăng dần. Tới lượt bắt đầu ngày aa, nếu có phòng đã rảnh trước ngày aa thì dùng lại, không thì mở phòng mới. Cách này dùng đúng số phòng bằng số lượt cùng chung một ngày nhiều nhất: mỗi lần phải mở phòng mới là lúc mọi phòng đang có đều bận vào ngày aa. Không thể ít hơn số đó, nên là tối ưu.

Bước 1: sắp chỉ số theo ngày bắt đầu. Giữ chỉ số gốc để in phòng theo thứ tự đầu vào.

C++
sort(id + 1, id + n + 1, cmp);

Bước 2: phòng rảnh sớm nhất. pq chứa các phòng đang dùng dưới dạng (ngày cuối, số phòng), sắp theo ngày cuối tăng dần. Ngày cuối nhỏ hơn hẳn aa (ngày đóng, nên phải nhỏ hơn hẳn) thì dùng lại phòng đó; ngược lại mở phòng mới. Cuối cùng đẩy lại (ngày cuối của lượt này, phòng).

C++
for (ll t = 1; t <= n; t++) {
    ll i = id[t];
    if (!pq.empty() && pq.top().first < a[i]) {
        room[i] = pq.top().second;
        pq.pop();
    } else room[i] = ++k;
    pq.push({b[i], room[i]});
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, a[N], b[N], id[N], room[N];

bool cmp(ll x, ll y) {
    return a[x] < a[y];
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> a[i] >> b[i];
        id[i] = i;
    }
    sort(id + 1, id + n + 1, cmp);
    priority_queue<pair<ll, ll>, vector<pair<ll, ll>>, greater<pair<ll, ll>>> pq;
    ll k = 0;
    for (ll t = 1; t <= n; t++) {
        ll i = id[t];
        if (!pq.empty() && pq.top().first < a[i]) {
            room[i] = pq.top().second;
            pq.pop();
        } else room[i] = ++k;
        pq.push({b[i], room[i]});
    }
    cout << k << '\n';
    for (ll i = 1; i <= n; i++) cout << room[i] << ' ';
    cout << '\n';
}
4
1 2
2 3
4 4
3 5

O(nlog⁡n)O(n \log n).

Quét kèm Fenwick

Khi câu hỏi có hai chiều, quét theo chiều thứ nhất và giữ chiều thứ hai trong Fenwick (bài 16). Đoạn [c,d][c, d] nằm trong [a,b][a, b] khi a≤ca \le c và d≤bd \le b: sắp các đoạn theo đầu trái tăng dần (bằng nhau thì đầu phải giảm dần), đoạn nằm trong [a,b][a, b] là đoạn đứng sau nó có đầu phải không quá bb. Quét từ cuối, Fenwick theo đầu phải đã nén đếm các đoạn đã thêm có đầu phải ≤b\le b. Giao điểm giữa đoạn ngang và đoạn dọc: quét theo xx, đoạn ngang được thêm vào Fenwick theo yy khi đường quét gặp đầu trái và bị gỡ khi gặp đầu phải; đoạn dọc đếm số đoạn ngang đang có trong khoảng yy của nó.

Ví dụĐếm Đoạn Bao Chứa

Đề. n≤2⋅105n \le 2 \cdot 10^5 đoạn khác nhau. Với mỗi đoạn, đếm số đoạn khác nằm trong nó và số đoạn khác chứa nó.

Lời giải. Sắp các đoạn theo xx tăng, cùng xx thì yy giảm. Khi đó đoạn chứa đoạn ii luôn đứng trước ii (đầu trái không lớn hơn, cùng đầu trái thì dài hơn), và đoạn nằm trong ii luôn đứng sau. Đoạn chứa ii là đoạn đứng trước có y≥yiy \ge y_i; đoạn nằm trong ii là đoạn đứng sau có y≤yiy \le y_i. Hai câu đếm này làm bằng Fenwick theo yy đã nén, một lần quét xuôi và một lần quét ngược.

Bước 1: nén yy và sắp. r[i] là hạng của yiy_i, dùng làm chỉ số Fenwick.

C++
sort(ys + 1, ys + n + 1);
for (ll i = 1; i <= n; i++) r[i] = lower_bound(ys + 1, ys + n + 1, y[i]) - ys;
sort(id + 1, id + n + 1, cmp);

Bước 2: quét xuôi, đếm đoạn chứa. Có t−1t - 1 đoạn đã thêm; trừ đi những đoạn có yy nhỏ hơn hẳn (get(r[i] - 1)), vì chúng không chứa ii, còn lại là số đoạn chứa ii.

C++
for (ll t = 1; t <= n; t++) {
    ll i = id[t];
    out[i] = (t - 1) - get(r[i] - 1);
    update(r[i], 1);
}

Bước 3: xóa Fenwick, quét ngược, đếm đoạn nằm trong. get(r[i]) là số đoạn đứng sau có y≤yiy \le y_i.

C++
for (ll i = 1; i <= n; i++) bit[i] = 0;
for (ll t = n; t >= 1; t--) {
    ll i = id[t];
    in[i] = get(r[i]);
    update(r[i], 1);
}

Hai đoạn khác nhau cùng xx có yy khác nhau, nên không có trường hợp bằng nhau cần tách.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, x[N], y[N], id[N], ys[N], r[N], bit[N], in[N], out[N];

bool cmp(ll a, ll b) {
    return x[a] != x[b] ? x[a] < x[b] : y[a] > y[b];
}

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> x[i] >> y[i];
        id[i] = i;
        ys[i] = y[i];
    }
    sort(ys + 1, ys + n + 1);
    for (ll i = 1; i <= n; i++) r[i] = lower_bound(ys + 1, ys + n + 1, y[i]) - ys;
    sort(id + 1, id + n + 1, cmp);
    for (ll t = 1; t <= n; t++) {
        ll i = id[t];
        out[i] = (t - 1) - get(r[i] - 1);
        update(r[i], 1);
    }
    for (ll i = 1; i <= n; i++) bit[i] = 0;
    for (ll t = n; t >= 1; t--) {
        ll i = id[t];
        in[i] = get(r[i]);
        update(r[i], 1);
    }
    for (ll i = 1; i <= n; i++) cout << in[i] << ' ';
    cout << '\n';
    for (ll i = 1; i <= n; i++) cout << out[i] << ' ';
    cout << '\n';
}
5
1 10
2 5
3 4
6 9
1 7

O(nlog⁡n)O(n \log n).

Ví dụGiao Điểm Vuông Góc

Đề. n≤105n \le 10^5 đoạn ngang hoặc dọc, tọa độ trong [−106,106][-10^6, 10^6], không đầu mút nào là giao điểm. Đếm số giao điểm giữa đoạn ngang và đoạn dọc.

Lời giải. Quét một đường thẳng đứng từ trái sang phải. Đoạn ngang yy từ x1x_1 tới x2x_2 “sống” khi đường quét nằm giữa hai đầu của nó: thêm yy vào Fenwick tại x1x_1, gỡ tại x2x_2. Đoạn dọc tại xx từ y1y_1 tới y2y_2 giao với đúng các đoạn ngang đang sống có yy trong [y1,y2][y_1, y_2], đếm bằng hai tổng tiền tố.

Bước 1: sự kiện. Mỗi sự kiện là (x, loại, y1, y2): loại 0 là thêm đoạn ngang, 1 là câu hỏi của đoạn dọc, 2 là gỡ đoạn ngang; sắp theo xx rồi theo loại. OFF dời yy từ [−106,106][-10^6, 10^6] sang chỉ số dương cho Fenwick.

C++
if (y1 == y2) {
    ev.push_back({x1, 0, y1 + OFF, 0});
    ev.push_back({x2, 2, y1 + OFF, 0});
} else ev.push_back({x1, 1, y1 + OFF, y2 + OFF});

Bước 2: quét.

C++
for (array<ll, 4> e : ev) {
    if (e[1] == 0) update(e[2], 1);
    else if (e[1] == 2) update(e[2], -1);
    else ans += get(e[3]) - get(e[2] - 1);
}

Thứ tự cùng xx (thêm, hỏi, gỡ) sẽ đếm cả giao điểm ở đầu mút; đề bảo đảm không có, nên thứ tự nào cũng cho cùng đáp án.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll OFF = 1e6 + 1, SZ = 2e6 + 5;
ll n, bit[SZ];
vector<array<ll, 4>> ev;

void update(ll pos, ll val) {
    for (; pos < SZ; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        ll x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        if (y1 == y2) {
            ev.push_back({x1, 0, y1 + OFF, 0});
            ev.push_back({x2, 2, y1 + OFF, 0});
        } else ev.push_back({x1, 1, y1 + OFF, y2 + OFF});
    }
    sort(ev.begin(), ev.end());
    ll ans = 0;
    for (array<ll, 4> e : ev) {
        if (e[1] == 0) update(e[2], 1);
        else if (e[1] == 2) update(e[2], -1);
        else ans += get(e[3]) - get(e[2] - 1);
    }
    cout << ans << '\n';
}
5
0 2 6 2
1 5 5 5
2 0 2 6
4 1 4 4
7 0 7 3

O(nlog⁡C)O(n \log C) với C≈2⋅106C \approx 2 \cdot 10^6.

Câu hỏi ngoại tuyến theo ngưỡng

“Đếm phần tử lớn hơn kk trên đoạn [l,r][l, r]” có hai chiều: chỉ số và giá trị. Coi giá trị là trục quét: sắp phần tử và câu hỏi theo giá trị giảm dần; tới câu hỏi ngưỡng kk thì mọi phần tử lớn hơn kk đã được bật trong Fenwick theo chỉ số, đáp án là tổng đoạn [l,r][l, r]. Mỗi phần tử bật một lần, tổng O((n+q)log⁡n)O((n + q) \log n). Cùng kỹ thuật cho câu hỏi theo thời điểm, theo trọng số cạnh (thêm cạnh khi ngưỡng tới, như bài 23), theo độ cao.

Hỏi nhanh

Quét các phần tử theo giá trị giảm dần để trả lời câu hỏi “đếm phần tử lớn hơn kk trên [l,r][l, r]”. Khi tới một câu hỏi, những phần tử nào phải đang được bật trong Fenwick?

Ví dụĐếm Phần Tử Vượt Ngưỡng

Đề. Mảng n≤3⋅104n \le 3 \cdot 10^4 số, q≤2⋅105q \le 2 \cdot 10^5 câu hỏi (l,r,k)(l, r, k): đếm phần tử lớn hơn kk trong [l,r][l, r].

Lời giải. Trả lời ngoại tuyến: xếp câu hỏi theo kk giảm dần. Với ngưỡng kk, chỉ các phần tử lớn hơn kk được “bật”; ngưỡng giảm dần thì tập phần tử bật chỉ lớn thêm, không bao giờ phải tắt. Đáp án của câu hỏi là số phần tử đã bật trong [l,r][l, r], đếm bằng Fenwick theo vị trí.

Bước 1: hai thứ tự. ia là chỉ số phần tử sắp theo giá trị giảm, iq là chỉ số câu hỏi sắp theo kk giảm.

C++
sort(ia + 1, ia + n + 1, ca);
sort(iq + 1, iq + q + 1, cq);

Bước 2: con trỏ trên phần tử. Con trỏ j chỉ tiến: trước mỗi câu hỏi, bật mọi phần tử có giá trị lớn hơn kk chưa bật. Đáp án ghi theo chỉ số câu hỏi gốc, cuối cùng in lại theo thứ tự đầu vào.

C++
for (ll t = 1, j = 1; t <= q; t++) {
    ll i = iq[t];
    while (j <= n && a[ia[j]] > qk[i]) update(ia[j++]);
    ans[i] = get(qr[i]) - get(ql[i] - 1);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 3e4 + 5, Q = 2e5 + 5;
ll n, q, a[N], ia[N], bit[N], ql[Q], qr[Q], qk[Q], iq[Q], ans[Q];

bool ca(ll x, ll y) {
    return a[x] > a[y];
}

bool cq(ll x, ll y) {
    return qk[x] > qk[y];
}

void update(ll pos) {
    for (; pos <= n; pos += pos & -pos) bit[pos]++;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
        ia[i] = i;
    }
    cin >> q;
    for (ll i = 1; i <= q; i++) {
        cin >> ql[i] >> qr[i] >> qk[i];
        iq[i] = i;
    }
    sort(ia + 1, ia + n + 1, ca);
    sort(iq + 1, iq + q + 1, cq);
    for (ll t = 1, j = 1; t <= q; t++) {
        ll i = iq[t];
        while (j <= n && a[ia[j]] > qk[i]) update(ia[j++]);
        ans[i] = get(qr[i]) - get(ql[i] - 1);
    }
    for (ll i = 1; i <= q; i++) cout << ans[i] << '\n';
}
6
4 1 7 4 9 2
4
1 6 4
2 5 3
3 3 7
4 6 1

O((n+q)log⁡n)O((n + q) \log n) cộng hai lần sắp.

Diện tích hợp hình chữ nhật

Quét theo xx; mỗi hình chữ nhật cho sự kiện +1+1 trên khoảng [y1,y2)[y_1, y_2) tại x1x_1 và −1-1 tại x2x_2. Giữa hai sự kiện liên tiếp, diện tích phủ là (tổng độ dài các yy đang được phủ ít nhất một lần) nhân (khoảng cách hai sự kiện). Độ dài phủ giữ trong cây phân đoạn trên các khoảng yy cơ sở sau khi nén: mỗi nút lưu cnt là số lần nó được phủ trọn và len là độ dài phủ trong đoạn của nó. Nút có cnt > 0 thì phủ trọn, ngược lại len là tổng hai con. Không cần đẩy nhãn xuống, vì mỗi lần trừ luôn khớp với một lần cộng trước đó trên đúng các nút ấy.

Kéo chuột trên lưới để vẽ thêm hình; mỗi dải giữa hai vạch đứt là một đoạn quét, phần cam là độ dài y được phủ.

Ví dụDiện Tích Hợp Hình Chữ Nhật

Đề. n≤105n \le 10^5 hình chữ nhật song song trục, tọa độ trong [−106,106][-10^6, 10^6]. Tính diện tích phần được ít nhất một hình phủ.

Lời giải. Quét theo xx. Giữa hai sự kiện liên tiếp, phần được phủ trên trục yy không đổi, nên diện tích của dải đó là độ dài phủ nhân bề rộng dải. Cần một cấu trúc giữ “tổng độ dài các khoảng yy đang được ít nhất một hình phủ” khi thêm và bớt khoảng: cây phân đoạn trên các tọa độ yy đã nén.

Bước 1: nén yy. Các yy thành dãy ys[1..k]; lá ii của cây là khoảng [ysi,ysi+1)[ys_i, ys_{i+1}), nên cây có k−1k - 1 lá.

C++
sort(ys + 1, ys + k + 1);
k = unique(ys + 1, ys + k + 1) - ys - 1;

Bước 2: độ dài phủ của một nút. cnt[id] đếm số hình phủ trọn đoạn của nút. Nút có cnt > 0 được phủ trọn, độ dài là cả khoảng của nó; ngược lại là tổng hai con (lá chưa phủ thì 0).

C++
void pull(ll id, ll l, ll r) {
    if (cnt[id]) len[id] = ys[r + 1] - ys[l];
    else if (l == r) len[id] = 0;
    else len[id] = len[id * 2] + len[id * 2 + 1];
}

Bước 3: thêm, bớt một khoảng. Cộng val vào cnt của các nút phủ trọn đoạn lá cần cập nhật, rồi pull ngược lên. Không đẩy nhãn xuống: mỗi lần trừ khớp đúng một lần cộng trước đó trên cùng các nút, nên cnt không bao giờ âm và len[1] luôn đúng.

C++
void update(ll id, ll l, ll r, ll u, ll v, ll val) {
    if (v < l || r < u) return;
    if (u <= l && r <= v) {
        cnt[id] += val;
        pull(id, l, r);
        return;
    }
    ll mid = (l + r) >> 1;
    update(id * 2, l, mid, u, v, val);
    update(id * 2 + 1, mid + 1, r, u, v, val);
    pull(id, l, r);
}

Bước 4: quét sự kiện. Trước khi xử lý sự kiện tại xx, cộng len[1] nhân khoảng cách từ sự kiện trước. Khoảng yy từ y1y_1 tới y2y_2 ứng với các lá từ vị trí của y1y_1 tới ngay trước vị trí của y2y_2.

C++
for (array<ll, 4> e : ev) {
    area += len[1] * (e[0] - px);
    px = e[0];
    ll l = lower_bound(ys + 1, ys + k + 1, e[2]) - ys;
    ll r = lower_bound(ys + 1, ys + k + 1, e[3]) - ys - 1;
    update(1, 1, k - 1, l, r, e[1]);
}

Diện tích tới 4⋅10124 \cdot 10^{12}, dùng ll.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, k, ys[N], cnt[4 * N], len[4 * N];
vector<array<ll, 4>> ev;

void pull(ll id, ll l, ll r) {
    if (cnt[id]) len[id] = ys[r + 1] - ys[l];
    else if (l == r) len[id] = 0;
    else len[id] = len[id * 2] + len[id * 2 + 1];
}

void update(ll id, ll l, ll r, ll u, ll v, ll val) {
    if (v < l || r < u) return;
    if (u <= l && r <= v) {
        cnt[id] += val;
        pull(id, l, r);
        return;
    }
    ll mid = (l + r) >> 1;
    update(id * 2, l, mid, u, v, val);
    update(id * 2 + 1, mid + 1, r, u, v, val);
    pull(id, l, r);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        ll x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        ev.push_back({x1, 1, y1, y2});
        ev.push_back({x2, -1, y1, y2});
        ys[++k] = y1;
        ys[++k] = y2;
    }
    sort(ys + 1, ys + k + 1);
    k = unique(ys + 1, ys + k + 1) - ys - 1;
    sort(ev.begin(), ev.end());
    ll area = 0, px = ev[0][0];
    for (array<ll, 4> e : ev) {
        area += len[1] * (e[0] - px);
        px = e[0];
        ll l = lower_bound(ys + 1, ys + k + 1, e[2]) - ys;
        ll r = lower_bound(ys + 1, ys + k + 1, e[3]) - ys - 1;
        update(1, 1, k - 1, l, r, e[1]);
    }
    cout << area << '\n';
}
3
0 0 4 2
2 1 5 4
5 0 7 1

O(nlog⁡n)O(n \log n).

Khi đi thi

  • Đoạn, khoảng thời gian, hình chữ nhật: nghĩ ngay tới sự kiện mở và đóng. Viết rõ đoạn đóng hay nửa mở, rồi chọn rr hay r+1r + 1 và thứ tự khi trùng tọa độ cho đúng.
  • Hai chiều: quét một chiều, giữ chiều kia trong Fenwick hay cây phân đoạn; tọa độ lớn thì nén trước.
  • Câu hỏi có ngưỡng biết trước: sắp câu hỏi theo ngưỡng, thêm phần tử dần, trả lời ngoại tuyến.

Tóm tắt

  • Đoạn [l,r][l, r] là +1+1 tại ll, −1-1 tại r+1r + 1; quét sự kiện đã sắp cho mức phủ trên từng đoạn giữa hai sự kiện.
  • Gán phòng: quét theo đầu, hàng đợi ưu tiên theo thời điểm rảnh, số phòng bằng mức phủ lớn nhất.
  • Bao chứa và giao điểm: quét một chiều, Fenwick chiều còn lại.
  • Ngưỡng ngoại tuyến: quét theo giá trị, bật dần phần tử, hỏi đoạn bằng Fenwick.
  • Diện tích hợp: quét xx, cây phân đoạn với cnt và len trên yy đã nén.

Bài tập

1
Quan Hệ Bao Chứacanuc80k3903Như đếm bao chứa nhưng chỉ cần biết có hay không: quét xuôi giữ đầu phải lớn nhất đã gặp, quét ngược giữ đầu phải nhỏ nhất.
LQDOJ
2
Lịch Chiếu Chungcanuc80k3905Sắp phim theo giờ kết thúc; giữ multiset giờ rảnh của kk người, mỗi phim gán cho người rảnh muộn nhất mà vẫn kịp.
LQDOJ
3
Cặp Điểm Gần Nhấtcanuc80k3906Sắp theo xx, giữ set các điểm có xx cách điểm hiện tại không quá khoảng cách tốt nhất, theo yy; chỉ xét điểm có yy trong dải ±d\pm d.
LQDOJ
4
Chướng Ngại Trên Đườngcanuc80k3909Người xuất phát lúc DD gặp công trình ii khi Si−Xi≤D<Ti−XiS_i - X_i \le D < T_i - X_i; quét theo thời điểm, giữ set vị trí đang chặn, người hỏi lấy vị trí nhỏ nhất.
LQDOJ
5
Điểm Phủ Dày Nhấtcanuc80k3910Mảng hiệu trên tọa độ tới 10610^6, lấy giá trị lớn nhất sau khi cộng dồn.
LQDOJ
6
Thống Kê Ngày Hoạt Độngcanuc80k3911Sự kiện +1+1 tại AiA_i, −1-1 tại Ai+BiA_i + B_i; mỗi khoảng giữa hai sự kiện cộng độ dài vào ô của mức phủ hiện tại.
LQDOJ
7
Giới Hạn Chi Phí Mỗi Ngàycanuc80k3912Quét sự kiện theo ngày, chi phí mỗi đoạn là min⁡(C,tổng phıˊ đang coˊ)\min(C, \text{tổng phí đang có}) nhân độ dài đoạn.
LQDOJ
8
Hợp Các Khoảng Nửa Mởcanuc80k3913Sắp theo đầu trái, gộp khi đoạn mới bắt đầu không sau đầu phải hiện tại, in các đoạn đã gộp.
LQDOJ
9
Hai Thiết Bị Ghi Lịchcanuc80k3915Gán phòng với đúng hai phòng; khoảng đóng nên phòng chỉ rảnh khi kết thúc trước hẳn lúc bắt đầu mới.
LQDOJ
10
Miền Được Phủ Ít Nhất K Lầncanuc80k3916Sự kiện mở trước đóng khi trùng tọa độ; ghi các đoạn mà mức phủ đạt ít nhất kk, nối các đoạn liền nhau.
LQDOJ
11
Đếm Cặp Đoạn Giao Nhaucanuc80k3919Đếm phần bù: cặp không giao là cặp mà một đoạn kết thúc trước khi đoạn kia bắt đầu; với mỗi đoạn tìm kiếm nhị phân trên mảng đầu phải đã sắp.
LQDOJ
12
Chọn K Đoạn Cùng Giaocanuc80k3921Quét các đèn theo thời điểm bật; khi đèn ii bật, mọi bộ kk đèn chứa ii và các đèn đang sáng có chung thời điểm: cộng (ck−1)\binom{c}{k-1} với cc là số đèn đang sáng.
LQDOJ
13
Đếm Đoạn Nằm Trongcanuc80k3922Sắp theo đầu trái giảm dần, Fenwick theo đầu phải đã nén: số đoạn nằm trong là số đoạn đã thêm có đầu phải nhỏ hơn.
LQDOJ
14
Tổng Khoảng Cách Nhỏ Nhấtcanuc80k3929Hai điểm không bao giờ gặp thì khoảng cách nhỏ nhất là hiệu tọa độ khi điểm trái chậm hơn; sắp theo xx, Fenwick theo vận tốc đã nén giữ số lượng và tổng tọa độ.
LQDOJ
15
So Sánh Tần Suất Hai Phíacanuc80k3946Tính f(1,i)f(1, i) và f(j,n)f(j, n) bằng đếm tần suất; quét jj từ phải sang, Fenwick theo giá trị f(j,n)f(j, n), mỗi ii đếm các j>ij > i có giá trị nhỏ hơn.
LQDOJ

Đọc thêm