T
thienvamai
Chắc suất Đại học top - Giữ chỗ ngay!! ĐĂNG BÀI NGAY để cùng trao đổi với các thành viên siêu nhiệt tình & dễ thương trên diễn đàn.
Bài viết được lấy từ blog của coder Nguyễn Tiền Trung Kiên (kiencoi1997) , mọi sao chép phải được sự đồng ý của chủ nhân bài viết.
Chặt nhị phân
Để tìm kiếm một phần tử trong mảng. Với cách thông thường, ta phải duyệt tất cả các số từ a[1] đến a[n], tức là mất độ phức tạp O(n). Tuy nhiên với mảng đơn điệu (dãy không tăng hoặc không giảm), thì ta chỉ mất độ phức tạp O(logn) để tìm kiếm, với bài toán này, ta dùng chặt nhị phân để tìm kiếm. Bài viết này tôi sẽ viết về cách chặt nhị phân và chặt tam phân.
Lưu ý:
- Chặt nhị phân chỉ dùng đưuọc cho dãy đơn điệu
- Chặt tam phân chỉ dùng được cho dãy tăng rồi giảm (hoặc giảm rồi tăng), tức là tồn tại một điểm i mà a[1..i] đơn điệu, a[i..n] đơn điệu.
- Có rất nhiều cách chặt nhị phân, và mỗi người lại code một cách khác nhau. Ở đây tôi xin đưuọc trình bày cách mà tôi thương dùng, vì cách này có tương đối nhiều ưu điểm.
1. Chặt nhị phân trên mảng
Giả sử ta có bài toán sau:
Cho một dãy n số. Với mỗi số k được nhập vào, in ra một số nằm trong dãy ban đầu, nhỏ nhất và lớn hơn hoặc bằng số k. (Sẽ có Q số k được nhập, Q<=100000, n<=100000)
Trước hết ta thấy rằng có thể sort lại mảng a (gọi a là dãy số ban đầu). Vì vậy ta có thể giả sử a là dãy không giảm. Ta sẽ làm như sau:
Dưới đây là code không dùng chặt nhị phân.
Với thuật toán như trên, độ phức tạp là O(mn) (với m là số truy vấn/câu hỏi). Tôi sẽ thêm một số lệnh để giảm độ phức tạp xuống O((m+n) logn). Các lệnh mà tôi thêm vào sẽ được tô màu đỏ.
Với cách làm này, ta có thể kiểm tra độ chính xác của thuật toán bằng hàm bsearch ban đầu. Sau đó bạn có thể thêm một số dòng lệnh để cho chương trình chạy nhanh hơn.
Code trên đảm bảo rằng, với các a giống nhau, hàm sẽ trả về a đầu tiên trong mảng, để rõ hơn về điều này, ta xét bài toán sau.
Cho một dãy a đã sắp xếp tăng dần. Lần lượt nhập m số k. Có hai bài toán:
a, Với mỗi k, in ra số đầu tiên bé nhất lơn hơn hoặc bằng k.
b, Với mỗi k, in ra số cuối cùng lớn nhất bé hơn hoặc bằng k.
Tức là ở bài toán a, trong các a thỏa mãn và bằng nhau, chọn a có i nhỏ nhất. Ở bài toán b, trong các a thỏa mãn và bằng nhau, chọn a có i lớn nhất.
Trở lại với cách làm ở trên, đây là nội dung mã giả ở trên
Để ý rằng dòng tô đỏ (if a>k ...) ở trên, sẽ gán right = i cả khi a = k. Vì thế kết quả luôn là a với i nhỏ nhất. Để chọn i lớn nhất, ta thay đổi hai dòng tô đỏ ở trên thành:
2. Chặt nhị phân trên hàm
Với các hàm số thực, ta cũng làm tương tự như chặt nhị phân trên mảng. Giả sử ta có bài toán sau:
Nhập một số thực nằm trong khoảng từ 0 đến 1 (0 <= key <= 1). Tìm x để sin x = key.
Với bài này, ta đặt left=0, right =pi/2, sau đó chặt nhị phân giống như thuạt toán bên trên:
Thuật toán như trên chắc chắn sẽ dừng tại một lúc nào đó, khi mà left rất gần với right, i sẽ bằng một trong hai giá trị left hoặc right, và lệnh while sẽ dừng lại. Bởi vậy, cách code ở trên không cần dùng epsilon, đây là một cách chặt nhị phân khá hay giúp bạn không cần phải quan tâm đến epsilon.
3. Chặt tam phân
Có những hàm không thể chặt nhị phân mà có thể chặt tam phân. Ví dụ : Cho hai điểm A, B trên mặt phẳng. Chọn một điểm C nằm trên trục Ox. Tìm điểm C để tổng AC + BC là nhỏ nhất. Gọi f(x) là tổng AC + BC với C ở tọa độ (x, 0). Ta cần tìm f(x) bé nhất.
Giả sử ta đã tìm đưuọc một điểm C0 thỏa mãn đề bài. Xét các điểm C nằm bên phải C0, ta tháy rằng khoảng cách từ C đến C0 càng tăng. Tương tự vơi bên trái. Có nghĩa là: bên phải điểm C0 là hàm tăng, bên trái C0 là hàm giảm, bới vậy ta dùng chặt tam phân.
Dòng chữ đỏ đâu tiên, có nghĩa là chia đoạn LL..RR thành ba khoảng: LL..ML, ML..MR, MR..RR. Dòng chữ đỏ thứ hai có nghĩa là: nếu f(ML) > f(MR) thì tất cả f(LL)..f(ML) đều lớn hơn f(MR) suy ra cả đoạn LL..ML đều không phải là đáp án của mình. Tương tự với trường hợp f(ML) < f(MR). Vì vậy thuật toán trên là đúng.
4. Dùng thư viện có sẵn
Dưới đây là các hàm có sẵn trong thư viện algorithm và ví dụ về cách sử dụng. Các hàm này chỉ hoạt động khi dãy số đưuọc sắp xếp tăng dần (chính xác hơn là dãy không giảm).
hàm lower_bound(first, last, value) : trả về vị trí đầu tiên lớn hơn hoặc bằng value trong dãy.
hàm upper_bound(first, last, value) : trả về vị trí đầu tiên lớn hơn value trong dãy.
Nguồn: https://sites.google.com/site/kc97ble/
Chặt nhị phân
Để tìm kiếm một phần tử trong mảng. Với cách thông thường, ta phải duyệt tất cả các số từ a[1] đến a[n], tức là mất độ phức tạp O(n). Tuy nhiên với mảng đơn điệu (dãy không tăng hoặc không giảm), thì ta chỉ mất độ phức tạp O(logn) để tìm kiếm, với bài toán này, ta dùng chặt nhị phân để tìm kiếm. Bài viết này tôi sẽ viết về cách chặt nhị phân và chặt tam phân.
Lưu ý:
- Chặt nhị phân chỉ dùng đưuọc cho dãy đơn điệu
- Chặt tam phân chỉ dùng được cho dãy tăng rồi giảm (hoặc giảm rồi tăng), tức là tồn tại một điểm i mà a[1..i] đơn điệu, a[i..n] đơn điệu.
- Có rất nhiều cách chặt nhị phân, và mỗi người lại code một cách khác nhau. Ở đây tôi xin đưuọc trình bày cách mà tôi thương dùng, vì cách này có tương đối nhiều ưu điểm.
1. Chặt nhị phân trên mảng
Giả sử ta có bài toán sau:
Cho một dãy n số. Với mỗi số k được nhập vào, in ra một số nằm trong dãy ban đầu, nhỏ nhất và lớn hơn hoặc bằng số k. (Sẽ có Q số k được nhập, Q<=100000, n<=100000)
Trước hết ta thấy rằng có thể sort lại mảng a (gọi a là dãy số ban đầu). Vì vậy ta có thể giả sử a là dãy không giảm. Ta sẽ làm như sau:
Mã:
sort(a);
left=1, right=n;
for i = left to right do
if a[i]>=k then return a[i];
print "Not found"
Dưới đây là code không dùng chặt nhị phân.
PHP:
Code này của Nguyễn Tiến Trung Kiên
#include <stdio.h>
#include <algorithm>
using namespace std;
int n, k, m;
int a[1230997];
void bsearch(int k){
int ll=1, rr=n, i;
for (i=ll; i<=rr; i++)
if (a[i]>=k) { printf("%d\n", a[i]); return; }
printf("Not found\n");
}
main(){
int i;
scanf("%d", &n);
for (i=1; i<=n; i++) scanf("%d", &a[i]);
sort(a+1, a+n+1);
scanf("%d", &m);
for (i=1; i<=m; i++){
scanf("%d", &k);
bsearch(k);
}
}
Mã:
sort(a);
left=1, right=n;
i = (left + right) / 2;
while (left != i) and (i != right) do
if a[i]>=k then right = i;
else left = i;
i = (left + right) / 2;
for i = left to right do
if a[i]>=k then return a[i];
print "Not found"
PHP:
Code này của Nguyễn Tiến Trung Kiên
#include <stdio.h>
#include <algorithm>
using namespace std;
int n, k, m;
int a[1230997];
void bsearch(int k){
int ll=1, rr=n, i=(ll+rr)/2;
while (ll!=i && i!=rr){
if (a[i]>=k) rr=i;
else ll=i;
i=(ll+rr)/2;
}
for (i=ll; i<=rr; i++)
if (a[i]>=k) { printf("%d\n", a[i]); return; }
printf("Not found\n");
}
main(){
int i;
scanf("%d", &n);
for (i=1; i<=n; i++) scanf("%d", &a[i]);
sort(a+1, a+n+1);
scanf("%d", &m);
for (i=1; i<=m; i++){
scanf("%d", &k);
bsearch(k);
}
}
Với cách làm này, ta có thể kiểm tra độ chính xác của thuật toán bằng hàm bsearch ban đầu. Sau đó bạn có thể thêm một số dòng lệnh để cho chương trình chạy nhanh hơn.
Code trên đảm bảo rằng, với các a giống nhau, hàm sẽ trả về a đầu tiên trong mảng, để rõ hơn về điều này, ta xét bài toán sau.
Cho một dãy a đã sắp xếp tăng dần. Lần lượt nhập m số k. Có hai bài toán:
a, Với mỗi k, in ra số đầu tiên bé nhất lơn hơn hoặc bằng k.
b, Với mỗi k, in ra số cuối cùng lớn nhất bé hơn hoặc bằng k.
Tức là ở bài toán a, trong các a thỏa mãn và bằng nhau, chọn a có i nhỏ nhất. Ở bài toán b, trong các a thỏa mãn và bằng nhau, chọn a có i lớn nhất.
Trở lại với cách làm ở trên, đây là nội dung mã giả ở trên
Mã:
sort(a);
left=1, right=n;
i = (left + right) / 2;
while (left != i) and (i != right) do
if a[i]>=k then right = i;
else left = i;
i = (left + right) / 2;
for i = left to right do
if a[i]>=k then return a[i];
print "Not found"
Để ý rằng dòng tô đỏ (if a>k ...) ở trên, sẽ gán right = i cả khi a = k. Vì thế kết quả luôn là a với i nhỏ nhất. Để chọn i lớn nhất, ta thay đổi hai dòng tô đỏ ở trên thành:
Mã:
sort(a);
left=1, right=n;
i = (left + right) / 2;
while (left != i) and (i != right) do
if a[i]>k then right = i;
else left = i;
i = (left + right) / 2;
for i = right to left do
if a[i]<=k then return a[i];
print "Not found"
Với các hàm số thực, ta cũng làm tương tự như chặt nhị phân trên mảng. Giả sử ta có bài toán sau:
Nhập một số thực nằm trong khoảng từ 0 đến 1 (0 <= key <= 1). Tìm x để sin x = key.
Với bài này, ta đặt left=0, right =pi/2, sau đó chặt nhị phân giống như thuạt toán bên trên:
Mã:
left=0, right=PI/2;
i = (left + right) / 2;
while (left != i) and (i != right) do
if sin(i)>=key then right = i;
else left = i;
i = (left + right) / 2;
print (l+r)/2;
PHP:
Code này của Nguyễn Tiến Trung Kiên
#include <stdio.h>
#include <math.h>
double bsearch(double key){
double ll=0, rr=M_PI/2, i=(ll+rr)/2;
while (ll!=i && i!=rr){
if (sin(i) >= key) rr=i;
else ll=i;
i=(ll+rr)/2;
}
return (ll+rr)/2;
}
double key;
main(){
scanf("%lf", &key); // 0 <= key <= 1
printf("answer = %lf\n", bsearch(key));
printf("arcsin = %lf\n", asin(key));
}
3. Chặt tam phân
Có những hàm không thể chặt nhị phân mà có thể chặt tam phân. Ví dụ : Cho hai điểm A, B trên mặt phẳng. Chọn một điểm C nằm trên trục Ox. Tìm điểm C để tổng AC + BC là nhỏ nhất. Gọi f(x) là tổng AC + BC với C ở tọa độ (x, 0). Ta cần tìm f(x) bé nhất.
Giả sử ta đã tìm đưuọc một điểm C0 thỏa mãn đề bài. Xét các điểm C nằm bên phải C0, ta tháy rằng khoảng cách từ C đến C0 càng tăng. Tương tự vơi bên trái. Có nghĩa là: bên phải điểm C0 là hàm tăng, bên trái C0 là hàm giảm, bới vậy ta dùng chặt tam phân.
Mã:
LL=minX, RR=maxX;
ML=(LL+LL+RR)/3, MR=(LL+RR+RR)/3;
while (LL!=ML) and (ML!=MR) and (MR!=RR) do
if f(ML)>f(MR) then LL=ML;
else RR=MR:
ML=(LL+LL+RR)/3, MR=(LL+RR+RR)/3;
print (LL+RR)/2;
Dòng chữ đỏ đâu tiên, có nghĩa là chia đoạn LL..RR thành ba khoảng: LL..ML, ML..MR, MR..RR. Dòng chữ đỏ thứ hai có nghĩa là: nếu f(ML) > f(MR) thì tất cả f(LL)..f(ML) đều lớn hơn f(MR) suy ra cả đoạn LL..ML đều không phải là đáp án của mình. Tương tự với trường hợp f(ML) < f(MR). Vì vậy thuật toán trên là đúng.
PHP:
Code này của Nguyễn Tiến Trung Kiên
#include <stdio.h>
#include <algorithm>
#include <math.h>
using namespace std;
double xa, ya, xb, yb;
double f(double x){
double AC = sqrt((xa-x)*(xa-x) + ya*ya);
double BC = sqrt((xb-x)*(xb-x) + yb*yb);
return AC+BC;
}
main(){
scanf("%lf%lf%lf%lf", &xa, &ya, &xb, &yb);
double rr=max(xa, xb);
double ll=min(xa, xb);
double ml=(ll+ll+rr)/3;
double mr=(ll+rr+rr)/3;
while (ll!=ml && ml!=mr && mr!=rr){
if (f(ml) > f(mr)) ll=ml;
else rr=mr;
ml=(ll+ll+rr)/3;
mr=(ll+rr+rr)/3;
}
printf("answer = %lf\n", (ll+rr)/2);
printf("cost = %lf\n", f((ll+rr)/2));
}
4. Dùng thư viện có sẵn
Dưới đây là các hàm có sẵn trong thư viện algorithm và ví dụ về cách sử dụng. Các hàm này chỉ hoạt động khi dãy số đưuọc sắp xếp tăng dần (chính xác hơn là dãy không giảm).
hàm lower_bound(first, last, value) : trả về vị trí đầu tiên lớn hơn hoặc bằng value trong dãy.
hàm upper_bound(first, last, value) : trả về vị trí đầu tiên lớn hơn value trong dãy.
PHP:
Code này của Nguyễn Tiến Trung Kiên
#include <stdio.h>
#include <algorithm>
using namespace std;
int n, m;
int a[1230997];
main(){
int i, p, q;
scanf("%d", &n);
for (i=1; i<=n; i++)
scanf("%d", &a[i]);
sort(a+1, a+n+1); // a[1..n]
for (i=1; i<=n; i++) printf(i==n?"%d\n":"%d ", a[i]);
scanf("%d", &m); // m times
while (m--){
scanf("%d", &p);
q = lower_bound(a+1, a+n+1, p) - a; // a[1..n], a[q]>=p
if (q==n+1) printf("No numbers are bigger than %d\n", p);
else printf("The 1st number >= %d is a[%d]=%d\n", p, q, a[q]);
q = upper_bound(a+1, a+n+1, p) - a; // a[1..n], a[q]>p
if (q==n+1) printf("No numbers are bigger than %d\n", p);
else printf("The 1st number > %d is a[%d]=%d\n", p, q, a[q]);
}
}
Nguồn: https://sites.google.com/site/kc97ble/