Ứng dụng của tìm kiếm nhị phân

Cấu trúc dữ liệu và giải thuật

0.0 (0 đánh giá)
Tạo bởi Katsu Cập nhật lần cuối 14:50 15-05-2022 8.163 lượt xem 2 bình luận
Tác giả/Dịch giả: huulam3011 K9
Học nhanh

Danh sách bài học

Ứng dụng của tìm kiếm nhị phân

Trong bài học ngày hôm trước, chúng ta đã cùng nhau đi tìm hiểu về tìm kiếm nhị phân. Ngày hôm nay, chúng ta sẽ tiếp tục tìm hiểu về ứng dụng của thuật toán này trong các bài toán khác.


Nội dung

Để có thể tiếp thu bài học này một cách tốt nhất, các bạn nên có những kiến thức cơ bản về:

Trong bài học ngày hôm nay, chúng ta sẽ cùng nhau tìm hiểu về:

  • Ứng dụng của tìm kiếm nhị phân
  • Nhận biết sử dụng tìm kiếm nhị phân

Bài toán đặt ra

Ở trong bài học trước, mình đã nêu ra cho các bạn một bài toán. Bây giờ, mình sẽ trình bày lại đề bài:

Có một phân xưởng gồm n máy (n \leq 10^{5} ), máy thứ i cần chính xác a_{i} ngày  (0<a_{i}\leq 10^{9})để hoàn thành 1 sản phẩm như nhau. Hãy tính thời gian ít nhất để có thể hoàn thành tối thiểu k sản phẩm (0< k< 10^{9}).

Ví dụ

Input Output

3 9

3 2 7

10

Giải thích: Trong vòng 10 ngày, máy 1 làm được 3 sản phẩm, máy 2 làm được 5 sản phẩm, máy 3 làm được 1 sản phẩm, tổng cộng là 9 sản phẩm như yêu cầu đề bài.

Đã có bạn nào tìm ra được cách giải của riêng mình chưa? Nếu rồi thì hãy xem cách giải của mình có giống với các bạn không nhé còn nếu các bạn chưa tìm ra cách giải thì hãy xem mình sẽ giải quyết bài toán này như thế nào nhé!


Lời giải ban đầu

Cách giải đơn giản nhất khi nhìn vào bài toán này chính là thử. Ta sẽ thử số lượng ngày cần để hoàn thành k sản phẩm. Nếu đủ để tạo ra k sản phẩm ta sẽ ghi nhận kết quả, nếu không thì ta sẽ tăng số lượng ngày cần để hoàn thành và tiếp tục thử.

Từ suy nghĩ trên, ta có code như sau:

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

typedef long long ll;

const int MaxN = 1 + 1e6;

int n, k, a[MaxN];

bool check(ll days){
    ll products = 0;
    for(int i = 0 ; i < n ; ++i) products += days / a[i];
    return products >= k;
}

int main(){
    freopen("CTDL.inp","r",stdin);
    freopen("CTDL.out","w",stdout);
    cin >> n >> k;
    for(int i = 0 ; i < n ; ++i) cin >> a[i];
    ll res = 0;
    while(!check(res)) res++;
    cout << res << endl;

    return 0;
}

Tuy nhiên, hẳn các bạn cũng nhận ra là cách làm này sẽ rất chậm. Giả sử n = 1, a_{0}=10^{9}, k=10^{9}  thì khi đó kết quả là 10^{18} hay có nghĩa là vòng lặp của chúng ta sẽ lặp 10^{18} lần. Vậy thì có cách làm nào nhanh hơn không?


Nhận xét

Ta thấy, bài toán này sẽ có một tính chất đặc biệt sau: Nếu như ngày thứ m ta không hoàn thành được k sản phẩm thì từ ngày 1 đến ngày m - 1 ta cũng không hoàn thành được k sản phẩm. Tính chất này sẽ giúp chúng ta giải quyết bài toán như thế nào đây?

Giả sử ta đang cần tìm kiếm thời gian ngắn nhất trong đoạn [l, r] và ta đã biết chắc chắn đến ngày r sẽ hoàn thành được ít nhất k sản phẩm.

Ta sẽ chọn một ngày h nào đó sao cho h \epsilon [l, r]. Nếu như đến ngày h mà ta cũng không thể hoàn thành đủ k sản phẩm thì theo nhận xét bên trên, xét từ ngày l đến ngày h là không cần thiết và ta sẽ tiếp tục tìm kiếm kết quả trong đoạn [h+1, r]. 

