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

Điểm chung

Điểm chung
Nguồn: Codeforces Group Độ khó: Khá Mảng đánh dấu Giao đoạn Mảng sai phân

Đề bài

Trên trục số Ox, cho $N$ đoạn thẳng, mỗi đoạn thẳng được xác định bởi hai điểm đầu và cuối là hai số nguyên. Một điểm $M$ được gọi là nằm trong đoạn thẳng $AB$ nếu $A \le M \le B$.

Yêu cầu: đếm xem có bao nhiêu điểm có toạ độ nguyên nằm trong đúng $K$ đoạn thẳng.

Dữ liệu vào

  • Dòng đầu tiên gồm hai số nguyên $N$ và $K$ ($1 \le K \le N \le 10^5$);
  • $N$ dòng sau, mỗi dòng gồm hai số nguyên $a, b$ mô tả hai điểm đầu và cuối của đoạn thẳng ($1 \le a \le b \le 10^{18}$).

Kết quả

  • Một số nguyên duy nhất là số lượng điểm có toạ độ nguyên nằm trong đúng $K$ đoạn thẳng.

Ràng buộc:

  • Có 50% số test ứng với 50% số điểm của bài thoả mãn: $a, b \le 10^3$;
  • 30% số test khác ứng với 30% số điểm của bài thoả mãn: $K = N$;
  • 20% số test còn lại ứng với 20% số điểm của bài không có ràng buộc gì thêm.
Ví dụ 1 – input
3 2
1 5
2 8
3 7
Output
3
Toạ độ của 3 điểm nằm trong đúng 2 đoạn thẳng là: 2, 6, 7.
Điểm có toạ độ 2 nằm trong 2 đoạn thẳng: đầu tiên và thứ hai.
Điểm có toạ độ 6, 7 nằm trong 2 đoạn thẳng: thứ hai và thứ ba.
Ví dụ 2 – input
3 1
1 5
2 8
3 7
Output
2
Toạ độ của 2 điểm nằm trong đúng 1 đoạn thẳng là: 1, 8.
Điểm có toạ độ 1 chỉ nằm trong đoạn thẳng đầu tiên.
Điểm có toạ độ 8 chỉ nằm trong đoạn thẳng thứ ba.
Ví dụ 3 – input
3 3
1 5
2 8
3 7
Output
3
Toạ độ của 3 điểm nằm trong cả 3 đoạn thẳng là: 3, 4, 5.

Lờ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 😇.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ
Dành cho tất cả mọi ngườiNộp bài trên Codeforces

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên Codeforces

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

50%Cách 1: thống kê số lần xuất hiện bằng map

Một cách làm rất đơn giản cho bài này mà chắc nhiều bạn sẽ nhìn ra: mỗi đoạn thẳng gồm 2 điểm $a$, $b$ với $a$ là điểm đầu, $b$ là điểm cuối. Với mỗi đoạn thẳng ta cứ lặp xét tất cả các điểm từ $a$ đến $b$ và đánh dấu số lần xuất hiện của các điểm đó được +1. Cứ thế xét qua tất cả các đoạn thẳng là ta có thống kê cụ thể từng điểm phân biệt xuất hiện bao nhiêu lần. Sau đó đếm số lượng các điểm xuất hiện đúng $K$ lần thì đó là đáp án.

Ví dụ với các test mẫu đề cho đều có 3 đoạn thẳng là (1, 5), (2, 8), (3, 7) thì ta thống kê như sau (ô xanh là ô vừa được +1):

Điểm12345678
Sau đoạn (1, 5)11111000
Sau đoạn (2, 8)12222111
Sau đoạn (3, 7)12333221

Vậy khi đã thống kê ra được rồi:

  • Test ví dụ 1 có $K = 2$: chỉ có 3 điểm xuất hiện 2 lần là các điểm 2, 6, 7.
  • Test ví dụ 2 có $K = 1$: chỉ có 2 điểm xuất hiện 1 lần là các điểm 1, 8.
  • Test ví dụ 3 có $K = 3$: chỉ có 3 điểm xuất hiện 3 lần là các điểm 3, 4, 5.

Để thống kê số lần xuất hiện của từng điểm phân biệt thì tiện nhất ta dùng cấu trúc dữ liệu map nhé các bạn, với key là điểm còn value là số lần xuất hiện.

Xem code cách 1 (map)
#include <bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    long long n, k;
    cin >> n >> k;
    map<long long, long long> mp;
    while(n--){
        long long a, b;
        cin >> a >> b;
        while(a <= b){
            mp[a]++;
            a++;
        }
    }
    long long res = 0;
    for(auto it = mp.begin(); it != mp.end(); ++it){
        if(it->second == k){
            res++;
        }
    }
    cout << res;
    return 0;
}
import sys
n_k = list(map(int, sys.stdin.readline().split()))
while len(n_k) < 2:
    n_k += list(map(int, sys.stdin.readline().split()))
n, k = n_k
mp = {}
count = 0
while count < n:
    parts = []
    while len(parts) < 2:
        parts += list(map(int, sys.stdin.readline().split()))
    a, b = parts
    count += 1
    while a <= b:
        mp[a] = mp.get(a, 0) + 1
        a += 1
res = 0
for key in mp:
    if mp[key] == k:
        res += 1
