Tập đoạn tốt (HSG 9 Hà Nội 2024)
Đề bài
Một đoạn thẳng được mô tả bằng một cặp số nguyên $[L, R]$. Hai đoạn thẳng được gọi là giao nhau nếu chúng có ít nhất một điểm chung.
Một tập đoạn tốt là tập các đoạn thẳng sao cho với mỗi một đoạn thẳng trong tập, đều giao nhau với ít nhất một đoạn thẳng khác trong tập đó (coi tập đoạn thẳng chỉ có một đoạn thẳng duy nhất là tập đoạn tốt). Ví dụ, tập $\{[1,3], [4,5], [5,7], [3,4]\}$ hay $\{[1,4], [2,3]\}$ là một tập đoạn tốt, còn $\{[1,4], [3,5], [6,7]\}$ thì không phải là tập đoạn tốt. Độ tốt của một tập đoạn tốt là số lượng các đoạn trong tập đó.
Lưu ý: hai tập đoạn tốt có ít nhất một điểm chung sẽ được gộp thành một tập đoạn tốt.
Yêu cầu: ban đầu tập đoạn thẳng không có đoạn thẳng nào. Cho $N$ đoạn thẳng, mỗi lần lấy một đoạn thẳng theo thứ tự thêm vào tập đoạn thẳng, yêu cầu tính tích độ tốt của các tập đoạn tốt được sinh ra từ tập đoạn thẳng hiện tại. Do kết quả rất lớn, in ra phần dư của đáp án sau khi chia cho $10^9 + 7$.
Dữ liệu vào từ file văn bản DOANTOT.INP: dòng đầu tiên chứa số nguyên dương $N$ $(N \le 10^5)$ là số đoạn thẳng lần lượt được thêm vào; $N$ dòng tiếp, mỗi dòng chứa hai số nguyên dương $L, R$ $(L \le R \le 10^9)$ mô tả một đoạn thẳng.
Kết quả ghi ra file văn bản DOANTOT.OUT gồm $N$ dòng, dòng thứ $i$ $(1 \le i \le N)$ chứa một số nguyên là kết quả theo yêu cầu đề bài khi thêm đoạn thẳng thứ $i$.
Ràng buộc:
- Có $20\%$ số test có $L = R$; $N \le 30$;
- Có $30\%$ số test có $R \le 1000$;
- Có $20\%$ số test có $N \le 2000$;
- Có $30\%$ số test không có ràng buộc gì thêm.
6
1 3
4 5
5 7
3 4
8 10
9 111
1
2
4
4
8Giải thích: lần 1 có 1 tập $\{[1,3]\}$ → $1$. Lần 2 có $\{[1,3]\}$ và $\{[4,5]\}$ → $1 \times 1 = 1$. Lần 3 có $\{[1,3]\}$ và $\{[4,5], [5,7]\}$ → $1 \times 2 = 2$. Lần 4 đoạn $[3,4]$ nối tất cả thành 1 tập 4 đoạn → $4$. Lần 5 thêm tập $\{[8,10]\}$ → $4 \times 1 = 4$. Lần 6 có $\{[8,10], [9,11]\}$ → $4 \times 2 = 8$.
if (fopen("DOANTOT.INP", "r")) { … } nên nộp đi thi đọc tệp cũng được.Lời mở đầu
"Tập đoạn tốt" thực chất là các nhóm đoạn dính liền nhau (thành phần liên thông nếu nối hai đoạn giao nhau). Mỗi lần thêm một đoạn, đoạn mới có thể "dán" nhiều nhóm lại với nhau. Có hai khó khăn: tìm nhanh các nhóm bị dán, và cập nhật tích theo modulo khi một số thừa số biến mất – việc này cần đến nghịch đảo modulo, kiến thức trọng tâm của chương đồng dư.
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Bộ testBộ test tự sinh
| Test | Mức | $N$ | Toạ độ lớn nhất |
|---|---|---|---|
| 1 – 8 | $L = R$, $N \le 30$ | 10 – 28 | 59 |
| 9 – 20 | $R \le 1000$ | 50 000 – 100 000 | 1 000 |
| 21 – 28 | $N \le 2000$ | 2 000 | $\approx 10^9$ |
| 29 – 40 | tổng quát | 100 000 | $10^9$ |
Ý tưởngMỗi tập đoạn tốt phủ đúng một khoảng liền
Các đoạn trong cùng một tập nối với nhau thành một dải không đứt, nên hợp của chúng là một khoảng $[\min L, \max R]$. Hai tập khác nhau thì hai khoảng rời nhau (nếu có điểm chung thì đã bị gộp). Vậy ta chỉ cần lưu danh sách các khoảng rời nhau, sắp theo đầu trái, kèm số đoạn của mỗi khoảng.
Khi thêm đoạn $[l, r]$: các khoảng có điểm chung với nó là các khoảng $[a, b]$ có $a \le r$ và $b \ge l$ – chúng nằm liền nhau trong danh sách. Gộp tất cả lại thành một khoảng mới, số đoạn bằng tổng cộng $+1$.
Cập nhật tích. Gọi $P$ là tích hiện tại (mod $p = 10^9+7$). Gộp các tập có kích thước $c_1, \ldots, c_t$ thành một tập kích thước $c = 1 + \sum c_j$: ta phải chia $P$ cho từng $c_j$ rồi nhân với $c$. Trong đồng dư không có phép chia, nhưng vì $p$ nguyên tố và $c_j < p$ nên có nghịch đảo modulo theo định lý Fermat nhỏ:
Mỗi khoảng bị gộp thì biến mất, nên tổng số lần gộp trong cả quá trình không quá $N$. Em nhập các đoạn (mỗi đoạn "L-R", cách nhau dấu cách) rồi bấm Tiếp:
Cách 1DSU nối với mọi đoạn cũ – O(N²)
Dùng cấu trúc DSU (hợp nhất – tìm gốc): mỗi khi thêm đoạn $i$, duyệt mọi đoạn cũ $j < i$, nếu giao nhau ($L_j \le R_i$ và $L_i \le R_j$) thì hợp nhất hai tập. Sau đó duyệt mọi gốc để nhân kích thước các tập. Không cần nghịch đảo vì tính lại tích từ đầu.
- Mỗi lần thêm duyệt $i$ đoạn cũ và $i$ gốc: tổng $O(N^2)$ – khoảng $4 \cdot 10^6$ bước khi $N = 2000$, nhưng $5 \cdot 10^9$ khi $N = 10^5$.
- Cách này đúng với mọi dữ liệu nên mình dùng nó để kiểm tra đáp án các test $N \le 2000$.
Kết quả chạy thật (bộ test tự sinh): bản C++ đúng 16/40 test (Mức 1: 8/8, Mức 2: 0/12, Mức 3: 8/8, Mức 4: 0/12). Bản Python đúng 15/40 (Mức 1: 8/8, Mức 2: 0/12, Mức 3: 7/8, Mức 4: 0/12).
Cách 2map các khoảng rời nhau + nghịch đảo modulo – O(N log N)
Lưu các khoảng trong map<trái, (phải, số đoạn)>. Thêm $[l, r]$: tìm khoảng cuối cùng có đầu trái $\le r$ (bằng upper_bound(r) rồi lùi lại), lùi tiếp chừng nào khoảng còn có đầu phải $\ge l$; mỗi khoảng như vậy được gộp vào, xóa khỏi map, và tích được nhân với nghịch đảo kích thước của nó.
- Mỗi khoảng chỉ được thêm vào map một lần và xóa tối đa một lần, nên tổng số lần gộp $\le N$. Mỗi thao tác map và mỗi lần tính nghịch đảo tốn $O(\log)$: tổng $O(N \log N + N \log p)$.
- Kích thước tập luôn $\le 10^5 < p$ nên không bao giờ chia hết cho $p$ – nghịch đảo luôn tồn tại và tích không bao giờ bằng $0$.
- Điều kiện "không giao" là
phải < l: hai đoạn chạm nhau tại một điểm (ví dụ $[3,4]$ và $[4,5]$) có giao nhau; còn $[1,3]$ và $[4,5]$ thì không. - Bản Python dùng một danh sách đầu trái đã sắp xếp với
bisectthay chomap, và gom các tập bị gộp để chỉ tính một lần nghịch đảo cho mỗi đoạn mới. Ở 3 test lớn nhiều đoạn ngắn (rất nhiều tập nhỏ) bản Python chạy sát 1 giây (1,0 – 1,1 giây CPU, chấm lại vẫn vậy) nên bị tính quá thời gian.
Kết quả chạy thật (bộ test tự sinh): bản C++ đúng 40/40 test, lâu nhất 0,169s; bản Python đúng 37/40, lâu nhất 0,835s.
Tổng kếtKết quả chạy thật trên bộ test tự sinh
Mình chấm cả 2 cách (C++ và Python) trên 40 test tự sinh với giới hạn 1 giây, đo thời gian CPU:
| Lời giải | Mức 1 test 1–8 | Mức 2 test 9–20 | Mức 3 test 21–28 | Mức 4 test 29–40 | Tổng | CPU lớn nhất (test đúng) |
|---|---|---|---|---|---|---|
| Cách 1 – C++ | 8/8 | 0/12 | 8/8 | 0/12 | 16/40 | 0,028s |
| Cách 1 – Python | 8/8 | 0/12 | 7/8 | 0/12 | 15/40 | 0,928s |
| Cách 2 – C++ | 8/8 | 12/12 | 8/8 | 12/12 | 40/40 | 0,169s |
| Cách 2 – Python | 8/8 | 12/12 | 8/8 | 9/12 | 37/40 | 0,835s |
- 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.
- 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
Đăng nhận xét