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 →

Tìm kiếm nhị phân 3 (SEARCH3)

Tìm kiếm nhị phân 3 (SEARCH3)
Nguồn: LuyenCode Độ khó: Trung bình Tìm kiếm nhị phân Sắp xếp Hai con trỏ Map

Đề bài

SEARCH3 – Tìm kiếm nhị phân 3

Cho hai dãy số nguyên $a_1, a_2, \ldots, a_n$ và $b_1, b_2, \ldots, b_m$. Với mỗi chỉ số $i$ $(1 \le i \le m)$ hãy tìm sự xuất hiện của $b_i$ trong dãy $a_1, a_2, \ldots, a_n$.

Dữ liệu vào:

  • Dòng đầu ghi hai số nguyên dương $n$ và $m$;
  • Dòng thứ hai ghi $n$ số nguyên $a_1, a_2, \ldots, a_n$;
  • Dòng thứ ba ghi $m$ số nguyên $b_1, b_2, \ldots, b_m$.

Hai số liên tiếp trên một dòng được ghi cách nhau một dấu cách.

Giới hạn: $1 \le n, m \le 10^5$; $|a_i|, |b_i| \le 10^9$.

Dữ liệu ra: Một dòng duy nhất chứa $m$ số nguyên, trong đó số thứ $i$ $(1 \le i \le m)$ là chỉ số $j$ nhỏ nhất mà $a_j = b_i$ (nếu tồn tại) và là $0$ nếu ngược lại. Hai số liên tiếp được ghi cách nhau một dấu cách.

Ví dụ – input
7 5
6 4 7 2 4 1 3
3 1 5 4 8
Ví dụ – output
7 6 0 2 0

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

Trước khi vào bài, các bạn nhớ giúp mình chuyện giới hạn thời gian: với các hệ thống chấm bài giới hạn 1 giây (như bài này), toàn bài chỉ nên lặp cỡ $(3 \div 5) \cdot 10^7$ lần là an toàn, tệ nhất $10^8$ lần (lúc này hên xui). Ngưỡng này là cho C/C++; Python, Java, C# chạy chậm hơn nên ngưỡng phải thấp hơn. Đề cho 2 giây thì nhân ngưỡng lên 2, 100 ms thì chia 10, cứ thế mà tính.

Giải thích ví dụ cho bạn nào chưa hiểu đề. Dãy $a$ (đánh số từ $1$):

j1234567
aj6472413
  • $b_1 = 3$: số $3$ nằm ở vị trí $7$ ⇒ in 7.
  • $b_2 = 1$: số $1$ nằm ở vị trí $6$ ⇒ in 6.
  • $b_3 = 5$: không có trong $a$ ⇒ in 0.
  • $b_4 = 4$: số $4$ xuất hiện ở cả vị trí 2 và 5, đề bắt lấy vị trí nhỏ nhất ⇒ in 2. Đây là chỗ "khó chịu" nhất của bài, lát nữa các bạn sẽ thấy.
  • $b_5 = 8$: không có ⇒ in 0.
Link nộp bài

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 OJ

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

Cách 1Tìm kiếm tuần tự – O(N·M)

Cách đơn giản nhất dễ dàng nhìn ra là theo tư tưởng vét cạn: với từng giá trị $b_i$, so sánh lần lượt với các giá trị $a_j$ từ trái qua. Ngay tại vị trí $a_j$ nào bằng $b_i$ thì kết luận in ra vị trí $j$ đó luôn (đi từ trái qua nên gặp đầu tiên chắc chắn là vị trí nhỏ nhất) rồi xét qua $b_i$ tiếp theo. Còn xét tới tận cùng mà vẫn không thấy $a_j$ nào bằng $b_i$ thì in ra $0$. Cứ thế xét hết tất cả các giá trị $b_i$.

Không cần tạo mảng b
Lần lượt từng phần tử của $b$ đi tìm trong $a$, in kết quả xong là hết nhiệm vụ rồi qua phần tử tiếp theo. Vì thế không cần lưu cả mảng $b$ cho phí bộ nhớ: tạo biến x, đọc vào, tìm, in kết quả, rồi lại đọc giá trị mới vào, cứ thế.

Source code để các bạn tham khảo nếu làm theo hướng này:

#include <bits/stdc++.h>
using namespace std;

int main(){
    int n, m;
    cin >> n >> m;
    int a[n];
    for(int i = 0; i < n; ++i){
        cin >> a[i];
    }
    while(m--){
        int x;
        cin >> x;
        int idx = 0;
        for(int i = 0; i < n; ++i){
            if(a[i] == x){
                idx = i + 1;
                break;
            }
        }
        cout << idx << " ";
    }
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
a = list(map(int, data[2:2 + n]))
b = list(map(int, data[2 + n:2 + n + m]))
res = []
for x in b:
    idx = 0
    for i in range(n):
        if a[i] == x:
            idx = i + 1
            break
    res.append(idx)
print(*res)

Bấm Tiếp để chạy tay từng dòng code (mảng a ở đây đánh số từ $0$ nên vị trí in ra là i + 1):

C++▶ Chạy tay từng dòng codeCách 1: với mỗi x, duyệt a từ trái qua, gặp a[i] == x đầu tiên thì idx = i + 1 rồi break.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n, m;
6 cin >> n >> m;
7 int a[n];
8 for(int i = 0; i < n; ++i){
9 cin >> a[i];
10 }
11 while(m--){
12 int x;
13 cin >> x;
14 int idx = 0;
15 for(int i = 0; i < n; ++i){
16 if(a[i] == x){
17 idx = i + 1;
18 break;
19 }
20 }
21 cout << idx << " ";
22 }
23 return 0;
24}

Đánh giá độ phức tạp của cách làm này:

  • Không gian: $O(N)$ với $N$ là số phần tử của mảng $a$.
  • Thời gian: $O(N)$ đọc mảng $a$ + $O(M \cdot N)$ cho bước đọc từng số của $b$ rồi đi tìm. Theo quy tắc cộng Big-O lấy cái lớn nhất làm đại diện ⇒ $O(M \cdot N)$, với $M$ là số phần tử của $b$, $N$ là số phần tử của $a$.
Không gian$O(N)$
Thời gian$O(N \cdot M)$
Chắc chắn TLE
Trường hợp xấu nhất là $N = M = 10^5$ và toàn bộ các số của $b$ đều không có trong $a$ – lần nào cũng phải xét hết mảng $a$. Số lần lặp cả thảy là $10^5 + 10^5 \cdot 10^5 = 10^{10} + 10^5$, tức hơn 10 tỷ lần lặp, vượt quá xa ngưỡng 30–50 triệu, cao lắm là 100 triệu lần lặp ⇒ chắc chắn TLE. Tuy nhiên vẫn nên cài đặt nó để sau này lấy làm công cụ kiểm tra tính chính xác cho cách làm tối ưu.

Cách 2Sắp xếp + lọc trùng + tìm kiếm nhị phân – O(max(N, M)·logN)

Giờ ta nói về cách làm tốt hơn. Thực ra ngay từ chính tên đề bài đã nói rõ cách làm rồi còn gì 😆: ta phải tìm kiếm nhị phân để đẩy nhanh quá trình tìm kiếm, chứ tìm kiếm tuần tự như trên là TLE. Bạn nào chưa biết tìm kiếm nhị phân là gì thì trước tiên phải tìm đọc tài liệu cho hiểu đã nhé, còn bạn nào biết rồi thì xem tiếp. Có hai trở ngại cần xử lý trước khi tìm kiếm nhị phân được.

Trở ngại 1: sắp xếp làm mất vị trí ban đầu

Muốn tìm kiếm nhị phân thì danh sách bắt buộc phải có trật tự (tăng dần hoặc giảm dần), nghĩa là ta phải sắp xếp. Mà sắp xếp xong thì vị trí các phần tử đã bị đổi khác so với ban đầu, trong khi đề lại muốn in ra vị trí theo dãy trước khi sắp xếp.

Lưu kèm vị trí ban đầu
Mỗi phần tử $a_j$ ta lưu thành một cặp (value, index) = $(a_j, j)$. Sắp xếp theo value, tìm kiếm nhị phân gặp đúng phần tử thì in ra index mà nó mang theo – tức vị trí $j$ ban đầu – chứ không phải vị trí hiện tại sau khi sắp xếp. Trong C++ mình gom 2 thông tin này vào một struct Data.

Trở ngại 2: phần tử trùng nhau

Mảng $a$ có thể có các phần tử trùng nhau. Khi tìm kiếm nhị phân, giả sử tìm ra một $a_j$ bằng $b_i$ thì cũng không đảm bảo $a_j$ đó đang mang theo index nhỏ nhất (vị trí xuất hiện đầu tiên). Thêm nữa, hàm sort của STL không giữ nguyên thứ tự ban đầu của các phần tử bằng nhau, nên ngay cả phần tử đứng đầu nhóm trùng cũng chưa chắc có index nhỏ nhất.

Vậy cần một bước tiền xử lý sau khi sắp xếp: loại bớt các phần tử trùng, chỉ để lại duy nhất 1 phần tử cho mỗi giá trị, và index nó giữ phải là nhỏ nhất trong nhóm. Việc này khá đơn giản vì sau khi sắp xếp thì các phần tử trùng nhau đã nằm sát cạnh nhau, chỉ cần duyệt 1 vòng là xét hết.

Đừng đi cài đặt xoá!
Kinh nghiệm: thấy đề hướng tới việc thêm hay xoá phần tử trên mảng thì hãy tìm cách giải quyết mà không cần đụng tới thêm/xoá. Vì xoá một phần tử trên mảng tốn $O(N)$ trong trường hợp xấu nhất – mọi phần tử phía sau phải đồng loạt nhích lên. Xoá các phần tử trùng trong mảng $a$ kiểu đó sẽ thành $O(N^2)$. Bản chất ở đây ta chỉ cần lọc ra các phần tử phân biệt chứ không nhất thiết phải xoá đi phần tử trùng.

Các bạn tưởng tượng ta tạo thêm một mảng c để chứa các phần tử phân biệt lấy ra từ a: xét $a_j$ hiện tại, trùng với phần tử cuối cùng của c thì coi như bị trùng, không lấy; khác thì đưa $a_j$ vào cuối c. Chỉ cần 1 vòng lặp – $O(N)$. Ta chấp nhận tốn thêm bộ nhớ cho c nhưng về mặt thời gian thì tối ưu hơn rất nhiều so với xoá.

Không cần cả mảng c: hai con trỏ (two pointers)
Xem chính phần đầu mảng a là mảng c. Biến cnt là số phần tử phân biệt đã có (chúng nằm ở a[0..cnt-1], ban đầu cnt = 1), biến i duyệt từ 1:
  • a[i].value khác a[cnt-1].value (phần tử cuối của "mảng c") ⇒ giá trị mới: cnt++ rồi chép a[i] vào a[cnt-1].
  • Trùng nhau ⇒ giữ nguyên cnt, nhưng nếu a[i].index nhỏ hơn thì cập nhật a[cnt-1].index để giá trị đó luôn mang theo vị trí ban đầu nhỏ nhất.
Xong vòng lặp nhớ gán n = cnt – số phần tử phân biệt. Các bạn thử suy ngẫm xem nhé.

Ở bước sắp xếp nhớ dùng thuật toán tối ưu $O(N \log N)$ chứ cài mấy thuật $O(N^2)$ (nổi bọt, chọn...) thì vẫn TLE. Đơn giản nhất là gọi hàm sort có sẵn trong STL, kèm hàm so sánh cmp để nó biết so 2 phần tử theo value.

Xử lý xong 2 trở ngại thì phần còn lại quá đơn giản: với mỗi $b_i$, tìm kiếm nhị phân trên mảng $a$ (đã sắp xếp, đã lọc trùng); tìm thấy thì in index mà nó lưu trữ, không thấy thì in $0$.

Em nhập dãy $a$, dãy $b$ nhỏ rồi bấm Tiếp để xem đủ 3 giai đoạn: sắp xếp, lọc trùng bằng 2 con trỏ, rồi tìm kiếm nhị phân cho từng $b_i$:

Ví dụ nhanh:

Source code để các bạn tham khảo nếu làm theo hướng này:

#include <bits/stdc++.h>
using namespace std;

struct Data{
    int value;
    int index;
};

// Hàm so sánh cho sort: so theo value, sắp xếp tăng dần
bool cmp(const Data &a, const Data &b){
    return a.value < b.value;
}

int main(){
    int n, m;
    cin >> n >> m;
    Data a[n];
    for(int i = 0; i < n; ++i){
        cin >> a[i].value;
        a[i].index = i + 1;
    }
    // Sắp xếp tăng dần theo value, O(N * logN)
    sort(a, a + n, cmp);
    // Lọc phần tử phân biệt ngay trên mảng a (2 con trỏ)
    int cnt = 1;
    for(int i = 1; i < n; ++i){
        if(a[i].value != a[cnt - 1].value){
            cnt++;
            a[cnt - 1] = a[i];
        }
        else{
            if(a[i].index < a[cnt - 1].index){
                a[cnt - 1].index = a[i].index;
            }
        }
    }
    n = cnt; // cập nhật lại số phần tử phân biệt
    // Tìm kiếm nhị phân
    while(m--){
        int x;
        cin >> x;
        int idx = 0;
        int left = 0, right = n - 1;
        while(left <= right){
            int mid = (left + right) / 2;
            if(a[mid].value < x){
                left = mid + 1;
            }
            else if(a[mid].value > x){
                right = mid - 1;
            }
            else{
                idx = a[mid].index;
                break;
            }
        }
        cout << idx << " ";
    }
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
b = data[2 + n:2 + n + m]
# mỗi phần tử là [value, index]
a = [[int(data[2 + i]), i + 1] for i in range(n)]
a.sort(key=lambda p: p[0])  # sắp xếp tăng dần theo value
# lọc phần tử phân biệt ngay trên mảng a (2 con trỏ)
cnt = 1
for i in range(1, n):
    if a[i][0] != a[cnt - 1][0]:
        cnt += 1
        a[cnt - 1] = a[i]
    elif a[i][1] < a[cnt - 1][1]:
        a[cnt - 1][1] = a[i][1]
n = cnt  # cập nhật lại số phần tử phân biệt
res = []
for t in b:
    x = int(t)
    idx = 0
    left, right = 0, n - 1
    while left <= right:
        mid = (left + right) // 2
        if a[mid][0] < x:
            left = mid + 1
        elif a[mid][0] > x:
            right = mid - 1
        else:
            idx = a[mid][1]
            break
    res.append(idx)
print(*res)

Bấm Tiếp để chạy tay từng dòng code (bảng "Dữ liệu" có 2 hàng value / index của mảng a; ô mờ là phần đã bị bỏ sau khi lọc trùng):

C++▶ Chạy tay từng dòng codeCách 2: sort theo value, lọc trùng bằng 2 con trỏ (giữ index nhỏ nhất), rồi tìm kiếm nhị phân cho từng x.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4struct Data{
5 int value;
6 int index;
7};
8
9// Hàm so sánh cho sort: so theo value, sắp xếp tăng dần
10bool cmp(const Data &a, const Data &b){
11 return a.value < b.value;
12}
13
14int main(){
15 int n, m;
16 cin >> n >> m;
17 Data a[n];
18 for(int i = 0; i < n; ++i){
19 cin >> a[i].value;
20 a[i].index = i + 1;
21 }
22 // Sắp xếp tăng dần theo value, O(N * logN)
23 sort(a, a + n, cmp);
24 // Lọc phần tử phân biệt ngay trên mảng a (2 con trỏ)
25 int cnt = 1;
26 for(int i = 1; i < n; ++i){
27 if(a[i].value != a[cnt - 1].value){
28 cnt++;
29 a[cnt - 1] = a[i];
30 }
31 else{
32 if(a[i].index < a[cnt - 1].index){
33 a[cnt - 1].index = a[i].index;
34 }
35 }
36 }
37 n = cnt; // cập nhật lại số phần tử phân biệt
38 // Tìm kiếm nhị phân
39 while(m--){
40 int x;
41 cin >> x;
42 int idx = 0;
43 int left = 0, right = n - 1;
44 while(left <= right){
45 int mid = (left + right) / 2;
46 if(a[mid].value < x){
47 left = mid + 1;
48 }
49 else if(a[mid].value > x){
50 right = mid - 1;
51 }
52 else{
53 idx = a[mid].index;
54 break;
55 }
56 }
57 cout << idx << " ";
58 }
59 return 0;
60}

Viết gọn hơn với upper_bound

Ở trên ta tự cài đặt tìm kiếm nhị phân. Trong STL cũng có hàm binary_search, nhưng nó chỉ cho biết có tìm thấy hay không, còn ở đây ta cần biết tìm thấy ở ô nào để lấy ra index mà phần tử đó lưu trữ, nên không áp dụng hàm đó được. Muốn lấy được vị trí thì có 2 hàm lower_bound, upper_bound làm được điều này (các bạn Google tìm hiểu thêm về 2 hàm này nhé). Ví dụ mình làm mẫu với upper_bound: nó trả về ô đầu tiên có value lớn hơn x, nên nếu x có trong mảng thì nó nằm ngay ô liền trước, upper - 1.

#include <bits/stdc++.h>
using namespace std;

struct Data{
    int value;
    int index;
};

// Hàm so sánh cho sort: so theo value, sắp xếp tăng dần
bool cmp(const Data &a, const Data &b){
    return a.value < b.value;
}

int main(){
    int n, m;
    cin >> n >> m;
    Data a[n];
    for(int i = 0; i < n; ++i){
        cin >> a[i].value;
        a[i].index = i + 1;
    }
    sort(a, a + n, cmp);
    int cnt = 1;
    for(int i = 1; i < n; ++i){
        if(a[i].value != a[cnt - 1].value){
            cnt++;
            a[cnt - 1] = a[i];
        }
        else{
            if(a[i].index < a[cnt - 1].index){
                a[cnt - 1].index = a[i].index;
            }
        }
    }
    n = cnt;
    // Tìm kiếm nhị phân bằng hàm upper_bound
    while(m--){
        Data x;
        cin >> x.value;
        int upper = upper_bound(a, a + n, x, cmp) - a;
        if(upper == 0 || a[upper - 1].value != x.value){
            cout << "0 ";
        }
        else{
            cout << a[upper - 1].index << " ";
        }
    }
    return 0;
}
import sys
from bisect import bisect_right

data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
b = data[2 + n:2 + n + m]
a = [[int(data[2 + i]), i + 1] for i in range(n)]
a.sort(key=lambda p: p[0])
cnt = 1
for i in range(1, n):
    if a[i][0] != a[cnt - 1][0]:
        cnt += 1
        a[cnt - 1] = a[i]
    elif a[i][1] < a[cnt - 1][1]:
        a[cnt - 1][1] = a[i][1]
n = cnt
vals = [a[i][0] for i in range(n)]  # dãy value phân biệt, tăng dần
res = []
for t in b:
    x = int(t)
    upper = bisect_right(vals, x)  # tương đương upper_bound
    if upper == 0 or vals[upper - 1] != x:
        res.append(0)
    else:
        res.append(a[upper - 1][1])
print(*res)
Cẩn thận khi upper = 0
Nếu $x$ nhỏ hơn mọi phần tử của mảng thì upper_bound trả về ô $0$, lúc đó a[upper - 1] chính là a[-1] – truy cập ra ngoài mảng. Trong code gốc mình viết trên điện thoại thiếu kiểm tra này, nên ở đây mình bổ sung thêm điều kiện upper == 0 || phía trước cho chắc ăn nha.

Về mặt độ phức tạp thì tự cài tìm kiếm nhị phân hay dùng upper_bound đều như nhau. Tuy nhiên rõ ràng bạn nào biết dùng upper_bound thì code ngắn hơn, hoàn thành bài nhanh hơn – trong cuộc thi có tính thời gian thì đây là lợi thế.

Đánh giá độ phức tạp của cách làm này:

  • Không gian: $O(N)$ với $N$ là số phần tử của mảng $a$.
  • Thời gian: $O(N)$ đọc mảng $a$ + $O(N \log N)$ sắp xếp + $O(N)$ lọc phần tử phân biệt + $O(M \log N)$ tìm kiếm nhị phân. Theo quy tắc cộng Big-O lấy cái lớn nhất ⇒ $O(\max(N, M) \cdot \log N)$.
Không gian$O(N)$
Thời gian$O(\max(N, M) \cdot \log N)$
Cái hay của giải thuật
Với $N = M = 10^5$: $\log_2(10^5) \approx 17$, nên số lần lặp cỡ $10^5 + 17 \cdot 10^5 + 10^5 + 17 \cdot 10^5 = 36 \cdot 10^5$, tức khoảng 3 triệu 600 ngàn lần lặp – yên tâm không TLE. Từ hơn 10 tỷ lần lặp của cách 1 rút xuống chỉ còn khoảng 3,6 triệu lần, các bạn thấy cái hay của giải thuật rồi chứ? 😎

Cách 3Dùng map – O((N + M)·logN)

Vẫn còn một cách nữa, độ phức tạp cỡ tương tự cách 2 nhưng code rất gọn: dùng cấu trúc map trong STL. Bạn nào chưa biết map thì Google tìm hiểu thêm nhé, ở đây mình mặc định các bạn đã biết và chỉ nói cách xử lý trên nó thôi.

map bản chất lưu các cặp <key, value>, nên tự nó đã là giải pháp cho trở ngại 1: key chính là giá trị $a_j$, còn value chính là vị trí $j$ ban đầu. Nó cài đặt bằng cây đỏ đen nên không lưu trùng key; truy cập a[x] với key chưa có thì value mặc định là $0$. Vậy khi đọc $a_j$:

  • a[x] == 0 ⇒ key $x$ chưa từng xuất hiện ⇒ gán a[x] = j.
  • a[x] != 0 ⇒ $x$ đã xuất hiện trước đó ở vị trí nhỏ hơn ⇒ không đụng tới.

Sau cùng map lưu các giá trị phân biệt (key) và value của mỗi key chính là vị trí xuất hiện đầu tiên – trở ngại 2 cũng xong luôn. Khi tìm kiếm, dữ liệu trong map đã được sắp xếp và cân bằng cây nên mỗi lần tìm chỉ tốn $O(\log N)$. Với mỗi $b_i$ chỉ việc in a[b_i]: có thì ra vị trí, không có thì ra $0$ đúng như đề muốn. Nói chung dùng map thì bản thân cấu trúc của nó đã làm luôn bước sắp xếp và lọc trùng cho ta, không phải mất công tự cài đặt.

Source code để các bạn tham khảo nếu làm theo hướng này:

#include <bits/stdc++.h>
using namespace std;

int main(){
    int n, m;
    cin >> n >> m;
    map<int, int> a;
    int x;
    for(int i = 1; i <= n; ++i){
        cin >> x;
        if(a[x] == 0){
            a[x] = i;
        }
    }
    while(m--){
        cin >> x;
        cout << a[x] << " ";
    }
    return 0;
}
import sys

data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
a = {}
for i in range(1, n + 1):
    x = int(data[1 + i])
    if a.get(x, 0) == 0:
        a[x] = i
res = []
for t in data[2 + n:2 + n + m]:
    res.append(a.get(int(t), 0))
print(*res)
Bản Python
Trong Python mình dùng dict (bảng băm) với a.get(x, 0) để thay cho map: không có key thì trả về $0$ mà không tạo thêm phần tử mới.

Đánh giá độ phức tạp của cách làm này:

  • Không gian: $O(N)$ cho các key phân biệt (cộng thêm tối đa $M$ key rỗng mà a[x] tự tạo ra khi tìm số không có).
  • Thời gian: $N$ lần chèn và $M$ lần tìm, mỗi lần $O(\log N)$ ⇒ $O((N + M) \cdot \log N)$.
Không gian$O(N + M)$
Thời gian$O((N + M) \cdot \log N)$

Thực ra cách dùng map này có thể chạy chậm hơn cách 2 một chút vì tốn thêm chi phí cân bằng cây, nhưng nói chung vẫn không bị TLE khi nộp bài. Nên nếu đang trong cuộc thi có tính thời gian thì là mình, mình sẽ dùng cách map này để code ra nhanh gọn xong bài ahihi 😁.

Ghi chú của mình: code C++ và Python của cả 3 cách (kể cả bản upper_bound) đều cho đúng output ví dụ của đề, và đã được so với một chương trình vét cạn độc lập trên 500 bộ test ngẫu nhiên (có số âm, nhiều số trùng, số cần tìm nhỏ hơn mọi phần tử) – khớp hết. Với $N = M = 10^5$, trên máy mình bản C++ cách 2 chạy khoảng 0,12 – 0,26 giây, cách 3 khoảng 0,16 – 0,65 giây (giá trị ngẫu nhiên tới $10^9$ thì map chậm hơn rõ), còn cách 1 với test xấu nhất (mọi số của $b$ đều không có trong $a$) mất khoảng 6,7 giây ⇒ TLE đúng như phân tích. Bản Python cách 2 chạy khoảng 1,2 – 1,5 giây, bản bisect và bản dict nhanh hơn (dưới 1 giây).
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 😇
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