Module ১০ · unordered_set আর unordered_map: hashing, collision আর anti-hash test
unordered_set আর unordered_map: সোজা ঠিক bucket-এ চলে যাওয়া
এই lesson-এ যা শিখবে
- একটা
std::unordered_setআর একটাstd::unordered_mapdeclare করতে পারবে, key insert করতে পারবে,countআরfindদিয়ে খুঁজতে পারবে, erase করতে পারবে, আর[]দিয়ে value রাখতে ও পড়তে পারবে। - এক বাক্যে বুঝিয়ে বলতে পারবে, একটা খোঁজা কীভাবে সোজা একটা bucket-এ চলে যায়, আর হাঁটার ক্রম কেন সাজানোও না, insert-এর ক্রমও না।
- একটা খোঁজার খরচ বলতে পারবে, গড়ে
O(1)আর worst case-এO(n), আর বলতে পারবে worst case-টা কখন হয়।
Zara একটা online game-এর server চালায়, আর প্রতিটা নতুন player নিজের একটা username বেছে নেয়। সেকেন্ডে হাজার হাজার বার server-কে একটা প্রশ্নের উত্তর দিতে হয়: এই নামটা কি কেউ নিয়ে ফেলেছে? দশ লাখ নামের তালিকা একটা একটা করে দেখলে প্রতিটা প্রশ্নে লাগে দশ লাখ তুলনা। Module 8-এর set-এ লাগে কয়েক ডজন, আর নাম দিয়ে index করা array তো হতেই পারে না, কারণ নাম কোনো সংখ্যা না। কিন্তু নামটা নিজেই যদি বলে দিত, কোথায় খুঁজতে হবে?
একটা সংখ্যা, যেটা বলে দেয় কোথায় খুঁজতে হবে
একটা hash function একটা key-কে একটা পূর্ণসংখ্যায় বদলে দেয়, আর program চলার সময় একই key সবসময় একই সংখ্যা দেয়। C++-এ প্রতিটা সংখ্যার type আর string-এর জন্য এমন একটা তৈরিই আছে: std::hash। hash<int>() ছোট্ট একটা hashing object বানায়, আর (42) ওর হাতে key 42 তুলে দেয়।
#include <functional>
#include <iostream>
#include <string>
using namespace std;
int main() {
cout << hash<int>()(42) << '\n';
cout << hash<string>()("cat") << '\n';
cout << hash<string>()("act") << '\n';
for (int key : {12, 7, 20, 9, 31}) {
cout << key << " % 5 = " << hash<int>()(key) % 5 << '\n';
}
return 0;
}
42
5726227304015210059
18297190751667054611
12 % 5 = 2
7 % 5 = 2
20 % 5 = 0
9 % 5 = 4
31 % 5 = 1
GCC 12-এ একটা int-এর hash হলো সংখ্যাটা নিজেই, তাই 42 দেয় 42। String-এর hash প্রতিটা byte মিশিয়ে বানানো, তাই cat আর act, একই তিনটা অক্ষর হয়েও, পায় একদম আলাদা সংখ্যা। এই সংখ্যাগুলো GCC 12-এর নিজের: C++ standard এদের একটাও বেঁধে দেয় না, আর অন্য library অন্য সংখ্যা print করতে পারে।
একটা hash container ভেতরে রাখে bucket-এর একটা array, আর প্রতিটা bucket হলো key-এর একটা ছোট্ট তালিকা। একটা key রাখার সময় container-টা key-এর hash-কে bucket-এর সংখ্যা দিয়ে ভাগ করে ভাগশেষ নেয়, program-এর শেষ লাইনগুলো যেমন করেছে, আর সেটাই key-এর bucket। পরে key-টা খুঁজতে গেলে আবার সেই একই bucket বের করে, আর খোঁজে শুধু ওই bucket-এ। এক bucket-এ দুইটা key, যেমন এখানে 12 আর 7, মানে একটা collision।
তাই খুঁজতে গিয়ে কখনো পুরো table ঘাঁটতে হয় না: একটা hash, একটা %, আর ছোট্ট একটা bucket-এ খোঁজা।
Table-টাকে ভরতে, collision হতে আর বড় হতে দেখো
unordered_set<int> s(5) পাঁচটা bucket-এর একটা table চায়। GCC 12 এটা নিয়ে কী করেছে, widget-টা একটা একটা call ধরে সেটাই আবার চালিয়ে দেখায়। প্রতিটা অবস্থা Compiler Explorer-এ একটা program নিজে print করেছে। খেয়াল করো, একটা bucket-এর ভেতরে ক্রমটা কেমন, আর ছয় নম্বর key এলে কী হয়।
Load factor হলো key-এর সংখ্যাকে bucket-এর সংখ্যা দিয়ে ভাগ করলে যা হয়। GCC 12 এটাকে 1 পর্যন্ত উঠতে দেয়, তাই পাঁচটা key পাঁচটা bucket ভরে ফেলতে পারে। ছয় নম্বর key এলে এটা 1 পেরিয়ে যেত, তাই table আগে বড় হয়ে 11টা bucket হলো, আর প্রতিটা key সরে গেল নিজের নতুন bucket-এ: এটাই rehash। GCC 12 বেছেছে 11, একটা prime সংখ্যা, যেটা আগের সংখ্যার অন্তত দ্বিগুণ; পুরো তালিকাটা দেখাবে lesson 05।
একটা bucket-এর ভেতরে নতুন key থাকে সবার আগে: 7 বসেছে 12-এর সামনে। find(31) bucket 9-এর তিনটা key-এর সাথে তুলনা করেছে, আর অন্য কোনো bucket ছোঁয়নি। find(5) একটা খালি bucket পেয়ে একটাও তুলনা না করেই থেমে গেছে। তাই একটা খোঁজায় খরচ হয় শুধু এক bucket-এর তুলনা, আর rehash প্রতিটা bucket-কে ছোট রাখে।
প্রথম দিনেই যে syntax লাগবে
std::unordered_set আর std::unordered_map, আর ওদের রোজকার call-গুলো
#include <unordered_set>
#include <unordered_map>
unordered_set<T> s; an empty set of keys of type T
unordered_set<T> s{a, b, c}; a set made from a list; repeats are dropped
s.insert(x); add x; nothing happens if x is already there
s.erase(x); remove x, if it is there
s.count(x) 1 if x is there, 0 if not
s.find(x) an iterator to x, or s.end() if x is not there
s.size() s.empty() how many keys, and whether there are none
for (T x : s) every key once, in an order you cannot count on
unordered_map<K, V> m; keys of type K, each with a value of type V
m[k] = v; store v under k; k is added if it is missing
m[k] the value under k; a missing k is added first
m.find(k) an iterator to the pair (k, value), or m.end()
for (auto& [k, v] : m) every key with its value, in no fixed order
unordered_set<T>key রাখে, প্রতিটা বড়জোর একবার।unordered_map<K, V>রাখে pair, মানে একটা key আর ওর value, আর প্রতিটা key বড়জোর একবার। দুটোরই নিজের নিজের header আছে।insert,erase,countআরfind, সবাই একটা key নেয়, set-এর মতোই। কোন key কোথায় বসবে, তুমি কখনো বলো না: ওর hash-ই ঠিক করে।findফেরত দেয় একটা iterator, মানে একটা জায়গার সেই চিহ্ন, যেটা Module 2-এ দেখেছ। Map-এ এটা একটা pair-কে দেখায়:it->firstহলো key, আরit->secondহলো value।m[k]সবসময় একটা value ফেরত দেয়, তাইkনা থাকলে আগে ওটাকে 0 দিয়ে যোগ করে নেয়,stringহলে একটা খালি value দিয়ে। এতে কীভাবে ভুল করে map বড় হয়ে যায়, দেখাবে lesson 02।
তাই call-গুলো set আর map-এর মতোই; বদলায় শুধু ক্রম আর খরচ।
insert, আর কোনো নির্দিষ্ট ক্রম ছাড়া হাঁটা
s.insert(x) x-কে ওর bucket-এ বসায়, আর একটা range-for প্রতিটা key-এর উপর দিয়ে একবার করে হাঁটে। Module 8-এর set হাঁটত সাজানো ক্রমে। এটা তা করে না।
#include <iostream>
#include <unordered_set>
using namespace std;
int main() {
unordered_set<int> s{5, 3, 9, 1, 7};
s.insert(3);
cout << "walk:";
for (int x : s) {
cout << ' ' << x;
}
cout << " (size " << s.size() << ")\n";
return 0;
}
walk: 7 1 9 3 5 (size 5)
দ্বিতীয় 3 কিছুই বদলায়নি, তাই size রইল 5। হাঁটাটা সাজানো না, আবার তালিকার ক্রমেও না। এটা GCC 12-এর নিজের ক্রম, যেটা আসে bucket-গুলো থেকে, আর কোন key কখন এসেছে, তা থেকে।
Key-গুলো একই থাকলেও এই ক্রম বদলে যেতে পারে। এই program পাঁচটা key রাখে, তারপর আরও নয়টা যোগ করে আবার erase করে দেয়। বাড়তি key-গুলো table-টাকে বড় করে দেয়, আর পরের হাঁটাটা হয় অন্যরকম।
#include <iostream>
#include <unordered_set>
using namespace std;
void show(const unordered_set<int>& s) {
cout << s.bucket_count() << " buckets:";
for (int x : s) {
cout << ' ' << x;
}
cout << '\n';
}
int main() {
unordered_set<int> s;
for (int x : {40, 7, 21, 3, 15}) {
s.insert(x);
}
show(s);
for (int x = 50; x < 59; x++) {
s.insert(x);
}
for (int x = 50; x < 59; x++) {
s.erase(x);
}
show(s);
return 0;
}
13 buckets: 15 3 21 7 40
29 buckets: 40 7 3 15 21
একই পাঁচটা key, অথচ দুই রকম হাঁটা: table 13টা bucket থেকে বড় হয়ে 29টা হয়েছে, আর rehash ওকে নতুন করে সাজিয়েছে। নয়টা key erase করার পরেও table আর ছোট হয়নি। তাই hash container-এর হাঁটার ক্রমের উপর কোনো program-কে কখনো ভরসা করতে দিয়ো না: পরের rehash, বা অন্য একটা compiler, সেটা বদলে দিতে পারে।
আছে তো? count আর find
s.count(x) ফেরত দেয় 1, যদি x থাকে, আর 0, যদি না থাকে। s.find(x) ফেরত দেয় x-এর দিকে একটা iterator, আর x না থাকলে s.end()।
#include <iostream>
#include <unordered_set>
using namespace std;
int main() {
unordered_set<int> s{3, 9, 14, 27, 31};
cout << s.count(14) << ' ' << s.count(5) << '\n';
auto it = s.find(27);
if (it != s.end()) {
cout << "found " << *it << '\n';
}
if (s.find(5) == s.end()) {
cout << "5 is not there\n";
}
return 0;
}
1 0
found 27
5 is not there
* দিয়ে পড়ার আগে find-এর উত্তর end()-এর সাথে মিলিয়ে নাও, কারণ *s.end() পড়া হলো undefined behaviour। তাই সোজা হ্যাঁ বা না জানতে count, আর জায়গাটাও লাগলে find।
key ধরে erase
s.erase(x) x থাকলে সেটা সরায়, না থাকলে কিছুই করে না।
#include <iostream>
#include <unordered_set>
using namespace std;
int main() {
unordered_set<int> s{3, 9, 14, 27, 31};
s.erase(14);
s.erase(5);
cout << "size " << s.size() << ", 14 there: " << s.count(14) << '\n';
return 0;
}
size 4, 14 there: 0
erase(14) 14-কে ওর bucket-এ পেয়ে খুলে নিল। erase(5) কিছুই পেল না, কিছুই করল না, কোনো error-ও দিল না। তাই নেই এমন key erase করা নিরাপদ।
unordered_map: প্রতিটা key-এর সাথে একটা value
একটা unordered_map প্রতিটা key-এর পাশে একটা value রাখে, যেমন দোকানের একটা জিনিসের নামের পাশে তার দাম। Key ঠিক করে কোন bucket, আর value চলে ওর সাথে সাথে। Value হতে পারে এমন যেকোনো কিছু, যেটা key-এর মনে রাখা দরকার: একটা দাম, একটা level, বা কোনো কিছু কতবার ঘটেছে, সেই সংখ্যা।
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<string, int> price;
price["tea"] = 20;
price["bun"] = 15;
price["tea"] = 25;
cout << "tea " << price["tea"] << ", size " << price.size() << '\n';
auto it = price.find("bun");
if (it != price.end()) {
cout << it->first << " costs " << it->second << '\n';
}
cout << "cake " << price["cake"] << ", size " << price.size() << '\n';
for (const auto& [item, cost] : price) {
cout << item << ' ' << cost << '\n';
}
return 0;
}
tea 25, size 2
bun costs 15
cake 0, size 3
cake 0
bun 15
tea 25
price["tea"] = 25 20-কে বদলে দিল, কারণ প্রতিটা key থাকে একবারই। find("bun") pair-টার দিকে একটা iterator দিল, যার first-এ key আর second-এ value। price["cake"] পড়তেই cake যোগ হয়ে গেল 0 value নিয়ে, তাই size 2 থেকে হলো 3। শেষ loop প্রতিটা pair-এর নাম দেয় [item, cost], মানে একটা structured binding, আর loop pair-গুলোর উপর দিয়ে যায় GCC 12-এর ক্রমে।
তাই value রাখতে [], আর কিছু যোগ না করে শুধু জিজ্ঞেস করতে find।
একটা খোঁজার খরচ: গড়ে O(1), worst case-এ O(n)
একটা খোঁজা একটা hash আর একটা % বের করে, তারপর key-টাকে একটা bucket-এর key-গুলোর সাথে তুলনা করে। Load factor 1 পেরোনোর আগেই GCC 12 rehash করে ফেলে, তাই গড়ে একটা bucket-এ থাকে মোটামুটি একটা key। তাই table-এ যত key-ই থাকুক, একটা খোঁজায় তুলনা লাগে একটা কি দুইটা: গড়ে O(1)। Insert আর erase-এর খরচও তাই, সাথে মাঝে মাঝে একটা rehash, যেটা প্রতিটা key-কে একবার করে সরায়।
Worst case হলো সব key একই bucket-এ। তখন একটা খোঁজায় তুলনা লাগে n-টা key-এর সবগুলোর সাথে, O(n), সাধারণ একটা তালিকার মতো। এমন হতে হলে কাউকে hash জেনে-বুঝে key বাছতে হয়, আর lesson 04 এমন key বানিয়ে দেখাবে। Module 8-এর set-এ দশ লাখ key-তে লাগে মোটামুটি 20 থেকে 40 ধাপ, key যেমনই হোক। কিন্তু সেটা কখনো একটা-দুইটায় নামে না।
তাই hash container ক্রম আর একটা পাকা worst case ছেড়ে দেয়, আর বদলে একটা খোঁজা শুধু ছোট্ট একটা bucket ঘাঁটে।
সবচেয়ে ছোট program: একটা repeat-সহ পাঁচটা সংখ্যা ঢোকে, আর বেরোয় চারটা, GCC 12-এর ক্রমে।
#include <iostream>
#include <unordered_set>
using namespace std;
int main() {
unordered_set<int> s;
for (int x : {42, 7, 19, 7, 3}) {
s.insert(x);
}
cout << s.size() << " different values:";
for (int x : s) {
cout << ' ' << x;
}
cout << '\n';
return 0;
}
4 different values: 19 7 3 42
দুইবার আসা 7-কে গোনা হলো একবার। হাঁটাটা সাজানোও না, সংখ্যাগুলো যে ক্রমে এসেছিল সেই ক্রমেও না। অন্য compiler তৃতীয় আরেকটা ক্রমও print করতে পারে।
Run in CompilerZara-র server-এ রাখা আছে আগে থেকে নেওয়া নামগুলো। প্রতিটা নতুন player একটা নাম চায়। নামটা নেওয়া থাকলে server নামের শেষে 1, 2, 3 এভাবে বসিয়ে চেষ্টা করে, যতক্ষণ না count বলে নামটা খালি।
#include <iostream>
#include <string>
#include <unordered_set>
using namespace std;
int main() {
unordered_set<string> taken{"zara", "kenji", "bob", "zara1"};
for (string want : {"maria", "zara", "bob"}) {
string name = want;
int n = 1;
while (taken.count(name) == 1) {
name = want + to_string(n);
n++;
}
taken.insert(name);
cout << want << " gets " << name << '\n';
}
cout << taken.size() << " names taken\n";
return 0;
}
maria gets maria
zara gets zara2
bob gets bob1
7 names taken
maria খালি ছিল। zara আর zara1 দুটোই নেওয়া, তাই ওই player পেল zara2, আর bob পেল bob1। গড়ে প্রতিটা check-এর খরচ একটা hash আর ছোট্ট একটা bucket, নাম সাতটা হোক বা দশ লাখ।
Run in CompilerKenji প্রতিটা player-এর level রাখে একটা unordered_map<string, int>-এ। কেউ একটা level শেষ করলেই level[who]++ এক বাড়িয়ে দেয়। কোনো player-কে print করার আগে find দেখে নেয় player-টা আছে কি না, তাই নেই এমন কোনো নাম কখনো যোগ হয় না।
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<string, int> level{{"zara", 4}, {"kenji", 2}, {"amara", 7}};
for (string who : {"kenji", "zara", "kenji"}) {
level[who]++;
}
for (string who : {"bob", "kenji"}) {
auto it = level.find(who);
if (it == level.end()) {
cout << who << " has not played yet\n";
} else {
cout << who << " is on level " << it->second << '\n';
}
}
for (const auto& [name, lv] : level) {
cout << name << ": level " << lv << '\n';
}
return 0;
}
bob has not played yet
kenji is on level 4
amara: level 7
kenji: level 4
zara: level 5
Kenji দুইটা level শেষ করেছে আর Zara একটা, তাই 2 হলো 4, আর 4 হলো 5। Bob কখনো যোগ হয়নি, কারণ find শুধু জিজ্ঞেস করে। শেষ loop তিনজন player-কে print করেছে GCC 12-এর ক্রমে, ওরা যে ক্রমে খেলতে এসেছিল সেই ক্রমে না।
এটা কোথায় কাজে লাগে
- CPython-এর dict. Python-এর প্রতিটা dictionary একটা hash table। CPython-এর
Objects/dictobject.cপ্রথমে কোন slot দেখবে, তা ঠিক করে key-এর hash-এর নিচের দিকের bit-গুলো দিয়ে। Collision হলে ও একই table-এর অন্য slot-গুলো দেখে, bucket-প্রতি কোনো তালিকা রাখে না। Table দুই-তৃতীয়াংশ ভরে গেলেই ও table বড় করে। - Java-র
HashMap. ওর documentation কথা দেয়, hash যদি key-গুলো bucket-এ ঠিকমতো ছড়িয়ে দেয়, তাহলেgetআরputহবে constant সময়ে, আর কোনো ক্রমের কথা দেয় না। Bucket-প্রতি 0.75-এর বেশি entry হয়ে গেলে, মানে ওর default load factor পেরোলে, ওকে rehash করে মোটামুটি দ্বিগুণ bucket-এ নেওয়া হয়। - Redis. Redis database-এর প্রতিটা key থাকে Redis-এর নিজের
src/dict.c-এর hash table-এ, যেটা bucket-প্রতি একটা তালিকা রাখে। Table বড় করতে হলে Redis key-গুলো সরায় একবারে একটা bucket করে, সাধারণ lookup আর update-এর ফাঁকে ফাঁকে। তাই কাজটা ভাগ হয়ে যায় অনেকগুলো command-এর মধ্যে।
যে ভুলগুলো সবাই করে
১. Header লিখতে ভুলে যাওয়া।
#include <iostream>
using namespace std;
int main() {
unordered_set<int> s{3, 1, 2};
cout << s.size() << '\n';
return 0;
}
Runner-এর flag-এ error: error: 'unordered_set' was not declared in this scope, তারপর GCC 12 নিজেই সমাধান বলে দেয়: note: 'std::unordered_set' is defined in header '<unordered_set>'; did you forget to '#include <unordered_set>'?। Header ছাড়া map-ও পায় একই জোড়া: error: 'unordered_map' was not declared in this scope, আর একটা note, যেটা <unordered_map>-এর নাম বলে। তুমি এটা ভুলবে, কারণ এই container সাধারণত program-এ ঢোকে দেরিতে, include-গুলো থেকে অনেক দূরে।
২. বর্ণমালার ক্রম আশা করা।
unordered_set<string> names{"maria", "bob", "zara", "amara"};
for (const string& name : names) {
cout << name << '\n';
}
Runner-এর flag-এ কোনো বার্তা নেই, আর GCC 12 print করে amara, zara, bob আর maria: বর্ণমালার ক্রমেও না, তালিকার ক্রমেও না। Hash container কোনো ক্রমই রাখে না। বর্ণমালার ক্রম লাগলে Module 8-এর set নাও, নয়তো key-গুলো একটা vector-এ copy করে sort করো। তুমি ক্রম আশা করবে, কারণ Module 8-এর set সেটা এমনিতেই দিত।
৩. জায়গার নম্বর দিয়ে key চাওয়া।
unordered_set<int> s{10, 20, 30};
cout << s[0] << '\n';
Runner-এর flag-এ error: error: no match for 'operator[]' (operand types are 'std::unordered_set<int>' and 'int')। Hash set-এর কোনো জায়গার নম্বর নেই, তাই চাওয়ার মতো প্রথম key বলেও কিছু নেই। Range-for দিয়ে হাঁটো, আর জায়গার নম্বর লাগলে পাশে একটা vector রাখো। তুমি এটা চেষ্টা করবে, কারণ ওর ভাই map-এ m[k] দিব্যি চলে।
Zara স্কুলের coding club-এর chat দেখাশোনা করে। ওর কাছে নিষিদ্ধ শব্দের একটা তালিকা আছে, আর কিছু শব্দ তাতে একাধিকবার লেখা। প্রতিটা message-এ প্রতিটা নিষিদ্ধ শব্দ দেখাতে হবে *** হিসেবে, আর বাকি শব্দগুলো থাকবে যেমন ছিল তেমনই। শুধু পুরো শব্দই ধরা হয়: spam নিষিদ্ধ হলেও spammer আলাদা একটা শব্দ।
Input. এক লাইনে n, তারপর n-টা নিষিদ্ধ শব্দ, প্রতি লাইনে একটা। তারপর এক লাইনে m, তারপর এক লাইনে message-এর m-টা শব্দ, মাঝে একটা করে space।
Output. এক লাইন: message-এর m-টা শব্দ, ওদের নিজের ক্রমে, মাঝে একটা করে space, আর প্রতিটা নিষিদ্ধ শব্দের জায়গায় ***।
Constraints. 1 <= n <= 30000 আর 1 <= m <= 50000। প্রতিটা শব্দ 1 থেকে 10টা ছোট হাতের ইংরেজি অক্ষর।
Sample. Input 3, তারপর spam, scam আর fake, তারপর 8 আর free spam is spammer talk not fake news দিলে free *** is spammer talk not *** news। spammer শুরু হয় একটা নিষিদ্ধ শব্দ দিয়ে, কিন্তু নিজে নিষিদ্ধ না।
#include <iostream>
#include <string>
#include <unordered_set>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n = 0;
cin >> n;
unordered_set<string> banned; // every banned word
for (int i = 0; i < n; i++) {
string word;
cin >> word;
// Add word to banned.
}
int m = 0;
cin >> m;
for (int i = 0; i < m; i++) {
string word;
cin >> word;
// Print word, or *** if it is banned. Put one space between two
// words, and end the line after the last one.
}
return 0;
}
banned-words নামে grade হয়। এটা এই module-এর problem set-এর একটা free problem। Hidden test-এ আছে এক শব্দের একটা message, এমন একটা message যার প্রতিটা শব্দই নিষিদ্ধ, আর তিনবার লেখা একটা শব্দ। আরও আছে নিষিদ্ধ শব্দ দিয়ে শুরু হওয়া শব্দ, আর 30000টা নিষিদ্ধ শব্দের সাথে 50000 শব্দের একটা message।
Kenji club-এর game night চালায়। প্রতিটা খেলার পরে ও এক লাইন লেখে: একজন player-এর নাম, আর সে কত point জিতল। রাতের শেষে যার মোট point সবচেয়ে বেশি, trophy যায় তার হাতে। কয়েকজনের মোট সমান হলে trophy পায় সেই নাম, যেটা বর্ণমালার ক্রমে আগে আসে।
Input. এক লাইনে n, তারপর n-টা লাইন, প্রতিটায় একটা নাম আর একটা খেলার point।
Output. এক লাইন: বিজয়ীর নাম আর মোট point, মাঝে একটা space।
Constraints. 1 <= n <= 100000। একটা নাম 1 থেকে 10টা ছোট হাতের ইংরেজি অক্ষর, আর একটা খেলায় point 0 থেকে 1000।
Sample. Input 6, তারপর zara 30, bob 50, kenji 20, zara 25, bob 5 আর amara 40 দিলে bob 55। Bob আর Zara দুজনেই 55-এ পৌঁছায়, আর বর্ণমালার ক্রমে bob আগে আসে।
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
unordered_map<string, int> total; // each player's points so far
for (int i = 0; i < n; i++) {
string name;
int points;
cin >> name >> points;
// Add points to name's total.
}
// Walk total and keep the winner: more points wins, and a tie goes to
// the name that comes first in alphabetical order. Print the winner's
// name and total.
return 0;
}
আলাদা করে grade হয় না। total[name] += points দিয়ে যোগ করো, তারপর map-টার উপর দিয়ে একবার হাঁটো। Module 9-এর stamp গোনায় map-এর হাঁটাই সমান মোটের নামগুলো বর্ণমালার ক্রমে এনে দিত। এই হাঁটা নামগুলো কোনো কাজের ক্রমে দেখায় না, তাই মোট সমান হলে নামগুলো সরাসরি তুলনা করে বিজয়ী ঠিক করো।
Zara-র সংখ্যার খেলা শুরু হয় একটা পূর্ণসংখ্যা n দিয়ে। প্রতিটা ধাপে সংখ্যাটার জায়গায় বসে ওর অঙ্কগুলোর বর্গের যোগফল: 19 হয়ে যায় 1 + 81 = 82, তারপর 68, 100 আর 1। কোনো কোনো সংখ্যা 1-এ পৌঁছায়, আর খেলা সেখানেই শেষ; বাকিরা কখনো পৌঁছায় না। Zara জানতে চায় কোন সংখ্যা কোন দলে, আর পৌঁছাতে কয় ধাপ লাগে।
Input. এক লাইনে n।
Output. এক লাইন: 1-এ পৌঁছাতে কয় ধাপ লাগে, আর খেলা কখনো 1-এ না পৌঁছালে loops শব্দটা।
Constraints. 1 <= n <= 1000000000। 1 থেকে শুরু হওয়া খেলায় লাগে 0 ধাপ।
Sample. Input 19 দিলে 4: খেলা চলে 19, 82, 68, 100, 1। Input 4 দিলে loops, কারণ আট ধাপ পরে খেলা আবার 4-এ ফিরে আসে।
#include <iostream>
#include <unordered_set>
using namespace std;
int main() {
int n;
cin >> n;
unordered_set<int> seen; // every number the game has reached so far
// Play the game from n: each step replaces the number by the sum of the
// squares of its digits. Print how many steps it takes to reach 1, or
// loops if the game never gets there.
return 0;
}
আলাদা করে grade হয় না। Lesson যে কথাটা শুধু ইঙ্গিতে বলেছে: কোনো সংখ্যা ফিরে এলে খেলা চিরকাল ঘুরতেই থাকবে, তাই প্রতিটা সংখ্যা একটা unordered_set-এ মনে রাখো।
সচরাচর যে প্রশ্নগুলো আসে
আমার program অন্য computer-এ key-গুলো অন্য ক্রমে print করে কেন?
ক্রমটা আসে hash আর bucket-এর সংখ্যা থেকে, আর অন্য standard library অন্যভাবে hash করে। GCC 12-এ একই program যতবার চালাও, একই ক্রম print করে, কিন্তু অন্য কোথাও সেই কথা কেউ দেয় না। তাই ক্রমটাকে অজানা ধরে নাও।
একটা
unordered_setকি সবসময়set-এর চেয়ে দ্রুত?গড়ে একটা খোঁজায় লাগে একটা hash আর একটা-দুইটা তুলনা, যেখানে দশ লাখ key-এর set-এ লাগে মোটামুটি 20 থেকে 40টা তুলনা। কিন্তু hash container worst case-এ পড়তে পারে, কোনো ক্রম রাখে না, আর পরের key-টা দিতে পারে না। দুটোকে মেপে কখন কোনটা নেবে, বলবে lesson 04।
একটা
unordered_setকি একই key দুইবার রাখতে পারে?না। দ্বিতীয় insert key-টাকে ওর bucket-এ খুঁজে পায়, আর কিছুই বদলায় না।
unordered_multisetrepeat রাখে, সেটা দেখাবে lesson 02।Bucket কয়টা হবে, সেটা কি আমাকেই বেছে দিতে হয়?
না। GCC 12-এ একটা খালি table শুরু হয় একটা bucket নিয়ে, আর key আসতে আসতে নিজে নিজেই বড় হয়।
s(5)-এর মতো একটা সংখ্যা শুধু শুরুর জায়গা, আর lesson 02 দেখাবেreserve, যেটা জানা সংখ্যার key-এর জন্য আগেই জায়গা করে রাখে।
মূল কথা
- Hash function একটা key-কে একটা সংখ্যায় বদলায়, আর সেই সংখ্যাকে bucket-এর সংখ্যা দিয়ে ভাগ করে পাওয়া ভাগশেষই বেছে দেয় সেই একটা bucket, যেখানে খোঁজা হয়।
insert,erase,countআরfindএকটা key নেয়; map-এm[k] = vএকটা value রাখে, আরm[k]পড়লে না থাকা key যোগ হয়ে যায়।- Load factor 1 পেরিয়ে যাওয়ার মুখে GCC 12 table বড় করে আর প্রতিটা key সরায়: এটাই rehash।
- হাঁটার ক্রম সাজানোও না, insert-এর ক্রমও না, আর একটা rehash সেটা বদলে দিতে পারে।
- একটা খোঁজার খরচ গড়ে
O(1), আর worst case-এO(n), যখন সব key একই bucket-এ। - আরও গভীরে যেতে চাইলে: Under the Hood, bucket-এর chain, prime সংখ্যার bucket আর anti-hash test (Pro)।
এরপর lesson 02 চারটা hash container-এর প্রতিটা operation একটা একটা করে দেখাবে, প্রতিটার খরচসহ।
lesson ১ শেষ
শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।
পরেরটা: unordered_set আর unordered_map-এর প্রতিটা operation, একটা একটা করে, খরচসহ