Learn C++ STL

lesson ১ / ৯ · binary_search, lower_bound, upper_bound আর equal_range

Module ১৩ · binary_search, lower_bound, upper_bound আর equal_range

Binary search: সাজানো সারিকে অর্ধেক করতে করতে একটা জায়গায় পৌঁছানো

Freeপড়া

এই lesson-এ যা শিখবে

  • এক বাক্যে বলতে পারবে binary search কী করে, আর বুঝিয়ে বলতে পারবে কেন ও শুধু সাজানো range-এই কাজ করে।
  • একটা সাজানো vector-এ binary_search, lower_bound, upper_bound আর equal_range call করতে পারবে, আর প্রতিটার উত্তর পড়তে পারবে: হ্যাঁ বা না, প্রথম জায়গা, নয়তো দুই bound একসাথে।
  • it - v.begin() দিয়ে জায়গাকে index বানাতে পারবে, "পাওয়া যায়নি" check করবে v.end()-এর সাথে, x-এর কপি গুনবে upper বিয়োগ lower দিয়ে, আর খরচটা বলতে পারবে: O(log n)।

Amara-র বইয়ের দোকানে 16টা বইয়ের দামের একটা ছাপানো তালিকা আছে, সবচেয়ে সস্তাটা থেকে শুরু। একজন customer জানতে চায়, ঠিক 300 টাকার কোনো বই আছে কি না। আরেকজন জানতে চায়, 310 টাকার একটা নতুন বই তালিকার কোথায় বসবে। সামনে থেকে পড়লে প্রতিটা প্রশ্নে 16 বার পর্যন্ত দেখতে হতে পারে। ছোটদের "বেশি না কম" খেলায় লাগে বড়জোর 5 বার। এই lesson সেই খেলাটাই, শুধু খেলছে library, একটা সাজানো vector-এর উপর।

অনুমানের খেলা: বেশি না কম

Zara 1 থেকে 100-এর মধ্যে একটা পূর্ণসংখ্যা মনে মনে ঠিক করে, আর তুমি অনুমান করো। প্রতিবার ও বলে "বেশি", "কম" নয়তো "হ্যাঁ"। অসাবধান খেলোয়াড় 1 থেকে শুরু করে একটা একটা করে ওঠে, আর তার 100 বার লাগতে পারে। সাবধানী খেলোয়াড় সবসময় বলে, যা এখনো সম্ভব, তার ঠিক মাঝখানের সংখ্যাটা।

প্রথমে বলো 50। Zara "কম" বললে 50 থেকে 100 পর্যন্ত সব সংখ্যা এক উত্তরেই বাদ। বাকি থাকে মোটামুটি 50টা, তারপর 25, তারপর 12, 6, 3 আর 1। তাই সাবধানী খেলোয়াড়ের কখনো 7 বারের বেশি লাগে না, কারণ 7 বার অর্ধেক করলেই 100টা সংখ্যা নেমে আসে একটায়।

Binary search-এর পুরো কথাটা এটাই। যে অংশটা এখনো বাকি, তার মাঝখানের সাথে তুলনা করো, আর যে অর্ধেকে উত্তর থাকতেই পারে না, সেটা ফেলে দাও। "Binary" মানে দুই: প্রতিবার দেখায় বাকি অংশ ভাগ হয় দুই অর্ধেকে। তাই কতবার দেখতে হবে, সেটা বাড়ে n-কে কতবার অর্ধেক করা যায় তার সাথে, n-এর সাথে না।

সাজানো থাকাটাই দাম

খেলাটা চলে, কারণ 1 থেকে 100 সংখ্যাগুলো ক্রমে সাজানো। 50 বলার পরে Zara "কম" বললে তুমি জানো, 50-এর উপরের প্রতিটা সংখ্যাও ভুল। Amara-র তালিকাও একইভাবে কাজ করে, কারণ ওটা ছাপানো হয়েছে সস্তা থেকে দামির দিকে। নিচে ওর 16টা দামের উপর প্রথম 300 খোঁজার কাজটা, lower_bound যেভাবে করে, একবার একবার করে দেখা।

Amara-র সাজানো 16টা দামের উপর lower_bound(300), প্রতিবার দেখে একটা করে অর্ধেক কাটা Amara-র 16টা দামের উপর lower_bound(300): চারবার দেখা 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 120 150 150 200 240 300 300 300 350 420 450 480 520 600 640 700 ধাপ 1 350 ধাপ 2 240 ধাপ 3 300 ধাপ 4 300 ফল থামল index 5-এ, প্রথম 300-এ প্রতিবার মাঝের দাম মেলানো হয় 300-এর সাথে, আর একটা অর্ধেক কাটা যায়।