print(res)
C++▶ Chạy tay từng dòng codeCách 1: với mỗi đoạn, duyệt từng điểm từ a đến b và đếm bằng map.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main(){
4 ios::sync_with_stdio(0);
5 cin.tie(0);
6
7 long long n, k;
8 cin >> n >> k;
9 map<long long, long long> mp;
10 while(n--){
11 long long a, b;
12 cin >> a >> b;
13 while(a <= b){
14 mp[a]++;
15 a++;
16 }
17 }
18 long long res = 0;
19 for(auto it = mp.begin(); it != mp.end(); ++it){
20 if(it->second == k){
21 res++;
22 }
23 }
24 cout << res;
25 return 0;
26}

Giải thích một số chỗ trong code ở trên:

  1. Với mỗi đoạn thẳng ta đọc vào 2 số $a$, $b$ là điểm bắt đầu và kết thúc. Vòng lặp while(a <= b) và cứ thế a++; để biến a là biến duyệt từ $a$ đến $b$ luôn. Với mỗi giá trị a ta gọi mp[a]++;: nó sẽ đi tìm xem a có trong map chưa. Nếu chưa thì thêm a vào, mặc định khi khởi tạo có giá trị 0 nên ++ thành 1. Còn nếu đã có thì ++ để tăng lên 1.
  2. Bước cuối duyệt toàn bộ map xem Node nào có số lần xuất hiện bằng đúng $K$ thì res++;, sau cùng in ra res. Việc duyệt map dùng con trỏ iterator đặt tên là it, do mình lười nên khai báo kiểu auto để compiler tự xác định kiểu, còn muốn khai báo rõ ràng thì phải là map<long long, long long>::iterator it = mp.begin(); nha các bạn.
  3. Do $a$, $b$ có thể lên đến $10^{18}$ nên key phải dùng kiểu long long (số nguyên 8 byte) mới chứa nổi, kiểu 4 byte như int sẽ không lưu nổi. Còn value là số lần xuất hiện thì tối đa cũng chỉ $N = 10^5$, nên value, $N$, $K$ dùng int là đủ. Tuy nhiên bộ nhớ lúc nào cũng dư dả nên bình thường mình cứ để long long hết cho yên tâm không sợ tràn số trong bất kỳ tình huống nào.

Đánh giá độ phức tạp:

  • Không gian: $O(X)$ với $X$ là số lượng điểm phân biệt trong tất cả các đoạn thẳng được đưa vào map.
  • Thời gian: đọc $N$ đoạn thẳng tốn $O(N)$; mỗi đoạn lặp từ $A$ đến $B$ tốn $O(B - A + 1)$; mỗi lần tìm kiếm, cập nhật trong map tốn $O(\log X)$; sau cùng duyệt map tốn $O(X)$. Tổng cộng: $O(N \times (B - A + 1) \times \log X) + O(X)$.

$N$ tối đa chỉ $10^5$ nhưng chi phí phát sinh rất nhiều từ $A$, $B$ và $X$. Tưởng tượng gặp đoạn thẳng $A = 1$, $B = 10^{18}$ thì cách làm này toang ngay. Máy tính trung bình 1 giây chạy $10^8$ phép tính, tính sơ sơ:

$10^{18} / 10^8 = 10^{10}$ giây⇔2 777 777 giờ⇔115 740 ngày⇔317 năm

Quá khủng khiếp đúng không các bạn? Chưa kể đó mới là 1 đoạn thẳng, giả sử có $N = 10^5$ đoạn đều là $A = 1$, $B = 10^{18}$ thì phải nhân thêm $10^5$ lần nữa. Đó là mới tính thời gian, chứ bộ nhớ cũng không thể lưu nổi $10^{18}$ giá trị: mỗi giá trị lưu 2 thông tin là $8 \times 2 = 16$ byte, cần $16 \times 10^{18}$ byte thì kiểu gì lưu nổi hả các bạn haha.

Vậy cách làm trên chỉ nhắm được 50% test có $A, B \le 10^3$. Lúc này trường hợp xấu nhất là $N = 10^5$, $X = 10^3$, $B - A + 1 = 10^3$, tức $10^5 \times 10^3 \times 10 = 10^9$, phải chạy cả 10 giây mới ra kết quả. Nên khả năng không ăn trọn được 50% này nếu làm theo kiểu này. 50% test giới hạn nhỏ là để cho điểm, vậy làm sao lấy trọn được? Cùng xem tiếp nhé.

50%Cải tiến: unordered_map và mảng đánh dấu

Ta thấy phải nhân thêm 10 là do map được cài đặt theo cây nhị phân tìm kiếm (cụ thể là cây đỏ đen) nên mỗi thao tác tìm kiếm tốn $O(\log)$, cụ thể là $\log_2$. Khi $X = 1000$ thì $\log_2(1000) \approx 10$. Muốn tối ưu chỗ này ta dùng bảng băm (Hash Map) chỉ tốn $O(1)$ khi tìm kiếm, tức là chuyển sang unordered_map: chỉ cần đổi dòng khai báo map<long long, long long> mp; thành unordered_map<long long, long long> mp;, còn lại giữ nguyên.

Nhưng khi dùng Hash Map hãy cẩn thận: trường hợp xấu nhất khi hàm băm không chất lượng thì có thể tốn $O(N)$ để tìm kiếm, với $N$ là số phần tử bảng băm chứa. Dùng unordered_map là ta đã phó mặc cho nó tự thiết kế hàm băm, nên vẫn có những trường hợp nó thiết kế "ngu" nha các bạn :))

