Chiều dài phủ (DOANP2)
Đề bài
DOANP2 – Chiều dài phủ
Giới hạn thời gian: 1.0 giây · Giới hạn bộ nhớ: 128 megabyte
Trên trục số cho $n$ đoạn thẳng $[s_i, d_i]$, $s_i$ là điểm đầu và $d_i$ là điểm cuối ($s_i < d_i$ với $1 \le i \le n$). Hỏi tổng chiều dài được phủ bởi các đoạn trên là bao nhiêu.
Ví dụ cho 3 đoạn $[1,3]$, $[2,4]$ và $[3,5]$ như hình dưới thì tổng chiều dài phủ là 4.
Dữ liệu nhập: gồm các dòng sau
- Dòng thứ nhất là số nguyên $n$ ($1 \le n \le 10^5$) là số lượng các đoạn.
- Trong $n$ dòng tiếp theo, dòng thứ $i$ là hai số nguyên $s_i, d_i$ cách nhau một khoảng trắng ($0 \le s_i < d_i \le 2 \times 10^9$).
Dữ liệu xuất: Tổng chiều dài được phủ bởi các đoạn trên.
3
1 3
2 4
3 542
5 8
2 45Lời mở đầu
Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇. Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:
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:
Chuẩn bịNhìn giới hạn đoán độ phức tạp
Khi nhìn vào bài mà có giới hạn $N$ tối đa $10^5$ thì các bạn có thể nhẩm tính chắc chắn muốn chạy trong giới hạn 1 giây thì toàn bộ bài làm phải có độ phức tạp thời gian là $O(N \log N)$ trở xuống, chứ nếu mà $O(N^2)$ trở lên thì chắc chắn là sẽ bị TLE. Vì $N = 10^5$ thì $N^2 = 10^{10}$ là quá ngưỡng $10^8$ rồi. Còn $N \log N \approx 17 \cdot 10^5$ thì ít hơn ngưỡng $10^8$ nhiều nên yên tâm không bị TLE, hoặc nếu có thể độ phức tạp ít hơn nữa ví dụ $O(N)$ thì quá tốt.
Ngoài ra bài này còn một giới hạn nữa cần để ý: tọa độ lên tới $2 \times 10^9$. Nên nhìn vào 1 bài ta nhìn ngay các giới hạn độ lớn mà ước tính thuật toán phải có độ phức tạp bao nhiêu, từ đó sẽ định hình ra cách giải quyết.
Cách 1Vét cạn: tô từng đoạn đơn vị
Cách dễ nghĩ nhất: chia trục số thành các đoạn đơn vị $[x, x+1]$. Mỗi đoạn $[s, d]$ sẽ phủ đúng các đoạn đơn vị $[s, s+1],$ $[s+1, s+2],$ $\dots,$ $[d-1, d]$, tức là $x$ chạy từ $s$ đến $d - 1$. Ta dùng mảng đánh dấu phu[x] = true nghĩa là đoạn đơn vị $[x, x+1]$ đã được phủ. Đọc hết các đoạn và tô xong thì đếm số ô được tô – đó chính là tổng chiều dài phủ. Ô nào bị nhiều đoạn chồng lên cũng chỉ tính 1 lần nên không sợ bị đếm trùng.
Ví dụ đề: $[1,3]$ tô ô 1, 2; $[2,4]$ tô ô 2, 3; $[3,5]$ tô ô 3, 4. Các ô được tô là 1, 2, 3, 4 → chiều dài phủ là 4.
Code dưới đây chỉ dùng được khi tọa độ nhỏ (ở đây mình cho tối đa 1000) để các bạn hiểu ý tưởng:
Code cách 1 – vét cạn tô đoạn đơn vị (chỉ cho tọa độ ≤ 1000)
#include <bits/stdc++.h>
using namespace std;
int main(){
int n;
cin >> n;
vector<bool> phu(1001, false); // phu[x] = true: đoạn đơn vị [x, x+1] đã được phủ
int maxD = 0;
for(int i = 0; i < n; ++i){
int s, d;
cin >> s >> d;
maxD = max(maxD, d);
for(int x = s; x < d; ++x){
phu[x] = true;
}
}
int res = 0;
for(int x = 0; x < maxD; ++x){
if(phu[x]){
res++;
}
}
cout << res;
return 0;
}
n = int(input())
phu = [False] * 1001 # phu[x] = True: đoạn đơn vị [x, x+1] đã được phủ
for _ in range(n):
s, d = map(int, input().split())
for x in range(s, d):
phu[x] = True
print(sum(phu))
Ý tưởngSắp xếp theo điểm đầu rồi gộp đoạn
Đầu tiên ta cần sắp xếp danh sách các đoạn thẳng tăng dần theo điểm bắt đầu, làm như thế để tạo quy luật cho các đoạn thẳng trên trục số: đoạn nào bắt đầu trước thì xét trước. Ở bước sắp xếp này như đã phân tích ở trên thì ta phải dùng thuật toán sắp xếp tối ưu $O(N \log N)$ (hàm sort có sẵn), chứ nếu dùng những thuật toán sắp xếp $O(N^2)$ thì sẽ bị TLE.
Sau khi sắp xếp, ta đi từ trái sang phải và luôn giữ một khối đang phủ $[L, R]$ (ban đầu chính là đoạn đầu tiên). Xét đoạn tiếp theo $[s, d]$:
- Nếu $s > R$: đoạn mới tách rời khối hiện tại (có khoảng trống ở giữa). Vì các đoạn sau đều có điểm đầu $\ge s > R$ nên không còn đoạn nào nối dài được khối cũ nữa → chốt khối cũ: cộng $R - L$ vào kết quả, rồi bắt đầu khối mới $[L, R] = [s, d]$.
- Ngược lại $s \le R$: đoạn mới chồng (hoặc chạm) lên khối hiện tại → gộp vào khối. Nếu $d > R$ thì khối được nối dài ra: $R = d$; còn nếu $d \le R$ thì đoạn mới nằm gọn trong khối, không thay đổi gì.
Xét hết các đoạn thì nhớ cộng nốt khối cuối cùng $R - L$ vào kết quả. Mỗi chỗ chồng lên nhau chỉ nằm trong đúng 1 khối nên không bị đếm trùng.
Các đoạn $[1,3], [2,4], [3,5]$ gộp thành khối $[1,5]$ dài 4; đoạn $[7,9]$ tách rời thành khối riêng dài 2 → tổng 6.
Em nhập các đoạn rồi bấm Tiếp để xem từng bước gộp đoạn:
Cách 2Code sắp xếp + gộp đoạn – O(N log N)
Source code các bạn tham khảo để nhìn thấy rõ với tất cả những gì mình đã phân tích ở trên:
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
vector<pair<int, int>> a(n);
for(int i = 0; i < n; ++i){
cin >> a[i].first >> a[i].second;
}
sort(a.begin(), a.end());
long long res = 0;
int L = a[0].first, R = a[0].second;
for(int i = 1; i < n; ++i){
if(a[i].first > R){
res += R - L;
L = a[i].first;
R = a[i].second;
}
else if(a[i].second > R){
R = a[i].second;
}
}
res += R - L;
cout << res;
return 0;
}
import sys
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0])
a = sorted(zip(map(int, data[1:2 * n + 1:2]), map(int, data[2:2 * n + 2:2])))
res = 0
L, R = a[0]
for s, d in a[1:]:
if s > R: # đoạn mới nằm tách rời: chốt khối cũ
res += R - L
L, R = s, d
elif d > R: # chồng lên khối cũ: nối dài khối
R = d
res += R - L
print(res)
main()
Giải thích một số chỗ trong code ở trên:
ios::sync_with_stdio(0); cin.tie(0);2 dòng đầu tiên của hàm main sẽ giúp đẩy nhanh tốc độ nhập xuất dữ liệu nếu các bạn code C++ (để hiểu vì sao thì các bạn cứ gõ mấy từ này lên Google là sẽ ra rất nhiều bài viết giải thích nhé). Bài này input có thể lên đến $2 \cdot 10^5$ số cần nhập nên ta thêm 2 dòng này cho chương trình chạy nhanh hơn.- Mỗi đoạn lưu bằng
pair<int, int>vớifirstlà điểm đầu,secondlà điểm cuối. Hàmsortsắp xếppairtheofirsttrước nên ta không cần viết hàm so sánh riêng. - Kết quả
resmình để kiểulong longcho an toàn. Thực ra tổng chiều dài phủ không vượt quá $2 \times 10^9$ (vẫn vừa kiểuintvìintchứa tối đa khoảng $2.147 \times 10^9$) nhưng sát nút quá, các bạn tập thói quen dùnglong longcho các tổng là tốt nhất. - Đề cho $s_i < d_i$ và $n \ge 1$ nên luôn có ít nhất 1 đoạn, khởi tạo khối bằng
a[0]là an toàn.
Đánh giá độ phức tạp của cách làm này:
$O(N)$ cho bước đọc dữ liệu, $O(N \log N)$ cho bước sắp xếp, $O(N)$ cho bước gộp đoạn. Theo quy tắc cộng BigO lấy thành phần lớn nhất nên tổng quát là $O(N \log N)$, đến từ bước sắp xếp. Với $N = 10^5$ thì chỉ khoảng $1.7 \cdot 10^6$ phép toán, yên tâm không bị TLE. Và độ phức tạp này không phụ thuộc vào độ lớn tọa độ, đó chính là điểm hơn hẳn cách 1.
- 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