প্রথমবার মাঝখানে পাওয়া গেল 350। সাজানো তালিকায় ওর ডানের সবকিছু 350 বা তার বেশি, তাই প্রথম 300 ওখানে থাকতেই পারে না। সাজানো না হলে একটা 300 যেকোনো জায়গায় লুকিয়ে থাকতে পারত, আর অর্ধেক কেটে দিলে সেটাও ফেলে দেওয়া হতো। তাই সাজানোটাই দাম, একবারই দিতে হয়, আর বদলে প্রতিটা খোঁজা শেষ হয় হাতে গোনা কয়েকবার দেখায়, লম্বা হাঁটা লাগে না।

Library কীভাবে Amara-র তালিকা অর্ধেক করে, দেখো

এই খোঁজাটা C++ library তোমার হয়ে করে দেয়। নিচে Amara-র তালিকায় ওর সবচেয়ে কাজের দুইটা খোঁজা, ধাপে ধাপে। প্রতিটা ধাপে যে অংশটা এখনো বাকি, সেটা লেখা [lo, hi) হিসেবে: index lo ভেতরে, index hi বাইরে। প্রতিবার তুলনা হয় মাঝের element-এর সাথে, যেটা থাকে lo + (hi - lo) / 2-এ। প্রতিটা তুলনা GCC 12-এর library, libstdc++, সত্যিই করেছে; Compiler Explorer-এ একটা run থেকে print করা।

একই খোঁজা, table হিসেবে: প্রতিটা সারি একটা ধাপ, আর যে অংশটা এখনো বাকি, সেটা লেখা [lo, hi) হিসেবে।

ধাপএখনো যেগুলো বাকিতুলনা হলোকী হলো
1[0, 16): 16টাইএখনো কিছু না16টা দামই এখনো বাকি। lower_bound খুঁজছে প্রথম দামটা, যেটা 300-এর কম না।
2[0, 16): 16টাindex 8: 350Index 8-এ 350, যেটা 300-এর কম না। তাই উত্তর index 8 বা তার আগে: রাখো [0, 8)।
3[0, 8): 8টাindex 4: 240Index 4-এ 240, যেটা 300-এর কম। উত্তর ওর ডানে: রাখো [5, 8)।
4[5, 8): 3টাindex 6: 300Index 6-এ 300, যেটা 300-এর কম না। উত্তর index 6 বা তার আগে: রাখো [5, 6)।
5[5, 6): 1টাindex 5: 300Index 5-এ 300, যেটা 300-এর কম না। উত্তর index 5 বা তার আগে: রাখো [5, 5)।
6[5, 5): কিছুই নাকিছু নাআর কিছু বাকি নেই: lower_bound থামল index 5-এ, প্রথম 300-টায়। তুলনা লাগল 4টা।

lower_bound চায় প্রথম দামটা, যেটা 300-এর কম না। মাঝের দাম 300-এর কম হলে উত্তর নিশ্চয়ই ওর ডানে। কম না হলে উত্তর ওই index-ই, নয়তো ওর বাঁয়ে। চারবার দেখার পরে আর কিছু বাকি থাকে না, আর bound বসে index 5-এ।

একই খোঁজা, table হিসেবে: প্রতিটা সারি একটা ধাপ, আর যে অংশটা এখনো বাকি, সেটা লেখা [lo, hi) হিসেবে।

ধাপএখনো যেগুলো বাকিতুলনা হলোকী হলো
1[0, 16): 16টাইএখনো কিছু না16টা দামই এখনো বাকি। upper_bound খুঁজছে প্রথম দামটা, যেটা 300-এর বেশি।
2[0, 16): 16টাindex 8: 350Index 8-এ 350, যেটা 300-এর বেশি। তাই উত্তর index 8 বা তার আগে: রাখো [0, 8)।
3[0, 8): 8টাindex 4: 240Index 4-এ 240, যেটা 300-এর বেশি না। উত্তর ওর ডানে: রাখো [5, 8)।
4[5, 8): 3টাindex 6: 300Index 6-এ 300, যেটা 300-এর বেশি না। উত্তর ওর ডানে: রাখো [7, 8)।
5[7, 8): 1টাindex 7: 300Index 7-এ 300, যেটা 300-এর বেশি না। উত্তর ওর ডানে: রাখো [8, 8)।
6[8, 8): কিছুই নাকিছু নাআর কিছু বাকি নেই: upper_bound থামল index 8-এ, শেষ 300-টার ঠিক পরের 350-এ। তুলনা লাগল 4টা।

upper_bound চায় প্রথম দামটা, যেটা 300-এর বেশি। প্রথম দুইবার ও একই জায়গা দেখে, তারপর 300-গুলো পেরিয়ে ডানে যেতে থাকে, আর থামে index 8-এ। তাই একই তালিকায় দুই খোঁজা থামে 300-গুলোর দুই মাথায়: ওদের আগে index 5, ওদের পরে index 8।

প্রথম দিনেই যে syntax লাগবে

সাজানো range-এ চারটা খোঁজা

#include <algorithm>

binary_search(v.begin(), v.end(), x)   true if x is there, false if not
lower_bound(v.begin(), v.end(), x)     the first position whose element is not less than x
upper_bound(v.begin(), v.end(), x)     the first position whose element is greater than x
equal_range(v.begin(), v.end(), x)     both positions at once, as a pair
it - v.begin()                         a position turned into an index
it == v.end()                          the position past the last element: nothing there
  • চারটাই থাকে <algorithm> header-এ, আর নেয় একটা range: দুইটা iterator, শুরু আর শেষের ঠিক পরের জায়গা, Module 2 যেমন দেখিয়েছে।
  • Range-টা আগে থেকেই ছোট থেকে বড় সাজানো থাকতে হবে, ঠিক সেই < দিয়ে, যেটা খোঁজাটা ব্যবহার করে।
  • binary_search ফেরত দেয় একটা bool। বাকি তিনটা ফেরত দেয় জায়গা: iterator, মানে একটা জায়গার সেই চিহ্ন, যেটা Module 2-এ দেখেছ।
  • জায়গাটা v.end()-ও হতে পারে, মানে "শেষ element-এর পরে"। *it পড়ার আগে এটা check করো।

তাই একটা খোঁজা উত্তর দেয় "আছে তো?", আর বাকি তিনটা উত্তর দেয় "কোথায়?"।

binary_search বলে হ্যাঁ বা না, এর বেশি কিছু না

binary_search একটাই প্রশ্নের উত্তর দেয়: range-এর কোথাও x-এর সমান কোনো element আছে কি? Print করলে true আসে 1 হয়ে, false আসে 0 হয়ে।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 150, 200, 240, 300, 300, 300,
                      350, 420, 450, 480, 520, 600, 640, 700};
    cout << "a book at 300? " << binary_search(price.begin(), price.end(), 300) << '\n';
    cout << "a book at 310? " << binary_search(price.begin(), price.end(), 310) << '\n';
    return 0;
}
a book at 300? 1
a book at 310? 0

300 টাকার বই আছে, 310 টাকার নেই। কিন্তু উত্তর ওখানেই শেষ। 300-টা কোথায়, কয়টা আছে, বা 310 কোথায় বসবে, কিছুই বলে না। নতুনরা জায়গা খুঁজতে binary_search ধরে, অথচ ওর কাছে দেওয়ার মতো কোনো জায়গাই নেই। তাই সাদামাটা হ্যাঁ-না-র জন্য এটা নাও, আর জায়গা চাইলে lower_bound-কে জিজ্ঞেস করো।

lower_bound আর upper_bound: 300-গুলোর দুই পাশে দুইটা বেড়া

ভাবো, পাশাপাশি দুইটা দামের মাঝখানে একটা বেড়া দাঁড় করানো যায়। lower_bound বেড়াটা বসায় প্রথম সেই element-এর ঠিক আগে, যেটা x-এর কম না। upper_bound বসায় শেষ সেই element-এর ঠিক পরে, যেটা x-এর বেশি না। দুই বেড়ার মাঝে থাকে ঠিক x-এর কপিগুলো।

তিনটা 300-এর দুই পাশে দুইটা বেড়া: lower_bound আর upper_bound 300-গুলোর দুই পাশে দুইটা বেড়া 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 120 150 150 200 240 300 300 300 350 420 450 480 520 600 640 700 end() lower_bound(300) = index 5 upper_bound(300) = index 8 8 - 5 = 300-এর 3টা কপি বেড়া দাঁড়ায় দুইটা দামের মাঝখানে, তাই bound হলো একটা জায়গা, কোনো দাম না।

এই যে দুই বেড়া, একটা program হিসেবে। প্রতিটা জায়গা থেকে price.begin() বিয়োগ করে index বানানো হয়েছে, আর program প্রতিটা বেড়ার ঠিক পরের দামটা পড়ে।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 150, 200, 240, 300, 300, 300,
                      350, 420, 450, 480, 520, 600, 640, 700};
    auto first = lower_bound(price.begin(), price.end(), 300);
    auto after = upper_bound(price.begin(), price.end(), 300);
    cout << "lower_bound(300): index " << first - price.begin() << ", price " << *first << '\n';
    cout << "upper_bound(300): index " << after - price.begin() << ", price " << *after << '\n';
    cout << "copies of 300: " << after - first << '\n';
    return 0;
}
lower_bound(300): index 5, price 300
upper_bound(300): index 8, price 350
copies of 300: 3

lower_bound থামল index 5-এ, আর ওখানে একটা 300 পেল। upper_bound থামল index 8-এ, 350-এ, শেষ 300-টার এক ঘর পরে। দুইয়ের তফাত 3: 300 টাকার বই তিনটা। তাই x কয়টা আছে, সেটা সবসময় upper বিয়োগ lower। x না থাকলে উত্তর 0, কারণ তখন দুইটা বেড়াই দাঁড়ায় একই ফাঁকে।

equal_range: এক call-এ দুইটা বেড়াই

equal_range দুইটা বেড়া একসাথে ফেরত দেয়, একটা pair হিসেবে: .first হলো lower bound, আর .second upper-টা। Module 1-এর pair এখানে হুবহু একই।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 150, 200, 240, 300, 300, 300,
                      350, 420, 450, 480, 520, 600, 640, 700};
    for (int x : {300, 310}) {
        auto range = equal_range(price.begin(), price.end(), x);
        cout << x << " at " << range.first - price.begin() << " to "
             << range.second - price.begin() << ", count "
             << range.second - range.first << '\n';
    }
    return 0;
}
300 at 5 to 8, count 3
310 at 8 to 8, count 0

300-এর জন্য pair-টা ছড়িয়ে আছে index 5 থেকে 8, তিনটা বই। 310-এর জন্য pair-এর দুই অংশই index 8: একটা খালি অংশ, গোনা 0, ঠিক সেই ফাঁকে, যেখানে 310 টাকার বই বসত। তাই equal_range মানে এক call-এ lower_bound আর upper_bound, আর ওর তুলনাগুলো গুনবে lesson 02।

জায়গা থেকে index, আর end() check

জায়গা হলো একটা iterator, কোনো সংখ্যা না। Vector-এ it - v.begin() ওকে index বানায়: ওর আগে কয়টা element আছে। প্রতিটা element x-এর কম হলে lower_bound ফেরত দেয় v.end(), শেষ element-এর পরের জায়গা। ওখানে কোনো element নেই, তাই *it পড়ার আগে v.end()-এর সাথে মিলিয়ে নাও।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 150, 200, 240, 300, 300, 300,
                      350, 420, 450, 480, 520, 600, 640, 700};
    for (int x : {310, 100, 800}) {
        auto it = lower_bound(price.begin(), price.end(), x);
        int index = it - price.begin();
        if (it == price.end()) {
            cout << x << ": nothing costs that much; it goes last, at index " << index << '\n';
        } else {
            cout << x << ": goes at index " << index << ", before the " << *it << "-taka book\n";
        }
    }
    return 0;
}
310: goes at index 8, before the 350-taka book
100: goes at index 0, before the 120-taka book
800: nothing costs that much; it goes last, at index 16

310 টাকার বই বসবে index 8-এ, 300-গুলো আর 350-এর মাঝখানে। 100 টাকার বই বসত সবার আগে। তালিকায় 800 বা তার বেশি দামের কিছুই নেই, তাই lower_bound ফেরত দিল end(), আর index 16 হলো তালিকার size। তাই জায়গার বেলায় C++-এর "পাওয়া যায়নি" হলো end(), যেখানে অন্য অনেক ভাষা -1 ব্যবহার করে।

খরচ কত: O(log n)

প্রতিবার দেখায় বাকি অংশ অর্ধেক হয়। তাই কতবার দেখতে হবে, সেটা মোটামুটি n-কে কতবার অর্ধেক করলে একটা জায়গা বাকি থাকে, তত। সেই সংখ্যাটা হলো n-এর base-2 logarithm, লেখা হয় log n। খরচটা O(log n), পড়ে "order log n"। সামনে থেকে পড়া মানে O(n): সবচেয়ে খারাপ বেলায় প্রতিটা element একবার করে।

Table-এ আছে GCC 12-এর lower_bound সত্যিই কয়টা তুলনা করেছে। একটা গোনার comparator -1 থেকে 2n পর্যন্ত প্রতিটা x খুঁজেছে, 0, 2, 4, এভাবে ভরা vector-এ। গোনাগুলো এসেছে Compiler Explorer-এ একটা run থেকে, GCC 12.2, g++ -O2 -std=c++17, আর একই input সবসময় একই গোনা দেয়।

Element nসামনে থেকে পড়া, সবচেয়ে খারাপ বেলায়lower_bound, মেপে সবচেয়ে কমlower_bound, মেপে সবচেয়ে বেশি
161645
10001000910
100000010000001920

দশ লাখ দামের জন্য বড়জোর 20টা তুলনা লাগে, কারণ 20টা 2 পরপর গুণ করলে হয় 1048576। তালিকা হাজার গুণ লম্বা করায় তুলনা বাড়ল মাত্র 10টার মতো। তাই range সাজানো থাকলে binary search লম্বা হাঁটাকে বানিয়ে দেয় ছোট্ট কয়েকটা লাফ।

Example 1: কোন বাস route স্কুলে থামে