Hoặc bạn nào không biết unordered_map, hay không yên tâm giao phó cho nó thiết kế hàm băm, thì ta có thể chủ động làm luôn bằng kỹ thuật mảng đánh dấu (bản chất tư tưởng vẫn là bảng băm): tạo mảng 1001 phần tử để index từ 0 đến 1000 tương ứng tối đa 1000 điểm. Ban đầu khởi tạo tất cả là 0, mỗi điểm $a$ ta gọi vào vị trí index $a$ và tăng số lượng lên, bước này chỉ tốn $O(1)$. Sau cùng duyệt lại 1000 phần tử và kết luận. Do chính ta nắm rõ nhất quy luật của dữ liệu nên tự thiết kế "hàm băm" thì yên tâm hơn phải không nè ^^

Xem code cách 2 (mảng đánh dấu)
#include <bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    long long n, k;
    cin >> n >> k;
    long long cnt[1001] = {0};
    while(n--){
        long long a, b;
        cin >> a >> b;
        while(a <= b){
            cnt[a]++;
            a++;
        }
    }
    long long res = 0;
    for(long long i = 1; i <= 1000; ++i){
        if(cnt[i] == k){
            res++;
        }
    }
    cout << res;
    return 0;
}
import sys
tokens = []
while len(tokens) < 2:
    tokens += list(map(int, sys.stdin.readline().split()))
n, k = tokens[:2]
cnt = [0] * 1001
while n > 0:
    line = []
    while len(line) < 2:
        line += list(map(int, sys.stdin.readline().split()))
    a, b = line
    while a <= b:
        cnt[a] += 1
        a += 1
    n -= 1
res = 0
for i in range(1, 1001):
    if cnt[i] == k:
        res += 1
print(res)
C++▶ Chạy tay từng dòng codeCách 2: thay map bằng mảng đánh dấu cnt[1001], rồi duyệt lại cả mảng.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main(){
4 ios::sync_with_stdio(0);
5 cin.tie(0);
6
7 long long n, k;
8 cin >> n >> k;
9 long long cnt[1001] = {0};
10 while(n--){
11 long long a, b;
12 cin >> a >> b;
13 while(a <= b){
14 cnt[a]++;
15 a++;
16 }
17 }
18 long long res = 0;
19 for(long long i = 1; i <= 1000; ++i){
20 if(cnt[i] == k){
21 res++;
22 }
23 }
24 cout << res;
25 return 0;
26}

Đánh giá độ phức tạp: không gian $O(1000)$ của mảng cnt. Thời gian $O(1000)$ khởi tạo mảng + $O(N \times (B - A + 1))$ duyệt thống kê + $O(1000)$ duyệt mảng kết luận. Xấu nhất khi $N = 10^5$, $A = 1$, $B = 10^3$: $1000 + 10^5 \times 10^3 + 1000$ $= 2 \times 10^3 + 10^8$, theo quy tắc cộng Big O lấy thành phần lớn nhất là $10^8$, vừa đủ chạy trong 1 giây nên ăn trọn vẹn 50% test.

50%Tối ưu thêm: đếm ngay khi thống kê

Muốn chạy nhanh hơn chút thì tối ưu thêm vài chỗ. Chỗ khởi tạo mảng toàn số 0 long long cnt[1001] = {0}; có thể dùng memset cho hiệu suất nhanh hơn nhiều: long long cnt[1001]; memset(cnt, 0, sizeof(cnt));

Ngoài ra không cần bước cuối duyệt toàn bộ mảng, mà làm luôn trong bước thống kê: mỗi lần cnt[a]++; xong ta kiểm tra ngay cnt[a]:

  • Nếu bằng $K$ thì res++;
  • Nếu bằng $K + 1$ thì res--;, vì trước đó nó đã bằng $K$ và ta đã res++;, giờ vượt quá $K$ thì phải trả lại.
Lưu ý chỉ xét bằng đúng $K + 1$ thì mới res--;, còn về sau nó bằng $K + 2$, $K + 3$… thì không trừ gì hết, vì trước đó chỉ cộng 1 lần thì giờ chỉ trừ 1 lần. Bạn nào xét if(cnt[a] > k) { res--; } là sai đấy nhé, vì nó có thể bị trừ nhiều lần.
Xem code cách 3 (đếm ngay khi thống kê)
#include <bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    long long n, k;
    cin >> n >> k;
    long long cnt[1001];
    memset(cnt, 0, sizeof(cnt));
    long long res = 0;
    while(n--){
        long long a, b;
        cin >> a >> b;
        while(a <= b){
            cnt[a]++;
            if(cnt[a] == k){
                res++;
            }
            else if(cnt[a] == k + 1){
                res--;
            }
            a++;
        }
    }
    cout << res;
    return 0;
}
import sys
tokens = []
while len(tokens) < 2:
    tokens += list(map(int, sys.stdin.readline().split()))
n, k = tokens[:2]
cnt = [0] * 1001
res = 0
while n > 0:
    line = []
    while len(line) < 2:
        line += list(map(int, sys.stdin.readline().split()))
    a, b = line
    while a <= b:
        cnt[a] += 1
        if cnt[a] == k:
            res += 1
        elif cnt[a] == k + 1:
            res -= 1
        a += 1
    n -= 1
