Hướng dẫn giải của Tìm kiếm nhị phân 2
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Editorial for search2: Tìm kiếm nhị phân 2
Hiểu bài toán
Cho hai dãy số A (đã sắp xếp không giảm) và B. Với mỗi phần tử trong B, cần tìm vị trí xuất hiện đầu tiên của nó trong A. Nếu không t tồn tại, in ra 0. Lưu ý yêu cầu chỉ số bắt đầu từ 1 (theo mô tả bài toán và mẫu test), nhưng các giải pháp thường xử lý mảng 0-indexed và cộng 1 khi in.
Các cách tiếp cận
Cách Tìm kiếm nhị phân đệ quy (Duyệt từng phần tử)
#include <stdio.h>
#include <stdlib.h>
int binarySearch(int a[], int l, int r, int x)
{
if(l <= r)
{
int m = (l + r) / 2;
if(a[m] == x)
{
// Nếu là phần tử đầu tiên hoặc phần tử trước nó khác x thì tìm thấy
if(m == 0 || a[m - 1] != x)
return m + 1; // Trả về chỉ số 1-based
else
return binarySearch(a, l, m - 1, x); // Tìm tiếp về bên trái
}
else
{
if(a[m] > x)
return binarySearch(a, l, m - 1, x);
else
return binarySearch(a, m + 1, r, x);
}
}
return 0;
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
int *a = (int *)malloc(n * sizeof(int));
int *b = (int *)malloc(m * sizeof(int));
for(int i = 0; i<n; i++)
scanf("%d", &a[i]);
for(int i = 0; i<m; i++)
scanf("%d", &b[i]);
for(int i = 0; i<m; i++)
{
printf("%d ", binarySearch(a, 0, n - 1, b[i]));
}
free(a); free(b);
}
- Time Complexity: O(m log n)
- Space Complexity: O(n + m)
Đây là cách tiếp cận trực quan nhất. Với mỗi phần tử trong mảng B, ta thực hiện tìm kiếm nhị phân (Binary Search) trong mảng A. Để tìm được chỉ số đầu tiên (first occurrence), khi tìm thấy một phần tử bằng giá trị cần tìm (x) tại vị trí mid, ta phải kiểm tra thêm điều kiện mid == 0 hoặc a[mid-1] != x. Nếu điều kiện này thỏa mãn, mid chính là vị trí đầu tiên. Ngược lại, ta tiếp tục tìm kiếm nửa bên trái của mảng (từ l đến mid - 1). Giải pháp này sử dụng đệ quy, dễ hiểu nhưng có thể gây tràn stack với input lớn (nếu không được tối ưu hóa đuôi) và overhead của việc gọi hàm.
Cách Tìm kiếm nhị phân vòng lặp (Cải tiến)
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
int b[100005];
typedef struct{
int val;
int pos;
} element;
element a[100005];
element l[100005],r[100005];
// Hàm tìm kiếm nhị phân chuẩn xác để tìm first occurrence
int bisch(element a[],int sz,int n){
int lo = 1, hi = sz, ans = n + 1;
while (lo <= hi){
int mid = (lo + hi)/2;
if (a[mid].val >= n){ // Nếu lớn hơn hoặc bằng, có thể là đáp án nhưng cần kiểm tra nhỏ hơn
ans = mid;
hi = mid - 1;
}
else lo = mid + 1;
}
// Kiểm tra xem giá trị tại ans có đúng là n không
if (a[ans].val == n) return ans;
else return -1;
}
// Hàm merge sort (để nếu input chưa sắp xếp, nhưng đề bài nói A đã sắp xếp)
void merge(element a[],int left,int mid,int right){
int n1 = mid - left + 1;
int n2 = right - mid;
for (int i = 1;i <= n1;i++) l[i] = a[left + i - 1];
for (int i = 1;i <= n2;i++) r[i] = a[mid + i];
int i = 1,j = 1,k = left;
while (i <= n1 && j <= n2){
if (l[i].val <= r[j].val) a[k++] = l[i++];
else a[k++] = r[j++];
}
while (i <= n1) a[k++] = l[i++];
while (j <= n2) a[k++] = r[j++];
}
// Main logic
int main(){
int n, m;
scanf("%d %d", &n, &m);
for(int i=1; i<=n; i++) {
scanf("%d", &a[i].val);
a[i].pos = i;
}
for(int i=1; i<=m; i++) scanf("%d", &b[i]);
// Cấu trúc này giả sử input có thể chưa sắp xếp và cần sort lại
// Nếu input đã sắp xếp theo đề bài, chỉ cần đọc vào mảng 1-indexed
for(int i=1; i<=m; i++){
int idx = bisch(a, n, b[i]);
if(idx != -1) printf("%d ", idx);
else printf("0 ");
}
return 0;
}
- Time Complexity: O(m log n)
- Space Complexity: O(n + m)
Đây là phiên bản dùng vòng lặp của tìm kiếm nhị phân thay vì đệ quy. Cách tiếp cận này an toàn hơn về bộ nhớ và thường chạy nhanh hơn do tránh overhead gọi hàm.
Nếu chỉ cần tìm kiếm đơn giản:
- Duyệt qua các phần tử
b[i]. - Duyệt tìm nhị phân
l <= r:- Nếu
a[mid] == b[i]: Ghi nhận vị trí, sau đó继续 tìm về bên trái (r = mid - 1) để đảm bảo tìm được vị trí nhỏ nhất. - Nếu
a[mid] < b[i]:l = mid + 1. - Nếu
a[mid] > b[i]:r = mid - 1.
- Nếu
Giải pháp trong code mẫu là một cách viết chi tiết hơn, thường dùng cho các bài toán phức tạp hơn (có thể sort lại nếu input乱序), nhưng logic tìm kiếm nhị phân để tìm first occurrence là phổ biến.
Cách Tối ưu tìm kiếm nhị phân với lối đi cố định (Optimized Binary Search)
#include <stdio.h>
// Hàm tìm kiếm nhị phân chuẩn để tìm first occurrence
// Trả về chỉ số 0-based (chưa cộng 1)
int binarySearchFirst(int arr[], int n, int x) {
int left = 0, right = n - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == x) {
result = mid; // Ghi nhận kết quả
right = mid - 1; // Tiếp tục tìm bên trái để tìm index nhỏ hơn
} else if (arr[mid] < x) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
int main() {
int n, m;
scanf("%d %d", &n, &m);
int a[n];
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = 0; i < m; i++) {
int b;
scanf("%d", &b);
int index = binarySearchFirst(a, n, b);
if (index != -1) printf("%d ", index + 1);
else printf("0 ");
}
return 0;
}
- Time Complexity: O(m log n)
- Space Complexity: O(n)
Đây là cách tiếp cận tối ưu và chuẩn nhất cho bài toán này.
Logic:
Thay vì dừng lại ngay khi gặp giá trị x (giống tìm kiếm nhị phân thông thường), ta tiếp tục thu hẹp phạm vi tìm kiếm về bên trái.
- Khởi tạo
result = -1. - Khi
arr[mid] == x:- Gán
result = mid(lưu vị trí hiện tại). - Gán
right = mid - 1(tiếp tục tìm trong đoạn bên tráimid).
- Gán
- Vòng lặp sẽ kết thúc khi
left > right. resultchính là vị trí đầu tiên (hoặc -1 nếu không tìm thấy).
Cách này loại bỏ được việc kiểm tra điều kiện rắc rối arr[mid-1] != x hay dùng đệ quy, giúp code ngắn gọn, hiệu năng cao và dễ xử lý với số lượng lớn.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(m log n) | O(n + m) | Tìm kiếm nhị phân đệ quy (Duyệt từng phần tử) |
| 2 | O(m log n) | O(n + m) | Tìm kiếm nhị phân vòng lặp (Cải tiến) |
| 3 | O(m log n) | O(n) | Tối ưu tìm kiếm nhị phân với lối đi cố định (Optimized Binary Search) |
Bài học kinh nghiệm
- Vì mảng A đã được sắp xếp, tìm kiếm nhị phân (Binary Search) là thuật toán tối ưu nhất với độ phức tạp O(log n) cho mỗi truy vấn.
- Để tìm kiếm First Occurrence (vị trí đầu tiên), ta không được dừng lại ngay lập tức khi gặp giá trị cần tìm, mà phải tiếp tục tìm kiếm ở nửa bên trái để đảm bảo không bỏ qua các phần tử trùng lặp ở trước đó.
- Lưu ý về quy ước chỉ số: Đề bài yêu cầu 1-based index (bắt đầu từ 1), trong khi lập trình C/C++ mặc định dùng 0-based index. Cần cộng thêm 1 vào kết quả trước khi in.
Lỗi thường gặp
- Lỗi chỉ số (Index out of bounds): Khi tính chỉ số mid hoặc truy cập mảng, cần chú ý
mid - 1có thể âm nếu không kiểm soát vòng lặp chặt chẽ. - Quên xử lý trường hợp không tìm thấy: Phải in ra 0 thay vì in ra chỉ số bất kỳ.
- Sử dụng đệ quy quá sâu: Với n, m lên tới 10^5, đệ quy có thể gây tràn bộ nhớ đệm (Stack Overflow) nếu trình biên dịch không tối ưu hóa đệ quy đuôi (Tail Recursion). Nên ưu tiên dùng vòng lặp (Iteration).
- Nhầm lẫn giữa
>và>=: Trong điều kiện tìm kiếm nhị phân, việc dùng>=thay vì>có thể làm lệch hướng tìm kiếm, đặc biệt khi dữ liệu có nhiều phần tử giống nhau.
Bình luận