Learn C++ STL

lesson ১ / ৯ · map আর multimap: key দিয়ে value খুঁজে পাওয়া

Module ৯ · map আর multimap: key দিয়ে value খুঁজে পাওয়া

map: প্রতিটা key-এর সাথে একটা value, আর key-গুলো থাকে সাজানো

Freeপড়া

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

  • একটা std::map declare করতে পারবে, m[k] = v দিয়ে একটা key-এর নিচে value রাখতে পারবে, m[k]++ দিয়ে গুনতে পারবে, আর প্রতিটা pair key-এর ক্রমে ফেরত পাবে।
  • count আর find দিয়ে key আছে কি না জিজ্ঞেস করতে পারবে, কিছু যোগ না করেই; পাওয়া value পড়বে it->second দিয়ে, আর একটা key erase করতে পারবে।
  • এক বাক্যে বলতে পারবে map কী, না থাকা key-এর জন্য m[k] পড়ার ফাঁদটা চিনবে, আর key বারবার এলে বেছে নেবে multimap।

Kenji-র café-তে প্রতিবার আসার জন্য একটা করে loyalty stamp মেলে। কেউ এলেই counter-কে জানতে হয়, এই customer-এর এখন কয়টা stamp। দোকান বন্ধের সময় Kenji চায় প্রতিটা customer আর তার stamp, বর্ণমালার ক্রমে। (name, stamps) pair-এর একটা vector হলে প্রতিটা প্রশ্নে পুরো তালিকা ঘাঁটতে হয়, Module 2-এর find দিয়ে। এই lesson Kenji-কে এমন একটা container দেবে, যেটা customer আসতে আসতেই দুটো কাজ সেরে ফেলে।

Key আর ওর value

Map হলো pair-এর একটা container, Module 1-এর pair-এর মতো। প্রতিটা pair-এর প্রথম অংশ ওর key, যেটা ধরে তুমি খোঁজো: এখানে customer-এর নাম। দ্বিতীয় অংশ ওর value, key-এর সাথে যা থাকে: এখানে stamp-এর সংখ্যা। Map প্রতিটা key বড়জোর একবার রাখে, আর pair-গুলো সাজিয়ে রাখে key ধরে।

Array-ও "কয়টা stamp?" প্রশ্নের উত্তর দিতে পারে, তবে শুধু সংখ্যা দিয়ে: stamps[3], কখনো stamps["maria"] না। Map-এর key হতে পারে একটা নাম, একটা শব্দ, বা < দিয়ে তুলনা করা যায় এমন যেকোনো type। এই যে সাতবার আসার পরে Kenji-র stamp, দুইভাবে আঁকা।

Kenji-র stamp: সাজানো একটা table, আর প্রতিটা node-এ একটা pair-ওয়ালা একটা tree সাতবার আসার পরে Kenji-র stamp: একটা map, দুইটা ছবি হাঁটলে পাওয়া যায় সাজানো table: key value amara bob david kenji maria zara 1 2 1 1 1 1 ভিতরে রাখা: একটা tree, প্রতিটা node-এ একটা pair: maria 1 bob 2 zara 1 amara 1 kenji 1 david 1 Tree সাজানো শুধু key ধরে; প্রতিটা value থাকে ওর key-এর node-এই।

দুইটা ছবিতেই একই ছয়টা pair। Table-টা হলো map-এর উপর দিয়ে হাঁটলে যা ফেরত আসে। Tree-টা হলো map ওদের আসলে কীভাবে রাখে, আর পরের অংশ সেটা খুলে দেখাবে। তাই stamps["bob"] হলো Bob-এর value, 2, আর key-এর ক্রমে Bob-এর pair বসে Amara আর David-এর মাঝখানে।

ভেতরে: Module 8-এর tree, প্রতিটা node-এ একটা pair

Module 8 set রেখেছিল একটা balanced binary search tree-তে, যেখানে খোঁজা নিচে নামে এক level করে। Map-ও সেই একই tree। তফাত শুধু node-এ কী থাকে: key আর ওর value একসাথে, একটা pair হিসেবে। Tree সাজানো শুধু key ধরে, আর value থাকে ওর সাথে সাথে।

C++ standard কোনো tree-র নাম বলে না। ও শুধু কথা দেয়, একটা key খোঁজা, যোগ করা বা erase করায় লাগবে O(log n) ধাপ, যেখানে n হলো key-এর সংখ্যা। GCC-র library, libstdc++, রাখে একটা red-black tree, set-এর বেলায় যেমন রাখে। এই module-এর ছবিগুলোতে রংগুলো বাদ দেওয়া। তাই নামগুলো সাজানো থাকে, কারণ tree-ই ওদের সাজিয়ে রাখে, শেষে কেউ sort করে বলে না।

Stamp-গুলো আসতে দেখো

এই যে Kenji-র প্রথম সকাল, প্রতিবার আসার জন্য একটা call: maria, bob, zara, kenji, আবার bob, amara আর david। তারপর counter find দিয়ে zara আর carol-কে খোঁজে, আর শেষে carol-কে পড়ে [] দিয়ে। প্রতিটা ছবি GCC 12-এর library-র বানানো আসল আকার, Compiler Explorer-এ একটা run থেকে print করা। Value-গুলো আছে note-এ, আর widget-এর নিচের table-এ।

একটা map<string, int> stamps-এর উপর দশটা ধাপ। প্রতিটা tree লেখা হয়েছে level ধরে ধরে, root থেকে নিচের দিকে, দুই level-এর মাঝে একটা স্ল্যাশ দিয়ে, আর প্রতিটা pair লেখা ওর key আর value দিয়ে।

CallPair-গুলো, level ধরে ধরেকী হলো
stamps["maria"]++maria 1maria হলো প্রথম key, তাই ওর node-ই হয় root, সাথে 1টা stamp।
stamps["bob"]++maria 1 / bob 1bob আসে maria-র আগে, তাই ওর node ঝোলে maria-র বাঁয়ে, সাথে 1টা stamp।
stamps["zara"]++maria 1 / bob 1, zara 1zara আসে maria-র পরে, তাই ওর node ঝোলে ডানে, সাথে 1টা stamp।
stamps["kenji"]++maria 1 / bob 1, zara 1 / kenji 1kenji আসে maria-র আগে আর bob-এর পরে, তাই ওর node ঝোলে bob-এর ডানে।
stamps["bob"]++maria 1 / bob 2, zara 1 / kenji 1পথে আগে maria-র সাথে তুলনা, তারপর bob পাওয়া গেল। নতুন কোনো node না: bob-এর এখন 2টা stamp।
stamps["amara"]++maria 1 / bob 2, zara 1 / amara 1, kenji 1amara আসে maria-র আগে, bob-এরও আগে, তাই ওর node ঝোলে bob-এর বাঁয়ে।
stamps["david"]++maria 1 / bob 2, zara 1 / amara 1, kenji 1 / david 1david যায় maria-তে বাঁয়ে, bob-এ ডানে, kenji-তে বাঁয়ে। 6 জন customer, level 4টা।
stamps.find("zara")maria 1 / bob 2, zara 1 / amara 1, kenji 1 / david 12টা তুলনাতেই পাওয়া গেল: zara-র 1টা stamp। find কিছুই বদলায় না।
stamps.find("carol")maria 1 / bob 2, zara 1 / amara 1, kenji 1 / david 1carol বসত david-এর বাঁয়ে, আর ওই জায়গাটা খালি: পাওয়া যায়নি। find ফেরত দেয় end(), কিছুই বদলায় না।
stamps["carol"]maria 1 / bob 2, zara 1 / amara 1, david 1 / carol 0, kenji 1[] দিয়ে পড়লেও পথ একই, তারপর carol যোগ হয় 0টা stamp নিয়ে, আর tree ঘুরে david উপরে উঠে আসে। এখন key 7টা, পড়ার আগের চেয়ে একটা বেশি।

Bob দ্বিতীয়বার আসায় পথটা ওর node পর্যন্ত নামল, আর বাড়ল শুধু ওর গোনাটা। find("carol") david-এর নিচে গিয়ে tree থেকে পড়ে গেল, কিছুই বদলাল না। শেষ ধাপটাই এই lesson-এর ফাঁদ: [] দিয়ে একবার পড়তেই carol যোগ হয়ে গেল 0টা stamp নিয়ে, আর জায়গা করতে tree ঘুরে গেল। তাই map বাড়ে শুধু নতুন key রাখলে, আর [] একটা key রেখে দেয়, তুমি শুধু পড়তে চাইলেও।

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

std::map আর std::multimap, আর ওদের রোজকার call-গুলো

#include <map>

map<K, V> m;                      an empty map from K keys to V values
map<K, V> m{{k1, v1}, {k2, v2}};  a map made from a list of pairs
m[k] = v;                         store v under k, or overwrite k's value
m[k]++;                           add 1 to k's value; a missing k starts at 0
m.count(k)                        1 if k is there, 0 if not; adds nothing
m.find(k)                         an iterator to k's pair, or m.end()
it->first   it->second            the key and the value of that pair
m.erase(k);                       remove k and its value, if k is there
m.size()                          how many keys
for (const auto& [k, v] : m)      every pair, in key order

multimap<K, V> mm;                the same, but a key may repeat; no []
  • map<K, V>: angle bracket-এ দুইটা type, key-এর আর value-র। <map> header আনে map আর multimap দুটোই।
  • m[k] k-কে খোঁজে, না পেলে আগে k-কে একটা শূন্য value দিয়ে যোগ করে, তারপর k-এর value তোমার হাতে দেয় পড়তে বা বদলাতে। ও কখনো ব্যর্থ হয় না, আর ফাঁদটা ওখানেই।
  • count আর find শুধু দেখে। find ফেরত দেয় একটা iterator, মানে একটা জায়গার সেই চিহ্ন, যেটা Module 2-এ দেখেছ, আর m.end() মানে "পাওয়া যায়নি"।
  • হাঁটার সময় প্রতিটা pair পাওয়া যায় .first আর .second দিয়ে, নয়তো Module 1-এর structured binding দিয়ে, একসাথে দুইটা নামে।

তাই রোজকার map-এর code প্রতিবার একটা key-এর নাম বলে, আর pair কোথায় থাকবে, সেটা map নিজেই ঠিক করে।

m[k] value রাখে, আর না থাকা key শুরু হয় শূন্য থেকে

m[k] = v k-এর নিচে v রাখে। k নতুন হলে map আগে ওকে যোগ করে value type-এর শূন্য দিয়ে, C++-এ যেটা লেখা হয় V()। এই শূন্য একটা int-এর জন্য 0, আর একটা string-এর জন্য খালি string। তারপর = ওই শূন্যের উপর লিখে দেয়। m[k]++ আর m[k] += 2-ও একইভাবে কাজ করে, তাই গোনার আগে কিছুই সাজিয়ে নিতে হয় না।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> stamps;
    stamps["maria"] = 3;
    stamps["bob"]++;
    stamps["bob"]++;
    stamps["maria"] += 2;
    cout << "maria " << stamps["maria"] << ", bob " << stamps["bob"] << '\n';

    map<string, string> note;
    note["kenji"] += "tea";
    note["kenji"] += ", no sugar";
    cout << "kenji: " << note["kenji"] << '\n';
    cout << stamps.size() << " customers, " << note.size() << " note\n";
    return 0;
}
maria 5, bob 2
kenji: tea, no sugar
2 customers, 1 note

Bob-এর প্রথম ++ কোনো bob পায়নি, তাই map ওকে 0 দিয়ে যোগ করে 1-এ তুলল। Kenji-র note শুরু হয়েছিল একটা খালি string হিসেবে, আর += শব্দগুলো ওতে জুড়ে দিল। দুইজন customer, একটা note: যতবারই ছোঁয়া হোক, প্রতিটা key যোগ হয়েছে একবারই। তাই m[k]++ নিজেই গোনার পুরো program, কোনো check বা শুরুর মান ছাড়াই।

count আর find: কিছু যোগ না করে দেখা

m.count(k) বলে k key-ওয়ালা কয়টা pair আছে, আর map-এ সেটা 1 বা 0। m.find(k) ফেরত দেয় k-এর pair-এর দিকে একটা iterator, আর k না থাকলে m.end()। ওই iterator দিয়ে it->first হলো key, আর it->second হলো value। এদের কোনোটাই কখনো key যোগ করে না।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> stamps{{"bob", 2}, {"maria", 5}, {"zara", 1}};
    cout << stamps.count("zara") << ' ' << stamps.count("kenji") << '\n';

    auto it = stamps.find("maria");
    if (it != stamps.end()) {
        cout << it->first << " has " << it->second << " stamps\n";
    }
    if (stamps.find("kenji") == stamps.end()) {
        cout << "kenji is not a customer yet\n";
    }
    cout << "still " << stamps.size() << " customers\n";
    return 0;
}
1 0
maria has 5 stamps
kenji is not a customer yet
still 3 customers

দুইবার kenji-কে নিয়ে জিজ্ঞেস করার পরেও map-এ customer 3 জনই। it->second পড়ার আগে find-এর উত্তর end()-এর সাথে মিলিয়ে নাও; end() পড়লে একবার চালাতে কী print হয়েছিল, দেখাবে lesson 02। তাই সোজা হ্যাঁ বা না-এর জন্য count নাও, আর value-টাও লাগলে find।

ফাঁদ: m[k] পড়লেই k যোগ হয়ে যায়

m[k] পড়া আর লেখার তফাত বোঝে না। k না থাকলে ও আগে k-কে শূন্য value দিয়ে যোগ করে, একটা cout-এর ভিতরে হলেও। তাই যে map তুমি শুধু পড়ছ, সেটাও বেড়ে যেতে পারে।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> stamps{{"bob", 2}, {"maria", 5}};
    cout << "size before: " << stamps.size() << '\n';
    cout << "carol has " << stamps["carol"] << " stamps\n";
    cout << "size after:  " << stamps.size() << '\n';
    cout << "count(\"carol\") is now " << stamps.count("carol") << '\n';
    return 0;
}
size before: 2
carol has 0 stamps
size after:  3
count("carol") is now 1

একবার পড়তেই print হলো বিশ্বাস করার মতো একটা 0, আর carol চিরদিনের জন্য যোগ হয়ে গেল। Size 2 থেকে 3 হলো, আর count এখন ওকে খুঁজে পায়। Kenji-র দিনশেষের তালিকায় থাকত এমন একজন customer, যে কখনো আসেইনি। তাই শুধু জিজ্ঞেস করতে চাইলে জিজ্ঞেস করো count বা find দিয়ে।

Key-এর ক্রমে হাঁটা

একটা range-for map-এর উপর দিয়ে হাঁটে সবচেয়ে ছোট key থেকে সবচেয়ে বড় key পর্যন্ত, একবারে একটা pair। প্রতিটা pair-এর দুইটা অংশ: p.first, মানে key, আর p.second, মানে value। Module 1-এর structured binding দুই অংশেরই একসাথে নাম দেয়, তাই [name, count] পড়তে .first আর .second-এর চেয়ে সহজ।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> stamps;
    stamps["zara"] = 1;
    stamps["amara"] = 4;
    stamps["kenji"] = 2;

    for (const auto& p : stamps) {
        cout << p.first << ": " << p.second << '\n';
    }

    cout << "again:";
    for (const auto& [name, count] : stamps) {
        cout << ' ' << name << '=' << count;
    }
    cout << '\n';
    return 0;
}
amara: 4
kenji: 2
zara: 1
again: amara=4 kenji=2 zara=1

Key-গুলো ঢুকেছিল zara, amara, kenji ক্রমে, আর দুই loop-এই বেরোল বর্ণমালার ক্রমে। const auto& প্রতিটা pair যেখানে আছে সেখানেই পড়ে, copy না করে। তাই map-এর উপর দিয়ে প্রতিটা হাঁটাই key-এর ক্রমে হাঁটা।

Key ধরে erase

m.erase(k) k-কে ওর value-সহ সরিয়ে দেয়, আর ফেরত দেয় কয়টা pair সরাল। Map-এ সেটা 1 বা 0। নেই এমন key erase করলে কিছুই হয় না, কোনো error-ও না।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> stamps{{"bob", 2}, {"maria", 5}, {"zara", 1}};
    cout << "erase(\"bob\") removed " << stamps.erase("bob") << '\n';
    cout << "erase(\"carol\") removed " << stamps.erase("carol") << '\n';
    for (const auto& [name, count] : stamps) {
        cout << name << ' ' << count << '\n';
    }
    return 0;
}
erase("bob") removed 1
erase("carol") removed 0
maria 5
zara 1

Bob-এর pair চলে গেছে, হাঁটার সময় ওকে আর পাওয়া যায় না। আর carol-এর বেলায় 0 বলছে, ও কখনো ছিলই না। তাই নেই এমন key erase করা নিরাপদ, আর ওর উত্তরই বলে দেয় কিছু গেল কি না।

প্রতিটা key একবারই

Map কখনো একই key-এর দুইটা pair রাখে না। দ্বিতীয় m[k] = v প্রথম pair-টা খুঁজে পায় আর ওর value-র উপর লিখে দেয়। তাই সবচেয়ে নতুন লেখাটাই টিকে থাকে।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, string> phone;
    phone["maria"] = "555-0101";
    phone["bob"] = "555-0102";
    phone["maria"] = "555-0188";
    cout << phone.size() << " names\n";
    for (const auto& [name, number] : phone) {
        cout << name << ": " << number << '\n';
    }
    return 0;
}
2 names
bob: 555-0102
maria: 555-0188

Maria-র পুরোনো নম্বর আর নেই, আর map-এ নাম এখনো দুইটাই। তাই map মানায় সেই কাজে, যেখানে প্রতিটা key-এর একটাই চলতি value, যেমন বদলে যাওয়া একটা ফোন নম্বর।

multimap: যে map বারবার আসা key-ও রাখে

কখনো কখনো একটা key-এর কয়েকটা value লাগে: Bob-এর একটা বাসার নম্বর আছে, একটা অফিসের নম্বরও আছে। Multimap প্রতিটা pair রাখে, বারবার আসা key-সহ, তবু key ধরে সাজিয়ে। একই header-এ থাকে, আর insert, count, find আর erase-ও একই। ওর count(k) বলে k key-ওয়ালা কয়টা pair আছে।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    multimap<string, string> phones;
    phones.insert({"bob", "555-0102"});
    phones.insert({"maria", "555-0101"});
    phones.insert({"bob", "555-0177"});
    for (const auto& [name, number] : phones) {
        cout << name << ": " << number << '\n';
    }
    cout << "bob has " << phones.count("bob") << " numbers, " << phones.size() << " in all\n";
    return 0;
}
bob: 555-0102
bob: 555-0177
maria: 555-0101
bob has 2 numbers, 3 in all

Bob-এর দুইটা নম্বরই রইল, পাশাপাশি, যে ক্রমে এসেছিল সেই ক্রমে। Multimap-এ কোনো [] নেই, কারণ phones["bob"] বুঝতেই পারত না, Bob-এর কোন নম্বরের কথা বলছ।

multimap<string, string> phones;
phones["bob"] = "555-0102";

Runner-এর flag-এ GCC 12 থেমে যায় error: no match for 'operator[]' (operand types are 'std::multimap<std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >' and 'const char [4]') দিয়ে। Lesson 02 দেখাবে equal_range, যেটা একটা key-এর প্রতিটা value তোমার হাতে দেয়। তাই একটা key-এর কয়েকটা value লাগলে multimap নাও, আর একটা হলেই চললে map।

Example 1: key-এর ক্রমে ফলের দাম

সবচেয়ে ছোট map program: চারটা দাম ঢোকে যেকোনো ক্রমে, আর বেরোয় ফলের নাম ধরে সাজানো অবস্থায়।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> price;
    price["mango"] = 120;
    price["banana"] = 60;
    price["apple"] = 180;
    price["guava"] = 80;

    cout << price.size() << " fruits on the board:\n";
    for (const auto& [fruit, taka] : price) {
        cout << fruit << ' ' << taka << '\n';
    }
    return 0;
}
4 fruits on the board:
apple 180
banana 60
guava 80
mango 120

কেউ কিছু sort করেনি: হাঁটা চলে tree ধরে, তাই board-টা বেরোয় বর্ণমালার ক্রমে। চারবার [] দিয়ে লেখায় হলো চারটা pair, প্রতিটা ফলের জন্য একটা।

Run in Compiler
Example 2: এক সকালে Kenji-র stamp

Widget-এর সেই সাতবার আসা, [] দিয়ে গোনা। Counter Bob-কে check করে find দিয়ে, তারপর program দিনশেষের তালিকা print করে।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> stamps;
    for (string name : {"maria", "bob", "zara", "kenji", "bob", "amara", "david"}) {
        stamps[name]++;
    }

    auto it = stamps.find("bob");
    if (it != stamps.end()) {
        cout << "bob has " << it->second << " stamps\n";
    }

    cout << "closing list:\n";
    for (const auto& [name, count] : stamps) {
        cout << "  " << name << ' ' << count << '\n';
    }
    return 0;
}
bob has 2 stamps
closing list:
  amara 1
  bob 2
  david 1
  kenji 1
  maria 1
  zara 1

গোনাগুলো মিলে যায় carol-এর আগের widget ছবির সাথে: bob 2, বাকি সবাই 1। Counter-এর প্রশ্নে লেগেছে find, তাই জিজ্ঞেস করায় কেউ যোগ হয়নি। দশ লাখ customer থাকলেও প্রতিবার আসায় লাগত মোটে কয়েক ডজন ধাপ।

Run in Compiler
Example 3: Zara-র calling code-এর খাতা

Zara বেড়াতে যাওয়ার plan করছে, আর ওর কাছে দেশ থেকে ফোনের calling code-এর একটা map আছে। ও তিনটা দেশ খোঁজে, আর তার একটা ওর খাতায় নেই।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> calling_code{
        {"bangladesh", 880}, {"brazil", 55}, {"japan", 81},
        {"kenya", 254}, {"peru", 51}, {"spain", 34}};

    for (string country : {"japan", "chile", "kenya"}) {
        auto it = calling_code.find(country);
        if (it != calling_code.end()) {
            cout << country << ": +" << it->second << '\n';
        } else {
            cout << country << ": not found\n";
        }
    }
    cout << calling_code.size() << " countries in the book\n";
    return 0;
}
japan: +81
chile: not found
kenya: +254
6 countries in the book

Chile ছিল না, তাই find ফেরত দিল end(), আর program সেটাই বলল। খাতায় দেশ এখনো 6টা: কোনো খোঁজায় একটাও যোগ হয়নি। Map-টা বানানো pair-এর একটা brace list থেকে, প্রতিটা দেশের জন্য একটা pair।

Run in Compiler
Example 4: Amara-র contact, map বনাম multimap

Amara ওর টেবিলের ভিজিটিং কার্ডগুলো দুইটা contact list-এ তোলে। Bob ওকে দুইটা কার্ড দিয়েছিল, দুইটায় দুই নম্বর।

#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <vector>
using namespace std;

int main() {
    vector<pair<string, string>> cards{
        {"bob", "555-0102"}, {"zara", "555-0144"}, {"bob", "555-0177"}, {"amara", "555-0110"}};

    map<string, string> latest;
    multimap<string, string> every;
    for (const auto& [name, number] : cards) {
        latest[name] = number;
        every.insert({name, number});
    }

    cout << "map keeps " << latest.size() << ":\n";
    for (const auto& [name, number] : latest) {
        cout << "  " << name << ' ' << number << '\n';
    }
    cout << "multimap keeps " << every.size() << ":\n";
    for (const auto& [name, number] : every) {
        cout << "  " << name << ' ' << number << '\n';
    }
    return 0;
}
map keeps 3:
  amara 555-0110
  bob 555-0177
  zara 555-0144
multimap keeps 4:
  amara 555-0110
  bob 555-0102
  bob 555-0177
  zara 555-0144

Map প্রতিটা নামের জন্য একটা নম্বর রাখল, আর Bob-এর বেলায় রাখল শেষেরটা, কারণ ওর দ্বিতীয় latest[name] = number প্রথমটার উপর লিখে দিয়েছে। Multimap চারটা কার্ডই রেখেছে। প্রতিটা নামের একটাই নম্বর কি না নিশ্চিত না হলে Amara ঠিক এই program-টাই লিখবে।

Run in Compiler

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

  • Java-র TreeMap. Java-র সাজানো map একটা red-black tree, libstdc++ যে গঠন ব্যবহার করে সেটাই। ওর documentation containsKey, get, put আর remove-এর জন্য log(n) সময়ের কথা দেয়; map-এর খোঁজা, insert আর erase-এর জন্য C++ ঠিক এই কথাটাই দেয়।
  • SQLite-এর table. SQLite প্রতিটা সাধারণ table disk-এ রাখে একটা "table b-tree" হিসেবে, 64-bit-এর একটা integer key ধরে সাজিয়ে: row-টার rowid। rowid দিয়ে একটা row খোঁজা মানে ওই tree বেয়ে নামা, ঠিক map-এর find-এর মতো।
  • LevelDB-এর memtable. LevelDB, key-value জমা রাখার একটা library, ওর data রাখে key ধরে সাজিয়ে। প্রতিটা নতুন লেখা আগে একটা log-এ জমা হয়, তারপর যায় memory-র একটা table-এ, যার নাম memtable, আর সেটা key ধরে সাজানো একটা skip list। ওটা যথেষ্ট বড় হয়ে গেলে disk-এ লেখা হয় একটা সাজানো table হিসেবে। Skip list কোনো tree না, তবু map-এর কথাটা রাখে: ওর উপর দিয়ে হাঁটলে key-গুলো বেরোয় ক্রমে।

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

১. [] দিয়ে key আছে কি না check করা।

map<string, int> stamps{{"bob", 2}, {"maria", 5}};
if (stamps["zara"] == 0) {
    cout << "zara has no stamps yet\n";
}
for (const auto& [name, count] : stamps) {
    cout << name << ' ' << count << '\n';
}

Runner-এর flag-এ কোনো বার্তা নেই। Print হলো zara has no stamps yet, তারপর bob 2, maria 5 আর zara 0: check-টাই Zara-কে যোগ করে দিয়েছে, তাই দিনশেষের তালিকায় নাম উঠল এমন একজনের, যে কখনো আসেনি। বরং জিজ্ঞেস করো stamps.count("zara") == 0 দিয়ে। তুমি []-ওয়ালা রূপটাই লিখবে, কারণ ওটা পড়তে হুবহু প্রশ্নটার মতো।

২. Header লিখতে ভুলে যাওয়া।

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

int main() {
    map<string, int> stamps;
    stamps["maria"]++;
    cout << stamps.size() << '\n';
    return 0;
}

Runner-এর flag-এ error: error: 'map' was not declared in this scope, তারপর GCC 12 নিজেই সমাধান বলে দেয়: note: 'std::map' is defined in header '<map>'; did you forget to '#include <map>'?। multimap-এরও একই header লাগে। তুমি এটা ভুলবে, কারণ <string> আগে থেকেই উপরে আছে, আর map-টা যোগ হয়েছে পরে।

৩. Iterator দিয়ে key বদলাতে যাওয়া।

map<int, int> guests{{12, 4}, {15, 2}};
auto it = guests.find(12);
it->first = 13;

Runner-এর flag-এ error: error: assignment of read-only member 'std::pair<const int, int>::first'। Kenji একটা দলকে 12 নম্বর টেবিল থেকে 13 নম্বরে সরাতে চেয়েছিল। কিন্তু pair-টা tree-র কোথায় বসবে, সেটা ঠিক করে key-ই, তাই map প্রতিটা key দেয় const করে: read-only। পুরোনো key erase করো, আর value-টা রাখো নতুন key-এর নিচে; node রেখেই এই কাজ করার একটা উপায় দেখাবে lesson 02।

৪. যে ক্রমে যোগ করেছিলে, সেই ক্রম ফেরত চাওয়া।

map<string, int> course;
course["soup"] = 1;
course["rice"] = 2;
course["sweet"] = 3;
for (const auto& [dish, number] : course) {
    cout << dish << '\n';
}

কোনো বার্তা নেই, আর print হয় rice, soup আর sweet: বর্ণমালার ক্রমে, খাবারের পদের ক্রমে না। Key-গুলো কোন ক্রমে এসেছিল, map তার কোনো হিসাব রাখে না। ক্রমটা জরুরি হলে pair-এর একটা vector রাখো। তুমি ক্রমটা ফেরত আশা করবে, কারণ vector সবসময় সেটা রেখেছে।

মাথা খাটাও

Zara একটা শব্দের অক্ষরগুলো গোনে একটা map দিয়ে, যার key-গুলো char।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<char, int> letters;
    string word = "banana";
    for (char c : word) {
        letters[c]++;
    }
    for (const auto& [c, n] : letters) {
        cout << c << ' ' << n << '\n';
    }
    return 0;
}

Program লাইন ধরে ধরে কী print করে? চালানোর আগে অক্ষরগুলোর ক্রম বলো, আর কেন ঠিক ওই ক্রম, সেটাও।

char key তুলনা হয় ওর code দিয়ে। ASCII table-এ 'a', 'b' আর 'n'-এর code দেখে নাও।

অনুশীলন ১সহজ

David স্কুলের canteen চালায়। ওর দামের তালিকায় প্রতিটা খাবারের নাম আর টাকায় দাম লেখা, আর দুইটা খাবারের নাম কখনো এক না। সারা দিন ছাত্রছাত্রীরা order দেয়: একটা খাবার, আর কয়টা চাই। প্রতিটা order-এর দাম David-এর জানা দরকার, আর canteen বন্ধের সময় সারা দিনের মোট।

Input. এক লাইনে n, তারপর n-টা লাইন, প্রতিটায় একটা খাবার আর ওর দাম। তারপর এক লাইনে m, তারপর m-টা লাইন, প্রতিটায় একটা খাবার আর কয়টা। Order-এর প্রতিটা খাবারই দামের তালিকায় আছে।

Output. প্রতিটা order-এর জন্য এক লাইন, যে ক্রমে এসেছে সেই ক্রমে: ওর দাম, মানে খাবারের দাম গুণ কয়টা। তারপর আরেকটা লাইন: সব order মিলিয়ে মোট।

Constraints. 1 <= n, m <= 30000। একটা খাবারের নাম 1 থেকে 10টা ছোট হাতের ইংরেজি অক্ষর। 1 <= দাম <= 100000 আর 1 <= কয়টা <= 1000। মোট 2147483647 ছাড়িয়ে যেতে পারে।

Sample. Input 4, তারপর samosa 15, chai 10, cake 40 আর singara 12, তারপর 5, তারপর chai 2, cake 1, samosa 3, chai 1 আর singara 4 দিলে 20, 40, 45, 10, 48 আর 163। শেষ লাইনটা সারা দিনের মোট।

#include <iostream>
#include <map>
#include <string>
using namespace std;

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

    int n = 0;
    cin >> n;
    map<string, int> price;   // item name -> its price in taka
    for (int i = 0; i < n; i++) {
        string item;
        int taka = 0;
        cin >> item >> taka;
        // Remember that item costs taka.
    }

    int m = 0;
    cin >> m;
    long long total = 0;   // the day's total can pass 2147483647
    for (int i = 0; i < m; i++) {
        string item;
        int qty = 0;
        cin >> item >> qty;
        // Print the cost of this order, and add it to total.
    }

    // Print the day's total.

    return 0;
}

price-lookup নামে grade হয়। এটা এই module-এর problem set-এর একটা free problem। Hidden test-এ আছে একটা খাবার আর একটা order, একই খাবারের 1000টা order, আর এমন নাম, যেগুলো দিয়ে অন্য নাম শুরু হয়। আরও আছে সবচেয়ে বেশি দাম আর সবচেয়ে বেশি পরিমাণে 30000টা order, যার মোট 3000000000000।

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

Maria-র ক্লাস একই quiz দিয়েছে দুইবার, এক সপ্তাহ পরপর। কয়েকজন দুই দফার একটায় ছিল না। শিক্ষক চান সেই ছাত্রছাত্রীদের নাম, যারা দুই দফাতেই ছিল, দুইটা score-সহ, যাতে বোঝা যায় কে এগিয়েছে।

Input. এক লাইনে n, তারপর প্রথম দফার n-টা লাইন, প্রতিটায় একটা নাম আর একটা score। তারপর এক লাইনে m, তারপর দ্বিতীয় দফার m-টা লাইন।

Output. দুই দফাতেই ছিল এমন প্রত্যেকের জন্য এক লাইন, বর্ণমালার ক্রমে: নাম, প্রথম score আর দ্বিতীয় score। কেউ দুই দফাতে না থাকলে none শব্দটা print করো।

Constraints. 1 <= n, m <= 100000। একটা নাম 1 থেকে 10টা ছোট হাতের ইংরেজি অক্ষর, আর একই দফায় কোনো নাম দুইবার আসে না। প্রতিটা score 0 থেকে 100-এর একটা পূর্ণসংখ্যা।

Sample. Input 4, bob 70, maria 85, zara 60, kenji 90, তারপর 3, zara 75, maria 80 আর alice 66 দিলে maria 85 80 আর zara 60 75। Bob আর Kenji দ্বিতীয় দফায় ছিল না, আর Alice ছিল না প্রথমটায়।

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    int n;
    cin >> n;
    map<string, int> first;   // name -> score in the first round
    for (int i = 0; i < n; i++) {
        string name;
        int score;
        cin >> name >> score;
        first[name] = score;
    }
    int m;
    cin >> m;
    map<string, int> second;   // name -> score in the second round
    for (int i = 0; i < m; i++) {
        string name;
        int score;
        cin >> name >> score;
        second[name] = score;
    }

    // Walk first in key order. For every name that second also holds,
    // print the name and both scores. Print none if nobody sat both.

    return 0;
}

আলাদা করে grade হয় না। একটা map-এর উপর দিয়ে key-এর ক্রমে হাঁটো, আর প্রতিটা নামের উত্তর দিক অন্য map-এর find; হাঁটাটাই বর্ণমালার ক্রম দিয়ে দেয়।

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

মাস শেষে Kenji পুরস্কার দেয় সবচেয়ে বেশি stamp পাওয়া customer-কে। কয়েকজন সমান সমান হলে প্রত্যেকেই পুরস্কার পায়। ওর খাতায় প্রতিবার আসার জন্য একটা করে নাম লেখা।

Input. এক লাইনে n, তারপর n-টা নাম, প্রতিবার আসার জন্য একটা, মাঝে space বা নতুন লাইন।

Output. দুই লাইন। প্রথমে কোনো customer-এর সবচেয়ে বেশি কয়টা stamp। তারপর ঠিক ততগুলো stamp পাওয়া প্রত্যেক customer, বর্ণমালার ক্রমে, মাঝে একটা করে space।

Constraints. 1 <= n <= 100000। একটা নাম 1 থেকে 10টা ছোট হাতের ইংরেজি অক্ষর। Map দিয়ে গোনো, তারপর ওর উপর দিয়ে হাঁটো মাত্র একবার।

Sample. Input 7 আর bob zara bob maria zara kenji amara দিলে 2, তারপর bob zara। Bob আর Zara দুজনেই দুইবার করে এসেছে।

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

int main() {
    int n;
    cin >> n;
    map<string, int> stamps;
    for (int i = 0; i < n; i++) {
        string name;
        cin >> name;
        stamps[name]++;
    }

    // Walk stamps once. Keep the most stamps seen so far, and the names
    // that have that many. Then print the most stamps on one line, and
    // those names on the next, separated by single spaces.

    return 0;
}

আলাদা করে grade হয় না। Lesson যে কথাটা শুধু ইঙ্গিতে বলেছে: হাঁটার সময় নামগুলো আসে বর্ণমালার ক্রমে। এ পর্যন্ত সবচেয়ে বড় গোনাটা যাদের, তাদের নাম রাখো, আর আরও বড় গোনা পেলে তালিকাটা নতুন করে শুরু করো।

Run in Compiler

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

  • আমি তো অন্য ক্রমে key যোগ করেছিলাম, তাহলে ওগুলো সাজানো কেন?

    প্রতিটা নতুন key tree-তে ওর সাজানো জায়গায় বসে, আর তুমি কোন ক্রমে যোগ করেছিলে, সেটা কোথাও রাখা থাকে না। পরে ক্রমটা লাগলে key-গুলো একটা vector-এও রেখে দাও।

  • একটা map কি সমান দুইটা key রাখতে পারে?

    না। দ্বিতীয় m[k] = v প্রথম value-র উপর লিখে দেয়। multimap দুইটা pair-ই রাখে, আর ওর count বলে কয়টা আছে।

  • যে key কখনো রাখিনি, তার value কত?

    কিছুই না: key-টা map-এ নেই। তবে [] দিয়ে পড়লে map ওকে যোগ করে value type-এর শূন্য দিয়ে, int-এর জন্য 0 আর string-এর জন্য খালি string। count আর find কখনো যোগ করে না।

  • Map কি ধীর?

    একটা খোঁজা tree বেয়ে একটাই পথে নামে, প্রতি level-এ একটা তুলনা। দশ লাখ key-এর জন্য অন্তত 20টা level লাগে, আর libstdc++ tree-টাকে বড়জোর তার দ্বিগুণের কাছাকাছি উঁচু রাখে, Module 8 যেমন দেখিয়েছে। দশ লাখ pair-এর একটা vector ঘাঁটতে লাগতে পারে দশ লাখ ধাপ।

  • কখন map, আর কখন set?

    Set, যখন প্রশ্ন শুধু একটাই: "আছে তো?"। Map, যখন প্রতিটা key-এর সাথে একটা value থাকে, যেমন একটা গোনা, একটা দাম বা একটা ফোন নম্বর।

মূল কথা

  • Map key-value pair রাখে, প্রতিটা key বড়জোর একবার, আর যে ক্রমেই আসুক, ফেরত দেয় key ধরে সাজিয়ে।
  • m[k] = v রাখে বা আগের value-র উপর লেখে, আর m[k]++ গোনে; না থাকা key শুরু হয় ওর শূন্য থেকে, int-এর জন্য 0 আর string-এর জন্য খালি string।
  • count আর find কিছু যোগ না করে দেখে; find-এর উত্তর end()-এর সাথে check করার পরে it->first হলো key আর it->second হলো value।
  • না থাকা key-এর জন্য m[k] পড়লেই ওটা যোগ হয়ে যায়: size বাড়ে, আর দিনশেষের তালিকায় নাম ওঠে এমন একজনের, যে কখনো আসেনি।
  • ভিতরে আছে Module 8-এর balanced tree, প্রতিটা node-এ একটা pair, key ধরে সাজানো; multimap বারবার আসা key রাখে, আর ওর কোনো [] নেই।
  • আরও গভীরে যেতে চাইলে: Under the Hood, প্রতিটা node-এ একটা pair আর না পেলে কত খরচ (Pro)।

এরপর lesson 02 map আর multimap-এর প্রতিটা operation একটা একটা করে দেখাবে, প্রতিটার খরচসহ।

lesson ১ শেষ

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

পরেরটা: map আর multimap-এর প্রতিটা operation, একটা একটা করে, খরচসহ