print(res)
C++▶ Chạy tay từng dòng codeCách 3: memset + cập nhật res ngay khi cnt[a] chạm k hoặc k + 1.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main(){
4 ios::sync_with_stdio(0);
5 cin.tie(0);
6
7 long long n, k;
8 cin >> n >> k;
9 long long cnt[1001];
10 memset(cnt, 0, sizeof(cnt));
11 long long res = 0;
12 while(n--){
13 long long a, b;
14 cin >> a >> b;
15 while(a <= b){
16 cnt[a]++;
17 if(cnt[a] == k){
18 res++;
19 }
20 else if(cnt[a] == k + 1){
21 res--;
22 }
23 a++;
24 }
25 }
26 cout << res;
27 return 0;
28}

50%Cách khác: duyệt từ Min đến Max

Ngoài các cách trên, để ăn được 50% test còn có thể làm thế này: các điểm cần xét luôn nằm trong đoạn từ $\min(A_i)$ đến $\max(B_i)$. Như ví dụ đề cho thì $\min(A_i) = 1$ và $\max(B_i) = 8$, đoạn từ 1 đến 8 bao phủ hết 3 đoạn thẳng. Ta tìm $Min$, $Max$ rồi duyệt từ $Min$ đến $Max$, với mỗi giá trị $x$ xét qua $N$ đoạn thẳng đếm xem nó nằm trong bao nhiêu đoạn (điểm $x$ nằm trong đoạn $A\,B$ khi $A \le x \le B$), đếm xong mà bằng đúng $K$ thì res++;.

Xem code cách 4 (Min đến Max)
#include <bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    long long n, k;
    cin >> n >> k;
    vector<pair<long long, long long>> a(n);
    long long Max = LLONG_MIN, Min = LLONG_MAX;
    for(long long i = 0; i < n; ++i){
        cin >> a[i].first >> a[i].second;
        Min = min(Min, a[i].first);
        Max = max(Max, a[i].second);
    }
    long long res = 0;
    for(long long x = Min; x <= Max; ++x){
        long long cnt = 0;
        for(long long i = 0; i < n; ++i){
            if(a[i].first <= x && x <= a[i].second){
                cnt++;
                if(cnt > k){
                    break;
                }
            }
        }
        if(cnt == k){
            res++;
        }
    }
    cout << res;
    return 0;
}
import sys
tokens = []
while len(tokens) < 2:
    tokens += list(map(int, sys.stdin.readline().split()))
n, k = tokens[:2]
a = [None] * n
Max = -9223372036854775808
Min = 9223372036854775807
read = []
while len(read) < 2 * n:
    read += list(map(int, sys.stdin.readline().split()))
for i in range(n):
    first = read[2 * i]
    second = read[2 * i + 1]
    a[i] = (first, second)
    Min = min(Min, first)
    Max = max(Max, second)
res = 0
x = Min
while x <= Max:
    cnt = 0
    for i in range(n):
        if a[i][0] <= x and x <= a[i][1]:
            cnt += 1
            if cnt > k:
                break
    if cnt == k:
        res += 1
    x += 1
print(res)
C++▶ Chạy tay từng dòng codeCách 4: với mỗi điểm x từ Min đến Max, đếm số đoạn chứa x.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main(){
4 ios::sync_with_stdio(0);
5 cin.tie(0);
6
7 long long n, k;
8 cin >> n >> k;
9 vector<pair<long long, long long>> a(n);
10 long long Max = LLONG_MIN, Min = LLONG_MAX;
11 for(long long i = 0; i < n; ++i){
12 cin >> a[i].first >> a[i].second;
13 Min = min(Min, a[i].first);
14 Max = max(Max, a[i].second);
15 }
16 long long res = 0;
17 for(long long x = Min; x <= Max; ++x){
18 long long cnt = 0;
19 for(long long i = 0; i < n; ++i){
20 if(a[i].first <= x && x <= a[i].second){
21 cnt++;
22 if(cnt > k){
23 break;
24 }
25 }
26 }
27 if(cnt == k){
28 res++;
29 }
30 }
31 cout << res;
32 return 0;
33}

Trường hợp xấu nhất $N = 10^5$, $A = 1$, $B = 10^3$ thì lặp hết $10^5 + 10^3 \times 10^5 = 10^5 + 10^8$, tổng quát vẫn là $10^8$, chạy trong 1 giây nha các bạn. Thực tế những thông số này chỉ tương đối, vì tùy dữ liệu đầu vào, với mỗi điểm có thể kết thúc sớm khi thấy cnt đã vượt quá k thì thoát vòng lặp, không cần đếm nữa vì điểm đó chắc chắn không hợp lệ.

+30%Trường hợp K = N: giao các đoạn thẳng

Khi $K = N$ là trường hợp rất dễ làm và sẽ thật tiếc nếu bạn nào bỏ qua. Dù $A$, $B$ có thể lên đến $10^{18}$, chỉ cần $K = N$ thì mọi thứ cực kỳ đơn giản.

