Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Chuyên đề cơ bản 2: Sắp xếp, kỹ thuật sự kiện (sweep line) và nén số (rời rạc hoá) trong C++ và Python: gộp đoạn, khách nhà hàng, xếp phòng

Chuyên đề cơ bản 2: Sắp xếp, kỹ thuật sự kiện (sweep line) và nén số (rời rạc hoá) trong C++ và Python: gộp đoạn, khách nhà hàng, xếp phòng
Series: Chuyên đề cơ bản Độ khó: Trung bình Chuyên đề cơ bản 2 / Thuật toán cơ bản Sắp xếp Sự kiện Nén số
// Chuyên đề cơ bản · Chuyên đề 2
Sắp xếp, kỹ thuật sự kiện (sweep line) và nén số
Có rất nhiều bài thoạt nhìn phải so từng cặp, nhưng chỉ cần sắp xếp lại là lời giải hiện ra. Bài này dạy em ba “chiêu” đi cùng sắp xếp: gộp đoạn, quét sự kiện và nén toạ độ tới $10^9$ về $0 \ldots m - 1$.

Lời mở đầu

Trong đề HSG có cả một họ bài nói về các đoạn trên trục số: khách ra vào nhà hàng, cuộc họp chiếm phòng, đèn bật tắt theo giờ, sơn từng khúc hàng rào… Đề hỏi “lúc đông nhất có bao nhiêu người?”, “tổng chiều dài được sơn là bao nhiêu?”, “cần ít nhất mấy phòng?”. Nếu toạ độ nhỏ, em đã biết dùng mảng hiệu ở bài 13. Nhưng khi toạ độ lên tới $10^9$, mảng hiệu không khai báo nổi, và lúc đó sắp xếp trở thành chìa khoá.

Chuyên đề này giả sử em đã học bài 10: Struct và sắp xếp (hàm sort, sắp xếp pair, hàm so sánh), bài 16: Tìm kiếm nhị phân (lower_bound) và bài 18: Tham lam. Ta không học lại cách sắp xếp, mà học cách dùng sắp xếp để biến bài $O(n^2)$ thành $O(n \log n)$.

Trong bài này bạn sẽ học: vì sao sau khi sắp xếp, cặp gần nhau nhất luôn kề nhau và các số bằng nhau đứng liền một cụm; gộp đoạn và tính tổng độ dài phủ trong một lượt duyệt; kỹ thuật sự kiện (sweep line): biến mỗi đoạn thành hai sự kiện +1 / −1, sắp xếp rồi quét để đếm số đoạn phủ cùng lúc nhiều nhất, và cách xử lý sự kiện trùng thời điểm cho đúng; nén số (rời rạc hoá) bằng sort + unique + lower_bound, kết hợp với mảng hiệu; ứng dụng tìm điểm được phủ nhiều nhất và xếp phòng ít nhất bằng priority_queue. Có 2 khung chạy tay, 3 mô phỏng bấm từng bước và 8 bài tập kèm gợi ý.

Lộ trình bài viết, bấm vào từng phần để nhảy tới:

Phần 1Sắp xếp để mở khoá lời giải

Nhiều bài hỏi về cặp phần tử: hai số gần nhau nhất, có bao nhiêu giá trị khác nhau, có hai số nào trùng không… Cách ngây thơ là hai vòng for xét mọi cặp, tốn $O(n^2)$: với $n = 2 \cdot 10^5$ là $2 \cdot 10^{10}$ phép so sánh, chạy cả phút. Sắp xếp trước, mọi thứ trở nên “có thứ tự”, và câu hỏi về cặp thường chỉ cần nhìn hai phần tử kề nhau.

Nhận xét 1: cặp gần nhau nhất là một cặp kề nhau. Giả sử dãy đã tăng $a_0 \le a_1 \le \ldots \le a_{n-1}$ và xét hai chỉ số $i < j$ không kề nhau. Khi đó $a_j - a_i = (a_j - a_{j-1}) + \ldots + (a_{i+1} - a_i)$ là tổng của nhiều hiệu kề nhau không âm, nên nó không nhỏ hơn hiệu kề $a_{i+1} - a_i$. Vậy chỉ cần xét $n - 1$ cặp kề nhau thay vì $\frac{n(n-1)}{2}$ cặp.

Nhận xét 2: các số bằng nhau đứng liền thành một cụm. Số giá trị khác nhau = 1 + số vị trí $i$ mà $a_i \ne a_{i-1}$. Kiểm tra trùng cũng chỉ cần so ô kề.

Chỉ số0123456
Dãy gốc317187251240
Sau khi sắp xếp771218253140
Hiệu với ô trước–056769
Giá trị mới?cótrùngcócócócócó

Bảng 1. Sau khi sắp xếp: hiệu kề nhỏ nhất là 0 (hai số 7), dãy có $1 + 5 = 6$ giá trị khác nhau. Chỉ cần một vòng for qua các ô kề.