Còn nếu đến ngày h mà ta hoàn thành tối thiểu k sản phẩm thì ta sẽ tìm kết quả trong đoạn [l, h] do lúc này ta biết ta sẽ cần nhiều nhất h ngày để hoàn thành k sản phẩm.

Đọc ý tưởng trên các bạn có thấy quen thuộc không? Đó chính là tư tưởng của tìm kiếm nhị phân. Cụ thể áp dụng vào lời giải như thế nào thì hãy cùng tìm hiểu tiếp nhé.


Lời giải cải tiến

Từ những nhận xét trên ta sẽ rút ra được lời giải như sau:

  • Tìm kiếm kết quả trên đoạn [l, h] mà ta biết chắc chắn tại ngày r sẽ hoàn thành
  • Lấy giá trị trung bình h của đoạn
  • Kiểm tra giá trị trung bình h. Nếu giá trị đó thỏa mãn ta sẽ tiếp tục tìm kiếm trên đoạn [l, h] còn ngược lại, ta sẽ tìm kiếm trên đoạn [h+1, r].
  • Quá trình kết thúc khi đoạn tìm kiếm chỉ chứa 1 phần tử duy nhất

Vậy cụ thể ta sẽ triển khai lời giải trên như thế nào?

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

typedef long long ll;

const int MaxN = 1 + 1e6;

int n, k, a[MaxN];

bool check(ll days){
    ll products = 0;
    for(int i = 0 ; i < n ; ++i) products += days / a[i];
    return products >= k;
}

int main(){
    freopen("CTDL.inp","r",stdin);
    freopen("CTDL.out","w",stdout);
    cin >> n >> k;
    for(int i = 0 ; i < n ; ++i) cin >> a[i];
    ll l = 0, r = 1e18;
    while(l < r){
        ll tg = (l + r) / 2;
        if(check(tg)) r = tg;
        else l = tg + 1;
    }
    cout << l << endl;

    return 0;
}

Mình sẽ giải thích một chi tiết mà các bạn có thể cảm thấy khó hiểu ở đoạn code trên, đó là phép gán r=10^{18}. Tại sao lại có con số 10^{18} ở đây? Như mình đã nói ở trên, khi ta xét đoạn [l, r]thì ta phải đảm bảo r chính xác. Có nghĩa là trong trường hợp xấu nhất thì r vẫn phải đúng. Vậy trường hợp xấu nhất của ta là gì? Đó chính là khi n = 1, a_{0}=10^{9}, k=10^{9}, kết quả khi này là r=10^{18}. Đó chính là lí do mình thực hiện phép gán trên.

Vậy thì độ phức tạp của chương trình trên sẽ là bao nhiêu. Do qua mỗi vòng while, phạm vi tìm kiếm nhỏ đi một nửa và mỗi lần kiểm tra mất O(n) nên độ phức tạp của toàn bộ chương trình là O(\log (r) \times n).


Nhận biết sử dụng tìm kiếm nhị phân

Vậy thì làm sao chúng ta có thể nhận biết được khi nào nên sử dụng tìm kiếm nhị phân hay dạng bài toán nào có thể áp dụng chúng?

Giả sử ta có một hàm P(x) trả về bool cho biết giá trị x ta đang xét có hợp lí hay không, đoạn đang được xét là [1, m], kết quả bài toán là k thì bài toán có thể áp dụng tìm kiếm nhị phân khi và chỉ khiP(x) ^{\wedge} P(y)= true\ \forall \ x<k, y>k.

Nói một cách dễ hiểu hơn thì đoạn ta đang xét phải có 1 trong 2 dạng sau:

Hoặc


Lưu ý khi sử dụng tìm kiếm nhị phân

Trong bài toán ví dụ của mình ở trên, ta đang đi tìm kết quả nhỏ nhất có thể. Thế nhưng, sẽ hơi khác một chút khi ta phải đi xét kết quả lớn nhất có thể. Sự khác biệt ở đây là gì?

Hãy giả sử chúng ta đang xét đoạn [m, m+1]. Cả hai giá trị này đều hợp lí, khi đó thì ta sẽ chọn m+1 do ta cần kết quả lớn nhất có thể. Tuy nhiên, hãy chú ý vào cách vòng while ở đoạn code mà mình viết chạy:

while(l < r){
    ll tg = (l + r) / 2;
    if(check(tg)) r = tg;
    else l = tg + 1;
}

Khi này,tg=m+m+1 / 2 hay tg=m. Do m là kết quả hợp nên khi này check(tg) trả về true dẫn đến r=tg hay có nghĩa là r=m nên kết quả thu được là m thay vì m+1. Do đó, nếu cần tìm kết quả lớn nhất, ta phải sửa lại như sau:

while(l < r){
    ll tg = (l + r + 1) / 2;
    if(check(tg)) l = tg;
    else r = tg - 1;
}

Điều này còn có nghĩa là khi cần tìm kết quả nhỏ nhất thì ta ưu tiên xét giá trị nhỏ hơn trước còn khi cần tìm giá trị lớn nhất ta sẽ cần ưu tiên xét giá trị lớn hơn trước.

Nói điều này ở đây có thể các bạn sẽ chưa thể hiểu ngay được. Tuy nhiên, hãy thử làm một số bài toán về tìm kiếm nhị phân mà cần tìm kết quả lớn nhất. Khi đó, các bạn sẽ hiểu được vấn đề mà mình đã trình bày ở đây.


Kết luận

  • Qua bài này chúng ta đã nắm được về Tìm kiếm nhị phân
  • Bài sau chúng ta sẽ tìm hiểu về Hai con trỏ
  • Cảm ơn các bạn đã theo dõi bài viết. Hãy để lại bình luận hoặc góp ý của mình để phát triển bài viết tốt hơn. Đừng quên “Luyện tập – Thử thách – Không ngại khó”.

Tải xuống

Tài liệu

Nhằm phục vụ mục đích học tập Offline của cộng đồng, Kteam hỗ trợ tính năng lưu trữ nội dung bài học Ứng dụng của tìm kiếm nhị phân dưới dạng file PDF trong link bên dưới.

Ngoài ra, bạn cũng có thể tìm thấy các tài liệu được đóng góp từ cộng đồng ở mục TÀI LIỆU trên thư viện Howkteam.com

Đừng quên likeshare để ủng hộ Kteam và tác giả nhé!


Thảo luận

Nếu bạn có bất kỳ khó khăn hay thắc mắc gì về khóa học, đừng ngần ngại đặt câu hỏi trong phần bên dưới hoặc trong mục HỎI & ĐÁP trên thư viện Howkteam.com để nhận được sự hỗ trợ từ cộng đồng.

Nội dung bài viết

Tác giả/Dịch giả

Mình là Nguyễn Hữu Lâm, một người có niềm đam mê rất lớn đối với lập trình. Hiện tại, mình đang là sinh viên Khoa học máy tính của Đại học Bách Khoa Hà Nội. Mong muốn của mình là có thể chia sẻ những kiến thức mà bản thân có cho mọi người, học hỏi, kết bạn với tất cả những người có cùng đam mê với mình.


K9

Nhà sáng lập Howkteam.com, KQuiz.vn & tác giả các khóa học C#, Auto, Unity3D, Python....

Với mong muốn mang đến kiến thức chất lượng, miễn phí cho mọi người, với tâm huyết phá bỏ rào cản kiến thức từ việc giáo dục thu phí. Tôi đã cùng đội ngũ Kteam đã lập nên trang website này để thế giới phẳng hơn.
Hãy cùng chúng tôi lan tỏa kiến thức đến cộng đồng! 

Khóa học

Cấu trúc dữ liệu và giải thuật

Bạn đã từng đau đầu với các cấu trúc stack, queue,.. hoặc cảm thấy cực kỳ khó khăn với các thuật toán sắp xếp, tìm kiếm được sử dụng trong lập trình. Đừng lo lắng! Trong khoá học này, chúng ta sẽ cùng nhau tìm hiểu một cách đơn giản nhất về cấu trúc dữ liệu và giải thuật, cũng như giúp bạn nắm rõ hơn về các kiến thức này.

Hãy cùng xem cấu trúc dữ liệu và giải thuật có gì đáng sợ không nhé!

Đánh giá

Bình luận

Để bình luận, bạn cần đăng nhập bằng tài khoản Howkteam.

Đăng nhập
AlanNguyen đã bình luận 11:56 02-03-2024

Tại sao phải chạy check() để for chạy nhiều lần vậy

ví dụ nếu trong 3 ngày có thể sử dụng

int a = 3;

kq = a/3 + a/2 + a/7;

Không có video.