Tại sao? Đề yêu cầu đếm các điểm nằm trong đúng $K$ đoạn thẳng, mà $K = N$ nghĩa là đếm các điểm nằm trong đủ $N$ đoạn thẳng. Giả sử có 5 đoạn dt1, dt2, dt3, dt4, dt5: ta tìm phần giao của dt1 và dt2, không có thì kết luận 0 và dừng. Còn có thì lấy phần giao đó tìm tiếp phần giao với dt3, và cứ thế lặp lại; tại đoạn nào không còn phần giao thì kết luận 0 và dừng, còn không thì cứ tiếp tục đến hết đoạn cuối rồi in ra kết luận.

Phần giao của 2 đoạn $[A, B]$ và $[C, D]$
Là đoạn $[X, Y]$ với $X = \max(A, C)$, $Y = \min(B, D)$. Nếu $X \le Y$ thì đoạn giao hợp lệ, còn $X > Y$ thì không giao.

Ví dụ giữa đoạn $[1, 10]$ và $[4, 15]$ thì đoạn giao là $[4, 10]$, các bạn thử kẻ ra trên trục số sẽ thấy rõ. Còn giữa $[1, 10]$ và $[15, 30]$ thì theo công thức sẽ là $[15, 10]$, mà $15 > 10$ nên không hợp lệ, tức là không giao.

Thử với ví dụ mẫu số 3 ($N = K = 3$):

123456789[1, 5][2, 8][3, 7]Giao: [3, 5]
  • Xét đoạn $[1, 5]$ và $[2, 8]$: đoạn giao là $[2, 5]$, hợp lệ vì $2 \le 5$ nên xét tiếp.
  • Xét đoạn giao $[2, 5]$ với $[3, 7]$: đoạn giao là $[3, 5]$, hợp lệ vì $3 \le 5$. Đoạn $[3, 5]$ có $5 - 3 + 1 = 3$ điểm, đáp án in ra 3.

Mình lấy code cuối cùng của phần 1 và phát triển tiếp để lấy thêm 30% này nha các bạn:

Xem code cách 5 (thêm trường hợp K = N)
#include <bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    long long n, k;
    cin >> n >> k;
    vector<pair<long long, long long>> a(n);
    long long Max = LLONG_MIN, Min = LLONG_MAX;
    for(long long i = 0; i < n; ++i){
        cin >> a[i].first >> a[i].second;
        Min = min(Min, a[i].first);
        Max = max(Max, a[i].second);
    }
    long long res = 0;
    if(k == n){
        pair<long long, long long> ans = a[0];
        for(int i = 1; i < n; ++i){
            ans.first = max(ans.first, a[i].first);
            ans.second = min(ans.second, a[i].second);
            if(ans.first > ans.second){
                break;
            }
        }
        if(ans.first <= ans.second){
            res = ans.second - ans.first + 1;
        }
    }
    else{
        for(long long x = Min; x <= Max; ++x){
            long long cnt = 0;
            for(long long i = 0; i < n; ++i){
                if(a[i].first <= x && x <= a[i].second){
                    cnt++;
                    if(cnt > k){
                        break;
                    }
                }
            }
            if(cnt == k){
                res++;
            }
        }
    }
    cout << res;
    return 0;
}
import sys
tokens = []
while len(tokens) < 2:
    tokens += list(map(int, sys.stdin.readline().split()))
n, k = tokens[:2]
a = [None] * n
Max = -9223372036854775808
Min = 9223372036854775807
read = []
while len(read) < 2 * n:
    read += list(map(int, sys.stdin.readline().split()))
for i in range(n):
    first = read[2 * i]
    second = read[2 * i + 1]
    a[i] = (first, second)
    Min = min(Min, first)
    Max = max(Max, second)
res = 0
if k == n:
    ans_first = a[0][0]
    ans_second = a[0][1]
    i = 1
    while i < n:
        ans_first = max(ans_first, a[i][0])
        ans_second = min(ans_second, a[i][1])
        if ans_first > ans_second:
            break
        i += 1
    if ans_first <= ans_second:
        res = ans_second - ans_first + 1
else:
    x = Min
    while x <= Max:
        cnt = 0
        i = 0
        while i < n:
            if a[i][0] <= x and x <= a[i][1]:
                cnt += 1
                if cnt > k:
                    break
            i += 1
        if cnt == k:
            res += 1
        x += 1
print(res)
C++▶ Chạy tay từng dòng codeCách 5: nếu k = n thì chỉ cần giao các đoạn, O(N) dù toạ độ tới 10^18.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main(){
4 ios::sync_with_stdio(0);
5 cin.tie(0);
6
7 long long n, k;
8 cin >> n >> k;
9 vector<pair<long long, long long>> a(n);
10 long long Max = LLONG_MIN, Min = LLONG_MAX;
11 for(long long i = 0; i < n; ++i){
12 cin >> a[i].first >> a[i].second;
13 Min = min(Min, a[i].first);
14 Max = max(Max, a[i].second);
15 }
16 long long res = 0;
17 if(k == n){
18 pair<long long, long long> ans = a[0];
19 for(int i = 1; i < n; ++i){
20 ans.first = max(ans.first, a[i].first);
21 ans.second = min(ans.second, a[i].second);
22 if(ans.first > ans.second){
23 break;
24 }
25 }
26 if(ans.first <= ans.second){
27 res = ans.second - ans.first + 1;
28 }
29 }
30 else{
31 for(long long x = Min; x <= Max; ++x){
32 long long cnt = 0;
33 for(long long i = 0; i < n; ++i){
34 if(a[i].first <= x && x <= a[i].second){
35 cnt++;
36 if(cnt > k){
37 break;
38 }
39 }
40 }
41 if(cnt == k){
42 res++;
43 }
44 }
45 }
46 cout << res;
47 return 0;
48}