Xem code C++ / Python: hiệu nhỏ nhất và số giá trị khác nhau sau khi sắp xếp (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    sort(a.begin(), a.end());
    long long best = LLONG_MAX;
    int khac = 1;
    for(int i = 1; i < n; i++){
        best = min(best, a[i] - a[i - 1]);   // chỉ cần so hai số KỀ nhau
        if(a[i] != a[i - 1]) khac++;         // gặp một giá trị mới
    }
    cout << best << "\n" << khac;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = sorted(map(int, data[1:n + 1]))
best = min(a[i] - a[i - 1] for i in range(1, n))          # chỉ cần so hai số KỀ nhau
khac = 1 + sum(1 for i in range(1, n) if a[i] != a[i - 1])  # gặp một giá trị mới
print(best)
print(khac)
Sắp xếp kèm chỉ số
Sắp xếp làm mất vị trí ban đầu. Nếu đề hỏi “in chỉ số của hai số gần nhau nhất” hay “thứ hạng của từng học sinh”, hãy sắp xếp các cặp (giá trị, chỉ số) như bài 10 đã dạy:
vector<pair<long long, int>> p(n);
for(int i = 0; i < n; i++) p[i] = {a[i], i};
sort(p.begin(), p.end());          // p[k].second là vị trí gốc của số nhỏ thứ k
Trong Python: order = sorted(range(n), key=lambda i: a[i]). Đây cũng là bước đầu tiên của mọi bài “xếp lịch” ở phần 5: sắp xếp khách theo giờ đến nhưng vẫn nhớ khách nào là ai.

Một vài “phản xạ sắp xếp” khác em nên có: đếm cặp có tổng $\le X$ (sắp xếp rồi dùng hai con trỏ), tìm số lần xuất hiện của $x$ (sắp xếp rồi upper_bound − lower_bound), chọn $k$ món rẻ nhất (sắp xếp rồi lấy $k$ món đầu). Còn từ phần 2 trở đi, ta sẽ sắp xếp những thứ phức tạp hơn một con số: các đoạn và các sự kiện.

Phần 2Gộp đoạn và tổng độ dài phủ

Bài toán. Cho $n$ đoạn $[l_i, r_i]$ trên trục số (toạ độ tới $10^9$). Tô màu tất cả các đoạn. Hỏi phần được tô gồm bao nhiêu khối rời nhau và tổng chiều dài là bao nhiêu? Cộng thẳng $\sum (r_i - l_i)$ sẽ sai, vì phần chồng nhau bị tính nhiều lần.

Ý tưởng. Sắp xếp các đoạn theo đầu trái $l$. Giữ một “khối đang gộp” $[L, R]$. Duyệt từng đoạn $[l, r]$ theo thứ tự đã sắp xếp:

  • Nếu $l \le R$: đoạn này chồng hoặc chạm vào khối, nối dài khối: $R = \max(R, r)$.
  • Nếu $l > R$: có một khe hở. Mọi đoạn phía sau còn có $l$ lớn hơn nữa, nên không đoạn nào lấp được khe này: chốt khối $[L, R]$ (cộng $R - L$ vào đáp án) rồi mở khối mới $[l, r]$.

Chính việc sắp xếp đảm bảo điều “không đoạn nào phía sau quay lại lấp khe được”. Nếu không sắp xếp, một đoạn ở cuối danh sách có thể nằm ngay giữa hai khối và nối chúng lại. Em xem mô phỏng bên dưới với 6 đoạn cho theo thứ tự lộn xộn:

MÔ PHỎNGGộp đoạn sau khi sắp xếp theo đầu tráiNhập tối đa 8 đoạn trên trục số (toạ độ 0 … 30). Hàng “hợp” ở dưới cùng là kết quả gộp.
chưa xétđoạn đang xétthuộc khối đang gộpkhối đã chốt
Đoạn (đã sắp xếp)So với RViệc làmKhối [L, R]Tổng đã chốt
[1, 3]–mở khối đầu[1, 3]0
[2, 5]2 ≤ 3nối dài, R = max(3, 5)[1, 5]0
[6, 9]6 > 5chốt [1, 5], mở khối mới[6, 9]4
[8, 10]8 ≤ 9nối dài, R = 10[6, 10]4
[12, 15]12 > 10chốt [6, 10], mở khối mới[12, 15]8
[13, 14]13 ≤ 15nằm gọn trong khối, R = max(15, 14) = 15[12, 15]8
(hết)chốt khối cuối11

Bảng 2. Gộp 6 đoạn của mô phỏng: được 3 khối [1, 5], [6, 10], [12, 15], tổng độ dài $4 + 4 + 3 = 11$.

Code rất ngắn. Để ý dòng cuối sau vòng lặp: khối cuối cùng chưa bao giờ gặp “khe hở” nên phải tự chốt. Em bấm Tiếp để chạy tay:

C++▶ Chạy tay từng dòng codeGộp đoạn: sắp xếp theo l, giữ khối [L, R]; chồng thì nối dài, có khe thì chốt khối (ô vàng: đoạn đang xét, ô xanh: đã duyệt).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n;
6 cin >> n;
7 vector<pair<long long, long long>> a(n);
8 for(auto &p : a) cin >> p.first >> p.second;
9 sort(a.begin(), a.end());
10 long long L = a[0].first, R = a[0].second, tong = 0;
11 int khoi = 1;
12 for(int i = 1; i < n; i++){
13 if(a[i].first <= R){
14 R = max(R, a[i].second);
15 } else {
16 tong += R - L; khoi++;
17 L = a[i].first; R = a[i].second;
18 }
19 }
20 tong += R - L;
21 cout << khoi << " " << tong;
22 return 0;
23}
Xem code C++ / Python: gộp đoạn và tổng độ dài phủ (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<pair<long long, long long>> a(n);
    for(auto &p : a) cin >> p.first >> p.second;
    sort(a.begin(), a.end());                       // theo đầu trái l
    long long L = a[0].first, R = a[0].second, tong = 0;
    int khoi = 1;
    for(int i = 1; i < n; i++){
        if(a[i].first <= R) R = max(R, a[i].second); // chồng hoặc chạm: nối dài khối
        else {
            tong += R - L; khoi++;                   // có khe hở: chốt khối cũ
            L = a[i].first; R = a[i].second;
        }
    }
    tong += R - L;                                   // đừng quên khối cuối cùng
    cout << khoi << " " << tong;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
seg = sorted((int(data[1 + 2 * i]), int(data[2 + 2 * i])) for i in range(n))   # theo đầu trái l
L, R = seg[0]
tong, khoi = 0, 1
for l, r in seg[1:]:
    if l <= R:                     # chồng hoặc chạm: nối dài khối
        R = max(R, r)
    else:                          # có khe hở: chốt khối cũ
        tong += R - L
        khoi += 1
        L, R = l, r
tong += R - L                      # đừng quên khối cuối cùng
print(khoi, tong)
Ba lỗi hay gặp khi gộp đoạn
1. Gán R = r thay vì R = max(R, r): sai ngay khi có đoạn nằm gọn trong khối (như [13, 14] trong [12, 15]).
2. Quên chốt khối cuối sau vòng lặp.
3. Nhầm điều kiện “chạm”. Với đoạn trên trục số thực, $[1, 3]$ và $[3, 5]$ chạm nhau ở điểm 3, gộp hay không gộp thì tổng độ dài vẫn đúng, chỉ khác số khối. Nhưng với đoạn số nguyên (các ô 1, 2, 3 và ô 4, 5) thì $[1, 3]$ và $[4, 5]$ liền nhau thành một khối 5 ô, điều kiện gộp phải là l <= R + 1 và độ dài một khối là $R - L + 1$. Đọc đề kỹ xem đoạn là “khúc trên trục số” hay “dãy ô”.

Phần 3Kỹ thuật sự kiện (sweep line)

Bài toán khách nhà hàng. Có $n$ đoạn $[l_i, r_i]$ (các điểm nguyên, toạ độ tới $10^9$). Hỏi có một điểm được nhiều đoạn phủ nhất là bao nhiêu đoạn? Đổi lời: khách $i$ ngồi ở nhà hàng từ phút $l_i$ tới hết phút $r_i$, lúc đông nhất có mấy khách?

Nếu toạ độ $\le 10^6$, em làm ngay bằng mảng hiệu: d[l]++, d[r + 1]--, cộng dồn rồi lấy max. Hãy nhìn kỹ mảng hiệu ấy: nó chỉ khác 0 tại $2n$ vị trí, còn lại toàn số 0 vô ích. Kỹ thuật sự kiện chính là mảng hiệu “chỉ lưu những ô khác 0”:

  • Mỗi đoạn $[l, r]$ sinh ra 2 sự kiện: $(l, +1)$ “bắt đầu phủ” và $(r + 1, -1)$ “hết phủ”.
  • Sắp xếp $2n$ sự kiện theo thời điểm.
  • Cho một đường quét chạy từ trái sang phải qua các sự kiện, giữ biến cur = số đoạn đang phủ đường quét: gặp $+1$ thì tăng, gặp $-1$ thì giảm. Đáp án là giá trị lớn nhất của cur.

Độ phức tạp $O(n \log n)$ cho bước sắp xếp, không phụ thuộc toạ độ lớn cỡ nào. Hình dưới là 5 đoạn của ví dụ, và cột ở dưới là số đoạn phủ mỗi điểm nguyên: cao nhất là 3.

#1 [1, 4]#2 [2, 6]#3 [5, 8]#4 [7, 9]#5 [3, 5]01234567891011123332221số đoạnphủ điểm t

Hình 1. Năm đoạn [1, 4], [2, 6], [5, 8], [7, 9], [3, 5]. Giữa hai sự kiện liên tiếp, số đoạn phủ không đổi, nên chỉ cần tính lại cur tại các sự kiện. Cột vàng: điểm được 3 đoạn phủ.

Em bấm Tự chạy để xem đường quét màu đỏ nhảy từ sự kiện này sang sự kiện khác. Hãy để ý thời điểm 5 và 7: có một đoạn kết thúc và một đoạn bắt đầu cùng lúc. Sau đó đổi sang chế độ “Khách rời lúc r” để thấy sự kiện $-1$ dời về đúng $r$:

MÔ PHỎNGĐường quét đi qua các sự kiện, đếm số đoạn đang mởNhập tối đa 7 đoạn (toạ độ 0 … 30), chọn cách hiểu đầu mút rồi bấm Tiếp / Tự chạy.
chưa tớiđang mở (được đếm trong cur)đã đóngsự kiện đang xử lýsự kiện −1

Thứ tự sự kiện trùng thời điểm là chỗ quyết định đúng sai. Tại $t = 5$ có sự kiện $(5, -1)$ của đoạn [1, 4] và $(5, +1)$ của đoạn [5, 8]. Đoạn [1, 4] không phủ điểm 5, nên phải trừ nó trước rồi mới cộng [5, 8]. Khi sắp xếp cặp (t, d), C++ và Python so t trước, bằng nhau thì so d; vì $-1 < +1$ nên sự kiện kết thúc tự động đứng trước. Rất tiện!

Thứ tự tại t = 5cur trướcSau sự kiện thứ nhấtSau sự kiện thứ haibest
−1 trước, +1 sau (đúng)3233
+1 trước, −1 sau (sai)3434 (sai)

Bảng 3. Nếu xử lý +1 trước, có một khoảnh khắc “ảo” mà cả 4 đoạn được đếm cùng lúc, dù không điểm nào được 4 đoạn phủ.

C++▶ Chạy tay từng dòng codeMỗi đoạn [l, r] thành hai sự kiện (l, +1) và (r + 1, −1); sắp xếp rồi quét, cur là số đoạn đang phủ (ô vàng: sự kiện đang xử lý).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n;
6 cin >> n;
7 vector<pair<long long, int>> ev;
8 for(int i = 0; i < n; i++){
9 long long l, r;
10 cin >> l >> r;
11 ev.push_back({l, +1});
12 ev.push_back({r + 1, -1});
13 }
14 sort(ev.begin(), ev.end());
15 int cur = 0, best = 0;
16 for(auto e : ev){
17 cur += e.second;
18 best = max(best, cur);
19 }
20 cout << best;
21 return 0;
22}
Xem code C++ / Python: đếm số đoạn phủ cùng lúc nhiều nhất bằng sự kiện (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<pair<long long, int>> ev;                  // (thời điểm, +1 hoặc -1)
    for(int i = 0; i < n; i++){
        long long l, r;
        cin >> l >> r;
        ev.push_back({l, +1});                        // đoạn bắt đầu phủ từ l
        ev.push_back({r + 1, -1});                    // hết phủ từ r + 1
    }
    sort(ev.begin(), ev.end());                       // cùng thời điểm: -1 đứng trước +1
    int cur = 0, best = 0;
    for(auto e : ev){
        cur += e.second;
        best = max(best, cur);
    }
    cout << best;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
ev = []                                   # (thời điểm, +1 hoặc -1)
for i in range(n):
    l, r = int(data[1 + 2 * i]), int(data[2 + 2 * i])
    ev.append((l, 1))                     # đoạn bắt đầu phủ từ l
    ev.append((r + 1, -1))                # hết phủ từ r + 1
ev.sort()                                 # cùng thời điểm: -1 đứng trước +1
cur = best = 0
for t, d in ev:
    cur += d
    best = max(best, cur)
print(best)
Ba kiểu đầu mút, ba cách đặt sự kiện
Đoạn đóng số nguyên $[l, r]$ (phủ cả $l$ lẫn $r$): sự kiện $(l, +1)$, $(r + 1, -1)$ như code trên.
Khách đến lúc $a$, rời lúc $b$, người rời lúc $b$ không gặp người đến lúc $b$ (nửa mở $[a, b)$): sự kiện $(a, +1)$, $(b, -1)$; trùng thời điểm thì $-1$ trước (sắp xếp cặp tự lo).
Chỉ cần chạm nhau ở đầu mút là tính gặp nhau (đoạn đóng trên trục số thực, toạ độ có thể là số thực): sự kiện đặt tại $l$ và $r$, nhưng trùng thời điểm thì bắt đầu phải trước kết thúc. Mẹo: lưu cặp (l, 0) cho bắt đầu và (r, 1) cho kết thúc, vì $0 < 1$ nên sắp xếp tự đưa bắt đầu lên trước; khi quét, gặp 0 thì cur++, gặp 1 thì cur--.
Luôn tự hỏi: “ở đúng thời điểm trùng, hai đoạn có được tính là cùng lúc không?”

Phần 4Nén số (rời rạc hoá)

Kỹ thuật sự kiện rất mạnh khi chỉ cần quét một lượt. Nhưng nhiều bài muốn dùng mảng đánh chỉ số theo giá trị: mảng đếm, mảng hiệu, cộng dồn, sau này là cây Fenwick… mà giá trị tới $10^9$ thì không thể khai báo mảng. Quan sát quan trọng: trong rất nhiều bài, ta chỉ quan tâm thứ tự lớn nhỏ của các giá trị, không quan tâm chính xác chúng bằng bao nhiêu. Khi đó có thể thay mỗi số bằng thứ hạng của nó trong các giá trị khác nhau. $n$ số thì có tối đa $n$ thứ hạng, mảng chỉ cần $n$ ô.

Ba bước nén, nhớ như một câu thần chú “sắp xếp, bỏ trùng, tra vị trí”:

  1. Chép dãy sang b rồi sort(b).
  2. b.erase(unique(b.begin(), b.end()), b.end()): unique dồn các phần tử không trùng ô kề lên đầu và trả về chỗ kết thúc, erase cắt phần thừa. Còn $m$ giá trị khác nhau tăng dần.
  3. Chỉ số nén của $x$ là lower_bound(b.begin(), b.end(), x) - b.begin(), tìm nhị phân $O(\log m)$.

Kết quả: mọi giá trị nằm trong $0 \ldots m - 1$ và giữ nguyên thứ tự: $a_i < a_j \Leftrightarrow c_i < c_j$. Muốn lấy lại giá trị gốc thì đọc b[c[i]].

MÔ PHỎNGNén số: sắp xếp, bỏ trùng rồi tra vị trí bằng lower_boundNhập tối đa 10 số nguyên (có thể rất lớn hoặc âm), bấm Tiếp để xem từng bước.
bị trùng, sẽ xoágiá trị khác nhau trong bđang trađã nén
Xem code C++ / Python: nén số bằng sort, unique, lower_bound (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    vector<long long> b = a;                              // 1. chép
    sort(b.begin(), b.end());                             // 2. sắp xếp
    b.erase(unique(b.begin(), b.end()), b.end());         // 3. bỏ trùng
    for(int i = 0; i < n; i++){
        int c = lower_bound(b.begin(), b.end(), a[i]) - b.begin();   // 4. tra vị trí
        cout << c << " ";
    }
    cout << "\n" << b.size();
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = list(map(int, data[1:n + 1]))
b = sorted(set(a))                       # sắp xếp + bỏ trùng
idx = {v: i for i, v in enumerate(b)}    # giá trị -> chỉ số nén
print(*[idx[x] for x in a])
print(len(b))
Python: dùng dict cho nhanh
Trong Python, sorted(set(a)) làm luôn bước 1 và 2, rồi tạo từ điển idx = {v: i for i, v in enumerate(b)} để tra $O(1)$ thay vì bisect_left. Trong C++ cũng có thể dùng map như vậy, nhưng lower_bound trên vector đã sắp xếp nhanh hơn map khá nhiều.

Nén + mảng hiệu. Bài toán: cho $n$ đoạn $[l_i, r_i]$ và $q$ câu hỏi, mỗi câu hỏi điểm $x$ được bao nhiêu đoạn phủ (toạ độ tới $10^9$). Ý tưởng: mảng hiệu chỉ “chạm” vào các toạ độ $l_i$, $r_i + 1$, và ta chỉ hỏi tại các $x_j$. Gom tất cả các toạ độ đó lại rồi nén. Giữa hai toạ độ nén liên tiếp, số đoạn phủ không đổi, nên mảng hiệu trên chỉ số nén cho đúng đáp án tại mọi điểm cần hỏi. Đây là đúng mảng hiệu của bài 13, chỉ khác là chỉ số đã được nén.

Toạ độ gốc1345610000000001000000001
Đến từ$l$ của [1, 5]$l$ của [3, $10^9$]$l$ của [4, 4] và câu hỏi $x = 4$$r + 1$ của [4, 4]$r + 1$ của [1, 5], câu hỏi $x = 6$câu hỏi $x = 10^9$$r + 1$ của [3, $10^9$]
Chỉ số nén0123456
Hiệu d+1+1+1−1−10−1
Cộng dồn = số đoạn phủ1232110

Bảng 4. Ba đoạn [1, 5], [3, 10^9], [4, 4] và các câu hỏi $x = 4, 6, 10^9$: mảng hiệu chỉ cần 7 ô thay vì $10^9$ ô.

Xem code C++ / Python: nén toạ độ kết hợp mảng hiệu để trả lời số đoạn phủ một điểm (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, q;
    cin >> n >> q;
    vector<long long> l(n), r(n), x(q), b;
    for(int i = 0; i < n; i++){
        cin >> l[i] >> r[i];
        b.push_back(l[i]); b.push_back(r[i] + 1);   // mọi toạ độ mảng hiệu sẽ chạm tới
    }
    for(int j = 0; j < q; j++){ cin >> x[j]; b.push_back(x[j]); }
    sort(b.begin(), b.end());
    b.erase(unique(b.begin(), b.end()), b.end());
    auto id = [&](long long v){ return int(lower_bound(b.begin(), b.end(), v) - b.begin()); };
    vector<int> d(b.size() + 1, 0);
    for(int i = 0; i < n; i++){
        d[id(l[i])]++;                              // mảng hiệu trên chỉ số đã nén
        d[id(r[i] + 1)]--;
    }
    for(int k = 1; k < (int)b.size(); k++) d[k] += d[k - 1];   // cộng dồn
    for(int j = 0; j < q; j++) cout << d[id(x[j])] << "\n";
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n, q = int(data[0]), int(data[1])
seg = [(int(data[2 + 2 * i]), int(data[3 + 2 * i])) for i in range(n)]
xs = list(map(int, data[2 + 2 * n:2 + 2 * n + q]))
b = sorted(set([l for l, r in seg] + [r + 1 for l, r in seg] + xs))
idx = {v: i for i, v in enumerate(b)}
d = [0] * (len(b) + 1)
for l, r in seg:
    d[idx[l]] += 1                        # mảng hiệu trên chỉ số đã nén
    d[idx[r + 1]] -= 1
for k in range(1, len(b)):
    d[k] += d[k - 1]                      # cộng dồn
print('\n'.join(str(d[idx[x]]) for x in xs))
Quên gom một loại toạ độ
Lỗi kinh điển khi nén: chỉ nén $l$ và $r$ mà quên $r + 1$, hoặc quên nén các điểm câu hỏi. Khi đó lower_bound trả về vị trí của một giá trị khác (lớn hơn) và đáp án sai mà không báo lỗi gì. Quy tắc: mọi giá trị mà sau này em sẽ gọi id(...) đều phải được đưa vào b trước khi sắp xếp. Mảng sau khi nén có tới $2n + q$ ô, nhớ khai báo đủ.

Phần 5Ứng dụng: điểm được phủ nhiều nhất, xếp phòng ít nhất

1. Điểm được phủ nhiều nhất là điểm nào? Ở phần 3 ta chỉ in số lượng. Muốn biết cả vị trí, chỉ cần nhớ thời điểm của sự kiện làm best tăng: ngay sau một sự kiện $+1$ tại $t$, các điểm từ $t$ tới trước sự kiện tiếp theo đều có đúng cur đoạn phủ. Vì ta cập nhật khi cur > best (lớn hơn hẳn), thời điểm lưu lại là điểm nhỏ nhất đạt kỷ lục. Trong ví dụ Hình 1 đó là điểm 3 (bài tập 5).

for(auto e : ev){
    cur += e.second;
    if(cur > best){ best = cur; diem = e.first; }   // e.first là thời điểm của sự kiện +1
}

2. Xếp phòng ít nhất. Khách sạn có $n$ khách, khách $i$ ở từ ngày $a_i$ tới hết ngày $b_i$. Hai khách ở cùng phòng thì khoảng ngày không được giao nhau. Cần ít nhất bao nhiêu phòng, và xếp ai vào phòng nào?

Số phòng ít nhất bằng đúng số khách có mặt cùng lúc nhiều nhất (phần 3): ít hơn thì vào ngày đông nhất sẽ thiếu phòng, còn cách tham lam dưới đây dùng đúng chừng đó phòng. Tham lam theo tinh thần bài 18: xét khách theo ngày đến tăng dần. Với mỗi khách, nhìn phòng sẽ được trả sớm nhất; nếu phòng ấy đã trống trước ngày khách đến thì cho khách vào đó, nếu không thì mọi phòng đều đang có người, phải mở phòng mới.

Để lấy nhanh “phòng trả sớm nhất”, ta dùng hàng đợi ưu tiên priority_queue: một cấu trúc luôn cho biết phần tử nhỏ nhất (hoặc lớn nhất) trong $O(1)$, thêm và xoá trong $O(\log n)$. Mặc định priority_queue của C++ đưa phần tử lớn nhất lên đỉnh; muốn nhỏ nhất thì khai báo thêm greater. Python dùng thư viện heapq, vốn luôn đưa nhỏ nhất lên đầu h[0].

Khách (theo ngày đến)[a, b]Đỉnh hàng đợi (ngày trả, phòng)Quyết định
1[1, 2](trống)mở phòng 1
4[1, 5](2, phòng 1): 2 không < 1mở phòng 2
2[2, 4](2, phòng 1): 2 không < 2, hôm nay khách 1 vẫn ởmở phòng 3
3[4, 4](2, phòng 1): 2 < 4vào phòng 1
5[6, 7](4, phòng 1): 4 < 6vào phòng 1

Bảng 5. Năm khách [1, 2], [2, 4], [4, 4], [1, 5], [6, 7] cần 3 phòng: ngày 2 có khách 1, 2, 4 cùng ở.

Xem code C++ / Python: xếp phòng ít nhất bằng priority_queue (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<array<int, 3>> c(n);                     // (ngày đến, ngày đi, chỉ số khách)
    for(int i = 0; i < n; i++){ cin >> c[i][0] >> c[i][1]; c[i][2] = i; }
    sort(c.begin(), c.end());                       // ai đến sớm xếp trước
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;   // (ngày đi, phòng), nhỏ nhất ở đỉnh
    vector<int> phong(n);
    int k = 0;
    for(auto &x : c){
        int room;
        if(!pq.empty() && pq.top().first < x[0]){   // phòng trả sớm nhất đã trống
            room = pq.top().second; pq.pop();
        } else room = ++k;                          // không có phòng trống: mở phòng mới
        phong[x[2]] = room;
        pq.push({x[1], room});
    }
    cout << k << "\n";
    for(int i = 0; i < n; i++) cout << phong[i] << " ";
    return 0;
}
import sys, heapq

data = sys.stdin.buffer.read().split()
n = int(data[0])
c = sorted((int(data[1 + 2 * i]), int(data[2 + 2 * i]), i) for i in range(n))   # ai đến sớm xếp trước
pq = []                                   # (ngày đi, phòng), nhỏ nhất ở đỉnh
phong = [0] * n
k = 0
for a, b, i in c:
    if pq and pq[0][0] < a:               # phòng trả sớm nhất đã trống
        room = heapq.heappop(pq)[1]
    else:                                 # không có phòng trống: mở phòng mới
        k += 1
        room = k
    phong[i] = room
    heapq.heappush(pq, (b, room))
print(k)
print(*phong)
Vì sao chỉ lấy phòng ở đỉnh?
Phòng ở đỉnh là phòng trả sớm nhất. Nếu đến nó còn chưa trống thì chắc chắn không phòng nào trống, mở phòng mới là bắt buộc. Nếu nó trống thì cho khách vào luôn; dùng phòng nào trong các phòng trống cũng không làm tăng số phòng. Đề yêu cầu “nhiều phòng trống thì chọn phòng số nhỏ nhất” sẽ cần thêm một hàng đợi các phòng trống (bài tập 7). Hàng đợi ưu tiên còn gặp lại rất nhiều ở các bài sau, nổi bật là thuật toán Dijkstra ở bài 29.
Tóm tắt: nhận ra dạng bài
“Lúc đông nhất”, “tối đa bao nhiêu cùng lúc”, “ít nhất bao nhiêu phòng / máy / làn đường” → sự kiện +1/−1, sắp xếp, quét.
“Tổng chiều dài được phủ”, “gộp các khoảng thời gian” → sắp xếp theo đầu trái, gộp đoạn (hoặc quét sự kiện, cộng độ dài khi cur > 0).
“Giá trị tới $10^9$ nhưng chỉ có $n \le 2 \cdot 10^5$ số”, cần mảng đếm / mảng hiệu → nén số trước.
Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Sắp xếp.

Bài tậpLuyện sắp xếp, sự kiện và nén số

Bài 1Hai bạn cao gần bằng nhauDễ

Lớp có $n$ bạn ($2 \le n \le 2 \cdot 10^5$), bạn thứ $i$ cao $a_i$ ($1 \le a_i \le 10^9$). Tìm $d$ là chênh lệch chiều cao nhỏ nhất giữa hai bạn khác nhau, và đếm xem có bao nhiêu cặp bạn $(i, j)$, $i < j$, có chênh lệch đúng bằng $d$. In $d$ và số cặp.

Input
6
5 1 9 3 7 11
Output
2 5

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Sắp xếp rồi $d$ là hiệu kề nhỏ nhất. Đếm cặp phải cẩn thận: nếu $d > 0$ thì dãy không có hai số bằng nhau, và cặp nào có hiệu $d$ cũng phải là cặp kề (có số chen giữa thì hiệu nhỏ hơn $d$), nên đếm số cặp kề có hiệu $d$. Nhưng nếu $d = 0$ thì mọi cặp trong một cụm $k$ số bằng nhau đều có hiệu 0, đóng góp $\frac{k(k-1)}{2}$ cặp, không chỉ $k - 1$ cặp kề! Ví dụ: dãy tăng 1 3 5 7 9 11 có 5 cặp kề cùng hiệu 2. Số cặp có thể tới $2 \cdot 10^{10}$, dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    sort(a.begin(), a.end());
    long long d = LLONG_MAX, cnt = 0;
    for(int i = 1; i < n; i++) d = min(d, a[i] - a[i - 1]);
    if(d > 0){
        for(int i = 1; i < n; i++) if(a[i] - a[i - 1] == d) cnt++;
    } else {
        for(int i = 0, j; i < n; i = j){              // duyệt từng cụm số bằng nhau
            j = i;
            while(j < n && a[j] == a[i]) j++;
            long long k = j - i;
            cnt += k * (k - 1) / 2;
        }
    }
    cout << d << " " << cnt;
    return 0;
}
import sys
from collections import Counter

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = sorted(map(int, data[1:n + 1]))
d = min(a[i] - a[i - 1] for i in range(1, n))
if d > 0:
    cnt = sum(1 for i in range(1, n) if a[i] - a[i - 1] == d)
else:
    cnt = sum(k * (k - 1) // 2 for k in Counter(a).values())   # mỗi cụm k số bằng nhau
print(d, cnt)
Bài 2Nén dãy sốDễ

Cho dãy $n$ số nguyên ($n \le 2 \cdot 10^5$, $|a_i| \le 10^{18}$). Hãy thay mỗi số bằng thứ hạng của nó: số nhỏ nhất có hạng 1, các số bằng nhau cùng hạng, và hạng của các giá trị khác nhau liên tiếp nhau (không bỏ trống hạng nào). In dãy thứ hạng.

Input
6
100 -7 100 3000000 0 -7
Output
3 1 3 4 2 1

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đúng ba bước nén ở phần 4, cộng thêm 1 vì hạng tính từ 1. Các giá trị khác nhau theo thứ tự tăng là −7, 0, 100, 3000000 nên −7 hạng 1, 0 hạng 2, 100 hạng 3, 3000000 hạng 4. Giá trị tới $10^{18}$ nên đọc bằng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    vector<long long> b = a;
    sort(b.begin(), b.end());
    b.erase(unique(b.begin(), b.end()), b.end());
    for(int i = 0; i < n; i++)
        cout << lower_bound(b.begin(), b.end(), a[i]) - b.begin() + 1 << " ";
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
a = list(map(int, data[1:n + 1]))
idx = {v: i + 1 for i, v in enumerate(sorted(set(a)))}
print(*[idx[x] for x in a])
Bài 3Sơn hàng ràoTrung bình

Hàng rào là trục số. Có $n$ lần sơn ($n \le 2 \cdot 10^5$), lần thứ $i$ sơn khúc từ toạ độ $l_i$ tới $r_i$ ($-10^9 \le l_i \le r_i \le 10^9$). Tính tổng chiều dài phần hàng rào đã được sơn (khúc được sơn nhiều lần chỉ tính một lần).

Input
5
-3 1
0 2
5 6
6 9
4 5
Output
10

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Gộp đoạn như phần 2. Sắp xếp được [−3, 1], [0, 2], [4, 5], [5, 6], [6, 9]. Hai đoạn đầu gộp thành [−3, 2] dài 5, ba đoạn sau chạm nhau thành [4, 9] dài 5. Tổng 10. Đáp án có thể tới $2 \cdot 10^9$, vượt int.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<pair<long long, long long>> a(n);
    for(auto &p : a) cin >> p.first >> p.second;
    sort(a.begin(), a.end());
    long long L = a[0].first, R = a[0].second, tong = 0;
    for(int i = 1; i < n; i++){
        if(a[i].first <= R) R = max(R, a[i].second);
        else { tong += R - L; L = a[i].first; R = a[i].second; }
    }
    tong += R - L;
    cout << tong;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
seg = sorted((int(data[1 + 2 * i]), int(data[2 + 2 * i])) for i in range(n))
L, R = seg[0]
tong = 0
for l, r in seg[1:]:
    if l <= R:
        R = max(R, r)
    else:
        tong += R - L
        L, R = l, r
print(tong + R - L)
Bài 4Nhà hàng đông nhấtTrung bình

Có $n$ nhóm khách ($n \le 2 \cdot 10^5$). Nhóm thứ $i$ gồm $w_i$ người ($1 \le w_i \le 10^9$), đến lúc $a_i$ và rời đi lúc $b_i$ ($0 \le a_i < b_i \le 10^9$). Nhóm rời đi lúc $t$ thì không còn ở đó khi một nhóm khác đến đúng lúc $t$. Hỏi lúc đông nhất nhà hàng có bao nhiêu người?

Input
4
1 5 3
2 4 2
4 8 5
5 6 1
Output
8

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Sự kiện có trọng số: $(a_i, +w_i)$ và $(b_i, -w_i)$. Khách rời lúc $b$ không gặp khách đến lúc $b$, nên trùng thời điểm thì trừ trước, cộng sau. Sắp xếp cặp (t, w) với $w$ âm cho sự kiện rời đi đã tự đưa sự kiện rời lên trước. Ví dụ: lúc 4, nhóm 2 (2 người) rời rồi nhóm 3 (5 người) đến, nhà hàng có $3 + 5 = 8$ người. Tổng số người có thể tới $2 \cdot 10^{14}$: dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<pair<long long, long long>> ev;
    for(int i = 0; i < n; i++){
        long long a, b, w;
        cin >> a >> b >> w;
        ev.push_back({a, w});
        ev.push_back({b, -w});         // -w < w nên rời đi được xử lý trước
    }
    sort(ev.begin(), ev.end());
    long long cur = 0, best = 0;
    for(auto e : ev){
        cur += e.second;
        best = max(best, cur);
    }
    cout << best;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
ev = []
for i in range(n):
    a, b, w = int(data[1 + 3 * i]), int(data[2 + 3 * i]), int(data[3 + 3 * i])
    ev.append((a, w))
    ev.append((b, -w))                 # -w < w nên rời đi được xử lý trước
ev.sort()
cur = best = 0
for t, d in ev:
    cur += d
    if cur > best:
        best = cur
print(best)
Bài 5Điểm nóngTrung bình

Cho $n$ đoạn số nguyên $[l_i, r_i]$ ($n \le 2 \cdot 10^5$, $-10^9 \le l_i \le r_i \le 10^9$), đoạn phủ mọi điểm nguyên từ $l_i$ tới $r_i$. Tìm số đoạn lớn nhất cùng phủ một điểm nguyên, và điểm nhỏ nhất đạt được số đó.

Input
4
1 3
2 5
5 7
4 6
Output
3 5

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Sự kiện $(l, +1)$, $(r + 1, -1)$, sắp xếp cặp (trùng thời điểm thì $-1$ trước). Khi cur > best (lớn hơn hẳn) thì cập nhật best và lưu thời điểm của sự kiện đó, như phần 5. Ví dụ: điểm 5 được [2, 5], [5, 7], [4, 6] phủ. Tại $t = 4$ có $(4, -1)$ của [1, 3] và $(4, +1)$ của [4, 6], trừ trước nên cur chỉ là 2.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<pair<long long, int>> ev;
    for(int i = 0; i < n; i++){
        long long l, r;
        cin >> l >> r;
        ev.push_back({l, +1});
        ev.push_back({r + 1, -1});
    }
    sort(ev.begin(), ev.end());
    int cur = 0, best = 0;
    long long diem = 0;
    for(auto e : ev){
        cur += e.second;
        if(cur > best){ best = cur; diem = e.first; }
    }
    cout << best << " " << diem;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n = int(data[0])
ev = []
for i in range(n):
    l, r = int(data[1 + 2 * i]), int(data[2 + 2 * i])
    ev.append((l, 1))
    ev.append((r + 1, -1))
ev.sort()
cur = best = diem = 0
for t, d in ev:
    cur += d
    if cur > best:
        best, diem = cur, t
print(best, diem)
Bài 6Đoạn chứa điểmTrung bình

Cho $n$ đoạn $[l_i, r_i]$ ($n \le 2 \cdot 10^5$, $0 \le l_i \le r_i \le 10^9$) và $q$ điểm $x_j$ ($q \le 2 \cdot 10^5$, $0 \le x_j \le 10^9$). Với mỗi điểm, in số đoạn chứa điểm đó ($l_i \le x_j \le r_i$).

Input
4 5
1 3
2 5
5 7
4 6
0 2 5 6 100
Output
0
2
3
2
0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Cách 1: nén toạ độ $l$, $r + 1$, $x$ rồi dùng mảng hiệu trên chỉ số nén, đúng code phần 4. Cách 2 gọn hơn: số đoạn chứa $x$ = (số đoạn có $l \le x$) − (số đoạn có $r < x$), vì đoạn có $r < x$ thì chắc chắn có $l \le r < x$. Sắp xếp riêng mảng $l$ và mảng $r$, mỗi câu hỏi làm hai lần tìm nhị phân upper_bound(L, x) và lower_bound(R, x). Với $x = 5$: có 4 đoạn có $l \le 5$, 1 đoạn có $r < 5$, đáp án 3.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, q;
    cin >> n >> q;
    vector<long long> L(n), R(n);
    for(int i = 0; i < n; i++) cin >> L[i] >> R[i];
    sort(L.begin(), L.end());
    sort(R.begin(), R.end());
    while(q--){
        long long x;
        cin >> x;
        long long batDau = upper_bound(L.begin(), L.end(), x) - L.begin();   // số đoạn có l <= x
        long long daHet = lower_bound(R.begin(), R.end(), x) - R.begin();    // số đoạn có r < x
        cout << batDau - daHet << "\n";
    }
    return 0;
}
import sys
from bisect import bisect_left, bisect_right

data = sys.stdin.buffer.read().split()
n, q = int(data[0]), int(data[1])
L = sorted(int(data[2 + 2 * i]) for i in range(n))
R = sorted(int(data[3 + 2 * i]) for i in range(n))
out = []
for j in range(q):
    x = int(data[2 + 2 * n + j])
    out.append(bisect_right(L, x) - bisect_left(R, x))   # (l <= x) - (r < x)
print('\n'.join(map(str, out)))
Bài 7Phân phòng khách sạnKhá

Có $n$ khách ($n \le 2 \cdot 10^5$), khách $i$ ở từ ngày $a_i$ tới hết ngày $b_i$ ($1 \le a_i \le b_i \le 10^9$). Khách sạn phân phòng như sau: xét khách theo ngày đến tăng dần (cùng ngày đến thì khách có số thứ tự nhỏ hơn trước). Phòng được coi là trống với khách đến ngày $a$ nếu người ở trước đã đi từ ngày $b < a$. Nếu có phòng trống, khách nhận phòng trống có số nhỏ nhất; nếu không, khách sạn mở phòng mới với số tiếp theo (1, 2, 3, …). In số phòng đã mở, rồi số phòng của từng khách theo thứ tự đề cho.

Input
6
1 3
2 5
4 6
1 1
7 8
6 9
Output
2
1 2 1 2 1 2

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Cần hai hàng đợi ưu tiên (min-heap): ban chứa các cặp (ngày trả, phòng) đang có người, trong chứa số các phòng trống. Khi khách đến ngày $a$: lấy hết các phòng ở đỉnh ban có ngày trả $< a$ chuyển sang trong; nếu trong khác rỗng thì lấy phòng số nhỏ nhất, ngược lại mở phòng mới. Mỗi phòng vào ra mỗi hàng đợi tối đa $n$ lần nên tổng $O(n \log n)$. Ví dụ: khách 4 ở ngày 1 nên khách 2 (đến ngày 2) nhận lại phòng 2; số phòng mở ra bằng số khách cùng lúc nhiều nhất là 2.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    vector<array<int, 3>> c(n);                     // (ngày đến, số thứ tự, ngày đi)
    for(int i = 0; i < n; i++){ cin >> c[i][0] >> c[i][2]; c[i][1] = i; }
    sort(c.begin(), c.end());                       // cùng ngày đến: số thứ tự nhỏ trước
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> ban;   // (ngày trả, phòng)
    priority_queue<int, vector<int>, greater<int>> trong;                                 // phòng trống
    vector<int> phong(n);
    int k = 0;
    for(auto &x : c){
        while(!ban.empty() && ban.top().first < x[0]){ trong.push(ban.top().second); ban.pop(); }
        int room;
        if(!trong.empty()){ room = trong.top(); trong.pop(); }
        else room = ++k;
        phong[x[1]] = room;
        ban.push({x[2], room});
    }
    cout << k << "\n";
    for(int i = 0; i < n; i++) cout << phong[i] << " ";
    return 0;
}
import sys, heapq

data = sys.stdin.buffer.read().split()
n = int(data[0])
c = sorted((int(data[1 + 2 * i]), i, int(data[2 + 2 * i])) for i in range(n))   # cùng ngày đến: số thứ tự nhỏ trước
ban, trong = [], []            # ban: (ngày trả, phòng); trong: số phòng trống
phong = [0] * n
k = 0
for a, i, b in c:
    while ban and ban[0][0] < a:
        heapq.heappush(trong, heapq.heappop(ban)[1])
    if trong:
        room = heapq.heappop(trong)
    else:
        k += 1
        room = k
    phong[i] = room
    heapq.heappush(ban, (b, room))
print(k)
print(*phong)
Bài 8Vùng phủ dàyKhá

Có $n$ tấm bạt ($n \le 2 \cdot 10^5$) trải trên trục số, tấm thứ $i$ che khúc $[l_i, r_i)$ ($0 \le l_i < r_i \le 10^9$). Cho số $k$ ($1 \le k \le n$). Tính tổng chiều dài phần trục số được ít nhất $k$ tấm bạt che.

Input
5 2
0 4
2 6
3 8
10 12
11 15
Output
5

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Quét sự kiện như phần 3 nhưng lần này quan tâm khoảng giữa hai sự kiện liên tiếp: sau khi xử lý sự kiện thứ $i$ tại thời điểm $t_i$, trên khúc $[t_i, t_{i+1})$ số tấm che không đổi và bằng cur. Nếu cur $\ge k$ thì cộng $t_{i+1} - t_i$. Sự kiện trùng thời điểm cho khúc dài 0 nên thứ tự không ảnh hưởng. Ví dụ: khúc [2, 6) được 2–3 tấm che (dài 4), khúc [11, 12) được 2 tấm che (dài 1), tổng 5. Gộp đoạn ở phần 2 chính là trường hợp $k = 1$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, k;
    cin >> n >> k;
    vector<pair<long long, int>> ev;
    for(int i = 0; i < n; i++){
        long long l, r;
        cin >> l >> r;
        ev.push_back({l, +1});
        ev.push_back({r, -1});
    }
    sort(ev.begin(), ev.end());
    int cur = 0;
    long long ans = 0;
    for(int i = 0; i + 1 < (int)ev.size(); i++){
        cur += ev[i].second;
        if(cur >= k) ans += ev[i + 1].first - ev[i].first;   // khúc [t_i, t_{i+1}) có cur tấm che
    }
    cout << ans;
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n, k = int(data[0]), int(data[1])
ev = []
for i in range(n):
    l, r = int(data[2 + 2 * i]), int(data[3 + 2 * i])
    ev.append((l, 1))
    ev.append((r, -1))
ev.sort()
cur = ans = 0
for i in range(len(ev) - 1):
    cur += ev[i][1]
    if cur >= k:
        ans += ev[i + 1][0] - ev[i][0]       # khúc [t_i, t_{i+1}) có cur tấm che
print(ans)
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét