এই পর্বে যা শিখবেন
- বাস্তব সমস্যাকে গ্রাফে রূপান্তর করা — কী vertex, কী edge, দিক আর ওজন লাগবে কি না
- degree, path, cycle, component, tree, DAG, bipartite, complete, dense আর sparse — গ্রাফের মূল পরিভাষা নির্ভুলভাবে ব্যবহার করা
- Handshake lemma আর “tree-তে ঠিক n − 1 টি edge” — এই দুটি প্রমাণ নিজে লিখতে পারা
- Adjacency matrix, list, edge list আর CSR-এর জায়গা ও কাজের খরচ তুলনা করে সঠিকটি বেছে নেওয়া, আর কনটেস্টের ইনপুট থেকে C++ ও Python-এ গ্রাফ বানানো
- গ্রিড আর state space-কে implicit graph হিসেবে দেখা, আর Euler path-এর শর্ত প্রমাণসহ যাচাই করা
১ · গ্রাফ সবখানে — রাস্তা, বন্ধুত্ব, লিংক আর টাকা
ধরুন আপনি মিরপুর ১০ থেকে মতিঝিল যাবেন। Google Maps খুললে সে কয়েক সেকেন্ডে একটি পথ দেখায় — কোন মোড়ে বাঁয়ে, কোন রাস্তা একমুখী, কোথায় জ্যাম। মানচিত্রের পেছনে কম্পিউটার যা দেখে, তা কোনো ছবি নয়: দেখে কিছু মোড় আর মোড়গুলোর মধ্যে কিছু রাস্তা, প্রতিটি রাস্তার সাথে একটি সংখ্যা (দূরত্ব বা সময়)। রাস্তার রং, দোকানের সাইনবোর্ড, ফুটপাতের চায়ের দোকান — কিছুই লাগে না। শুধু “কে কার সাথে যুক্ত” — এই তথ্যটুকু রেখে বাকি সব ফেলে দেওয়া কাঠামোটির নাম graph (গ্রাফ)।
আনুষ্ঠানিক সংজ্ঞা: একটি গ্রাফ G = (V, E), যেখানে V হলো vertex-এর (বিন্দু বা node) একটি সেট আর E হলো edge-এর (সংযোগ) একটি সেট; প্রতিটি edge দুটি vertex-কে জোড়ে। এই কোর্সে n = |V| (vertex-এর সংখ্যা) আর m = |E| (edge-এর সংখ্যা) — বইয়ে কখনো V আর E সরাসরি সংখ্যা হিসেবেও লেখা হয়, যেমন O(V + E). একটি edge u আর v-কে জুড়লে আমরা বলি u আর v adjacent (পাশাপাশি), আর v হলো u-এর একজন প্রতিবেশী (neighbour).
গ্রাফের শক্তি হলো, একেবারে আলাদা ধরনের সমস্যা একই ভাষায় লেখা যায়। নিচের টেবিলটি দেখুন — প্রতিটি সারি একটি বাস্তব জগৎ, কিন্তু সবগুলোর কাঠামো একই।
| বাস্তব জগৎ | vertex | edge | গ্রাফের ধরন | সাধারণ প্রশ্ন |
|---|---|---|---|---|
| ঢাকার রাস্তার মানচিত্র | মোড় | রাস্তা; একমুখী রাস্তা এক দিকে | directed, weighted (মিনিট বা কিলোমিটার) | সবচেয়ে কম সময়ের পথ |
| ফেসবুকের বন্ধুত্ব | মানুষ | দুজন বন্ধু | undirected | “বন্ধুর বন্ধু” সাজেশন, কয় ধাপে যুক্ত |
| ওয়েবের লিংক | ওয়েব পেজ | একটি পেজ থেকে অন্য পেজের লিংক | directed | কোন পেজ বেশি গুরুত্বপূর্ণ (PageRank) |
| কোর্সের prerequisite | কোর্স | “A না পড়ে B নয়”: A → B | directed, cycle ছাড়া (DAG) | কোন ক্রমে পড়লে নিয়ম ভাঙে না |
| বিদ্যুতের জাতীয় গ্রিড | বিদ্যুৎকেন্দ্র, সাবস্টেশন | সঞ্চালন লাইন | undirected, weighted (দৈর্ঘ্য, ক্ষমতা) | সবচেয়ে কম তারে সব জোড়া, কোন লাইন কাটলে এলাকা অন্ধকার |
| bKash লেনদেন | অ্যাকাউন্ট | একটি transfer: প্রেরক → প্রাপক | directed, weighted (টাকা), একই জোড়ায় বহু edge | টাকা ঘুরে আবার একই জায়গায় ফিরছে কি না (সন্দেহজনক চক্র) |
উপমা — বাসের রুট-ম্যাপ। ঢাকার বাসের রুট-ম্যাপে স্টপেজগুলো শুধু ফোঁটা, আর রুটগুলো সরল দাগ; কোন স্টপেজ মানচিত্রে ঠিক কোথায়, দাগটি বাঁকা না সোজা — এসবের কোনো মানে নেই। গ্রাফও ঠিক তাই: একই গ্রাফ কাগজে হাজার রকমভাবে আঁকা যায়।
“সবচেয়ে ছোট পথ”-এর একটি অ্যালগরিদম একবার শিখলে সেটি রাস্তায়, ইন্টারনেটের রাউটারে, এমনকি ধাঁধার সমাধানে — সবখানে চলে। মডিউল ৬-এর বাকি নয়টি পর্ব এমন অ্যালগরিদমে ভরা; এই পর্ব তাদের জন্য ভাষা আর ডেটা স্ট্রাকচার তৈরি করে। আপনি এর মধ্যেই দুটি বিশেষ গ্রাফ দেখেছেন: পর্ব ২৬-এর tree আর পর্ব ৩৩-এর DSU-র forest. আজ দেখব সেগুলো বড় পরিবারের কোন জায়গায় বসে।
২ · গ্রাফের প্রকারভেদ — দিক, ওজন, loop আর একাধিক edge
Undirected
Edge {u, v}-এর কোনো দিক নেই — u থেকে v আর v থেকে u, দুদিকেই যাওয়া যায়। বন্ধুত্ব পারস্পরিক, তাই ফেসবুকের বন্ধু-গ্রাফ undirected.
বন্ধুত্ব, দুই দিকের রাস্তা, বিদ্যুতের লাইনDirected
Edge (u, v) একটি ক্রমযুক্ত জোড়া: u থেকে v-তে যাওয়া যায়, উল্টো দিকে নয় (যদি না আলাদা একটি (v, u) থাকে)। একে arc-ও বলা হয়।
একমুখী রাস্তা, ওয়েব লিংক, X-এ followWeighted
প্রতিটি edge-এর সাথে একটি সংখ্যা w(u, v) — দূরত্ব, সময়, খরচ, টাকার পরিমাণ বা লাইনের ক্ষমতা। ওজন না থাকলে প্রতিটি edge-কে ওজন ১ ধরা যায়।
রাস্তার কিলোমিটার, bKash-এর টাকাMultigraph
একই দুই vertex-এর মধ্যে একাধিক edge (parallel edge) বা একটি vertex থেকে নিজের দিকেই একটি edge (self-loop) থাকতে পারে।
একই দুজনের মধ্যে বহু লেনদেনUndirected graph-এ edge লেখা হয় {u, v} — একটি অক্রমিক জোড়া। Directed graph-এ (সংক্ষেপে digraph) edge (u, v) একটি ক্রমিক জোড়া: u হলো লেজ (tail), v হলো মাথা (head), আর আঁকা হয় তীর দিয়ে u → v. ঢাকার অনেক একমুখী রাস্তা directed edge, আর দুমুখী রাস্তা মানে দুটি উল্টো directed edge. প্রতিটি undirected গ্রাফকে এভাবে directed হিসেবে দেখা যায় — {u, v}-এর জায়গায় u → v আর v → u; কোডেও ঠিক এটিই করা হয়।
Weighted graph-এ প্রতিটি edge-এর একটি ওজন (weight) থাকে। ওজন ঋণাত্মকও হতে পারে — যেমন কোনো রাস্তায় গেলে জ্বালানি-ভর্তুকি পাওয়া যায়, বা মুদ্রা-বিনিময়ে লাভ — আর তখন কিছু অ্যালগরিদম (Dijkstra) আর চলে না; সেই গল্প পর্ব ৪৩–৪৪-এ।
এখন দুটি বিশেষ জিনিস। Self-loop হলো একটি vertex থেকে নিজের দিকেই একটি edge, যেমন (v, v). আর parallel edge (multi-edge) মানে একই দুই vertex-এর মধ্যে একাধিক edge — যেমন একই দিনে আপনি মা-কে তিনবার bKash করলেন। এ দুটির কোনোটিই না থাকলে গ্রাফটি একটি simple graph; থাকতে পারলে multigraph. বইয়ের বেশিরভাগ উপপাদ্য simple graph ধরে নেয়, কিন্তু কনটেস্টের ইনপুটে প্রায়ই লেখা থাকে “there may be multiple edges and self-loops” — সমস্যার বিবরণ খুঁটিয়ে না পড়লে এখানেই অনেকে ভুল করেন।
কতগুলো edge থাকতে পারে?
n টি vertex-এর simple undirected গ্রাফে প্রতিটি জোড়া {u, v}-এর মধ্যে সর্বোচ্চ একটি edge. জোড়ার সংখ্যা nC2 = n(n − 1)/2. তাই 0 ≤ m ≤ n(n − 1)/2. Simple directed গ্রাফে প্রতিটি ক্রমিক জোড়া (u, v), u ≠ v, আলাদা — সর্বোচ্চ n(n − 1) টি edge. Multigraph-এ কোনো ওপরের সীমা নেই। ১,০০০ জন মানুষের মধ্যে সর্বোচ্চ বন্ধুত্ব 1000 × 999 / 2 = 4,99,500 টি — এই সংখ্যাটি অংশ ৪-এ dense আর sparse গ্রাফের পার্থক্য বুঝতে লাগবে।
৩ · Degree আর handshake lemma — প্রতিটি edge দুবার গোনা হয়
Undirected গ্রাফে একটি vertex v-এর degree, deg(v), হলো v-কে ছোঁয়া edge-এর সংখ্যা — অর্থাৎ v-এর প্রতিবেশী কয়জন (simple graph-এ)। ফেসবুকে আপনার degree মানে আপনার বন্ধু-সংখ্যা। Self-loop থাকলে নিয়ম: একটি loop degree-তে ২ যোগ করে, কারণ তার দুটি প্রান্তই v-তে। (পর্ব ২৬-এ rooted tree-তে “degree” বলতে শুধু সন্তানের সংখ্যা বোঝানো হয়েছিল — গ্রাফে degree মানে সব edge, parent-এর edge-সহ।)
Directed গ্রাফে দুটি আলাদা সংখ্যা: out-degree, deg+(v) — v থেকে বেরিয়ে যাওয়া edge, আর in-degree, deg−(v) — v-তে ঢোকা edge. Prerequisite গ্রাফে in-degree ০-এর কোর্সের কোনো পূর্বশর্ত নেই — সেগুলো দিয়েই শুরু করা যায় (পর্ব ৪২-এর topological sort এখান থেকেই শুরু)।
এবার এই পর্বের প্রথম উপপাদ্য। চারজন বন্ধু — রহিম (1), করিম (2), সেলিনা (3) আর দীপা (4) — একে একে ফেসবুকে বন্ধু হচ্ছেন। প্রতিটি নতুন বন্ধুত্বের পর degree-র যোগফল দেখুন:
| নতুন edge | |
| Σ deg(v) | |
| m আর 2m | |
| বিজোড় degree-র vertex |
- শুরু. চারজন, কোনো বন্ধুত্ব নেই: m = 0, সবার degree 0, যোগফল 0 = 2 × 0.
- 1–2. রহিম আর করিম বন্ধু হলেন। একটি edge-এর দুটি প্রান্ত, তাই দুজনের degree এক করে বাড়ল — যোগফল বাড়ল ঠিক ২. এখন 2 = 2 × 1. বিজোড় degree-র vertex দুটি: 1 আর 2.
- 1–3. রহিমের degree 2 হলো (জোড়), সেলিনার 1 (বিজোড়)। যোগফল 4 = 2 × 2. বিজোড়দের দল বদলেছে (1 বেরিয়ে গেল, 3 ঢুকল), কিন্তু সংখ্যাটি এখনো ২ — জোড়।
- 2–3. করিম আর সেলিনা — দুজনেই বিজোড় ছিলেন, এখন দুজনেই জোড় (2). যোগফল 6 = 2 × 3, আর বিজোড় vertex একটিও নেই। 1–2–3 মিলে একটি ত্রিভুজ, অর্থাৎ একটি cycle.
- 3–4. সেলিনার degree 3, দীপার 1. যোগফল 8 = 2 × 4. প্রতিটি ধাপে যোগফল ঠিক ২ বেড়েছে, আর বিজোড় degree-র vertex সবসময় ০ বা ২ — কখনো বিজোড় সংখ্যক নয়। এটি কাকতাল নয়, এটি একটি উপপাদ্য।
প্রমাণ — handshake lemma
উপপাদ্য (handshake lemma): যেকোনো undirected গ্রাফে (multigraph আর self-loop-সহ) Σv ∈ V deg(v) = 2m.
প্রমাণ (দুইভাবে গোনা): সব (vertex, edge-এর প্রান্ত) জোড়া গুনুন — অর্থাৎ “কোন edge-এর কোন প্রান্ত কোন vertex-এ বসে আছে”. Vertex ধরে গুনলে প্রতিটি v-তে ঠিক deg(v) টি প্রান্ত, মোট Σ deg(v). Edge ধরে গুনলে প্রতিটি edge-এর ঠিক দুটি প্রান্ত (self-loop-এর দুটিই একই vertex-এ — সেজন্যই loop degree-তে ২ যোগ করে), মোট 2m. একই জিনিস দুইভাবে গোনা হয়েছে, তাই দুটি সমান। ∎ (Induction-এও করা যায়, ওপরের ডায়াগ্রামের মতো: খালি গ্রাফে 0 = 0, আর প্রতিটি নতুন edge দুই পাশে ঠিক ২ যোগ করে।)
অনুসিদ্ধান্ত: বিজোড় degree-র vertex-এর সংখ্যা সবসময় জোড়। কারণ Σ deg(v) জোড় (2m); জোড় degree-গুলোর যোগফল জোড়; তাই বাকি বিজোড় degree-গুলোর যোগফলও জোড় — আর কয়েকটি বিজোড় সংখ্যার যোগফল জোড় হয় কেবল তখন, যখন তাদের সংখ্যা জোড়। নাম “handshake” এই অনুসিদ্ধান্ত থেকে: একটি অনুষ্ঠানে যতজন বিজোড় সংখ্যক মানুষের সাথে হাত মিলিয়েছেন, তাঁদের সংখ্যা সবসময় জোড়।
Directed গ্রাফে প্রতিটি edge ঠিক একটি out-degree আর একটি in-degree দেয়, তাই Σ deg+(v) = Σ deg−(v) = m. এর একটি সরাসরি ফল কোডের খরচে: adjacency list-এর সব list মিলিয়ে undirected গ্রাফে মোট 2m টি এন্ট্রি, directed গ্রাফে m টি। তাই “প্রতিটি vertex-এর সব প্রতিবেশী একবার ঘোরা” মোট Θ(n + m) — BFS আর DFS-এর O(V + E) সময়ের হিসাব এই lemma-র ওপরই দাঁড়িয়ে।
একটি দ্রুত প্রয়োগ: ৭ জন মানুষের একটি দলে কি প্রত্যেকে ঠিক ৩ জনের সাথে বন্ধু হতে পারেন? Degree-র যোগফল হতো 7 × 3 = 21 — বিজোড়, অথচ 2m সবসময় জোড়। তাই অসম্ভব — কোনো গ্রাফ না এঁকেই প্রমাণ। বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এমন প্রশ্ন প্রায়ই আসে।
৪ · পথ, cycle, সংযোগ আর বিশেষ গ্রাফের পরিবার
Walk, path আর cycle
একটি walk হলো vertex-এর একটি ক্রম v0, v1, …, vk, যেখানে পরপর প্রতিটি জোড়ার মধ্যে একটি edge আছে; দৈর্ঘ্য k (edge-এর সংখ্যা)। Walk-এ vertex বা edge বারবার আসতে পারে। কোনো vertex বারবার না এলে সেটি একটি simple path; অনেক বইয়ে শুধু “path” বলতে এটিকেই বোঝানো হয়। যেকোনো walk থেকে মাঝের ঘোরাঘুরি কেটে একটি path পাওয়া যায়, তাই “u থেকে v-তে যাওয়া যায় কি না” প্রশ্নে দুটির পার্থক্য নেই।
একটি cycle হলো দৈর্ঘ্য ≥ 1-এর এমন একটি বন্ধ walk, যার শুরু আর শেষ একই vertex, আর এর বাইরে কোনো vertex বা edge পুনরাবৃত্ত হয় না। Simple undirected গ্রাফে cycle-এর দৈর্ঘ্য অন্তত ৩ (u–v–u একই edge দুবার, তাই cycle নয়); directed গ্রাফে u → v → u দৈর্ঘ্য ২-এর বৈধ cycle. bKash-এ A → B → C → A ধাঁচের টাকার চক্র জালিয়াতি ধরার কাজে খোঁজা হয় — এটি directed cycle খোঁজা (পর্ব ৪১)।
সংযোগ আর component
Undirected গ্রাফে যেকোনো দুটি vertex-এর মধ্যে path থাকলে গ্রাফটি connected. না থাকলে গ্রাফটি কয়েকটি টুকরোয় ভাগ হয় — প্রতিটি টুকরো একটি connected component: সবচেয়ে বড় এমন vertex-এর দল, যার ভেতরে সবাই সবার কাছে পৌঁছাতে পারে। “u থেকে v-তে যাওয়া যায়” সম্পর্কটি reflexive, symmetric আর transitive — একটি equivalence relation; component-গুলো তার শ্রেণি, ঠিক যেমন পর্ব ৩৩-এ DSU-র দলগুলো ছিল।
Directed গ্রাফে ব্যাপারটি সূক্ষ্ম, কারণ u থেকে v-তে যাওয়া গেলেও v থেকে u-তে না-ও যাওয়া যেতে পারে। যদি প্রতিটি জোড়া u, v-এর জন্য u ⇝ v আর v ⇝ u দুই দিকেই path থাকে, গ্রাফটি strongly connected. দিক উপেক্ষা করলে যদি connected হয়, তবে weakly connected. যেকোনো directed গ্রাফ তার strongly connected component (SCC)-গুলোতে ভাগ হয়, আর প্রতিটি SCC-কে একটি বিন্দুতে সংকুচিত করলে যা থাকে, তাতে কোনো cycle নেই। SCC বের করার O(n + m) অ্যালগরিদম (Tarjan, Kosaraju) পর্ব ৪৬-এ।
Tree — cycle নেই, তবু সবাই যুক্ত
গ্রাফের ভাষায় একটি tree হলো connected আর acyclic (cycle-হীন) undirected গ্রাফ. পর্ব ২৬-এর rooted tree এরই একটি রূপ — যেকোনো একটি vertex-কে root বেছে নিলে বাকিদের parent-সন্তান সম্পর্ক আপনা থেকে ঠিক হয়ে যায়। Cycle-হীন কিন্তু হয়তো বিচ্ছিন্ন গ্রাফ হলো forest — কয়েকটি tree. এবার tree-র সবচেয়ে বিখ্যাত বৈশিষ্ট্যটি প্রমাণ করি।
উপপাদ্য: n টি vertex-এর যেকোনো tree-তে ঠিক n − 1 টি edge.
প্রমাণ (edge একে একে যোগ করে): n টি vertex, কোনো edge নেই — n টি component দিয়ে শুরু করুন, তারপর tree-র edge-গুলো যেকোনো ক্রমে একে একে যোগ করুন। প্রতিটি edge {u, v} যোগের সময় দুটি সম্ভাবনা: (ক) u আর v আগে থেকেই একই component-এ — তাহলে তাদের মধ্যে আগেই একটি path ছিল, নতুন edge-টি তার সাথে মিলে একটি cycle বানায়; (খ) আলাদা component-এ — তাহলে দুই component জুড়ে একটি হয়, component-এর সংখ্যা ঠিক ১ কমে। Tree-তে cycle নেই, তাই প্রতিটি edge (খ) ধরনের। শেষে tree connected, অর্থাৎ ১টি component. n থেকে ১-এ নামতে ঠিক n − 1 বার কমাতে হয় — তাই edge ঠিক n − 1 টি। ∎ এই যুক্তিটি আসলে পর্ব ৩৩-এর DSU-র cycle detection, আর পর্ব ৪৫-এর Kruskal-এর ভিত্তি।
একই যুক্তি থেকে একটি শক্তিশালী ফল: n টি vertex-এর undirected গ্রাফের জন্য (১) connected, (২) acyclic, (৩) ঠিক n − 1 টি edge — এই তিনটির যেকোনো দুটি সত্য হলে তৃতীয়টিও সত্য। কনটেস্টে “n টি শহর, n − 1 টি রাস্তা, সব শহর যুক্ত” দেখলেই বুঝবেন গ্রাফটি একটি tree (পর্ব ৪৮-এর কৌশল কাজে লাগবে)।
DAG, bipartite আর complete গ্রাফ
DAG (directed acyclic graph) হলো কোনো directed cycle নেই এমন directed গ্রাফ। কোর্সের prerequisite-এর গ্রাফ DAG হতেই হবে: “C++ পড়তে C লাগবে, C পড়তে C++ লাগবে” — এমন চক্র থাকলে কেউ কোনোদিন শুরুই করতে পারবেন না। প্রতিটি DAG-এর vertex-গুলোকে এমন এক সারিতে সাজানো যায়, যাতে প্রতিটি edge বাঁ থেকে ডানে যায় — topological sort (পর্ব ৪২)। Spreadsheet-এর ফর্মুলা বা build-এর ক্রমও DAG.
Bipartite graph-এর vertex-গুলো দুটি দলে ভাগ করা যায়, যাতে প্রতিটি edge এক দলের সাথে অন্য দলকে জোড়ে, দলের ভেতরে কোনো edge নেই। চাকরিপ্রার্থী আর পদ, রাইডার আর যাত্রী — দুই ধরনের জিনিসের সম্পর্ক স্বভাবতই bipartite. একটি সুন্দর উপপাদ্য, যা Dénes Kőnig-এর ১৯৩৬ সালের বইয়ে প্রমাণসহ আছে: একটি গ্রাফ bipartite হবে ঠিক তখনই, যখন তাতে কোনো বিজোড় দৈর্ঘ্যের cycle নেই। BFS দিয়ে দুই রঙে রাঙিয়ে এটি O(n + m)-এ যাচাই করা যায় (পর্ব ৪০), আর bipartite matching পর্ব ৪৭-এ।
Complete graph Kn-এ প্রতিটি জোড়া vertex-এর মধ্যে edge আছে: m = n(n − 1)/2, প্রত্যেকের degree n − 1. ১০টি দলের round-robin লিগের ম্যাচ-গ্রাফ K10, 45টি ম্যাচ। আর Km,n হলো complete bipartite: m জনের এক দল আর n জনের অন্য দলের প্রত্যেকে প্রত্যেকের সাথে যুক্ত, m·n টি edge.
Dense আর sparse — m কি n²-এর কাছাকাছি?
m সর্বোচ্চ প্রায় n²/2. যে গ্রাফে m প্রায় n²-এর ক্রমে (m = Θ(n²)), তা dense; যে গ্রাফে m অনেক কম — সাধারণত O(n) বা O(n log n) — তা sparse. বাস্তবের প্রায় সব বড় গ্রাফ খুব sparse: ঢাকার একটি মোড়ে সাধারণত ৩–৫টি রাস্তা এসে মেলে, n যত বড়ই হোক। ফেসবুকে কোটি কোটি মানুষ, কিন্তু একজনের বন্ধু সর্বোচ্চ ৫,০০০ — তাই m ≤ 2500n, যা n²/2-এর চেয়ে লাখো গুণ কম। অন্যদিকে “প্রতিটি শহর-জোড়ার সরাসরি দূরত্ব” dense, প্রায় complete.
৫ · গ্রাফ মেমরিতে রাখা — matrix, list, edge list আর CSR
কাগজের ছবি কম্পিউটারে চলে না; গ্রাফ রাখতে হয় অ্যারে বা list-এ। চারটি প্রচলিত উপায় আছে, আর প্রতিটি ভিন্ন প্রশ্নে দ্রুত। নিচের ডায়াগ্রামে একটি ছোট directed weighted গ্রাফ — চারটি bKash অ্যাকাউন্ট (1 = আপনি, 2 = মা, 3 = দোকানদার, 4 = ছোট ভাই) আর চারটি লেনদেন — একই সাথে দুটি উপস্থাপনে তৈরি হচ্ছে।
| পড়া edge (u v w) | |
| out-degree (1 2 3 4) | |
| in-degree (1 2 3 4) | |
| Σ out = Σ in = m |
- শুরু. n = 4. Matrix একটি 4 × 4 টেবিল, সব ঘর খালি — শুরু থেকেই n² = 16টি ঘরের জায়গা নিয়ে বসে আছে, edge থাকুক বা না থাকুক। List-এ চারটি খালি তালিকা।
- 1 2 500. আপনি মা-কে ৫০০ টাকা পাঠালেন:
mat[1][2] = 500, আর তালিকা 1-এর শেষে (2, 500). Directed, তাই শুধু এক দিকে —mat[2][1]খালিই রইল। out-degree(1) = 1, in-degree(2) = 1. - 1 3 200. দোকানদারকে ২০০: সারি 1-এ আরেকটি ঘর। List 1-এ এখন দুটি এন্ট্রি — out-degree(1) = 2 = তালিকা 1-এর দৈর্ঘ্য। List-এ out-degree সরাসরি পাওয়া যায়, matrix-এ পুরো সারি গুনতে হয়।
- 3 2 100. দোকানদার মা-কে ১০০ ফেরত দিলেন। in-degree(2) = 2 — matrix-এ এটি কলাম 2-এর ভরা ঘর; list থেকে in-degree বের করতে কিন্তু সব তালিকা ঘুরতে হয়।
- 2 4 300. মা ছোট ভাইকে ৩০০. m = 4: matrix-এর ১৬টি ঘরের মাত্র ৪টি ভরা, বাকি ১২টি নষ্ট জায়গা; list-এ ঠিক ৪টি এন্ট্রি। আর লক্ষ করুন — Σ out-degree = 2 + 1 + 1 + 0 = 4 = Σ in-degree = m, directed handshake.
Adjacency matrix
Adjacency matrix একটি n × n অ্যারে A, যেখানে A[u][v] = 1 (বা ওজন) যদি u → v edge থাকে, নইলে 0 (বা ∞, বা একটি “নেই” চিহ্ন)। Undirected গ্রাফে A প্রতিসম (symmetric): A[u][v] = A[v][u]. Multigraph-এ ঘরে edge-এর সংখ্যা রাখা যায় — পরের অংশের কোডে Königsberg ঠিক এভাবেই রাখা হয়েছে। সুবিধা: “u আর v-এর মধ্যে edge আছে?” — এক ঘর পড়া, O(1). অসুবিধা: জায়গা সবসময় Θ(n²), আর u-এর প্রতিবেশী খুঁজতে পুরো সারি, Θ(n) — u-এর মাত্র ২ জন প্রতিবেশী থাকলেও।
Adjacency list
Adjacency list-এ প্রতিটি vertex u-এর জন্য তার প্রতিবেশীদের একটি তালিকা adj[u] — C++-এ vector<vector<int>>, ওজনসহ হলে vector<vector<pair<int,int>>>; Python-এ list-এর list. মোট জায়গা Θ(n + m): n টি তালিকা আর handshake lemma অনুযায়ী মোট 2m (undirected) বা m (directed) টি এন্ট্রি। u-এর প্রতিবেশী ঘোরা Θ(deg(u)) — ঠিক যতটুকু দরকার। কিন্তু “u–v edge আছে?” প্রশ্নে adj[u] খুঁজতে হয়, O(deg(u)); তালিকা সর্ট করা থাকলে binary search-এ O(log deg(u)), বা প্রতিটি vertex-এ একটি hash set রাখলে গড়ে O(1). BFS, DFS, Dijkstra — এই মডিউলের প্রায় সব অ্যালগরিদম adjacency list-এ লেখা হয়।
Edge list
Edge list সবচেয়ে সরল: শুধু m টি (u, v) বা (u, v, w)-এর একটি অ্যারে — ইনপুট ঠিক যেভাবে আসে। জায়গা Θ(m). কোনো vertex-এর প্রতিবেশী খুঁজতে পুরো তালিকা, O(m) — তাই BFS-এর জন্য অচল। কিন্তু যে অ্যালগরিদম edge-গুলোকে একসাথে দেখে, তার জন্য এটিই সেরা: Kruskal ওজন অনুযায়ী edge সর্ট করে (পর্ব ৪৫), Bellman-Ford প্রতি ধাপে সব edge ঘোরে (পর্ব ৪৪)।
CSR — দুটি অ্যারেতে পুরো গ্রাফ
CSR (compressed sparse row) হলো adjacency list-এর “জমাট” রূপ। সব তালিকা পরপর জোড়া দিয়ে একটি অ্যারে to (দৈর্ঘ্য m, undirected হলে 2m), আর একটি অ্যারে start (দৈর্ঘ্য n + 1): u-এর প্রতিবেশীরা to[start[u]] থেকে to[start[u+1] − 1] পর্যন্ত। বানানো যায় দুই পাসে — প্রথমে out-degree গুনে prefix sum (পর্ব ১৭) দিয়ে start, তারপর প্রতিটি edge তার জায়গায় বসানো, ঠিক counting sort-এর মতো (পর্ব ২৪)।
Pseudocode — build CSR from a directed edge list (vertices 1..n)
start[1..n+1] = 0
for each edge (u, v): start[u+1] += 1 // count out-degree of u
for u = 1 to n: start[u+1] += start[u] // prefix sums: start[u] = first slot of u
pos = copy of start
for each edge (u, v): to[pos[u]] = v; pos[u] += 1
// neighbours of u: to[start[u]] .. to[start[u+1] - 1]লাভ: মাত্র দুটি অ্যারে, আলাদা আলাদা vector-এর বাড়তি খরচ নেই, আর সব প্রতিবেশী মেমরিতে পাশাপাশি — cache locality চমৎকার। অসুবিধা: পরে edge যোগ করতে পুরো কাঠামো আবার বানাতে হয়, তাই CSR স্থির (static) গ্রাফের জন্য।
খরচের তুলনা
| কাজ | Adjacency matrix | Adjacency list | Edge list | CSR |
|---|---|---|---|---|
| জায়গা | Θ(n²) | Θ(n + m) | Θ(m) | Θ(n + m) |
| u–v edge আছে? | O(1) | O(deg u); সর্ট করা থাকলে O(log deg u) | O(m) | O(deg u); সর্ট করা থাকলে O(log deg u) |
| u-এর সব প্রতিবেশী | Θ(n) | Θ(deg u) | Θ(m) | Θ(deg u) |
| সব edge একবার ঘোরা | Θ(n²) | Θ(n + m) | Θ(m) | Θ(n + m) |
| নতুন edge যোগ | O(1) | O(1) amortized | O(1) amortized | O(n + m) — আবার বানানো |
| edge মোছা | O(1) | O(deg u) | O(m) | O(n + m) |
| কখন বেছে নেবেন | n ছোট (কয়েক হাজার), dense, Floyd-Warshall | প্রায় সবসময় — BFS, DFS, Dijkstra | Kruskal, Bellman-Ford, ইনপুট জমা রাখা | বিশাল স্থির গ্রাফ, সর্বোচ্চ গতি |
সংখ্যায় দেখুন। কনটেস্টে সাধারণ সীমা n = 2 × 105, m = 2 × 105. Matrix-এ 4 × 1010 টি ঘর — int-এ ১৬০ GB, এক bit করে রাখলেও ৫ GB; মেমরি সীমা সাধারণত ২৫৬ MB. Adjacency list-এ মোটামুটি n টি vector আর 2m = 4 × 105 টি int — কয়েক MB মাত্র। তাই নিয়মটি সহজ: ডিফল্ট adjacency list; n কয়েক হাজারের মধ্যে আর বারবার “edge আছে?” জিজ্ঞেস করতে হলে matrix; edge সর্ট করতে হলে edge list.
Matrix-এ BFS চালালে সময়ও বদলায়। BFS আর DFS প্রতিটি vertex-এর সব প্রতিবেশী একবার দেখে। Adjacency list-এ সেটি Σ deg(u) = O(n + m); matrix-এ প্রতিটি vertex-এর পুরো সারি, n × n = Θ(n²) — গ্রাফ sparse হলেও। “BFS O(V + E)” কথাটি শুধু adjacency list (বা CSR)-এ সত্য।
৬ · Königsberg-এর সাত সেতু — Euler path-এর শর্ত
অষ্টাদশ শতকের প্রুশিয়ার শহর Königsberg (আজকের রাশিয়ার Kaliningrad) Pregel নদীর দুই পাড়ে, আর নদীর মাঝে দুটি দ্বীপ। দুই পাড় আর দুই দ্বীপ — এই চারটি স্থলভাগকে জুড়েছিল সাতটি সেতু। শহরবাসীর মধ্যে একটি ধাঁধা চালু ছিল: এমনভাবে হাঁটা যায় কি, যাতে প্রতিটি সেতু ঠিক একবার পার হতে হয়? কেউ পারেননি, আবার অসম্ভব বলে প্রমাণও করেননি।
তখন St. Petersburg-এর বিজ্ঞান একাডেমিতে কাজ করা Leonhard Euler সমস্যাটি নিয়ে ভাবলেন, আর ১৭৩৬ সালের বলে পরিচিত একটি প্রবন্ধে উত্তর দিলেন — এটিকেই গ্রাফ তত্ত্বের জন্ম ধরা হয়। তাঁর মূল চাল ছিল বিমূর্ত করা: গুরুত্বপূর্ণ শুধু কোন স্থলভাগ কোনটির সাথে কয়টি সেতুতে যুক্ত। আজকের ভাষায়: চারটি vertex (উত্তর পাড়, দক্ষিণ পাড়, Kneiphof দ্বীপ, পূর্বের দ্বীপ) আর সাতটি edge — একটি multigraph, কারণ Kneiphof-এর সাথে উত্তর পাড়ের দুটি আর দক্ষিণ পাড়ের দুটি সেতু ছিল। Degree: Kneiphof ৫, বাকি তিনটি স্থলভাগ ৩ করে।
এখন সংজ্ঞা। একটি Euler path (Eulerian trail) হলো এমন একটি walk, যা গ্রাফের প্রতিটি edge ঠিক একবার ব্যবহার করে (vertex বারবার আসতে পারে)। শুরু আর শেষ একই vertex হলে সেটি Euler circuit.
কেন বিজোড় vertex ২-এর বেশি হলে অসম্ভব (প্রয়োজনীয়তার প্রমাণ): ধরুন একটি Euler path আছে। হাঁটার সময় শুরু বা শেষ নয় এমন একটি vertex v-তে আপনি যতবার ঢোকেন, ততবারই বেরোন — প্রতিবার একটি edge দিয়ে ঢোকা, আরেকটি দিয়ে বেরোনো। প্রতিটি edge ঠিক একবার ব্যবহার হয়, তাই v-এর edge-গুলো ঢোকা-বেরোনোর জোড়ায় ভাগ হয়ে যায়: deg(v) জোড়। শুধু শুরুর vertex-এ একটি “বাড়তি” বেরোনো আর শেষের vertex-এ একটি “বাড়তি” ঢোকা আছে। তাই বিজোড় degree থাকতে পারে কেবল শুরু আর শেষে — সর্বোচ্চ ২টি; আর শুরু = শেষ হলে (circuit) একটিও নয়। Königsberg-এ চারটি স্থলভাগেরই degree বিজোড় — তাই ধাঁধাটির কোনো সমাধান নেই। ∎ (Handshake-এর অনুসিদ্ধান্তে বিজোড় vertex কখনো ১টি হয় না।)
উল্টো দিক (যথেষ্টতা): শর্ত পূরণ হলেই পথ আছে — এটি Euler সম্ভবত জানতেন, কিন্তু সম্পূর্ণ প্রমাণ প্রকাশ হয় অনেক পরে, Carl Hierholzer-এর মৃত্যুর পর ১৮৭৩ সালে। তাঁর গঠনমূলক যুক্তিটিই আজকের Hierholzer-এর অ্যালগরিদম: সব degree জোড় হলে একটি vertex থেকে অব্যবহৃত edge ধরে হাঁটতে থাকুন — আটকে যাবেন কেবল শুরুর vertex-এ ফিরে (অন্য যেকোনো vertex-এ ঢুকলে জোড় degree-র কারণে বেরোনোর একটি edge বাকি থাকে)। কোনো edge বাকি থাকলে চক্রের একটি vertex থেকে আরেকটি চক্র বানিয়ে প্রথমটির ভেতরে জুড়ে দিন। প্রতিটি edge একবার দেখা হয়, তাই সময় O(n + m). ঠিক ২টি বিজোড় vertex থাকলে তাদের মধ্যে একটি কাল্পনিক edge যোগ করে circuit বানান, তারপর সেটি কেটে ফেলুন — পাওয়া যায় এক বিজোড় vertex থেকে অন্যটিতে Euler path.
নিচের ছবিটি জার্মানির শিশুদের একটি পরিচিত ধাঁধা — “Das Haus vom Nikolaus”: কলম না তুলে, কোনো দাগ দুবার না টেনে বাড়িটি আঁকুন। পাঁচটি vertex, আটটি edge (বর্গের চার বাহু, দুটি কর্ণ আর ছাদের দুটি ঢাল)।
| এখন যে vertex-এ | |
| ব্যবহৃত / বাকি edge | |
| degree (1 2 3 4 5) |
- শর্ত যাচাই. Degree: 1 → 3, 2 → 3, 3 → 4, 4 → 4, 5 → 2. যোগফল 16 = 2 × 8 (handshake ঠিক আছে)। বিজোড় vertex ঠিক দুটি, 1 আর 2, আর গ্রাফ connected — তাই Euler path আছে, আর সেটি শুরু হতেই হবে 1 বা 2 থেকে। আমরা 1 থেকে শুরু করি।
- 1 → 4 → 5. বাঁ দেয়াল বেয়ে উঠে ছাদের বাঁ ঢাল। দুটি edge ব্যবহৃত, ছয়টি বাকি।
- 5 → 3 → 4. ছাদের ডান ঢাল, তারপর ছাদের নিচের আড়াআড়ি দাগ। আমরা আবার 4-এ — vertex পুনরাবৃত্তি বৈধ, শুধু edge নয়। 4-এর চারটি edge-এর দুটি “ঢোকা-বেরোনো” জোড়া এখানে পূর্ণ হলো।
- 4 → 2 → 3. একটি কর্ণ বেয়ে নিচে 2, তারপর ডান দেয়াল বেয়ে আবার 3. বাকি শুধু দুটি edge: 3–1 (অন্য কর্ণ) আর 1–2 (মেঝে)।
- 3 → 1 → 2. কর্ণ বেয়ে 1, তারপর মেঝে ধরে 2. আটটি edge-ই ঠিক একবার — পথ শেষ হলো অন্য বিজোড় vertex 2-এ, ঠিক যেমন প্রমাণ বলেছিল। 3 থেকে শুরু করলে (জোড় vertex) এক সময় আটকে যেতেন — নিজে চেষ্টা করে দেখুন।
Directed গ্রাফে শর্তটি একই যুক্তিতে: Euler circuit আছে যদি প্রতিটি vertex-এ in-degree = out-degree হয় আর edge-যুক্ত সব vertex (দিক উপেক্ষা করে) এক component-এ থাকে; Euler path আছে যদি ঠিক একটি vertex-এ out − in = 1 (শুরু), ঠিক একটিতে in − out = 1 (শেষ), আর বাকি সবখানে সমান হয় (একই connectivity শর্তসহ)। প্রয়োগ শুধু ধাঁধায় নয়: DNA sequencing-এ ছোট টুকরো জোড়া দেওয়ার একটি প্রধান পদ্ধতি (de Bruijn graph) আসলে Euler path খোঁজা।
Euler বনাম Hamilton — একটি শব্দের পার্থক্য, সময়ের বিশাল পার্থক্য। “প্রতিটি edge ঠিক একবার” — degree গুনেই O(n + m)-এ উত্তর। “প্রতিটি vertex ঠিক একবার” (Hamiltonian path) — শুনতে প্রায় একই, কিন্তু এটি NP-complete: কোনো দ্রুত অ্যালগরিদম জানা নেই, আর থাকলে P = NP হতো। Backtracking দিয়ে Hamiltonian cycle পর্ব ৫৬-এ, আর NP-complete কী, তা পর্ব ৬৪-এ।
৭ · পুরো কোড — ইনপুট পড়া, দুই উপস্থাপন, degree আর Euler-এর শর্ত
কনটেস্ট আর অনলাইন judge-এ গ্রাফ প্রায় সবসময় একই ছাঁচে আসে: প্রথম লাইনে n m, তারপর m টি লাইনে u v (ওজন থাকলে u v w), আর vertex-এর নম্বর সাধারণত 1 থেকে n — 1-based. Tree হলে প্রায়ই শুধু n আর n − 1 টি edge, বা 2…n প্রতিটি vertex-এর parent-এর একটি তালিকা।
Pseudocode — read an undirected graph (vertices 1..n)
read n, m
adj = n+1 empty lists // index 0 unused: vertices are 1..n
for i = 1 to m:
read u, v
append v to adj[u]
append u to adj[v] // undirected: store both directions
deg(u) = length of adj[u]নিচের প্রোগ্রামটি দুটি গ্রাফ পড়ে — অংশ ৬-এর “নিকোলাসের বাড়ি” আর Königsberg (1 = Kneiphof দ্বীপ, 2 = উত্তর পাড়, 3 = দক্ষিণ পাড়, 4 = পূর্বের দ্বীপ)। প্রতিটির জন্য adjacency matrix আর adjacency list দুটোই বানায়, প্রতিটি vertex-এর matrix-সারি, degree আর প্রতিবেশী ছাপে, handshake lemma যাচাই করে, আর বিজোড় degree গুনে Euler path-এর রায় দেয়। ইনপুট stdin থেকে না পড়ে একটি স্থির string থেকে পড়া হয়েছে, যাতে ফল সবসময় একই থাকে; আসল কনটেস্টে istringstream-এর জায়গায় সরাসরি cin বসাবেন।
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
void analyse(const string& name, const string& text) {
istringstream in(text);
int n, m; in >> n >> m; // first line: n m
vector<vector<int>> mat(n + 1, vector<int>(n + 1, 0)); // 1-based: rows 1..n
vector<vector<int>> adj(n + 1);
for (int i = 0; i < m; ++i) {
int u, v; in >> u >> v;
mat[u][v]++; mat[v][u]++; // counts allow parallel edges
adj[u].push_back(v); adj[v].push_back(u);
}
cout << name << ": n = " << n << ", m = " << m << '\n';
int total = 0, odd = 0;
for (int u = 1; u <= n; ++u) {
int d = (int)adj[u].size();
cout << " " << u << " | row:";
for (int v = 1; v <= n; ++v) cout << ' ' << mat[u][v];
cout << " | deg " << d << " | list:";
for (int v : adj[u]) cout << ' ' << v;
cout << '\n';
total += d; odd += d % 2;
}
cout << " sum of degrees = " << total << ", 2m = " << 2 * m << (total == 2 * m ? " (ok)" : " (BUG)") << '\n';
string verdict = odd == 0 ? "yes, even a closed circuit"
: odd == 2 ? "yes, from one odd vertex to the other" : "no";
cout << " odd-degree vertices: " << odd << " -> Euler path: " << verdict << '\n';
}
int main() {
analyse("house", "5 8 1 2 2 3 3 4 4 1 1 3 2 4 3 5 4 5");
analyse("Konigsberg", "4 7 1 2 1 2 1 3 1 3 1 4 2 4 3 4");
}আউটপুট:
house: n = 5, m = 8
1 | row: 0 1 1 1 0 | deg 3 | list: 2 4 3
2 | row: 1 0 1 1 0 | deg 3 | list: 1 3 4
3 | row: 1 1 0 1 1 | deg 4 | list: 2 4 1 5
4 | row: 1 1 1 0 1 | deg 4 | list: 3 1 2 5
5 | row: 0 0 1 1 0 | deg 2 | list: 3 4
sum of degrees = 16, 2m = 16 (ok)
odd-degree vertices: 2 -> Euler path: yes, from one odd vertex to the other
Konigsberg: n = 4, m = 7
1 | row: 0 2 2 1 | deg 5 | list: 2 2 3 3 4
2 | row: 2 0 0 1 | deg 3 | list: 1 1 4
3 | row: 2 0 0 1 | deg 3 | list: 1 1 4
4 | row: 1 1 1 0 | deg 3 | list: 1 2 3
sum of degrees = 14, 2m = 14 (ok)
odd-degree vertices: 4 -> Euler path: no
বাড়ির গ্রাফে vertex 3-এর সারি 1 1 0 1 1: 3-এর প্রতিবেশী 1, 2, 4, 5 — list-এও একই চারজন, শুধু ইনপুটের ক্রমে (2 4 1 5). Matrix প্রতিসম (সারি 1-এর কলাম 2 = সারি 2-এর কলাম 1), কারণ গ্রাফ undirected আর প্রতিটি edge দুই দিকেই লেখা হয়েছে। Königsberg-এ সারি 1 0 2 2 1 — matrix-এর ঘরে সংখ্যা রাখায় parallel edge গোনা গেছে, আর list-এ 2 আর 3 দুবার করে এসেছে। দুই গ্রাফেই Σ deg = 2m (16 আর 14), আর বিজোড় degree-র vertex জোড় সংখ্যক (২ আর ৪)। বাড়িতে ২টি — পথ আছে; Königsberg-এ ৪টি — Euler-এর উত্তর “না”. দুটি গ্রাফই connected, তাই এখানে শুধু degree গোনাই যথেষ্ট; সাধারণ কোডে connectivity আলাদা করে যাচাই করতে হবে (পর্ব ৪০–৪১-এর BFS বা DFS দিয়ে)। সময় O(n² + m) — matrix বানাতে আর ছাপতে n², list-এ n + m; জায়গাও একই কারণে Θ(n² + m).
def analyse(name, text):
data = iter(map(int, text.split()))
n, m = next(data), next(data) # first line: n m
mat = [[0] * (n + 1) for _ in range(n + 1)] # 1-based: rows 1..n
adj = [[] for _ in range(n + 1)]
for _ in range(m):
u, v = next(data), next(data)
mat[u][v] += 1 # counts allow parallel edges
mat[v][u] += 1
adj[u].append(v)
adj[v].append(u)
print(f"{name}: n = {n}, m = {m}")
total = odd = 0
for u in range(1, n + 1):
d = len(adj[u])
print(f" {u} | row:", *mat[u][1:], f"| deg {d} | list:", *adj[u])
total += d
odd += d % 2
print(f" sum of degrees = {total}, 2m = {2 * m}", "(ok)" if total == 2 * m else "(BUG)")
if odd == 0:
verdict = "yes, even a closed circuit"
elif odd == 2:
verdict = "yes, from one odd vertex to the other"
else:
verdict = "no"
print(f" odd-degree vertices: {odd} -> Euler path: {verdict}")
analyse("house", "5 8 1 2 2 3 3 4 4 1 1 3 2 4 3 5 4 5")
analyse("Konigsberg", "4 7 1 2 1 2 1 3 1 3 1 4 2 4 3 4")আউটপুট C++-এর সাথে হুবহু এক:
house: n = 5, m = 8
1 | row: 0 1 1 1 0 | deg 3 | list: 2 4 3
2 | row: 1 0 1 1 0 | deg 3 | list: 1 3 4
3 | row: 1 1 0 1 1 | deg 4 | list: 2 4 1 5
4 | row: 1 1 1 0 1 | deg 4 | list: 3 1 2 5
5 | row: 0 0 1 1 0 | deg 2 | list: 3 4
sum of degrees = 16, 2m = 16 (ok)
odd-degree vertices: 2 -> Euler path: yes, from one odd vertex to the other
Konigsberg: n = 4, m = 7
1 | row: 0 2 2 1 | deg 5 | list: 2 2 3 3 4
2 | row: 2 0 0 1 | deg 3 | list: 1 1 4
3 | row: 2 0 0 1 | deg 3 | list: 1 1 4
4 | row: 1 1 1 0 | deg 3 | list: 1 2 3
sum of degrees = 14, 2m = 14 (ok)
odd-degree vertices: 4 -> Euler path: no
iter(map(int, text.split())) পুরো ইনপুটকে সংখ্যার একটি স্রোত বানায়, আর next(data) একটি করে তুলে নেয়। কনটেস্টে: data = sys.stdin.buffer.read().split() দিয়ে একবারে সব পড়ে নিন — বারবার input() ডাকলে ২ × 105 লাইনে Python অনেক ধীর হয়। [[0] * (n + 1) for _ in range(n + 1)] লক্ষ করুন: [[0] * (n + 1)] * (n + 1) লিখলে সব সারি একই list হতো, একটি ঘর বদলালে পুরো কলাম বদলে যেত — Python-এ 2D অ্যারের সবচেয়ে পরিচিত ফাঁদ।
কনটেস্টের ইনপুটে যে ভুলগুলো সবচেয়ে বেশি হয়
- আকার n, n + 1 নয়: vertex 1…n হলে
adj(n + 1)নিন, অথবা পড়ার সময়ই--u; --v;করে 0-based করে নিন — কিন্তু দুটো মিশিয়ে নয়। এটি গ্রাফ-কোডের সবচেয়ে সাধারণ off-by-one ভুল। - উল্টো দিকের edge ভুলে যাওয়া: undirected গ্রাফে
adj[v].push_back(u)না লিখলে গ্রাফটি চুপচাপ directed হয়ে যায়; ছোট পরীক্ষায় প্রায়ই উত্তর ঠিক আসে। - একাধিক test case: প্রতিটি test-এর আগে
adjখালি করুন (শুধু 1…n পর্যন্ত, পুরো সর্বোচ্চ আকার নয় — নইলে সময় যায়)।
৮ · Implicit graph — গ্রিড আর ধাঁধার অবস্থা
অনেক সমস্যায় গ্রাফটি কোথাও লেখা নেই — একটি নিয়ম থেকে প্রতিবেশী হিসাব করে বের করা হয়। একে বলে implicit graph. সবচেয়ে পরিচিত উদাহরণ grid graph: একটি R × C গোলকধাঁধা বা মানচিত্র, যেখানে প্রতিটি খোলা ঘর একটি vertex, আর পাশাপাশি দুটি খোলা ঘরের মধ্যে একটি edge. Adjacency list বানানোর দরকার নেই — ঘর (r, c)-এর প্রতিবেশী সরাসরি:
- ৪ দিকের প্রতিবেশী (ওপর, নিচ, বাঁ, ডান): (r − 1, c), (r + 1, c), (r, c − 1), (r, c + 1).
- ৮ দিকের প্রতিবেশী: ওপরের চারটি আর চারটি কোনাকুনি — দাবার রাজার চাল।
- প্রতিটি প্রার্থীর জন্য দুটি পরীক্ষা: ঘরটি গ্রিডের ভেতরে কি না (0 ≤ r < R, 0 ≤ c < C), আর দেয়াল (
#) কি না। ক্রম জরুরি — আগে সীমা, পরেgrid[r][c], নইলে অ্যারের বাইরে পড়বেন। - কোনো অ্যারেতে (যেমন দূরত্ব) রাখতে হলে ঘরটিকে একটি সংখ্যা দিন: id = r · C + c, যা 0 থেকে R·C − 1; উল্টো দিকে r = id / C, c = id % C — 2D অ্যারের row-major ক্রমের মতোই।
দিকগুলো দুটি ছোট অ্যারেতে রাখলে কোড পরিষ্কার থাকে: dr = {-1, 1, 0, 0}, dc = {0, 0, -1, 1}. নিচে একটি ৩ × ৪ গ্রিডে ঘর (1, 2)-এর ৪ দিকের প্রতিবেশী খোঁজা হচ্ছে:
| দিক (dr, dc) | |
| যাচাই করা ঘর | |
| ফল | |
| প্রতিবেশী এ পর্যন্ত |
- শুরু. ঘর (1, 2) — চিহ্ন @ — এর id = 1 × 4 + 2 = 6. গ্রাফের কোনো তালিকা মেমরিতে নেই; চারটি দিক একে একে পরীক্ষা করব।
- ওপর. (0, 2): সীমার ভেতরে, কিন্তু সেখানে দেয়াল
#. Edge নেই। - নিচ. (2, 2): সীমার ভেতরে আর খোলা — প্রথম প্রতিবেশী পাওয়া গেল।
- বাঁ. (1, 1): আবার দেয়াল, বাদ।
- ডান. (1, 3): খোলা — দ্বিতীয় প্রতিবেশী। মোট deg = 2, খরচ O(1) — দিকের সংখ্যা স্থির। পুরো গ্রিডে BFS চালালে তাই সময় O(R·C), যদিও আমরা কখনো edge-তালিকা বানাইনি।
#include <iostream>
#include <string>
#include <utility>
#include <vector>
using namespace std;
int main() {
vector<string> grid = {"..#.",
".#..",
"...."}; // '#' is a wall, '.' is open
int R = (int)grid.size(), C = (int)grid[0].size();
const int DR[] = {-1, 1, 0, 0}, DC[] = {0, 0, -1, 1}; // up, down, left, right
auto is_open = [&](int r, int c) {
return r >= 0 && r < R && c >= 0 && c < C && grid[r][c] == '.';
};
for (auto [r, c] : vector<pair<int, int>>{{0, 0}, {1, 2}, {2, 1}}) {
cout << "cell (" << r << "," << c << ") id " << r * C + c << ": 4-nbrs";
for (int k = 0; k < 4; ++k)
if (is_open(r + DR[k], c + DC[k])) cout << " (" << r + DR[k] << "," << c + DC[k] << ")";
int eight = 0;
for (int dr = -1; dr <= 1; ++dr)
for (int dc = -1; dc <= 1; ++dc)
if ((dr != 0 || dc != 0) && is_open(r + dr, c + dc)) ++eight;
cout << " | 8-nbrs count " << eight << '\n';
}
int V = 0, E = 0;
for (int r = 0; r < R; ++r)
for (int c = 0; c < C; ++c)
if (is_open(r, c)) {
++V;
E += is_open(r, c + 1) + is_open(r + 1, c); // right and down: each edge once
}
cout << "vertices (open cells) = " << V << ", edges (4-neighbour) = " << E << '\n';
}আউটপুট:
cell (0,0) id 0: 4-nbrs (1,0) (0,1) | 8-nbrs count 2
cell (1,2) id 6: 4-nbrs (2,2) (1,3) | 8-nbrs count 6
cell (2,1) id 9: 4-nbrs (2,0) (2,2) | 8-nbrs count 4
vertices (open cells) = 10, edges (4-neighbour) = 10
কোনার ঘর (0, 0)-এর ওপর আর বাঁ গ্রিডের বাইরে — is_open-এর সীমা-পরীক্ষাই তাদের বাদ দেয়। দ্বিতীয় লাইনটি ওপরের ডায়াগ্রামের ফল, কোনাকুনি যোগ করলে ৬টি। শেষ লাইন: ১০টি খোলা ঘর, ১০টি edge. প্রতিটি ঘর থেকে শুধু ডান আর নিচের দিকে গুনেছি, যাতে প্রতিটি edge একবারই গোনা হয় — চার দিকেই গুনলে 20 আসত, অর্থাৎ Σ deg = 2m, আবার handshake. দেয়াল না থাকলে R × C গ্রিডে edge হতো R(C − 1) + C(R − 1); এখানে ৩ × ৪-এ সেটি 17. সময় Θ(R·C), বাড়তি জায়গা O(1).
grid = ["..#.",
".#..",
"...."] # '#' is a wall, '.' is open
R, C = len(grid), len(grid[0])
DR, DC = [-1, 1, 0, 0], [0, 0, -1, 1] # up, down, left, right
def is_open(r, c):
return 0 <= r < R and 0 <= c < C and grid[r][c] == "."
for r, c in [(0, 0), (1, 2), (2, 1)]:
four = [(r + dr, c + dc) for dr, dc in zip(DR, DC) if is_open(r + dr, c + dc)]
eight = sum(is_open(r + dr, c + dc) for dr in (-1, 0, 1) for dc in (-1, 0, 1)
if (dr, dc) != (0, 0))
cells = " ".join(f"({a},{b})" for a, b in four)
print(f"cell ({r},{c}) id {r * C + c}: 4-nbrs {cells} | 8-nbrs count {eight}")
V = E = 0
for r in range(R):
for c in range(C):
if is_open(r, c):
V += 1
E += is_open(r, c + 1) + is_open(r + 1, c) # right and down: each edge once
print(f"vertices (open cells) = {V}, edges (4-neighbour) = {E}")আউটপুট C++-এর সাথে হুবহু এক:
cell (0,0) id 0: 4-nbrs (1,0) (0,1) | 8-nbrs count 2
cell (1,2) id 6: 4-nbrs (2,2) (1,3) | 8-nbrs count 6
cell (2,1) id 9: 4-nbrs (2,0) (2,2) | 8-nbrs count 4
vertices (open cells) = 10, edges (4-neighbour) = 10
0 <= r < R — Python-এর শৃঙ্খলিত তুলনা সীমা-পরীক্ষাকে এক লাইনে রাখে, আর and-এর short-circuit নিশ্চিত করে যে সীমার বাইরে হলে grid[r][c] পড়াই হয় না। এটি জরুরি: Python-এ grid[-1] ভুল নয়, শেষ সারি ফেরত দেয় — তাই সীমা-পরীক্ষা ভুলে গেলে কোনো error ছাড়াই গ্রিডের উল্টো প্রান্তকে প্রতিবেশী ধরে বসবেন। True + True = 2, তাই edge গোনায় বুলিয়ান সরাসরি যোগ করা গেছে।
State space — প্রতিটি অবস্থা একটি vertex
Implicit graph-এর আরও বড় রূপ হলো state space: কোনো ধাঁধা বা প্রক্রিয়ার প্রতিটি সম্ভাব্য অবস্থা একটি vertex, আর এক চালে এক অবস্থা থেকে অন্যটিতে যাওয়া একটি edge. ৮-ধাঁধা (3 × 3 বোর্ডে ১–৮ টাইল আর একটি ফাঁকা ঘর) নিন: বোর্ডের প্রতিটি সাজানো একটি vertex, আর ফাঁকা ঘরের পাশের একটি টাইল সরানো একটি edge. মোট সাজানো 9! = 3,62,880, কিন্তু একটি নির্দিষ্ট শুরু থেকে ঠিক অর্ধেক, 1,81,440টিতে পৌঁছানো যায় — গ্রাফটির দুটি component. “সবচেয়ে কম চালে সমাধান” মানে এই গ্রাফে সবচেয়ে ছোট পথ, BFS দিয়ে (পর্ব ৪০)। প্রতিবেশী তৈরি হয় চলতে চলতে, আর দেখা অবস্থাগুলো একটি hash set-এ মনে রাখা হয়।
৯ · মডিউল ৬-এর মানচিত্র — কোন প্রশ্নে কোন অ্যালগরিদম
গ্রাফ চিনে ফেলার পর প্রশ্ন: “আমি আসলে কী জানতে চাই?” নিচের টেবিলটি এই মডিউলের বাকি পর্বগুলোর মানচিত্র। Complexity-গুলো adjacency list ধরে।
| প্রশ্ন | অ্যালগরিদম | সময় | পর্ব |
|---|---|---|---|
| ওজনহীন গ্রাফে সবচেয়ে কম edge-এর পথ, স্তরে স্তরে ঘোরা, bipartite কি না | BFS | O(n + m) | ৪০ |
| কোথায় কোথায় পৌঁছানো যায়, component, cycle আছে কি না | DFS | O(n + m) | ৪১ |
| DAG-এ নিয়ম মেনে ক্রম সাজানো (prerequisite, build order) | Topological sort (Kahn, DFS) | O(n + m) | ৪২ |
| অঋণাত্মক ওজনে এক উৎস থেকে সবচেয়ে ছোট পথ | Dijkstra (binary heap) | O((n + m) log n) | ৪৩ |
| ঋণাত্মক ওজন, ঋণাত্মক cycle; সব জোড়ার ছোট পথ | Bellman-Ford; Floyd-Warshall | O(n·m); O(n³) | ৪৪ |
| সবচেয়ে কম খরচে সব vertex জোড়া | MST: Kruskal, Prim | O(m log m); O((n + m) log n) | ৪৫ |
| Directed গ্রাফের SCC; কোন edge বা vertex কাটলে গ্রাফ ভাঙে | Tarjan, Kosaraju; bridge ও articulation point | O(n + m) | ৪৬ |
| পাইপে সর্বোচ্চ প্রবাহ, ন্যূনতম কাট, কাজ আর কর্মীর জোড়া | Network flow (Edmonds-Karp), Hopcroft-Karp | O(n·m²); O(m√n) | ৪৭ |
| Tree-তে দুই vertex-এর সবচেয়ে নিচু সাধারণ পূর্বপুরুষ, tree-র ব্যাস | LCA: binary lifting | O(n log n) প্রস্তুতি, প্রতি প্রশ্ন O(log n) | ৪৮ |
একটি জিনিস লক্ষ করুন: প্রথম তিনটি আর SCC — সবই O(n + m), অর্থাৎ গ্রাফটি একবার পড়ার সময়ের সমান। এগুলো সম্ভব হয় কারণ adjacency list-এ প্রতিটি edge মোট দুবার (undirected) বা একবার (directed) ছোঁয়া হয় — আবার handshake lemma.
আরও গভীরে — কোন degree-তালিকা দিয়ে গ্রাফ বানানো যায়? Handshake lemma বলে degree-র যোগফল জোড় হতে হবে, কিন্তু সেটি যথেষ্ট নয়: (3, 3, 1, 1) যোগফলে জোড়, তবু কোনো simple graph-এ এটি সম্ভব নয় (দুটি degree-3 vertex-এর প্রত্যেকে বাকি তিনজনের সাথে যুক্ত হলে degree-1 দুজনের প্রত্যেকের degree অন্তত ২)। কোন তালিকা “graphic”, তার পুরো উত্তর দেয় Havel–Hakimi পদ্ধতি (Václav Havel ১৯৫৫, S. L. Hakimi ১৯৬২): সবচেয়ে বড় degree d-টি সরিয়ে পরের d টি থেকে ১ করে কমান, আর পুনরাবৃত্তি করুন — কোথাও ঋণাত্মক হলে অসম্ভব। Erdős আর Gallai ১৯৬০ সালে একই প্রশ্নের একটি অসমতা-ভিত্তিক শর্ত দেন। দুটি গ্রাফ “আসলে একই” (isomorphic) কি না — এই সম্পর্কিত প্রশ্নটির জটিলতা আজও অমীমাংসিত; László Babai ২০১৫ সালে একটি quasi-polynomial অ্যালগরিদম ঘোষণা করেন।
পরীক্ষা ও ইন্টারভিউতে:
- ৮টি edge-এর একটি undirected গ্রাফে degree-র যোগফল কত? — 16; handshake lemma, Σ deg(v) = 2m.
- n টি vertex-এর simple undirected গ্রাফে সর্বোচ্চ কয়টি edge? — n(n − 1)/2 (complete graph Kn); directed হলে n(n − 1).
- Adjacency matrix আর list-এর জায়গা, আর কখন কোনটি? — Θ(n²) বনাম Θ(n + m); dense বা ছোট n আর O(1) edge-যাচাই লাগলে matrix, নইলে list.
- একটি connected গ্রাফে n টি vertex আর n − 1 টি edge — এটি কী? — একটি tree; acyclic, আর যেকোনো দুই vertex-এর মধ্যে path ঠিক একটি।
- কোন গ্রাফে প্রতিটি edge ঠিক একবার ঘুরে আসা যায়? — connected হলে যদি বিজোড় degree-র vertex ০টি (circuit) বা ২টি (path) থাকে; Euler, ১৭৩৬.
১০ · AI-এর যুগে গ্রাফ — মডেলিং আপনার, যাচাইও আপনার
“এই সমস্যার জন্য গ্রাফ বানিয়ে BFS লিখে দাও” — AI সহকারী এক নিমেষে কোড দেবে। কিন্তু গ্রাফ-সমস্যার কঠিন অংশটি প্রায়ই কোড নয়, মডেলিং: কী vertex, কী edge, দিক আছে কি না, ওজন কোথা থেকে আসে, state-এ কী কী রাখতে হবে। আর complexity-র প্রশ্নে AI প্রায়ই বলে “O(V + E)” — অথচ নিজেই কোডটি লিখেছে adjacency matrix দিয়ে।
গ্রাফের কোডে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
1-based ইনপুটে adj(n) — vertex n-এ অ্যারের বাইরে লেখা | vertex n-কে ছোঁয়া একটি edge দিয়ে চালান; sanitizer (-fsanitize=address) বা Python-এ IndexError |
Undirected গ্রাফে শুধু adj[u].push_back(v) | Edge 2–1 দিয়ে পরীক্ষা করুন: 1 থেকে 2-এ পৌঁছানো যায় কি না |
| n = 2 × 105-এ adjacency matrix | জায়গা হিসাব করুন: n² ঘর × আকার — মেমরি সীমার সাথে মেলান |
| “Matrix-এ BFS O(V + E)” | প্রতিটি vertex-এ পুরো সারি ঘোরা হচ্ছে কি না দেখুন — তাহলে Θ(V²) |
| Edge গুনতে সব list-এর দৈর্ঘ্য যোগ, ২ দিয়ে ভাগ না করা | Handshake: undirected-এ Σ |adj[u]| = 2m — একটি ত্রিভুজে 6 আসে, 3 নয় |
| “সব degree জোড় হলেই Euler circuit” | দুটি আলাদা ত্রিভুজ নিন: সব degree 2, কিন্তু circuit নেই — connectivity লাগে |
গ্রিডে আগে grid[r][c], পরে সীমা-পরীক্ষা; বা id = r·R + c | কোনার ঘর আর R ≠ C এমন গ্রিডে চালান; সব id আলাদা আর 0…R·C − 1-এর মধ্যে কি না assert করুন |
AI-কে সমাধান লিখতে না বলে, আপনার মডেল আর কোড যাচাই করতে বলুন — আর প্রতিটি দাবির কারণ চান:
গ্রাফের কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Königsberg-এর একটি শহুরে ধাঁধা কীভাবে গণিতের একটি নতুন শাখার জন্ম দিল — আর Euler-এর আসল অবদান কোথায়?
Euler নিজে সমস্যাটিকে খুব বড় কিছু মনে করেননি। সে সময়ের চিঠিপত্রে (Danzig শহরের মেয়র Carl Ehler আর ইতালির গণিতবিদ Giovanni Marinoni-কে লেখা) তিনি মোটামুটি এই অর্থে লিখেছিলেন যে সমাধানটি সাধারণ যুক্তির, গণিতের সাথে এর সম্পর্ক কম; তবু বিষয়টি আকর্ষণীয়, কারণ জ্যামিতি, বীজগণিত বা গণনার কোনো প্রচলিত কৌশল এখানে খাটে না। ঠিক এই কথাটিই আসলে নতুন শাখার ঘোষণা। জ্যামিতি দূরত্ব আর কোণ মাপে; বীজগণিত সমীকরণ সমাধান করে। Königsberg-এর প্রশ্নে দূরত্ব বা কোণের কোনো ভূমিকা নেই — শুধু “কোন জায়গা কোন জায়গার সাথে যুক্ত”. Euler তাঁর প্রবন্ধের শিরোনামে Leibniz-এর একটি ধারণার উল্লেখ করেন — “geometria situs”, অবস্থানের জ্যামিতি — যেখানে মাপজোক নয়, শুধু বিন্যাস গুরুত্বপূর্ণ।
Euler-এর আসল অবদান দুটি। প্রথমত, বিমূর্তকরণ: স্থলভাগকে অক্ষর আর সেতুকে তাদের মধ্যের সংযোগ ধরে সমস্যাটিকে এমন একটি রূপে আনা, যেখানে শহরের মানচিত্র অপ্রাসঙ্গিক। মজার ব্যাপার, তিনি আজকের মতো বিন্দু-রেখার ছবি আঁকেননি; সেই ছবি এসেছে অনেক পরে। দ্বিতীয়ত, তিনি শুধু Königsberg-এর উত্তর দেননি — সব সম্ভাব্য “সেতুর ধাঁধা”-র জন্য একটি সাধারণ নিয়ম দিয়েছেন: বিজোড় সংখ্যক সেতুর স্থলভাগ দুটির বেশি হলে অসম্ভব।
এরপর বিষয়টি ধীরে এগোয়। Gustav Kirchhoff ১৮৪৭ সালে বৈদ্যুতিক বর্তনীতে আর Arthur Cayley ১৮৫০-এর দশকে অণুর গঠন গুনতে tree ব্যবহার করেন; Hierholzer-এর Euler path-এর সম্পূর্ণ প্রমাণ ছাপা হয় ১৮৭৩ সালে। “Graph” শব্দটি প্রথম ব্যবহার করেন James Joseph Sylvester, ১৮৭৮ সালে, রসায়নের “graphic notation” থেকে ধার নিয়ে। আর বিষয়টির প্রথম পূর্ণাঙ্গ পাঠ্যবই লেখেন হাঙ্গেরির Dénes Kőnig, ১৯৩৬ সালে। Königsberg শহরটি দ্বিতীয় বিশ্বযুদ্ধে ব্যাপক ক্ষতিগ্রস্ত হয়, পরে নাম হয় Kaliningrad, আর মূল সাতটি সেতুর বেশিরভাগই আজ আর নেই।
মূল উপলব্ধি: গ্রাফ তত্ত্বের জন্ম হয়েছে অপ্রয়োজনীয় তথ্য ফেলে দেওয়ার সাহস থেকে। কোনো নতুন সমস্যায় প্রথমে নিজেকে জিজ্ঞেস করুন — কোন তথ্যটুকু আসলে গুরুত্বপূর্ণ? প্রায়ই উত্তর হয়: শুধু কে কার সাথে যুক্ত।
প্র ০২বাস্তবের বিশাল গ্রাফগুলো কেন প্রায় সবই sparse, আর এটি কীভাবে adjacency list আর CSR-কে প্রায় সর্বজনীন করে তুলেছে?
একটি সংখ্যা দিয়ে শুরু করি। ২০১১ সালে Facebook আর কয়েকজন গবেষকের একটি বিখ্যাত গবেষণায় তখনকার প্রায় ৭২ কোটি সক্রিয় ব্যবহারকারী আর প্রায় ৬,৯০০ কোটি বন্ধুত্ব বিশ্লেষণ করা হয়। Handshake lemma দিয়ে গড় degree = 2m / n ≈ 2 × 69 × 109 / (7.2 × 108) ≈ ১৯০ — বিশাল নেটওয়ার্কে একজন মানুষ গড়ে মাত্র দুশোর মতো মানুষের সাথে সরাসরি যুক্ত। Adjacency matrix লাগত প্রায় (7.2 × 108)² ≈ 5 × 1017 টি ঘর — এক bit করেও কয়েক কোটি GB, যার প্রায় সব শূন্য। Adjacency list-এ লাগে 2m ≈ 1.4 × 1011 টি এন্ট্রি — বড়, কিন্তু একটি ডেটা সেন্টারের সামর্থ্যের মধ্যে।
কেন এমন হয়? কারণ বাস্তবের সংযোগের একটি খরচ আছে: একজন মানুষ সীমিত সংখ্যক মানুষকে চেনেন, একটি মোড়ে কয়েকটির বেশি রাস্তা আসে না। তাই n বাড়লে m মোটামুটি n-এর সমানুপাতে বাড়ে, n²-এর নয়।
এই বাস্তবতা ডেটা স্ট্রাকচারের নকশা বদলে দিয়েছে। বড় গ্রাফ-প্রক্রিয়াকরণ ব্যবস্থাগুলো সাধারণত CSR-ধাঁচের অ্যারেতে গ্রাফ রাখে: একটি offsets অ্যারে আর একটি প্রতিবেশীর অ্যারে, যাতে একটি vertex-এর সব প্রতিবেশী মেমরিতে পাশাপাশি থাকে আর CPU-র cache পুরোপুরি কাজে লাগে। আরও এগিয়ে, ওয়েব-গ্রাফের জন্য Paolo Boldi আর Sebastiano Vigna-র WebGraph-এর মতো কাঠামো প্রতিবেশীর তালিকা সর্ট করে পরপর সংখ্যার পার্থক্য (gap) এনকোড করে, আর পাশাপাশি পেজের তালিকার মিল কাজে লাগিয়ে প্রতিটি লিংককে মাত্র কয়েক bit-এ নামিয়ে আনে।
তবু matrix হারিয়ে যায়নি। ছোট dense গ্রাফে (n কয়েক হাজার) matrix-এর O(1) edge-যাচাই আর সরল লুপ অপ্রতিরোধ্য; Floyd-Warshall (পর্ব ৪৪) পুরোপুরি matrix-এর ওপর চলে। কনটেস্টে matrix-এর সারি bitset-এ রাখলে দুটি সারির AND একবারে ৬৪টি ঘর হিসাব করে।
মূল উপলব্ধি: উপস্থাপন বেছে নেওয়ার আগে m আর n² তুলনা করুন। বাস্তবের গ্রাফে প্রায় সবসময় m ≪ n², তাই Θ(n + m)-এর কাঠামোই ডিফল্ট, আর matrix একটি সচেতন ব্যতিক্রম।
প্র ০৩“পৃথিবীর যেকোনো দুজন মানুষ মাত্র কয়েক ধাপে যুক্ত” — small world-এর এই দাবিটি গ্রাফের ভাষায় কী, আর কীভাবে মাপা হয়েছে?
১৯৬৭ সালে মার্কিন মনোবিজ্ঞানী Stanley Milgram একটি অদ্ভুত পরীক্ষা করেন। আমেরিকার মধ্যাঞ্চলের কিছু মানুষকে একটি চিঠি দেওয়া হলো, যা পৌঁছাতে হবে Boston-এর একজন নির্দিষ্ট মানুষের কাছে — কিন্তু শর্ত: চিঠিটি শুধু এমন কাউকে পাঠানো যাবে, যাঁকে আপনি নাম ধরে ব্যক্তিগতভাবে চেনেন, আর যিনি হয়তো লক্ষ্যের কাছাকাছি। যে চিঠিগুলো পৌঁছেছিল, সেগুলো গড়ে প্রায় ছয়টি হাত ঘুরে পৌঁছেছিল।
গ্রাফের ভাষায় প্রশ্নটি সহজ: মানুষ vertex, পরিচয় edge (undirected), আর দুজনের “দূরত্ব” হলো তাদের মধ্যের সবচেয়ে ছোট path-এর দৈর্ঘ্য। Small world মানে n বিশাল হলেও গড় দূরত্ব খুব ছোট — মোটামুটি log n-এর মতো বাড়ে। Milgram-এর পরীক্ষার পদ্ধতিগত দুর্বলতা ছিল (অনেক চিঠি পৌঁছায়ইনি), কিন্তু ২০১১ সালে Facebook-এর পুরো বন্ধু-গ্রাফে একটি সরাসরি মাপ সম্ভব হলো: প্রায় ৭২ কোটি ব্যবহারকারীর মধ্যে গড় দূরত্ব পাওয়া গেল প্রায় ৪.৭৪ — “চার ডিগ্রির পৃথক্করণ”. পরে (২০১৬ সালে) Facebook আরও বড় নেটওয়ার্কে জানায়, দুজনের মাঝে গড়ে প্রায় সাড়ে তিনজন মধ্যবর্তী মানুষ — অর্থাৎ গড় দূরত্ব প্রায় সাড়ে চার।
এত বড় গ্রাফে দূরত্ব মাপা কীভাবে? মূল অস্ত্র BFS (পর্ব ৪০) — এক উৎস থেকে স্তরে স্তরে ঘুরে প্রত্যেকের দূরত্ব O(n + m)-এ। কিন্তু প্রতিটি vertex থেকে BFS চালাতে লাগত O(n(n + m)) — কোটি কোটির বর্গ, অসম্ভব। গবেষকরা তাই probabilistic counting-এর কৌশলে (HyperLogLog-ধাঁচের ছোট “স্কেচ” দিয়ে) প্রতিটি vertex থেকে কত জনের দূরত্ব ≤ t, তার আনুমানিক হিসাব একসাথে করেছিলেন। ১৯৯৮ সালে Duncan Watts আর Steven Strogatz দেখান, অল্প কয়েকটি এলোমেলো দূরের edge-ই গড় দূরত্ব নাটকীয়ভাবে কমিয়ে দেয়।
গণিতবিদদের মধ্যেও একটি মজার রূপ চালু আছে: Erdős number. হাঙ্গেরির Paul Erdős প্রায় দেড় হাজার গবেষণাপত্র লিখেছিলেন শত শত সহলেখকের সাথে। তাঁর সহলেখকের Erdős number ১, তাঁদের সহলেখকের ২ — সহলেখক-গ্রাফে Erdős থেকে BFS দূরত্ব। বেশিরভাগ সক্রিয় গণিতবিদের সংখ্যাটি ছোট।
মূল উপলব্ধি: “কয় ধাপে যুক্ত” প্রশ্নটি একটি সবচেয়ে-ছোট-পথের প্রশ্ন, আর সমাজবিজ্ঞানের একটি কৌতূহল সঠিক উপস্থাপন আর BFS-এর হাত ধরে একটি মাপা যায় এমন সংখ্যায় পরিণত হয়েছে।
প্র ০৪Rubik’s cube বা ১৫-ধাঁধার state space এত বিশাল যে কোনো কম্পিউটারে রাখা যায় না — তবু কীভাবে তাদের সম্পর্কে নিশ্চিত প্রমাণ পাওয়া গেছে?
১৫-ধাঁধা (4 × 4 বোর্ডে ১–১৫ টাইল আর একটি ফাঁকা ঘর) ১৮৭০-এর দশকে আমেরিকায় তুমুল জনপ্রিয় হয়। শোনা যায়, ১৪ আর ১৫ অদলবদল করা একটি অবস্থা থেকে সমাধানের জন্য পুরস্কারও ঘোষণা হয়েছিল — কেউ পারেননি। ১৮৭৯ সালে William Johnson আর William Story প্রমাণ করেন কেন: state space-এর গ্রাফটি দুটি আলাদা component-এ ভাগ। প্রতিটি চাল টাইলগুলোর একটি permutation-এর “parity” (জোড়-বিজোড়তা) আর ফাঁকা ঘরের সারির সাথে মিলিয়ে একটি নির্দিষ্ট পরিমাণ অপরিবর্তিত রাখে — একটি invariant. 14–15 অদলবদল সেই invariant ভাঙে, তাই সেই অবস্থাটি অন্য component-এ। 16! প্রায় 2 × 1013, তার অর্ধেক একটি component — গ্রাফটি না বানিয়েই তার গঠন প্রমাণ হলো।
Rubik’s cube-এর state space আরও বিশাল: 43,252,003,274,489,856,000 — প্রায় 4.3 × 1019 অবস্থা। প্রতিটি vertex থেকে ১৮টি চাল (ছয় মুখের প্রতিটি ৯০°, ১৮০° বা উল্টো ৯০°), তাই এটি একটি বিশাল, নিয়মিত implicit graph. বহু দশক ধরে প্রশ্ন ছিল: সবচেয়ে এলোমেলো কিউবটি সমাধান করতে সর্বোচ্চ কয়টি চাল লাগে — অর্থাৎ গ্রাফটির ব্যাস (diameter) কত? এই সংখ্যার ডাকনাম “God’s number”. ২০১০ সালে Tomas Rokicki, Herbert Kociemba, Morley Davidson আর John Dethridge প্রমাণ করেন উত্তর ২০ (এই চাল-গণনার নিয়মে)। তাঁরা প্রতিসাম্য দিয়ে অবস্থাগুলোকে বড় শ্রেণিতে ভাগ করে Google-এর দান করা কম্পিউটার-সময়ে হিসাবটি শেষ করেন।
এই গল্পগুলো implicit graph-এর মূল শিক্ষা দেয়। প্রথমত, গ্রাফ “থাকা” মানে মেমরিতে থাকা নয় — একটি অবস্থা থেকে প্রতিবেশী তৈরির নিয়ম থাকলেই BFS বা DFS চালানো যায়। দ্বিতীয়ত, বিশাল গ্রাফে সাধারণ BFS-এর বাইরে কৌশল লাগে: invariant দিয়ে অংশ বাদ দেওয়া, দুই প্রান্ত থেকে একসাথে খোঁজা (meet in the middle), বা A*-এর মতো heuristic.
মূল উপলব্ধি: state space-এ গ্রাফটি বানানো নয়, গ্রাফটি বোঝাই আসল কাজ — একটি ভালো invariant কোটি কোটি vertex না দেখেই বলে দিতে পারে কোথায় পৌঁছানো অসম্ভব।
প্র ০৫বাংলাদেশের মানচিত্রের জেলাগুলো রং করতে কয়টি রং লাগে — আর পরীক্ষার রুটিন বানানোর সাথে এর সম্পর্ক কী?
একটি মানচিত্রে পাশাপাশি (সাধারণ সীমানা আছে এমন) দুটি জেলা যেন একই রঙের না হয় — এভাবে রং করতে কয়টি রং লাগে? গ্রাফে রূপান্তর করুন: প্রতিটি জেলা একটি vertex, আর দুটি জেলার সাধারণ সীমানা থাকলে তাদের মধ্যে একটি edge. মানচিত্র থেকে আসা এমন গ্রাফ planar — কাগজে এমনভাবে আঁকা যায় যাতে কোনো দুটি edge একে অন্যকে না কাটে। এখন প্রশ্নটি হলো graph coloring: প্রতিটি vertex-কে একটি রং দিন, যাতে প্রতিটি edge-এর দুই প্রান্ত আলাদা রঙের হয়।
১৮৫২ সালে Francis Guthrie নামের এক তরুণ ইংল্যান্ডের কাউন্টিগুলো রং করতে গিয়ে লক্ষ করেন, চারটি রঙেই চলছে — সব মানচিত্রেই কি চারটি যথেষ্ট? ১৮৭৯ সালে Alfred Kempe একটি “প্রমাণ” প্রকাশ করেন, যা এগারো বছর পর Percy Heawood ভুল প্রমাণ করেন (আর সাথে দেখান যে পাঁচটি রং সবসময় যথেষ্ট)। শেষ পর্যন্ত ১৯৭৬ সালে Kenneth Appel আর Wolfgang Haken চার রঙের উপপাদ্য প্রমাণ করেন — গণিতের ইতিহাসে প্রথম বড় প্রমাণ, যার একটি অংশ মানুষ হাতে যাচাই করতে পারেন না: কম্পিউটার দিয়ে এক হাজারেরও বেশি বিশেষ বিন্যাস একে একে পরীক্ষা করতে হয়েছিল। পরে ১৯৯৬ সালে Robertson, Sanders, Seymour আর Thomas একটি সরলতর (তবু কম্পিউটার-নির্ভর) প্রমাণ দেন, আর ২০০৫ সালে Georges Gonthier পুরো প্রমাণটি Coq নামের একটি প্রমাণ-যাচাই সফটওয়্যারে আনুষ্ঠানিকভাবে যাচাই করেন। তাই বাংলাদেশের ৬৪টি জেলার মানচিত্রে চারটি রংই যথেষ্ট — উপপাদ্যটি নিশ্চয়তা দেয়।
এবার একটি অপ্রত্যাশিত জায়গা — পরীক্ষার রুটিন: যে দুটি বিষয়ে অন্তত একজন পরীক্ষার্থী আছেন, সেই দুটির পরীক্ষা একই সময়ে হতে পারবে না। বিষয় = vertex, “একই পরীক্ষার্থী আছেন” = edge, সময়ের স্লট = রং। সবচেয়ে কম স্লট মানে সবচেয়ে কম রঙে গ্রাফ রং করা। কিন্তু এই গ্রাফ planar নয়, আর সাধারণ গ্রাফে ৩টি রঙে রং করা যায় কি না — সেটিও NP-complete (পর্ব ৬৪)। তাই বাস্তবে greedy কৌশল (বেশি degree-র বিষয় আগে) দিয়ে “যথেষ্ট ভালো” রুটিন বানানো হয় (পর্ব ৫০, ৬৫)।
মূল উপলব্ধি: মানচিত্র, রুটিন, রেডিও ফ্রিকোয়েন্সি বণ্টন বা compiler-এর register বণ্টন — বাইরে থেকে আলাদা, কিন্তু গ্রাফে আনলে একই প্রশ্ন। সঠিক মডেল চিনতে পারলে আপনি জানবেন সমস্যাটি সহজ (planar-এ ৪ রং আছেই) নাকি কঠিন (সাধারণ coloring).
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।
-
হাতে trace করুন: একটি directed গ্রাফের ইনপুট
5 6, তারপর edge1 2,1 3,2 3,3 1,4 3,4 5. (ক) ইনপুটের ক্রমে adjacency list লিখুন। (খ) প্রতিটি vertex-এর in-degree আর out-degree. (গ) adjacency matrix-এর সারি 3 আর কলাম 3. (ঘ) কোন vertex-এ কেউ ঢোকে না, কোনটি থেকে কেউ বেরোয় না? (ঙ) গ্রাফটি কি DAG?(ক) adj[1] = [2, 3], adj[2] = [3], adj[3] = [1], adj[4] = [3, 5], adj[5] = []. (খ) out-degree: 2, 1, 1, 2, 0; in-degree: 1 (3 থেকে), 1, 3 (1, 2, 4 থেকে), 0, 1. দুই সারির যোগফলই 6 = m — directed handshake.
(গ) সারি 3 =
1 0 0 0 0(শুধু 3 → 1); কলাম 3 =1 1 0 1 0(1, 2, 4 থেকে 3-এ)। সারির যোগফল out-degree, কলামের যোগফল in-degree. (ঘ) vertex 4-এর in-degree 0 — উৎস (source); vertex 5-এর out-degree 0 — শেষ বিন্দু (sink). (ঙ) না: 1 → 3 → 1 একটি directed cycle (1 → 2 → 3 → 1-ও)। তাই এর কোনো topological ক্রম নেই। -
Handshake দিয়ে ভাবুন: (ক) একটি tree-তে degree-3-এর ৩টি vertex, degree-2-এর ২টি vertex, আর বাকি সব leaf (degree 1). Leaf কয়টি? (খ) ৯ জনের একটি দলে কি প্রত্যেকে ঠিক ৫ জনের সাথে হাত মেলাতে পারেন?
(ক) leaf-এর সংখ্যা L ধরুন। মোট vertex n = 3 + 2 + L = 5 + L; tree-তে edge n − 1 = 4 + L. Handshake: degree-র যোগফল = 3·3 + 2·2 + L = 13 + L = 2(4 + L) = 8 + 2L. তাই L = 5. যাচাই: n = 10, m = 9, যোগফল 18 = 2 × 9.
(খ) না। Degree-র যোগফল হতো 9 × 5 = 45, বিজোড় — কিন্তু যেকোনো গ্রাফে যোগফল 2m, জোড়। অনুসিদ্ধান্তের ভাষায়: বিজোড় degree-র (৫) vertex বিজোড় সংখ্যক (৯) হতে পারে না।
-
Euler-এর শর্ত: একটি গ্রামে চারটি মোড় 1–4 আর পাঁচটি রাস্তা: 1–2, 2–3, 3–4, 4–1, 1–3. (ক) একজন রিকশাচালক কি প্রতিটি রাস্তা ঠিক একবার ঘুরতে পারবেন? পারলে কোথা থেকে শুরু করবেন, আর একটি পথ লিখুন। (খ) একটি নতুন রাস্তা 2–4 তৈরি হলে উত্তর কী?
(ক) Degree: 1 → 3, 2 → 2, 3 → 3, 4 → 2. বিজোড় ঠিক দুটি (1 আর 3), আর গ্রাফ connected — তাই পারবেন, কিন্তু শুরু করতে হবে 1 বা 3 থেকে, শেষ হবে অন্যটিতে। একটি পথ: 1 → 2 → 3 → 4 → 1 → 3 — পাঁচটি রাস্তাই একবার। 2 থেকে শুরু করলে কেন হবে না? 2-এর degree জোড়, তাই 2 থেকে শুরু হলে পথ শেষও 2-তেই হতে হতো (circuit) — কিন্তু circuit-এর জন্য সব degree জোড় লাগত।
(খ) 2–4 যোগ হলে চারটি vertex-এরই degree 3 — বিজোড় চারটি। তাই আর সম্ভব নয়; গ্রাফটি এখন K4, আর ঠিক Königsberg-এর মতোই সব বিজোড়। লক্ষ করুন: একটি রাস্তা বাড়ানোয় কাজটি অসম্ভব হয়ে গেল।
-
কোড লিখুন: ইনপুটে একটি directed গ্রাফ (প্রথম লাইনে
n m, তারপর m টিu v, 1-based). প্রতিটি vertex-এর in-degree আর out-degree ছাপুন, তারপর সব source (in-degree 0) আর সব sink (out-degree 0). অনুশীলন ১-এর ইনপুট দিয়ে মিলিয়ে নিন।Degree গুনতে কোনো list-ই লাগে না — দুটি গণনার অ্যারে যথেষ্ট। সময় O(n + m), জায়গা O(n).
#include <iostream> #include <vector> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> in(n + 1, 0), out(n + 1, 0); // 1-based for (int i = 0; i < m; ++i) { int u, v; cin >> u >> v; ++out[u]; ++in[v]; } for (int u = 1; u <= n; ++u) cout << u << ": in " << in[u] << ", out " << out[u] << '\n'; cout << "sources:"; for (int u = 1; u <= n; ++u) if (in[u] == 0) cout << ' ' << u; cout << "\nsinks:"; for (int u = 1; u <= n; ++u) if (out[u] == 0) cout << ' ' << u; cout << '\n'; }অনুশীলন ১-এর ইনপুটে শেষ দুই লাইন
sources: 4আরsinks: 5. Python-এ একই কাজ:data = sys.stdin.buffer.read().split(), তারপর জোড়ায় জোড়ায় পড়ে দুটি list-এ গোনা। পর্ব ৪২-এর Kahn-এর topological sort ঠিক এই in-degree অ্যারে দিয়ে শুরু হয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রম1-based ইনপুট থেকে undirected গ্রাফের adjacency list বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- প্রথম লাইন থেকে n আর m পড়া
- n + 1 টি খালি তালিকা তৈরি করা (index 0 অব্যবহৃত)
- পরের m টি লাইনের প্রতিটি থেকে u আর v পড়া
- adj[u]-তে v আর adj[v]-তে u যোগ করা
- সব edge পড়া শেষে প্রতিটি তালিকার দৈর্ঘ্য থেকে degree পাওয়া আর Σ deg = 2m যাচাই করা
১০টি vertex-এর একটি simple undirected গ্রাফে সর্বোচ্চ কয়টি edge থাকতে পারে?
- 100
- 90
- 45
- 10
১২টি edge-এর একটি undirected multigraph-এ (self-loop থাকতে পারে) সব vertex-এর degree-র যোগফল কত?
- 12
- 24
- 6
- vertex-এর সংখ্যা না জেনে বলা যায় না
n = 200000 টি vertex আর m = 200000 টি edge-এর একটি গ্রাফে BFS চালাতে হবে, মেমরি সীমা 256 MB. কোন উপস্থাপনটি উপযুক্ত?
- Adjacency list
- Adjacency matrix (int)
- Adjacency matrix (প্রতি ঘরে এক bit)
- শুধু edge list, প্রতিবেশী খুঁজতে প্রতিবার পুরো তালিকা ঘুরে
একটি connected undirected গ্রাফের vertex-গুলোর degree 3, 3, 2, 2, 4. প্রতিটি edge ঠিক একবার ব্যবহার করে হাঁটা (Euler path) সম্পর্কে কোনটি সঠিক?
- একটি Euler circuit আছে, যেকোনো vertex থেকে শুরু করা যায়
- কোনো Euler path নেই, কারণ সব degree জোড় নয়
- Euler path আছে, শুরু করতে হবে degree-4-এর vertex থেকে
- Euler path আছে, এক degree-3 vertex থেকে শুরু হয়ে অন্যটিতে শেষ
Adjacency list-এ রাখা একটি গ্রাফে “u আর v-এর মধ্যে edge আছে কি না” যাচাই করতে worst case-এ কত সময় লাগে (তালিকাগুলো সর্ট করা নয়)?
- O(1)
- O(deg(u))
- O(n)
- O(m)
দেয়ালবিহীন একটি 3 × 4 গ্রিডে প্রতিটি ঘর একটি vertex, আর ৪ দিকে পাশাপাশি দুটি ঘরের মধ্যে একটি edge. মোট edge কয়টি?
- 12
- 24
- 17
- 34
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- গ্রাফ G = (V, E): n = |V|, m = |E|; directed বা undirected, weighted বা না, simple বা multigraph (loop, parallel edge) — মডেল বানানোর সময় প্রথমে এগুলো ঠিক করুন।
- Handshake lemma: Σ deg(v) = 2m; বিজোড় degree-র vertex জোড় সংখ্যক; directed-এ Σ in = Σ out = m.
- Tree = connected + acyclic, ঠিক n − 1 টি edge; DAG = directed cycle নেই; bipartite ⇔ বিজোড় cycle নেই; Kn-এ n(n − 1)/2 edge.
- Matrix Θ(n²), edge-যাচাই O(1); list Θ(n + m), প্রতিবেশী Θ(deg) — ডিফল্ট; edge list Θ(m) Kruskal-এর জন্য; CSR বড় স্থির গ্রাফে।
- গ্রিড আর ধাঁধার অবস্থা implicit graph — প্রতিবেশী হিসাব করে বের করুন; connected গ্রাফে Euler path ⇔ বিজোড় vertex ০ বা ২ (Euler, ১৭৩৬)।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪০ · Breadth-first search (BFS)পরবর্তী পর্বQueue দিয়ে স্তরে স্তরে ঘোরা, ওজনহীন সবচেয়ে ছোট পথ আর গ্রিডে BFS.
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ২৬ · Tree — পরিভাষা, উপস্থাপন আর traversalআগের ভিত্তিRooted tree, parent-সন্তান আর traversal — গ্রাফের সবচেয়ে পরিচিত বিশেষ রূপ।
- পর্ব ৩৩ · Disjoint set union (union-find)আগের ভিত্তিComponent জোড়া, undirected cycle ধরা আর Kruskal-এর হাতিয়ার।
- C++ প্রোগ্রামিং · STL algorithmপাশের ট্র্যাক
vector,sortআর গ্রাফ-কোডে দরকারি তৈরি অ্যালগরিদম। - Python প্রোগ্রামিং · অ্যালগরিদম ও কনটেস্টে Pythonপাশের ট্র্যাকদ্রুত ইনপুট, recursion-এর সীমা আর কনটেস্টে Python-এর কৌশল।