Với $K = N$ thì độ phức tạp thời gian chỉ là $O(N)$, không phụ thuộc $A$, $B$ lớn cỡ nào, chỉ lặp đúng $N$ lần là ra kết quả. Với độ phức tạp $O(N)$ được xem là tốt nhất này thì chắc chắn thỏa giới hạn 1 giây, tự tin lấy trọn 30% điểm hen các bạn.

Gộp xử lý K = N vào lúc đọc input

Ở trên mình tách quá trình input với xử lý ra riêng để các bạn nhìn cho rõ. Bản chất ta có thể xử lý $K = N$ ngay khi đọc input luôn, vì input là đọc từng đoạn thẳng vào mà. Từ đầu ta khởi tạo đoạn giao ans với ans.first = 0 và ans.second = 10^18 + 1, để khi so sánh với đoạn đầu tiên a[0] thì nó được cập nhật chính là a[0]:

  • max(ans.first, a[0].first) luôn là a[0].first, do ans.first = 0 đảm bảo nhỏ hơn mọi a[i].first (đề cho các đoạn tối thiểu từ 1).
  • min(ans.second, a[0].second) luôn là a[0].second, do ans.second = 10^18 + 1 đảm bảo lớn hơn mọi a[i].second (đề cho tối đa $10^{18}$).
Xem code cách 6 (gộp K = N vào input)
#include <bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    long long n, k;
    cin >> n >> k;
    vector<pair<long long, long long>> a(n);
    long long Max = LLONG_MIN, Min = LLONG_MAX;
    pair<long long, long long> ans(0, 1000000000000000001LL);
    bool check = true;
    for(long long i = 0; i < n; ++i){
        cin >> a[i].first >> a[i].second;
        Min = min(Min, a[i].first);
        Max = max(Max, a[i].second);
        if(check == true){
            ans.first = max(ans.first, a[i].first);
            ans.second = min(ans.second, a[i].second);
            if(ans.first > ans.second){
                check = false;
            }
        }
    }
    long long res = 0;
    if(k == n){
        if(check == true){
            res = ans.second - ans.first + 1;
        }
    }
    else{
        for(long long x = Min; x <= Max; ++x){
            long long cnt = 0;
            for(long long i = 0; i < n; ++i){
                if(a[i].first <= x && x <= a[i].second){
                    cnt++;
                    if(cnt > k){
                        break;
                    }
                }
            }
            if(cnt == k){
                res++;
            }
        }
    }
    cout << res;
    return 0;
}
import sys
tokens = []
while len(tokens) < 2:
    tokens += list(map(int, sys.stdin.readline().split()))
n, k = tokens[:2]
a = [None] * n
Max = -9223372036854775808
Min = 9223372036854775807
ans_first = 0
ans_second = 1000000000000000001
check = True
read = []
while len(read) < 2 * n:
    read += list(map(int, sys.stdin.readline().split()))
for i in range(n):
    first = read[2 * i]
    second = read[2 * i + 1]
    a[i] = (first, second)
    Min = min(Min, first)
    Max = max(Max, second)
    if check:
        ans_first = max(ans_first, first)
        ans_second = min(ans_second, second)
        if ans_first > ans_second:
            check = False
res = 0
if k == n:
    if check:
        res = ans_second - ans_first + 1
else:
    x = Min
    while x <= Max:
        cnt = 0
        i = 0
        while i < n:
            if a[i][0] <= x and x <= a[i][1]:
                cnt += 1
                if cnt > k:
                    break
            i += 1
        if cnt == k:
            res += 1
        x += 1
print(res)
C++▶ Chạy tay từng dòng codeCách 6: tính luôn đoạn giao ans ngay trong lúc đọc input.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main(){
4 ios::sync_with_stdio(0);
5 cin.tie(0);
6
7 long long n, k;
8 cin >> n >> k;
9 vector<pair<long long, long long>> a(n);
10 long long Max = LLONG_MIN, Min = LLONG_MAX;
11 pair<long long, long long> ans(0, 1000000000000000001LL);
12 bool check = true;
13 for(long long i = 0; i < n; ++i){
14 cin >> a[i].first >> a[i].second;
15 Min = min(Min, a[i].first);
16 Max = max(Max, a[i].second);
17 if(check == true){
18 ans.first = max(ans.first, a[i].first);
19 ans.second = min(ans.second, a[i].second);
20 if(ans.first > ans.second){
21 check = false;
22 }
23 }
24 }
25 long long res = 0;
26 if(k == n){
27 if(check == true){
28 res = ans.second - ans.first + 1;
29 }
30 }
31 else{
32 for(long long x = Min; x <= Max; ++x){
33 long long cnt = 0;
34 for(long long i = 0; i < n; ++i){
35 if(a[i].first <= x && x <= a[i].second){
36 cnt++;
37 if(cnt > k){
38 break;
39 }
40 }
41 }
42 if(cnt == k){
43 res++;
44 }
45 }
46 }
47 cout << res;
48 return 0;
49}
Vậy là đã chính thức lấy được 80% số điểm. Còn 20% cuối là trường hợp tự do: $A$, $B$ tới $10^{18}$, $K$ bằng hay khác $N$ là ngẫu nhiên. Nếu may mắn có test $K = N$ thì ta lấy được điểm, còn $K \ne N$ thì code trên lặp từ $Min$ đến $Max$, gặp đoạn $[1, 10^{18}]$ là lặp $10^{18}$ lần, quá giới hạn thời gian. Các bạn suy nghĩ xem có cách nào lấy trọn điểm không?

+20%Lời giải 100%: chia đoạn + update đoạn

Để giải quyết trọn vẹn bài này mình dùng đến 2 kỹ thuật mà không có trong sách giáo khoa chuyên Tin hay những tài liệu phổ biến đâu, mình cũng tự nghiệm ra nó thông qua việc giải bài thôi 😀

Kỹ thuật 1: chia đoạn

Khi có $N$ đoạn $[l_1, r_1],$ $[l_2, r_2],$ $\ldots,$ $[l_N, r_N]$, ta cần chia chúng thành những đoạn con nhỏ rời rạc sao cho mỗi đoạn con hoặc là đoạn con của 1 hoặc nhiều đoạn, hoặc không là đoạn con của đoạn nào. Ví dụ 5 đoạn $[1, 9],$ $[3, 5],$ $[4, 13],$ $[7, 9],$ $[2, 15]$:

0123456789101112131415[1, 9][3, 5][4, 13][7, 9][2, 15]

chia được thành các đoạn con:

[1, 1][2, 2][3, 3][4, 5][6, 6][7, 9][10, 13][14, 15]

Đây là những đoạn con hoàn toàn rời rạc. Ví dụ $[4, 5]$ là đoạn con của cả 5 đoạn trên, $[10, 13]$ là đoạn con của $[4, 13]$ và $[2, 15]$. Mục đích của việc chia này là bảo đảm các đoạn con rời nhau, và chỉ cần biết 1 phần tử của đoạn con có $K$ đoạn phủ lên thì cả đoạn con đó cũng có $K$ đoạn phủ lên.

Cách chia đoạn:

  1. Tạo vector v, với mỗi đoạn $[l, r]$ đưa $l - 1$ và $r$ vào. Với ví dụ trên: $v = \{0, 9, 2, 5, 3, 13, 6, 9, 1, 15\}$.
  2. Sắp xếp tăng dần: $\{0, 1, 2, 3, 5, 6, 9, 9, 13, 15\}$.
  3. Với 2 phần tử kề nhau $v[i]$ và $v[i + 1]$ thì $[v[i] + 1, v[i + 1]]$ là một đoạn con: $\{0, 1\} \to [1, 1]$, $\{1, 2\} \to [2, 2]$, $\{2, 3\} \to [3, 3]$, $\{3, 5\} \to [4, 5]$, $\{5, 6\} \to [6, 6]$, $\{6, 9\} \to [7, 9]$, $\{9, 13\} \to [10, 13]$, $\{13, 15\} \to [14, 15]$. Cặp $\{9, 9\}$ cho đoạn $[10, 9]$ rỗng nên bỏ qua.

Kỹ thuật 2: update đoạn (mảng sai phân)

Bài toán: cho $N$ đoạn thẳng, đoạn thứ $i$ phủ các điểm $[l_i .. r_i]$. Hỏi với mỗi điểm, có bao nhiêu đoạn thẳng phủ lên đó? Ví dụ $N = 5$: $[1 .. 5],$ $[4 .. 10],$ $[3 .. 8],$ $[10 .. 10],$ $[10 .. 12]$.

Gọi f[i] là số đoạn phủ lên điểm $i$, ban đầu toàn 0. Với đoạn $[l .. r]$ ta tăng f[l] lên 1 và giảm f[r + 1] xuống 1:

  • $[1 .. 5]$: tăng f[1], giảm f[6]
  • $[4 .. 10]$: tăng f[4], giảm f[11]
  • $[3 .. 8]$: tăng f[3], giảm f[9]
  • $[10 .. 10]$: tăng f[10], giảm f[11]
  • $[10 .. 12]$: tăng f[10], giảm f[13]

Sau đó duyệt lại các vị trí, tính f[i] = f[i - 1] + f[i] (cộng dồn) là ra số đoạn phủ lên từng điểm:

Điểm i123456789101112
f[i] sau khi cập nhật10110-100-12-20
f[i] sau cộng dồn112332221311

Tức điểm 1 có 1 đoạn, điểm 3 có 2 đoạn, điểm 4 và 5 có 3 đoạn, điểm 10 có 3 đoạn, … đúng với các đoạn đề cho.

Ứng dụng 2 kỹ thuật vào bài

  1. Tạo bảng f với ý nghĩa tương tự: f[i] là số đoạn phủ lên điểm $i$. Do $i$ rất lớn ($1 \le l \le r \le 10^{18}$) nên f phải tổ chức bằng map.
  2. Với mỗi đoạn $[l .. r]$: f[l]++ và f[r + 1]--. Sau đó cập nhật lại bảng bằng cách cộng dồn f[sau] = f[trước] + f[sau].
  3. Chia $N$ đoạn thành những đoạn rời rạc: tạo vector save lưu $l - 1$ và $r$, rồi sort tăng dần.
  4. Xét từng đoạn con $[L .. R]$ với $L$ = save[i] + 1, $R$ = save[i + 1]. Nếu f[L] == K hoặc f[R] == K thì đã có 1 điểm trong đoạn có $K$ đoạn phủ, nghĩa là toàn bộ đoạn con đều có $K$ đoạn phủ, ta cộng ans += R - L + 1.
  5. Đáp án là ans.