সবচেয়ে ছোট program: route নম্বরের একটা বাঁধা তালিকা, আগে থেকেই সাজানো, আর চারটা হ্যাঁ-না প্রশ্ন।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> route{3, 8, 11, 17, 24, 31, 40};   // already sorted
    for (int r : {17, 18, 3, 41}) {
        bool stops = binary_search(route.begin(), route.end(), r);
        cout << "route " << r << ": " << (stops ? "stops here" : "does not stop") << '\n';
    }
    return 0;
}
route 17: stops here
route 18: does not stop
route 3: stops here
route 41: does not stop

Route 17 আর 3 তালিকায় আছে; 18 আর 41 নেই। উত্তরটা একটা bool, রাখা হয়েছে stops-এ, আর print হয়েছে কথায়। তালিকা ক্রমেই লেখা হয়েছিল, তাই sort করার দরকার পড়েনি।

Run in Compiler
Example 2: প্রতিটা তাপমাত্রার দুই bound

David এক সপ্তাহের তাপমাত্রা লিখে সাজিয়ে রেখেছে। তিনটা মাপের জন্য program দুই bound-ই index হিসেবে print করে, আর মাঝের কপিগুলোও গোনে।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> temp{18, 21, 21, 21, 24, 26, 26, 30};   // a week of readings, sorted
    for (int x : {21, 26, 25}) {
        int lo = lower_bound(temp.begin(), temp.end(), x) - temp.begin();
        int hi = upper_bound(temp.begin(), temp.end(), x) - temp.begin();
        cout << x << ": lower " << lo << ", upper " << hi << ", copies " << hi - lo << '\n';
    }
    return 0;
}
21: lower 1, upper 4, copies 3
26: lower 5, upper 7, copies 2
25: lower 5, upper 5, copies 0

21 আছে তিনবার, index 1 থেকে 3-এ, তাই ওর বেড়া 1 আর 4। 25 একবারও নেই, তাই দুই বেড়াই দাঁড়ায় index 5-এ, 24 আর 26-এর মাঝে, আর গোনা 0। Index বানানোর - temp.begin() প্রতিটা call-এর পরে একবার করে লেখা।

Run in Compiler
Example 3: Zara-র বানানের তালিকা

Zara ওর spelling bee-র শব্দগুলো বর্ণমালার ক্রমে রাখে। String-এর উপরেও একই খোঁজা চলে, কারণ < string তুলনা করে অভিধানের ক্রমে। না থাকা শব্দের বেলায় program বলে দেয়, ওটা কোথায় বসত।

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
    vector<string> words{"anchor", "bridge", "candle", "garden", "island",
                         "lantern", "meadow", "puzzle", "rocket", "violin"};
    for (string w : {"garden", "gardens", "harbor", "zebra"}) {
        if (binary_search(words.begin(), words.end(), w)) {
            cout << w << ": on the list\n";
        } else {
            auto it = lower_bound(words.begin(), words.end(), w);
            cout << w << ": not there; it would go at index " << it - words.begin();
            if (it != words.end()) {
                cout << ", before " << *it;
            }
            cout << '\n';
        }
    }
    return 0;
}
garden: on the list
gardens: not there; it would go at index 4, before island
harbor: not there; it would go at index 4, before island
zebra: not there; it would go at index 10

"garden" আছে, কিন্তু "gardens" নেই: খোঁজা চায় হুবহু সমান, কাছাকাছি না। "gardens" আর "harbor" দুটোই বসত index 4-এ, "island"-এর আগে। "zebra" আসে সব শব্দের পরে, তাই lower_bound ফেরত দিল end(), আর program পাশের শব্দ print করার আগে সেটা check করে নিল। আসলে তুমি এই রূপটাই লিখবে: আগে হ্যাঁ-না জিজ্ঞেস করো, তারপর জিজ্ঞেস করো কোথায়।

Run in Compiler

এটা কোথায় কাজে লাগে

  • git bisect. তুমি Git-কে বলো একটা commit-এ bug আছে, আর তার আগের একটা commit-এ নেই। Git ওই range-এর মাঝখানের একটা commit বেছে নেয়, আর তুমি সেটা test করে বলো ভালো না খারাপ। এভাবে range অর্ধেক হতে থাকে, যতক্ষণ না bug নিয়ে আসা commit-টাই বাকি থাকে।
  • Python-এর bisect module. bisect_left ফেরত দেয় x ঢোকানোর জায়গা, সমান entry-গুলোর আগে: C++-এর lower_bound। bisect_right দেয় ওদের পরের জায়গা: upper_bound।
  • Java-র Arrays.binarySearch. Key থাকলে ও ফেরত দেয় key-এর index। না থাকলে ফেরত দেয় -(insertion point) - 1, একটা negative সংখ্যা, যার ভেতরে ঢোকানোর জায়গাটা লুকানো। C++ এই কাজটা দুইটা call-এ ভাগ করে: হ্যাঁ-না-র জন্য binary_search, আর জায়গার জন্য lower_bound।

যে ভুলগুলো সবাই করে

১. সাজানো না এমন vector-এ খোঁজা।

vector<int> cleaned{204, 211, 207, 202, 215, 209};   // in the order Alice cleaned them
for (int room : {207, 202, 215, 211}) {
    cout << room << ": " << (binary_search(cleaned.begin(), cleaned.end(), room) ? "cleaned" : "not yet") << '\n';
}

Runner-এর flag-এ কোনো বার্তা নেই। চারটা room-এর প্রতিটার জন্য print হলো not yet, অথচ Alice প্রতিটাই পরিষ্কার করেছে। খোঁজাটা এমন একটা তালিকা অর্ধেক করেছে, যার কোনো ক্রম নেই, তাই room-গুলো যে অর্ধেকে ছিল, সেটাই কেটে দিয়েছে। আগে Module 12-এর sort দিয়ে সাজাও, নয়তো তালিকা বাড়ার সময়ই সাজিয়ে রাখো। এই ভুলটা তুমি করবে, কারণ সংখ্যার একটা vector দেখতে খোঁজার মতোই লাগে, আর কেউ আপত্তি করে না।

২. উত্তরটা -1-এর সাথে মেলানো।

vector<int> price{120, 150, 200, 300, 350};
if (lower_bound(price.begin(), price.end(), 310) == -1) {
    cout << "no such book\n";
}

Runner-এর flag-এ error: error: no match for 'operator==' (operand types are '__gnu_cxx::__normal_iterator<int*, std::vector<int> >' and 'int')। lower_bound ফেরত দেয় একটা জায়গা, একটা iterator, আর iterator কখনো সংখ্যা না। "পাওয়া যায়নি"-র check হলো it == price.end(); x নিজেই আছে কি না জানতে চাইলে *it == x-ও check করো। lower_bound(...) - price.begin() == -1 লিখলে compile হয়, কিন্তু কখনো true হয় না, কারণ lower_bound-এর index থাকে 0 থেকে size পর্যন্ত। তুমি -1 লিখবে, কারণ C-র function-গুলো আর Java "পাওয়া যায়নি" বোঝাতে negative সংখ্যা দেয়।

৩. it যখন end(), তখন *it পড়া।

vector<int> price{120, 150, 200, 300, 350};
auto it = lower_bound(price.begin(), price.end(), 800);
cout << "index " << it - price.begin() << '\n';
cout << "price " << *it << '\n';

Runner-এর flag-এ কোনো বার্তা নেই। Compiler Explorer-এ একটা run-এ print হলো index 5, তারপর price 0, এমন একটা দাম, যেটা তালিকায় নেই। Index 5 হলো end(), আর ওখানে পড়া undefined behaviour: যেকোনো value আসতে পারে, crash-ও হতে পারে। *it-এর আগে it != price.end() check করো। তুমি check-টা বাদ দেবে, কারণ তোমার test-এর value-গুলো সবসময় তালিকায় থাকে।

৪. <algorithm> লিখতে ভুলে যাওয়া।

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> price{120, 150, 200, 300, 350};
    cout << lower_bound(price.begin(), price.end(), 300) - price.begin() << '\n';
    cout << upper_bound(price.begin(), price.end(), 300) - price.begin() << '\n';
    return 0;
}

Runner-এর flag-এ error: error: 'upper_bound' was not declared in this scope। অদ্ভুত ব্যাপার হলো, upper_bound-এর লাইনটা সরিয়ে দিলে program compile হয়, আর print করে 3। GCC 12-এর <vector> ঘটনাচক্রে lower_bound-কে সাথে নিয়ে আসে, বাকি তিনটাকে না। এটা একটা library-র কাকতালীয় ব্যাপার, কোনো নিয়ম না, তাই সবসময় <algorithm> লেখো। তুমি এটা ভুলবে, কারণ প্রথম যে খোঁজাটা লিখেছিলে, সেটা ওটা ছাড়াই চলেছিল।

মাথা খাটাও

Amara-র 16টা দামের উপর Bob চালায় lower_bound(price.begin(), price.end(), 100), তারপর lower_bound(price.begin(), price.end(), 800)। প্রতিটা খোঁজা কয়টা তুলনা করে? দুই উত্তরের তফাত ঠিক একটা। Widget দিয়ে গোনার আগেই বলো, কেন।

Widget-এর নিয়মটা দুই মাথা থেকে চালিয়ে দেখো। মাঝের element x-এর কম না হলে কয়টা element বাকি থাকে? কম হলে মাঝের element-টা নিজে কি বাকি থাকে?

অনুশীলন ১সহজ

বাস স্টেশনের হারানো-প্রাপ্তি desk চালায় Zara। পাওয়া প্রতিটা জিনিসে একটা নম্বর লেখা tag লাগানো হয়, আর দুইটা জিনিসের নম্বর একই হতে পারে। যাত্রীরা আসে claim ticket নিয়ে: ticket-এর নম্বরের কোনো জিনিস desk-এ থাকলে claim ঠিক।

Input. এক লাইনে n, তারপর n-টা tag, জিনিসগুলো যে ক্রমে পাওয়া গেছে সেই ক্রমে। তারপর এক লাইনে q, তারপর q-টা ticket নম্বর।

Output. প্রতিটা ticket-এর জন্য এক লাইন, ক্রম ধরে: ওই tag-এর কোনো জিনিস desk-এ থাকলে yes, না থাকলে no।

Constraints. 1 <= n <= 200000, 1 <= tag <= 999999, আর একই tag বারবার আসতে পারে। 1 <= q <= 200000।

Sample. Input 6, তারপর 512 77 3020 77 918 45, তারপর 4, তারপর 918 100 77 3021 দিলে yes, no, yes আর no।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n = 0;
    cin >> n;
    vector<int> tags(n);   // the tags in the order the items were found
    for (int i = 0; i < n; i++) {
        cin >> tags[i];
    }
    // Get tags ready for a binary search, once.

    int q = 0;
    cin >> q;
    for (int i = 0; i < q; i++) {
        int ticket = 0;
        cin >> ticket;
        // Print yes if an item with this tag is at the desk, no if not.
    }
    return 0;
}

lost-and-found নামে grade হয়। এটা এই module-এর problem set-এর একটা free problem। Hidden test-এ আছে একটামাত্র tag, অনেকবার ফিরে আসা নম্বরসহ 60000টা tag, আর এমন ticket, যেগুলো সবচেয়ে ছোট tag-এর চেয়ে ছোট আর সবচেয়ে বড় tag-এর চেয়ে বড়। একটা test-এ আছে 200000টা tag আর 200000টা ticket, আর কোনো ticket-এর জিনিসই desk-এ নেই।

Run in Compiler
অনুশীলন ২মাঝারি

Alice বাসে ওঠে ওর বাড়ির সামনের stop থেকে। Timetable-এ লেখা প্রতিটা বাস কখন ছাড়ে, মাঝরাতের পর থেকে কত মিনিটে, আর প্রতিদিন একই। Alice stop-এ পৌঁছায় t মিনিটে। ওকে কতক্ষণ দাঁড়াতে হবে? ঠিক t মিনিটে যে বাস ছাড়ে, সেটাও ওকে নিয়ে যায়।

Input. এক লাইনে n, তারপর বাস ছাড়ার n-টা সময়, বাড়তে থাকা ক্রমে। তারপর এক লাইনে q, তারপর পৌঁছানোর q-টা সময়।

Output. প্রতিবার পৌঁছানোর জন্য এক লাইন: Alice কত মিনিট দাঁড়ায়। সেদিন t বা তার পরে আর কোনো বাস না থাকলে ও দাঁড়ায় পরের দিনের প্রথম বাসের জন্য।

Constraints. 1 <= n <= 1440, আর সময়গুলো 0 থেকে 1439-এর আলাদা আলাদা পূর্ণসংখ্যা। 1 <= q <= 100000, আর পৌঁছানোর প্রতিটা সময় 0 থেকে 1439।

Sample. বাস ছাড়ে 360 420 600 1080 1320-এ, আর পৌঁছানো 400 600 1400 0 দিলে 20, 0, 400 আর 360। 1400 মিনিটে শেষ বাস চলে গেছে, আর পরেরটা ছাড়ে 40 + 360 = 400 মিনিট পরে।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> leaves(n);   // departure times in minutes after midnight, sorted
    for (int i = 0; i < n; i++) {
        cin >> leaves[i];
    }
    int q;
    cin >> q;
    for (int i = 0; i < q; i++) {
        int t;
        cin >> t;
        // Print how many minutes Alice waits for the next bus. If no bus
        // leaves at t or later today, she waits for the first bus tomorrow.
    }
    return 0;
}

আলাদা করে grade হয় না। t বা তার পরের প্রথম বাসটা খুঁজে দেয় lower_bound। ও end() ফেরত দিলে পরের বাস হলো কালকের প্রথমটা, আর একটা দিনে 1440 মিনিট।

Run in Compiler
অনুশীলন ৩কঠিন

Amara আর Bob দুজনেই খাতা বিক্রি করে, প্রত্যেকের একটা সাজানো দামের তালিকা আছে। Maria দুজনের কাছ থেকে একটা করে খাতা কিনতে চায়, এমন দামে, যেগুলো যত কাছাকাছি সম্ভব। তাহলে কোনো বন্ধুরই মনে হবে না যে অন্যজন বেশি সুবিধা পেল। সবচেয়ে কম তফাত কত পাওয়া যায়?

Input. এক লাইনে n, তারপর Amara-র n-টা দাম, বাড়তে থাকা ক্রমে। তারপর এক লাইনে m, তারপর Bob-এর m-টা দাম, বাড়তে থাকা ক্রমে।

Output. এক লাইন: Amara-র একটা দাম আর Bob-এর একটা দামের মধ্যে সবচেয়ে কম তফাত।

Constraints. 1 <= n, m <= 100000, আর প্রতিটা দাম 1 থেকে 1000000000। একটা তালিকায় একই দাম বারবার থাকতে পারে।

Sample. Amara-র 120 300 450 700 আর Bob-এর 210 380 690 দিলে 10: Amara-র 700 আর Bob-এর 690।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<long long> amara(n);   // Amara's prices, sorted
    for (int i = 0; i < n; i++) {
        cin >> amara[i];
    }
    int m;
    cin >> m;
    vector<long long> bob(m);   // Bob's prices, sorted
    for (int i = 0; i < m; i++) {
        cin >> bob[i];
    }

    // Find the smallest difference between one of Amara's prices and one
    // of Bob's, and print it.

    return 0;
}

আলাদা করে grade হয় না। Lesson যে কথাটা শুধু ইঙ্গিতে বলেছে: Amara-র একটা দাম Bob-এর তালিকায় কোথায় বসত, সেটা দেখায় lower_bound, ওখানকার সবচেয়ে কাছের দুই প্রতিবেশীর মাঝখানে। দুই প্রতিবেশীকেই check করো, আর তালিকার দুই মাথার কথা মাথায় রাখো।

Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • Range সাজানো থাকতেই হবে কেন? Library নিজে check করে নিতে পারে না?

    Check করা মানে প্রতিটা element পড়া, আর সেটাই সেই O(n) হাঁটা, যেটা এড়াতেই binary search। তাই library তোমাকে বিশ্বাস করে। সাজানো না এমন range-এও ও অর্ধেক করতে থাকে, আর উত্তরগুলো সোজা ভুল আসে, যেমন ভুল ১ দেখাল।

  • lower_bound কি value-টা খুঁজে দেয়?

    ও খুঁজে দেয় একটা জায়গা: প্রথম জায়গা, যার element x-এর কম না। x থাকলে ওই জায়গায় থাকে x-এর প্রথম কপি। না থাকলে থাকে পরের বড় element, নয়তো জায়গাটা end()। তাই value নিজেই দরকার হলে check করো it != v.end() && *it == x।

  • x কয়েকবার থাকলে binary_search কোনটা খুঁজে পায়?

    বিশেষ কোনোটাই না: ও শুধু হ্যাঁ বলে। x থাকলে প্রথম কপিটা দেয় lower_bound, আর শেষটা upper_bound - 1।

  • Binary search কি সবসময় সামনে থেকে পড়ার চেয়ে দ্রুত?

    হাতে গোনা কয়েকটা element-এর বেলায় না। ছোট হাঁটায় খরচ সামান্য, আর binary search-এর প্রতিবার দেখা memory-র এদিক-ওদিক লাফায়। আমাদের compiler-এ কোথায় গিয়ে পাল্লা উল্টে যায়, সেটা মাপবে lesson 04।

মূল কথা

  • Binary search বাকি অংশের মাঝখানের সাথে তুলনা করে, আর যে অর্ধেকে উত্তর থাকতেই পারে না, সেটা ফেলে দেয়।
  • ওর লাগে সাজানো range: সাজানো না হলে উত্তর ভুল আসে, কোনো বার্তা ছাড়াই।
  • binary_search বলে হ্যাঁ বা না, এর বেশি কিছু না; lower_bound হলো x-এর কম না এমন প্রথম জায়গা, upper_bound x-এর বেশি এমন প্রথম জায়গা, আর equal_range দুটোই।
  • it - v.begin() হলো index, it == v.end() মানে "ওখানে কিছু নেই", আর upper বিয়োগ lower গোনে x-এর কপি।
  • খরচ O(log n): মেপে দেখা গেছে, 16টা element-এ 4 থেকে 5টা তুলনা, আর দশ লাখে 19 থেকে 20টা।
  • আরও গভীরে যেতে চাইলে: Under the Hood, GCC-র অর্ধেক করার loop, branch আর cache (Pro)।

এরপর lesson 02 চারটা call-এর প্রতিটা রূপ দেখাবে, একটা একটা করে, প্রতিটার খরচ আর ফাঁদসহ।

lesson ১ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: Binary search-এর প্রতিটা call, একটা একটা করে, খরচসহ