Độ phức tạp thời gian là $O(N \log N)$, yên tâm không bị TLE và ăn trọn 100% điểm bài này.

Em nhập các đoạn (mỗi đoạn 2 số l r, các đoạn cách nhau dấu phẩy) và $K$ rồi bấm Tiếp để xem từng bước: cập nhật mảng sai phân f tại các mốc, rồi cộng dồn qua từng khúc và cộng độ dài những khúc có đúng $K$ đoạn phủ. Ô xanh ở hàng "phủ" là khúc được tính:

Ví dụ nhanh:

Source code để các bạn tham khảo với tất cả những gì mình phân tích ở trên:

#include <bits/stdc++.h>
using namespace std;
struct segment {
    long long l, r;
};
vector <segment> listSegment;
int n, k;
map <long long, int> f;
void process() {
    vector <long long> save;
    vector <long long> listID;
    for (int i = 0; i < n; i++) {
        long long l = listSegment[i].l;
        long long r = listSegment[i].r;
        save.push_back(l - 1);
        save.push_back(r);
        f[l]++;
        f[r + 1]--;
        listID.push_back(l);
        listID.push_back(r + 1);
    }
    sort(save.begin(), save.end());
    sort(listID.begin(), listID.end());
    for (int i = 1; i < (int) listID.size(); i++) {
        if (listID[i] != listID[i - 1]) {
            f[listID[i]] += f[listID[i - 1]];
        }
    }
    long long ans = 0LL;
    for (int i = 0; i < (int) save.size() - 1; i++) {
        long long l = save[i] + 1;
        long long r = save[i + 1];
        if (l > r) {
            continue;
        }
        if (f[l] == k || f[r] == k) {
            ans += r - l + 1;
        }
    }
    cout << ans;
}
int main () {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        segment myseg;
        cin >> myseg.l >> myseg.r;
        listSegment.push_back(myseg);
    }
    process();
    return 0;
}
import sys
class segment:
    def __init__(self):
        self.l = 0
        self.r = 0
listSegment = []
f = {}
tokens = []
while len(tokens) < 2:
    tokens += list(map(int, sys.stdin.readline().split()))
n, k = tokens[:2]
read = []
while len(read) < 2 * n:
    read += list(map(int, sys.stdin.readline().split()))
for i in range(n):
    myseg = segment()
    myseg.l = read[2 * i]
    myseg.r = read[2 * i + 1]
    listSegment.append(myseg)
def process():
    for seg in listSegment:
        l = seg.l
        r = seg.r
        f[l] = f.get(l, 0) + 1
        f[r + 1] = f.get(r + 1, 0) - 1
    points = sorted(f.keys())
    cur = 0
    ans = 0
    for i in range(len(points) - 1):
        cur += f[points[i]]
        if cur == k:
            ans += points[i + 1] - points[i]
    print(ans)
process()
C++▶ Chạy tay từng dòng codeLời giải 100%: mảng sai phân trên map + chia thành các đoạn con rời nhau, O(N log N).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3struct segment {
4 long long l, r;
5};
6vector <segment> listSegment;
7int n, k;
8map <long long, int> f;
9void process() {
10 vector <long long> save;
11 vector <long long> listID;
12 for (int i = 0; i < n; i++) {
13 long long l = listSegment[i].l;
14 long long r = listSegment[i].r;
15 save.push_back(l - 1);
16 save.push_back(r);
17 f[l]++;
18 f[r + 1]--;
19 listID.push_back(l);
20 listID.push_back(r + 1);
21 }
22 sort(save.begin(), save.end());
23 sort(listID.begin(), listID.end());
24 for (int i = 1; i < (int) listID.size(); i++) {
25 if (listID[i] != listID[i - 1]) {
26 f[listID[i]] += f[listID[i - 1]];
27 }
28 }
29 long long ans = 0LL;
30 for (int i = 0; i < (int) save.size() - 1; i++) {
31 long long l = save[i] + 1;
32 long long r = save[i + 1];
33 if (l > r) {
34 continue;
35 }
36 if (f[l] == k || f[r] == k) {
37 ans += r - l + 1;
38 }
39 }
40 cout << ans;
41}
42int main () {
43 ios::sync_with_stdio(0);
44 cin.tie(0);
45 cin >> n >> k;
46 for (int i = 1; i <= n; i++) {
47 segment myseg;
48 cin >> myseg.l >> myseg.r;
49 listSegment.push_back(myseg);
50 }
51 process();
52 return 0;
53}
Code Python ở trên không hoàn toàn giống C++, vì Python chạy chậm hơn C++ mà bài này không cho giới hạn thời gian riêng cho Python, để chung là 1 giây. Nếu chuyển đổi y nguyên từ C++ sang Python thì sẽ bị TLE, nên mình phải tối ưu thêm một số chỗ, các bạn sẽ thấy code Python có phần khác code C++ nhé, và nó sẽ AC.
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 😇