এই পর্বে যা শিখবেন
- Flow network, capacity constraint আর flow conservation-এর সংজ্ঞা দিয়ে max-flow সমস্যাটি নিখুঁতভাবে লেখা
- Residual graph, augmenting path আর “বাতিল করা” ফেরত edge হাতে trace করা — আর কেন greedy ব্যর্থ হয় তা দেখানো
- Ford-Fulkerson, Edmonds-Karp আর Dinic-এর পার্থক্য, জটিলতা (O(E·|f*|), O(VE²), O(V²E)) আর তার যুক্তি বোঝা
- Max-flow min-cut theorem-এর বক্তব্য ও প্রমাণের ধারণা, আর শেষ residual graph থেকে min cut বের করা
- Bipartite matching, edge-disjoint path আর project selection-কে flow-তে রূপান্তর করা, C++ ও Python-এ
১ · সমস্যা — চট্টগ্রাম বন্দর থেকে ঢাকার গুদামে কত ট্রাক?
চট্টগ্রাম বন্দরে একটি জাহাজ ভিড়েছে, তাতে হাজার হাজার কনটেইনার। সব যেতে হবে ঢাকার গুদামে। মাঝে কয়েকটি শহর আর তাদের মধ্যে মহাসড়ক — কিন্তু প্রতিটি রাস্তায় ঘণ্টায় নির্দিষ্ট সংখ্যার বেশি ট্রাক চলতে পারে না: সরু রাস্তা, ফেরিঘাট, সেতুর ওজন-সীমা। পরিবহন ব্যবস্থাপক জানতে চান: ঘণ্টায় সর্বোচ্চ কতগুলো ট্রাক বন্দর থেকে ঢাকায় পাঠানো যায়, আর কোন রাস্তা দিয়ে কয়টি?
প্রশ্নটি সংক্ষিপ্ত পথের প্রশ্ন নয় (সেটি পর্ব ৪৩-এর Dijkstra). এখানে প্রতিটি ট্রাক কত দ্রুত পৌঁছাল তা নয়, একসাথে কতটা মাল চলতে পারে তা জানতে চাই — অনেকগুলো পথ ভাগাভাগি করে। একই কাঠামো সবখানে: ওয়াসার পানির পাইপলাইনে প্রতিটি পাইপের ব্যাস একটি সীমা দেয়; ইন্টারনেটে সাবমেরিন কেবল থেকে ঢাকার ডেটা সেন্টারে যেতে প্রতিটি লিংকের bandwidth সীমিত; বিদ্যুতের গ্রিডে প্রতিটি লাইনের সীমা আছে। সব প্রশ্ন একটাই: “উৎস থেকে গন্তব্যে সর্বোচ্চ কতটা প্রবাহ সম্ভব?”
সংজ্ঞা — flow network
একটি flow network হলো একটি directed graph G = (V, E), যেখানে প্রতিটি edge (u, v)-এর একটি অঋণাত্মক capacity c(u, v) আছে, আর দুটি বিশেষ vertex: source s (যেখান থেকে প্রবাহ শুরু) আর sink t (যেখানে শেষ)। যে জোড়ার মধ্যে edge নেই, তার capacity ০ ধরা হয়। একটি flow হলো প্রতিটি edge-এ একটি সংখ্যা f(u, v), যা দুটি শর্ত মানে:
- Capacity constraint: প্রতিটি edge-এ 0 ≤ f(u, v) ≤ c(u, v) — কোনো রাস্তায় তার সীমার বেশি ট্রাক নয়।
- Flow conservation: s আর t ছাড়া প্রতিটি vertex v-তে, v-তে যত প্রবাহ ঢোকে, ঠিক তত বের হয়: Σu f(u, v) = Σw f(v, w). ফেনী বা কুমিল্লায় ট্রাক জমেও থাকে না, হঠাৎ তৈরিও হয় না।
Flow-এর মান |f| = s থেকে বের হওয়া মোট প্রবাহ − s-এ ঢোকা মোট প্রবাহ (সাধারণত দ্বিতীয়টি ০)। Conservation-এর কারণে এটি t-তে ঢোকা মোট প্রবাহের সমান। Maximum flow সমস্যা: সবচেয়ে বড় |f|-এর একটি বৈধ flow খুঁজুন। ওপরের ছবিতে S থেকে বের হওয়ার রাস্তা দুটি, মোট capacity 10 + 4 = 14 — তাই উত্তর ১৪-এর বেশি হতেই পারে না। কিন্তু এটিই কি উত্তর? ফেনী থেকে বের হওয়ার রাস্তা মাত্র 3 + 6 = 9, তাই S → F রাস্তার 10 পুরো ব্যবহার করা যাবে না। এই পর্ব শেষে দেখবেন উত্তর ১৩, আর কেন ১৩-এর বেশি অসম্ভব তার একটি ছোট “সার্টিফিকেট”ও পাবেন।
উপমা — ওয়াসার পাইপ। একটি পানির পাম্প (source) থেকে শহরের ট্যাংকে (sink) পাইপের জাল দিয়ে পানি যাচ্ছে। প্রতিটি পাইপ একটি সীমার বেশি পানি নিতে পারে না (capacity). পাইপের জোড়ায় পানি জমে না, ছিটকেও পড়ে না (conservation). প্রশ্ন: পাম্প পুরো জোরে চালালে ট্যাংকে প্রতি মিনিটে সর্বোচ্চ কত লিটার পৌঁছাবে? উত্তর নির্ভর করে জালের সবচেয়ে “সরু গলা”-র ওপর — আর সেই সরু গলা একটি পাইপ না-ও হতে পারে, কয়েকটি পাইপের একটি দল হতে পারে। এটিই পরে min cut নামে আসবে।
কোডে নেটওয়ার্ক রাখার দুটি উপায় পর্ব ৩৯-এ দেখেছেন: adjacency matrix (n × n অ্যারে, cap[u][v]) আর adjacency list. ছোট উদাহরণে matrix সবচেয়ে সহজ, আর এই পর্বের প্রথম কোডে সেটিই ব্যবহার করব; বড় গ্রাফে (V = 104, E = 105) list লাগে, কারণ matrix-এর Θ(V²) জায়গা আর প্রতি vertex-এ Θ(V) সময়ের scan তখন অচল।
২ · Greedy কেন আটকে যায় — residual graph আর ফেরত edge
প্রথম স্বাভাবিক চিন্তা: s থেকে t-তে এমন একটি পথ খুঁজুন, যার সব edge-এ এখনো জায়গা আছে; পথের সবচেয়ে কম জায়গাটুকু (bottleneck) পরিমাণ প্রবাহ পাঠান; আবার খুঁজুন; পথ না পাওয়া পর্যন্ত চালিয়ে যান। এটি একটি greedy পদ্ধতি — আর এটি ভুল উত্তর দিতে পারে। চারটি vertex-এর একটি ছোট নেটওয়ার্ক নিন: s → a, s → b, a → b, a → t, b → t, প্রতিটির capacity 10. সর্বোচ্চ প্রবাহ স্পষ্টতই ২০ (s → a → t আর s → b → t). কিন্তু ধরুন প্রথম পথটি দুর্ভাগ্যক্রমে s → a → b → t বাছা হলো।
সমাধানের চাবি একটি ধারণা: residual graph Gf. বর্তমান flow f-এর জন্য প্রতিটি edge (u, v) থেকে দুটি সম্ভাবনা আসে:
- এগোনো edge u → v, residual capacity c(u, v) − f(u, v): আরও কতটা পাঠানো যায়।
- ফেরত edge (reverse edge) v → u, residual capacity f(u, v): ইতিমধ্যে পাঠানো প্রবাহের কতটা “ফিরিয়ে নেওয়া” যায়।
Residual graph-এ শুধু ধনাত্মক residual capacity-র edge থাকে। সেখানে s থেকে t-এর যেকোনো পথকে বলে augmenting path. পথটি কোনো ফেরত edge v → u ব্যবহার করলে আসলে মূল edge u → v-এর প্রবাহ কমানো হয় — অর্থাৎ আগের একটি সিদ্ধান্ত আংশিক বাতিল। নিচের ডায়াগ্রামে দেখুন, এই “বাতিল” কীভাবে greedy-র আটকে যাওয়া ঠিক করে।
| এই ধাপের কাজ | |
| residual-এ s থেকে যাওয়া যায় | |
| মোট প্রবাহ |
- শুরু. সব edge-এ প্রবাহ ০. Residual graph মূল গ্রাফের মতোই: প্রতিটি এগোনো edge-এ 10 জায়গা, কোনো ফেরত edge নেই (ফেরত capacity ০)।
- দুর্ভাগ্যের পথ. পথ s → a → b → t, তিনটি edge-এরই জায়গা 10, তাই bottleneck 10. তিনটি edge-ই ভরে গেল (10/10). প্রতিটির ফেরত capacity এখন 10 — যতটা পাঠানো হয়েছে, ততটা ফেরানো যায়।
- Greedy আটকে গেছে, residual নয়. মূল গ্রাফে জায়গা আছে শুধু s → b আর a → t-তে — কিন্তু b থেকে t-র রাস্তা ভরা, তাই সাধারণ greedy এখানে থেমে ১০ বলত। Residual graph-এ কিন্তু একটি নতুন edge আছে: b → a, capacity 10 (a → b-এর প্রবাহ ফেরানো)। পথ পাওয়া গেল: s → b → a → t.
- বাতিল করে পাঠানো. bottleneck min(10, 10, 10) = 10. s → b ভরল, a → t ভরল, আর ফেরত edge b → a ব্যবহারে a → b-এর প্রবাহ 10 থেকে ০ — আগের সিদ্ধান্ত পুরো বাতিল। ব্যাখ্যা: a-তে যে ১০ এসেছিল তা এখন সরাসরি t-তে যায়, আর b-কে t-তে পাঠানোর ১০ দেয় s → b. মোট প্রবাহ ২০.
- থামা আর প্রমাণ. s থেকে দুটি edge-ই ভরা, residual-এ s থেকে কোথাও যাওয়া যায় না। cut {s} | {a, b, t}-এর capacity 10 + 10 = 20 = প্রবাহ — তাই ২০-এর বেশি অসম্ভব (অংশ ৫). শিক্ষা: ফেরত edge ছাড়া greedy-র উত্তর ভুলও হতে পারে; ফেরত edge থাকলে কোনো ভুল সিদ্ধান্ত স্থায়ী নয়।
একটি খুঁটিনাটি: augmenting path দিয়ে প্রবাহ বাড়ালে দুটি শর্তই বজায় থাকে। পথের প্রতিটি মাঝের vertex-এ একটি edge দিয়ে Δ ঢোকে আর একটি দিয়ে Δ বের হয় (ফেরত edge হলে “ঢোকা কমে”, যার হিসাবও মেলে), তাই conservation ঠিক থাকে; আর Δ ≤ bottleneck বলে কোনো edge capacity ছাড়ায় না বা ঋণাত্মক হয় না। প্রতিবার |f| ঠিক Δ বাড়ে, কারণ পথের প্রথম edge s থেকে বের হয়।
৩ · Ford-Fulkerson পদ্ধতি — augmenting path যতক্ষণ আছে
ওপরের ধারণাটিই Ford-Fulkerson পদ্ধতি. একে “অ্যালগরিদম” না বলে “পদ্ধতি” বলা হয়, কারণ augmenting path কীভাবে খোঁজা হবে তা এটি বলে না — DFS, BFS বা অন্য কিছু, যেকোনোটি চলে।
Pseudocode — Ford-Fulkerson method (vertices 0-based, s and t given)
f(u, v) = 0 for every edge
while the residual graph G_f has a path P from s to t: // augmenting path
delta = min residual capacity on P // bottleneck
for each edge (u, v) on P:
if (u, v) is a forward edge: f(u, v) += delta
else: f(v, u) -= delta // cancel old flow
return f // f is a maximum flowকতক্ষণ চলে?
সব capacity integer হলে প্রতিটি bottleneck অন্তত ১, তাই প্রতিটি ধাপে |f| অন্তত ১ বাড়ে। সর্বোচ্চ প্রবাহ |f*| হলে সর্বোচ্চ |f*| টি ধাপ, আর প্রতিটি ধাপে একটি DFS বা BFS — O(E). মোট O(E · |f*|). এটি একটি pseudo-polynomial সীমা: ইনপুটের আকার নয়, ইনপুটের সংখ্যার মান-এর ওপর নির্ভর করে। ক্লাসিক খারাপ উদাহরণ: অংশ ২-এর চার-vertex নেটওয়ার্ক, কিন্তু বাইরের চারটি edge-এর capacity 106 আর মাঝের a → b-এর মাত্র 1. দুর্ভাগ্যক্রমে DFS যদি বারবার মাঝের edge দিয়ে যায় (একবার a → b, পরেরবার ফেরত b → a), প্রতিটি পথ মাত্র ১ বাড়ায় — সর্বোচ্চ 2 × 106 প্রবাহে পৌঁছাতে ২০ লাখ ধাপ, যেখানে সঠিক দুটি পথে দুই ধাপেই হতো।
আরও খারাপ: capacity অমূলদ (irrational) হলে Ford-Fulkerson কখনো না-ও থামতে পারে — প্রতিটি পথ আগেরটির চেয়ে ছোট বাড়ায়, আর প্রবাহ এমনকি সর্বোচ্চের চেয়ে কম একটি মানের দিকে এগোতে থাকে। Ford আর Fulkerson নিজেই এমন একটি উদাহরণ দিয়েছিলেন। Integer (বা মূলদ, যাকে integer-এ গুণ করে নেওয়া যায়) capacity-তে এই সমস্যা নেই। কম্পিউটারে সাধারণত integer-ই থাকে, কিন্তু floating-point capacity দিলে সাবধান।
Ford-Fulkerson সঠিক (থামলে সর্বোচ্চ প্রবাহ দেয় — অংশ ৫-এর theorem), কিন্তু ধীর হতে পারে। দ্রুত করার উপায় একটাই: কোন augmenting path নেবেন, তা চালাকি করে বাছা। সবচেয়ে কম edge-এর পথ নিলে Edmonds-Karp; একই দৈর্ঘ্যের সব পথ একসাথে নিলে Dinic.
১৯৫৫ সালে মার্কিন RAND Corporation-এর গবেষক Theodore E. Harris আর অবসরপ্রাপ্ত জেনারেল Frank S. Ross একটি গোপন প্রতিবেদন লেখেন — সোভিয়েত ইউনিয়ন থেকে পূর্ব ইউরোপে রেলপথে কতটা মাল যেতে পারে, আর কোন রেললাইনগুলো কাটলে সেই প্রবাহ বন্ধ হয়ে যায়। Harris এই প্রশ্নটি RAND-এর দুই গণিতবিদ L. R. Ford Jr. আর D. R. Fulkerson-কে দেন। তাঁদের “Maximal flow through a network” প্রবন্ধ ১৯৫৬ সালে Canadian Journal of Mathematics-এ প্রকাশিত হয় (RAND-এর অভ্যন্তরীণ প্রতিবেদন হিসেবে ১৯৫৪ সালেই লেখা), আর সেখানেই max-flow min-cut theorem. প্রায় একই সময়ে, স্বাধীনভাবে, Peter Elias, Amiel Feinstein আর Claude Shannon-ও theorem-টি প্রমাণ করেন। Harris–Ross প্রতিবেদনটি ১৯৯৯ সালে গোপনীয়তা থেকে মুক্ত হয়।
৪ · Edmonds-Karp — BFS দিয়ে সবচেয়ে ছোট augmenting path
১৯৭২ সালে Jack Edmonds আর Richard Karp একটি সরল নিয়ম দেন: প্রতিবার residual graph-এ BFS চালিয়ে সবচেয়ে কম edge-এর augmenting path নিন। এটিই Edmonds-Karp অ্যালগরিদম. প্রায় একই ধারণা (আরও উন্নত রূপে) ১৯৭০ সালেই সোভিয়েত গবেষক Yefim Dinitz প্রকাশ করেছিলেন — সেটি অংশ ৬-এ। মূল উদাহরণে চালিয়ে দেখুন:
| BFS-এর পথ | |
| পথের residual capacity | |
| bottleneck / পথের edge-সংখ্যা | |
| মোট প্রবাহ | |
| residual-এ S থেকে যাওয়া যায় |
- শুরু. সব প্রবাহ ০. BFS S থেকে শুরু করে স্তরে স্তরে এগোবে, তাই প্রথমে পাবে সবচেয়ে কম রাস্তার পথ — এখানে ৩টি রাস্তার।
- পথ ১: S→F→Cu→T. BFS প্রথমে F আর Ch-কে পায়, তারপর F থেকে Cu, তারপর Cu থেকে T. জায়গা 10, 6, 8 — bottleneck 6. F → Cu ভরে গেল।
- পথ ২: S→Ch→Cu→T. এখন F থেকে Cu-র জায়গা নেই, তাই Cu পৌঁছায় Ch দিয়ে। জায়গা 4, 2, আর Cu → T-তে 8 − 6 = 2. bottleneck 2. Ch → Cu আর Cu → T দুটিই ভরল।
- পথ ৩: S→Ch→M→T. জায়গা S → Ch-এ 4 − 2 = 2, Ch → M-এ 5, M → T-তে 10. bottleneck 2. S → Ch ভরল। এখনো প্রতিটি পথ ৩ রাস্তার।
- পথ ৪: S→F→Ch→M→T. ৩ রাস্তার আর কোনো পথ নেই; BFS এবার ৪ রাস্তার পথ পায়। জায়গা 10 − 6 = 4, 3, 5 − 2 = 3, 10 − 2 = 8 → bottleneck 3. মোট ১৩. খেয়াল করুন, পথের দৈর্ঘ্য কখনো কমেনি: 3, 3, 3, 4 — নিচের প্রমাণের মূল কথা এটিই।
- থামা আর min cut. শেষ BFS-এ S থেকে শুধু F-এ যাওয়া যায় (S → F-এ 1 জায়গা বাকি); F থেকে বের হওয়ার দুটি রাস্তাই ভরা, আর S → Ch-ও ভরা। তাই {S, F} এক পাশ, বাকিরা অন্য পাশ। এই দুই পাশের মধ্যের রাস্তা (✂): S → Ch 4 + F → Ch 3 + F → Cu 6 = 13 = প্রবাহ। এটিই প্রমাণ যে ১৩-এর বেশি অসম্ভব।
কোড — প্রতিটি augmenting path, max flow আর min cut
নিচের কোড ছয়টি জায়গা আর নয়টি রাস্তার নেটওয়ার্কে Edmonds-Karp চালায়। Residual capacity একটি adjacency matrix cap-এ: পথ দিয়ে Δ পাঠালে cap[u][v] -= Δ আর cap[v][u] += Δ — দ্বিতীয় লাইনটিই ফেরত edge খোলে। শেষ BFS (যেটি t-তে পৌঁছায়নি) যাদের ছুঁয়েছে, তারাই min cut-এর S-পাশ।
#include <algorithm>
#include <iostream>
#include <queue>
#include <string>
#include <tuple>
#include <vector>
using namespace std;
int main() {
vector<string> names = {"Port", "Feni", "Chandpur", "Cumilla", "Munshiganj", "Dhaka"};
vector<tuple<int, int, int>> edges = {{0, 1, 10}, {0, 2, 4}, {1, 2, 3}, {1, 3, 6}, {2, 3, 2},
{2, 4, 5}, {3, 4, 4}, {3, 5, 8}, {4, 5, 10}};
const int n = 6, s = 0, t = 5;
vector<vector<int>> cap(n, vector<int>(n, 0)); // residual capacities
for (auto [u, v, c] : edges) cap[u][v] += c;
int flow = 0, step = 0; vector<int> par(n);
while (true) {
fill(par.begin(), par.end(), -1); par[s] = s; // BFS: shortest augmenting path
queue<int> q({s});
while (!q.empty() && par[t] == -1) {
int u = q.front(); q.pop();
for (int v = 0; v < n; ++v)
if (par[v] == -1 && cap[u][v] > 0) { par[v] = u; q.push(v); }
}
if (par[t] == -1) break; // no augmenting path: flow is maximum
vector<int> path = {t}; while (path.back() != s) path.push_back(par[path.back()]);
reverse(path.begin(), path.end());
int push = cap[s][path[1]];
for (size_t i = 1; i + 1 < path.size(); ++i) push = min(push, cap[path[i]][path[i + 1]]);
flow += push;
cout << "path " << ++step << ": " << names[s];
for (size_t i = 0; i + 1 < path.size(); ++i) {
cap[path[i]][path[i + 1]] -= push; // use forward capacity
cap[path[i + 1]][path[i]] += push; // open the cancelling back edge
cout << " -> " << names[path[i + 1]];
}
cout << " | push " << push << '\n';
}
cout << "max flow = " << flow << "\nmin cut:"; string sep = " "; // last BFS reached side S
for (auto [u, v, c] : edges)
if (par[u] != -1 && par[v] == -1) { cout << sep << names[u] << "->" << names[v] << " (" << c << ")"; sep = ", "; }
cout << '\n';
}আউটপুট:
path 1: Port -> Feni -> Cumilla -> Dhaka | push 6
path 2: Port -> Chandpur -> Cumilla -> Dhaka | push 2
path 3: Port -> Chandpur -> Munshiganj -> Dhaka | push 2
path 4: Port -> Feni -> Chandpur -> Munshiganj -> Dhaka | push 3
max flow = 13
min cut: Port->Chandpur (4), Feni->Chandpur (3), Feni->Cumilla (6)
চারটি পথ হুবহু ওপরের ডায়াগ্রামের মতো, কারণ BFS প্রতিটি vertex-এর প্রতিবেশী 0, 1, 2, … ক্রমে দেখে। Min cut-এর তিনটি রাস্তার capacity-র যোগফল 4 + 3 + 6 = 13 = max flow. Matrix-এ BFS প্রতিটি vertex-এ n টি ঘর দেখে, তাই এক BFS O(V²); adjacency list-এ সেটি O(V + E). সাধারণ সীমা O(VE²) adjacency list ধরে। জায়গা Θ(V²) এই matrix সংস্করণে। একটি সূক্ষ্ম কথা: matrix-এ u → v আর v → u দুটি মূল edge থাকলে তারা একই ঘর ভাগ করে — max flow-এর মান তাতে বদলায় না, কিন্তু কোন edge-এ কত প্রবাহ, তা আলাদা করতে চাইলে মূল capacity আলাদা রাখতে হয়।
from collections import deque
names = ["Port", "Feni", "Chandpur", "Cumilla", "Munshiganj", "Dhaka"]
edges = [(0, 1, 10), (0, 2, 4), (1, 2, 3), (1, 3, 6), (2, 3, 2),
(2, 4, 5), (3, 4, 4), (3, 5, 8), (4, 5, 10)]
n, s, t = len(names), 0, 5
cap = [[0] * n for _ in range(n)] # residual capacities
for u, v, c in edges:
cap[u][v] += c
flow, step = 0, 0
while True:
par = [-1] * n # BFS: shortest augmenting path
par[s] = s
q = deque([s])
while q and par[t] == -1:
u = q.popleft()
for v in range(n):
if par[v] == -1 and cap[u][v] > 0:
par[v] = u
q.append(v)
if par[t] == -1:
break # no augmenting path: flow is maximum
path = [t]
while path[-1] != s:
path.append(par[path[-1]])
path.reverse()
push = min(cap[a][b] for a, b in zip(path, path[1:])) # bottleneck
for a, b in zip(path, path[1:]):
cap[a][b] -= push # use forward capacity
cap[b][a] += push # open the cancelling back edge
flow, step = flow + push, step + 1
print(f"path {step}:", " -> ".join(names[x] for x in path), f"| push {push}")
print("max flow =", flow)
cut = [f"{names[u]}->{names[v]} ({c})" for u, v, c in edges if par[u] != -1 and par[v] == -1]
print("min cut:", ", ".join(cut)) # last BFS reached exactly side Sআউটপুট C++-এর সাথে হুবহু এক:
path 1: Port -> Feni -> Cumilla -> Dhaka | push 6
path 2: Port -> Chandpur -> Cumilla -> Dhaka | push 2
path 3: Port -> Chandpur -> Munshiganj -> Dhaka | push 2
path 4: Port -> Feni -> Chandpur -> Munshiganj -> Dhaka | push 3
max flow = 13
min cut: Port->Chandpur (4), Feni->Chandpur (3), Feni->Cumilla (6)
zip(path, path[1:]) পথের পাশাপাশি জোড়াগুলো দেয় — (Port, Feni), (Feni, Cumilla), … — তাই bottleneck বের করা আর হালনাগাদ দুটোই এক লাইনের লুপ। deque-এর popleft O(1); সাধারণ list-এর pop(0) O(n), তাই BFS-এ সবসময় deque. লুপ ভাঙার সময় par-এ শেষ BFS-এর ফল থাকে — সেটিই min cut দেয়, আলাদা কোনো খোঁজ লাগে না।
কেন O(VE²) — প্রমাণের নকশা
δf(s, v) দিয়ে বোঝাই residual graph Gf-এ s থেকে v-এর সবচেয়ে কম edge-এর পথের দৈর্ঘ্য। যুক্তিটি দুই ধাপের।
- দূরত্ব কখনো কমে না। প্রতিটি augmentation-এর পর প্রতিটি v-এর জন্য δ(s, v) হয় একই থাকে, নয় বাড়ে। কারণ: BFS-এর পথ সবসময় এক স্তর থেকে পরের স্তরে যায় (δ(v) = δ(u) + 1). Augmentation নতুন যে edge খোলে, তা কেবল ফেরত edge v → u — যা এক স্তর পেছনে যায়। পেছনে যাওয়া edge কোনো শর্টকাট বানাতে পারে না। (আনুষ্ঠানিক প্রমাণ contradiction দিয়ে: যে v-এর দূরত্ব প্রথম কমল, তার আগের vertex নিয়ে।)
- প্রতিটি edge সর্বোচ্চ O(V) বার “critical”. যে edge (u, v) একটি পথের bottleneck, সে ভরে যায় আর residual থেকে উধাও হয় — তাকে বলে critical. আবার ফিরতে হলে পরে কোনো পথকে v → u দিয়ে যেতে হবে; তখন δ(u) = δ(v) + 1 ≥ (আগের δ(u) + 1) + 1. অর্থাৎ দুবার critical হওয়ার মাঝে δ(s, u) অন্তত ২ বাড়ে। δ সর্বোচ্চ V − 1, তাই একটি edge সর্বোচ্চ প্রায় V/2 বার critical.
প্রতিটি augmentation-এ অন্তত একটি edge critical, আর residual-এ edge সর্বোচ্চ 2E টি — তাই মোট augmentation O(VE). প্রতিটি BFS O(E) (adjacency list-এ)। মোট সময় O(VE²), জায়গা O(V + E). সবচেয়ে বড় কথা: সীমাটি capacity-র মানের ওপর নির্ভর করে না — অংশ ৩-এর 106 উদাহরণে Edmonds-Karp প্রথমেই দুটি ২-edge-এর পথ নিয়ে দুই ধাপে শেষ করে।
৫ · Max-flow min-cut theorem — সবচেয়ে সরু গলা
একটি s-t cut (S, T) হলো vertex-গুলোকে দুই ভাগে ভাগ করা, যাতে s ∈ S আর t ∈ T. Cut-এর capacity c(S, T) = S থেকে T-তে যাওয়া সব edge-এর capacity-র যোগফল — T থেকে S-এ ফেরার edge গোনা হয় না। উপমায়: বন্দর আর ঢাকার মাঝে একটি কাল্পনিক রেখা টানুন; রেখা পার হয়ে ঢাকার দিকে যাওয়া রাস্তাগুলোর মোট ধারণক্ষমতাই cut-এর capacity. সবচেয়ে ছোট capacity-র cut হলো minimum cut.
দুর্বল দ্বৈততা (weak duality): যেকোনো flow f আর যেকোনো cut (S, T)-এর জন্য |f| ≤ c(S, T). কারণ: conservation থেকে |f| = (S থেকে T-তে মোট প্রবাহ) − (T থেকে S-এ মোট প্রবাহ) — প্রতিটি s-t প্রবাহকে কোথাও রেখা পার হতে হয়। প্রথম পদ ≤ c(S, T), দ্বিতীয় পদ ≥ ০. তাই যেকোনো cut একটি ওপরের সীমা দেয়। অংশ ১-এ S থেকে বের হওয়া ১৪ ছিল cut {S}-এর capacity; F-কে S-এর পাশে নিলে সীমা নামে ১৩-এ।
Max-flow min-cut theorem (Ford ও Fulkerson; Elias, Feinstein ও Shannon — ১৯৫৬): একটি flow f-এর জন্য নিচের তিনটি বক্তব্য সমতুল্য —
- f একটি maximum flow.
- Residual graph Gf-এ কোনো augmenting path নেই।
- কোনো cut (S, T)-এর জন্য |f| = c(S, T).
প্রমাণের ধারণা. (১ ⇒ ২): augmenting path থাকলে তা দিয়ে প্রবাহ বাড়ানো যেত, তাই f সর্বোচ্চ হতো না। (৩ ⇒ ১): weak duality বলে কোনো flow c(S, T) ছাড়াতে পারে না; f সেই সীমা ছুঁয়েছে, তাই সর্বোচ্চ। মূল অংশ (২ ⇒ ৩): ধরুন augmenting path নেই। S = residual graph-এ s থেকে যাওয়া যায় এমন সব vertex, T = বাকিরা; t ∈ T, কারণ পথ নেই। এখন S থেকে T-তে যেকোনো মূল edge (u, v) নিন: এটি অবশ্যই ভরা (f = c), নইলে residual-এ u → v থাকত আর v-ও S-এ পড়ত। আর T থেকে S-এ যেকোনো মূল edge (v, u)-এ প্রবাহ অবশ্যই ০, নইলে ফেরত edge u → v residual-এ থাকত। তাহলে |f| = c(S, T) − 0. ∎ এই প্রমাণই অ্যালগরিদম: শেষ residual graph-এ s থেকে একটি BFS/DFS চালালেই min cut — ঠিক যা কোডের শেষ লাইনগুলো করেছে।
Min cut একাধিক হতে পারে। মূল উদাহরণে {S, F} | বাকিরা ছাড়াও {S, F, Ch} | {Cu, M, T}-এর capacity F → Cu 6 + Ch → Cu 2 + Ch → M 5 = ১৩. শেষ residual graph-এ s থেকে খুঁজলে সবসময় পাওয়া যায় s-এর সবচেয়ে কাছের min cut. আর min cut মানে “সবচেয়ে ছোট capacity-র একটি edge” নয় — এখানে সবচেয়ে ছোট edge Ch → Cu (2), কিন্তু শুধু সেটি কাটলে বন্দর থেকে ঢাকা বিচ্ছিন্ন হয় না।
Integrality theorem — প্রবাহও integer
সব capacity integer হলে Ford-Fulkerson-এর প্রতিটি bottleneck integer, তাই প্রতিটি ধাপের পর প্রতিটি edge-এর প্রবাহ integer থাকে। ফল — integrality theorem: integer capacity-র নেটওয়ার্কে এমন একটি maximum flow আছে, যার প্রতিটি edge-এর প্রবাহ integer, আর Ford-Fulkerson-ধাঁচের যেকোনো অ্যালগরিদম ঠিক সেটিই দেয়। কথাটি নিরীহ শোনালেও এটিই পরের অংশগুলোর ভিত্তি: capacity 1 হলে প্রতিটি edge-এর প্রবাহ ০ বা ১ — “এই ছাত্র এই প্রজেক্ট পাবে কি না” ধরনের হ্যাঁ/না সিদ্ধান্ত। (সাবধান: এর মানে এই নয় যে সব maximum flow integer; মাঝামাঝি ভগ্নাংশের flow-ও সর্বোচ্চ হতে পারে।)
গভীরে তাকালে max-flow min-cut theorem আসলে linear programming duality-র একটি বিশেষ রূপ: max flow একটি linear program, আর তার dual হলো min cut-এর একটি linear program — দুইয়ের সর্বোত্তম মান সমান।
৬ · Dinic — level graph আর blocking flow
Edmonds-Karp প্রতিটি পথের জন্য নতুন BFS চালায় — একই দৈর্ঘ্যের দশটি পথ থাকলে দশটি BFS. Dinic-এর অ্যালগরিদম (Yefim Dinitz, ১৯৭০; পশ্চিমে “Dinic” বানানে পরিচিত) একটি BFS দিয়ে একই দৈর্ঘ্যের সব পথ একসাথে শেষ করে। প্রতিটি phase-এ দুটি কাজ:
- BFS → level graph. Residual graph-এ s থেকে BFS চালিয়ে প্রতিটি vertex-এর level (দূরত্ব) বের করুন। Level graph-এ রাখুন শুধু সেই residual edge u → v, যেখানে level(v) = level(u) + 1 — অর্থাৎ শুধু “এক ধাপ সামনে” যাওয়া edge. t-তে পৌঁছানো না গেলে থামুন।
- DFS → blocking flow. Level graph-এ DFS দিয়ে বারবার s থেকে t-তে পথ খুঁজে প্রবাহ পাঠান, যতক্ষণ না level graph-এ s থেকে t-তে আর কোনো পথ থাকে না। এমন প্রবাহকে বলে blocking flow — প্রতিটি s-t পথে অন্তত একটি edge ভরা। কৌশল: প্রতিটি vertex একটি current arc pointer রাখে — যে edge দিয়ে আর যাওয়া যায় না (ভরা বা কানাগলি), তাকে এই phase-এ আর কখনো দেখা হয় না।
| phase | |
| এই ধাপের কাজ | |
| s থেকে t-র দূরত্ব | |
| মোট প্রবাহ |
- Phase ১, BFS. Level: S = 0, F = Ch = 1, Cu = M = 2, T = 3. F → Ch (একই level-এ) আর Cu → M (একই level-এ) level graph-এ নেই — তারা “সামনে” যায় না। বাকি ৭টি edge নিয়ে level graph.
- DFS, প্রথম পথ. S → F → Cu → T, bottleneck 6. F → Cu ভরল। এখন level graph-এ F থেকে আর কোনো edge নেই — F একটি কানাগলি; current arc pointer S-এর pointer-কে F পার করে Ch-এ সরিয়ে দেয়।
- DFS, দ্বিতীয় পথ. S → Ch → Cu → T, bottleneck min(4, 2, 2) = 2. Ch → Cu আর Cu → T ভরল — Cu-ও এখন কানাগলি।
- DFS, তৃতীয় পথ. S → Ch → M → T, bottleneck 2; S → Ch ভরল। Level graph-এ আর কোনো S-T পথ নেই — এটি একটি blocking flow, মান ১০. একটিমাত্র BFS-এ তিনটি পথ — Edmonds-Karp-এর তিনটি BFS-এর কাজ।
- Phase ২. নতুন BFS: S = 0, F = 1, Ch = 2 (এবার F → Ch দিয়ে), M = 3, T = 4. (Cu-তে পৌঁছানো যায় শুধু T-এর ফেরত edge দিয়ে, level ৫ — T-এর চেয়ে দূরে, কাজে লাগে না।) পথ S → F → Ch → M → T, push 3. মোট ১৩. দূরত্ব 3 থেকে বেড়ে 4.
- Phase ৩. BFS শুধু S আর F-এ পৌঁছায়, T-তে নয় — থামুন। Edmonds-Karp-এর শেষ BFS-এর মতোই {S, F} হলো min cut-এর S-পাশ। দুটি কার্যকর phase, প্রতিটিতে দূরত্ব কঠোরভাবে বেড়েছে।
Pseudocode — Dinic (0-based; level[], it[] are arrays over vertices)
maxflow = 0
while bfs_levels(s) reaches t: // phase: build level graph
it[v] = first edge of v, for every v // current-arc pointers
while (d = dfs(s, INF)) > 0: maxflow += d // blocking flow
return maxflow
dfs(u, pushed):
if u == t: return pushed
for (; it[u] < deg(u); it[u]++): // never revisit a dead edge
e = edge it[u] of u, going to v
if level[v] == level[u] + 1 and rescap(e) > 0:
d = dfs(v, min(pushed, rescap(e)))
if d > 0: rescap(e) -= d; rescap(reverse(e)) += d; return d
return 0জটিলতা — O(V²E), আর unit নেটওয়ার্কে আরও কম
- Phase-এর সংখ্যা ≤ V − 1. Blocking flow-এর পর level graph-এর প্রতিটি সবচেয়ে ছোট পথে একটি edge ভরা; নতুন edge আসে শুধু ফেরত edge হিসেবে, যারা level-এ পেছনে যায়। তাই পরের phase-এ s থেকে t-র দূরত্ব কঠোরভাবে বাড়ে (ওপরে 3 → 4). দূরত্ব ১ থেকে V − 1-এর মধ্যে, তাই phase সর্বোচ্চ V − 1 টি।
- এক phase O(VE). প্রতিটি সফল DFS অন্তত একটি edge ভরায় (সর্বোচ্চ E টি পথ), আর প্রতিটি পথ O(V) দৈর্ঘ্যের। ব্যর্থ হাঁটাগুলোর মোট খরচ O(E), কারণ current arc প্রতিটি মৃত edge একবারই পার হয়। মোট O(VE + E) = O(VE).
- মোট O(V²E), জায়গা O(V + E). Unit-capacity নেটওয়ার্কে (সব capacity 1) সীমা নামে O(E · min(√E, V2/3)), আর bipartite matching-এর নেটওয়ার্কে (প্রতিটি মাঝের vertex-এ একটি ঢোকা বা একটি বের হওয়ার edge) O(E√V) — Hopcroft-Karp-এর সমান।
বাস্তবে Dinic তাত্ত্বিক সীমার চেয়ে অনেক দ্রুত চলে, তাই কনটেস্টে (V কয়েক হাজার, E কয়েক লাখ) এটিই প্রায় সবার প্রথম পছন্দ। Edmonds-Karp বোঝা আর প্রমাণ করা সহজ — পরীক্ষার জন্য; Dinic লেখা প্রায় সমান সহজ, কিন্তু অনেক দ্রুত — কাজের জন্য।
৭ · Bipartite matching — ছাত্রদের প্রজেক্ট ভাগ করা
একটি প্রশিক্ষণ কেন্দ্রে চারজন শিক্ষার্থী — রাফি, নাদিয়া, তানভীর আর মিতু — আর চারটি প্রজেক্ট: Web, Robot, App, Data. প্রত্যেক শিক্ষার্থী কয়েকটি প্রজেক্টে কাজ করতে রাজি, আর প্রতিটি প্রজেক্টে ঠিক একজন। সবচেয়ে বেশি কতজনকে তাদের পছন্দের প্রজেক্ট দেওয়া যায়? চাকরির মেলায় প্রার্থী আর পদ, হাসপাতালে ডাক্তার আর শিফট, বিমানবন্দরে ফ্লাইট আর গেট — একই প্রশ্ন।
এটি একটি bipartite graph: বাঁ পাশে শিক্ষার্থী (L), ডান পাশে প্রজেক্ট (R), আর edge মানে “রাজি”. একটি matching হলো এমন কিছু edge, যাদের কোনো দুটি একই vertex ছোঁয় না। সবচেয়ে বেশি edge-এর matching হলো maximum matching; সবাই জোড়া পেলে perfect matching.
Flow-তে রূপান্তর
একটি নতুন source s আর sink t যোগ করুন। s → প্রতিটি শিক্ষার্থী, capacity 1 (একজন শিক্ষার্থী একটিই প্রজেক্ট); শিক্ষার্থী → রাজি থাকা প্রজেক্ট, capacity 1; প্রতিটি প্রজেক্ট → t, capacity 1 (একটি প্রজেক্টে একজন)। Maximum flow = maximum matching. একটি matching থেকে সহজেই সমান মানের একটি flow পাওয়া যায়; উল্টো দিকে integrality theorem বলে একটি integer max flow আছে, যেখানে প্রতিটি edge-এ ০ বা ১ — আর প্রবাহ ১ বহন করা মাঝের edge-গুলোই একটি matching, কারণ প্রতিটি শিক্ষার্থীতে ঢোকে সর্বোচ্চ ১, প্রতিটি প্রজেক্ট থেকে বের হয় সর্বোচ্চ ১.
Kuhn-এর অ্যালগরিদম — augmenting path সরাসরি
পুরো flow নেটওয়ার্ক না বানিয়েও একই কাজ করা যায়। Kuhn-এর অ্যালগরিদম-এ প্রতিটি শিক্ষার্থীর জন্য একটি DFS: তার পছন্দের প্রজেক্ট p খালি হলে সেটি তাকে দিন; খালি না হলে p-এর বর্তমান মালিককে জিজ্ঞেস করুন “অন্য কোনো প্রজেক্টে সরা যায় কি?” — recursive ভাবে। এই শিকলটিই flow নেটওয়ার্কের একটি augmenting path: matched edge-গুলো ফেরত edge হিসেবে ব্যবহার হয়, আর পথ পাওয়া গেলে শিকলের প্রত্যেকে এক ঘর সরে।
| যার জন্য DFS | |
| খোঁজার শিকল | |
| augmenting path-এর edge-সংখ্যা | |
| matching-এর আকার |
- শুরু. কেউ কোনো প্রজেক্ট পায়নি। শিক্ষার্থীদের একে একে নেব: Rafi, Nadia, Tanvir, Mitu. প্রতিজনের জন্য নতুন “দেখা হয়েছে” (seen) তালিকা।
- Rafi. প্রথম পছন্দ Web খালি — নিয়ে নিল। এক edge-এর augmenting path: s → Rafi → Web → t.
- Nadia. তাঁর একমাত্র পছন্দ Web, কিন্তু সেটি Rafi-র। Rafi-কে জিজ্ঞেস: অন্য কোথাও যাবেন? Rafi-র পরের পছন্দ Robot খালি — Rafi সরলেন Robot-এ, Nadia পেলেন Web. পথ Nadia → Web ⇢ Rafi → Robot: মাঝের Web–Rafi edge-টি matching থেকে বাদ গেল — flow-এর ভাষায় ফেরত edge দিয়ে বাতিল।
- Tanvir. Robot চাইলেন — Rafi-র। Rafi-র বিকল্প Web — Nadia-র; Nadia-র আর কোনো পছন্দ নেই — শিকল ব্যর্থ, কেউ সরল না। Tanvir-এর পরের পছন্দ App খালি — নিয়ে নিলেন।
- Mitu. শুধু App চান — Tanvir-এর। Tanvir প্রথমে Robot দেখেন (Rafi → Web → Nadia, আবার ব্যর্থ), App ইতিমধ্যে দেখা, তারপর Data — খালি! Tanvir গেলেন Data-য়, Mitu পেলেন App. চারজনই জোড়া: perfect matching. এই seen তালিকা না থাকলে DFS একই প্রজেক্টে ঘুরপাক খেত।
#include <cstdio>
#include <string>
#include <vector>
using namespace std;
vector<string> students = {"Rafi", "Nadia", "Tanvir", "Mitu"};
vector<string> projects = {"Web", "Robot", "App", "Data"};
vector<vector<int>> likes = {{0, 1}, {0}, {1, 2, 3}, {2}}; // project ids each student accepts
vector<int> owner(4, -1); // owner[p] = student holding project p
bool tryKuhn(int st, vector<bool>& seen) {
for (int p : likes[st]) {
if (seen[p]) continue;
seen[p] = true;
// p is free, or its owner can move to another project
if (owner[p] == -1 || tryKuhn(owner[p], seen)) { owner[p] = st; return true; }
}
return false;
}
int main() {
int matched = 0;
for (int st = 0; st < 4; ++st) {
vector<bool> seen(4, false);
bool ok = tryKuhn(st, seen);
matched += ok;
printf("%-6s %s |", students[st].c_str(), ok ? "matched" : "no path");
const char* sep = " ";
for (int p = 0; p < 4; ++p)
if (owner[p] != -1) { printf("%s%s-%s", sep, students[owner[p]].c_str(), projects[p].c_str()); sep = ", "; }
printf("\n");
}
printf("maximum matching = %d\n", matched);
}আউটপুট:
Rafi matched | Rafi-Web
Nadia matched | Nadia-Web, Rafi-Robot
Tanvir matched | Nadia-Web, Rafi-Robot, Tanvir-App
Mitu matched | Nadia-Web, Rafi-Robot, Mitu-App, Tanvir-Data
maximum matching = 4
প্রতিটি লাইনে matching প্রজেক্টের ক্রমে (Web, Robot, App, Data) ছাপা, তাই শেষ লাইনে Mitu-App আগে, Tanvir-Data পরে। তৃতীয় লাইনে দেখুন: Tanvir-এর DFS ব্যর্থ শিকলে কারও প্রজেক্ট বদলায়নি — owner[p] = st শুধু সফল পথে, ফেরার সময় লেখা হয়। জটিলতা: প্রতিটি বাঁ-পাশের vertex-এর জন্য একটি DFS, প্রতিটি DFS প্রতিটি ডান-পাশের vertex সর্বোচ্চ একবার দেখে (seen), তাই O(E) — মোট O(V·E), জায়গা O(V + E).
students = ["Rafi", "Nadia", "Tanvir", "Mitu"]
projects = ["Web", "Robot", "App", "Data"]
likes = [[0, 1], [0], [1, 2, 3], [2]] # project ids each student accepts
owner = [-1] * len(projects) # owner[p] = student holding project p
def try_kuhn(st, seen):
for p in likes[st]:
if not seen[p]:
seen[p] = True
# p is free, or its owner can move to another project
if owner[p] == -1 or try_kuhn(owner[p], seen):
owner[p] = st
return True
return False
matched = 0
for st in range(len(students)):
ok = try_kuhn(st, [False] * len(projects))
matched += ok
pairs = [f"{students[owner[p]]}-{projects[p]}"
for p in range(len(projects)) if owner[p] != -1]
print(f"{students[st]:6} {'matched' if ok else 'no path'} | {', '.join(pairs)}")
print("maximum matching =", matched)আউটপুট C++-এর সাথে হুবহু এক:
Rafi matched | Rafi-Web
Nadia matched | Nadia-Web, Rafi-Robot
Tanvir matched | Nadia-Web, Rafi-Robot, Tanvir-App
Mitu matched | Nadia-Web, Rafi-Robot, Mitu-App, Tanvir-Data
maximum matching = 4
matched += ok কাজ করে, কারণ Python-এ True আসলে 1. f"{students[st]:6}" নামটিকে ৬ ঘরে বাঁ-দিকে সাজায় — C++-এর %-6s-এর সমান। Recursion-এর গভীরতা সর্বোচ্চ বাঁ-পাশের vertex-সংখ্যা; ১০৪ শিক্ষার্থীর বড় ইনপুটে Python-এর recursion সীমা (সাধারণত ১০০০) বাড়াতে হবে বা DFS-কে stack দিয়ে লিখতে হবে।
Hopcroft-Karp, Hall আর König
Hopcroft-Karp অ্যালগরিদম (John Hopcroft ও Richard Karp, ১৯৭৩) Dinic-এর ধারণাই matching-এ প্রয়োগ করে: প্রতিটি phase-এ BFS দিয়ে সবচেয়ে ছোট augmenting path-গুলোর দৈর্ঘ্য বের করে, তারপর সেই দৈর্ঘ্যের পরস্পর-বিচ্ছিন্ন সব পথ একসাথে নেয়। O(√V) টি phase যথেষ্ট, মোট O(E√V). V = 105, E = 106-এর মতো ইনপুটে Kuhn-এর O(VE)-র তুলনায় এটিই চলে।
কখন perfect matching অসম্ভব? Hall-এর theorem (Philip Hall, ১৯৩৫): বাঁ পাশের সবাইকে জোড়া দেওয়া যায় ঠিক তখনই, যখন বাঁ পাশের প্রতিটি উপসেট A-এর জন্য A-এর পছন্দের মোট প্রজেক্ট-সংখ্যা ≥ |A|. ওপরের উদাহরণে Tanvir যদি Data না চাইতেন, তাহলে চারজনের পছন্দ মিলিয়ে মাত্র তিনটি প্রজেক্ট (Web, Robot, App) — A = চারজনই নিলে Hall-এর শর্ত ভাঙে, perfect matching অসম্ভব।
একটি vertex cover হলো এমন vertex-এর সেট, যা প্রতিটি edge-এর অন্তত এক প্রান্ত ছোঁয়। প্রতিটি matching edge-কে আলাদা vertex দিয়ে ঢাকতে হয়, তাই যেকোনো গ্রাফে matching ≤ vertex cover. König-এর theorem (Dénes Kőnig, ১৯৩১): bipartite গ্রাফে maximum matching-এর আকার = minimum vertex cover-এর আকার. প্রমাণ max-flow min-cut থেকেই আসে: শেষ residual graph-এ s থেকে যাওয়া যায় এমন সেট Z নিন; তাহলে (L \ Z) ∪ (R ∩ Z) একটি vertex cover, আর তার আকার min cut-এর capacity = max matching. সাধারণ (bipartite নয়) গ্রাফে minimum vertex cover NP-hard (পর্ব ৬৪, ৬৬) — bipartite কাঠামোই সমস্যাটিকে সহজ করে।
৮ · আরও প্রয়োগ — edge-disjoint path আর project selection
Edge-disjoint path — নেটওয়ার্ক কতটা মজবুত
একটি অফিসের দুটি রাউটারের মধ্যে এমন কয়টি পথ আছে, যাদের কোনো দুটি একই কেবল ভাগ করে না? প্রতিটি edge-এর capacity 1 দিয়ে max flow চালান: integrality-র কারণে প্রবাহ ০/১, আর প্রবাহ বহন করা edge-গুলো ঠিক |f| টি edge-disjoint path-এ ভাগ হয়। উল্টো দিকে, min cut = সবচেয়ে কম কয়টি কেবল কাটলে দুই রাউটার বিচ্ছিন্ন হয়। দুই মিলে Menger-এর theorem (Karl Menger, ১৯২৭): edge-disjoint পথের সর্বোচ্চ সংখ্যা = s আর t-কে বিচ্ছিন্ন করতে সবচেয়ে কম যতগুলো edge সরাতে হয়। Vertex-disjoint পথ চাইলে প্রতিটি vertex v-কে দুই ভাগ করুন — vin → vout, capacity 1 — যাতে প্রতিটি vertex একটিই পথ বহন করে। Undirected গ্রাফে প্রতিটি edge-কে দুই দিকে capacity 1-এর দুটি directed edge বানালেই চলে।
Project selection — min cut দিয়ে লাভ সর্বোচ্চ
ঢাকার একটি সফটওয়্যার ফার্ম দুটি প্রজেক্টের কথা ভাবছে। প্রজেক্ট P1 (লাভ ৯ লাখ টাকা) চালাতে লাগবে সার্ভার A (খরচ ৪) আর লাইসেন্স B (খরচ ৩); প্রজেক্ট P2 (লাভ ৫) লাগবে B আর প্রশিক্ষণ C (খরচ ৪)। কোনগুলো নিলে নিট লাভ সর্বোচ্চ? শুধু P1: 9 − 7 = 2. শুধু P2: 5 − 7 = −2, লোকসান। দুটিই: 14 − 11 = 3 — কারণ B ভাগাভাগি হয়। এই project selection (বা maximum closure) সমস্যা min cut দিয়ে সমাধান হয়:
- s → প্রতিটি প্রজেক্ট, capacity = তার লাভ; প্রতিটি সরঞ্জাম → t, capacity = তার খরচ।
- প্রজেক্ট → তার দরকারি প্রতিটি সরঞ্জাম, capacity ∞ — যাতে এই edge কখনো cut-এ না পড়ে: প্রজেক্ট নিলে তার সরঞ্জামও নিতে হবে।
- যেকোনো সীমিত cut (S, T)-এ S-এর প্রজেক্ট আর সরঞ্জামগুলোই “বাছাই”; cut-এর capacity = (না-নেওয়া প্রজেক্টের লাভ) + (নেওয়া সরঞ্জামের খরচ)। তাই সর্বোচ্চ নিট লাভ = সব লাভের যোগফল − min cut. এখানে 14 − 11 = 3.
একই ছাঁচে পড়ে “কোন কাজগুলো একসাথে করা যায়, পূর্বশর্ত মেনে”, খনিতে কোন ব্লকগুলো খুঁড়লে লাভ (open-pit mining), আর ছবি থেকে সামনের বস্তু আলাদা করা (image segmentation, ভাবনার প্রশ্ন ৪)। কোডে ∞-কে একটি বড় সংখ্যা (যেমন 1018) দিন, যা সব লাভের যোগফলের চেয়ে বড় — আর যোগফল long long-এ রাখুন, নইলে integer overflow.
আরও গভীরে — ওজনসহ matching আর min-cost flow: প্রতিটি শিক্ষার্থী–প্রজেক্ট জোড়ার একটি খরচ বা দক্ষতার নম্বর থাকলে প্রশ্ন হয় “সবচেয়ে কম মোট খরচে perfect matching” — assignment problem. এর ক্লাসিক সমাধান Hungarian অ্যালগরিদম (Harold Kuhn, ১৯৫৫; নামটি König আর Jenő Egerváry-র হাঙ্গেরীয় কাজের সম্মানে), পরে O(n³)-এ নামানো হয়। আরও সাধারণ রূপ min-cost flow: প্রতিটি edge-এ capacity-র সাথে প্রতি একক প্রবাহের খরচ; নির্দিষ্ট পরিমাণ প্রবাহ সবচেয়ে কম খরচে পাঠাতে প্রতিবার residual graph-এ সবচেয়ে সস্তা augmenting path নিন (ফেরত edge-এর খরচ ঋণাত্মক, তাই Bellman-Ford বা potential-সহ Dijkstra — পর্ব ৪৩–৪৪)। আর তাত্ত্বিক দৌড়ে max flow-এর সীমা নেমেছে James Orlin-এর O(VE) (২০১৩) থেকে ২০২২ সালের প্রায়-রৈখিক সময়ের অ্যালগরিদম পর্যন্ত।
পরীক্ষা ও ইন্টারভিউতে:
- Max-flow min-cut theorem-এর বক্তব্য? — যেকোনো flow network-এ সর্বোচ্চ প্রবাহের মান = সবচেয়ে ছোট s-t cut-এর capacity; সমতুল্যভাবে, residual-এ augmenting path না থাকলেই flow সর্বোচ্চ।
- Edmonds-Karp-এর time complexity আর তার যুক্তি? — O(VE²): BFS-দূরত্ব কখনো কমে না, প্রতিটি edge O(V) বার critical, তাই O(VE) টি augmentation, প্রতিটি O(E).
- Residual graph-এ ফেরত edge কেন লাগে? — আগের প্রবাহ বাতিলের সুযোগ দিতে; না থাকলে greedy ভুল পথে আটকে সর্বোচ্চের কম উত্তর দেয়।
- Max flow শেষে min cut কীভাবে বের করবেন? — residual graph-এ s থেকে BFS/DFS; পৌঁছানো vertex-গুলো S, বাকিরা T; S থেকে T-তে যাওয়া মূল edge-গুলোই min cut.
- Bipartite matching-কে flow-তে কীভাবে আনবেন? — s → বাঁ পাশ, বাঁ → ডান, ডান → t, সব capacity 1; max flow = maximum matching (integrality theorem).
৯ · AI-এর যুগে network flow — দেখতে ঠিক, চলতে ভুল
“Max flow-এর কোড লিখে দাও” — AI সহকারী মুহূর্তে একটি Dinic দেবে, আর ছোট উদাহরণে উত্তরও প্রায়ই ঠিক আসবে। কিন্তু flow-এর ভুলগুলো খুব নিঃশব্দ: ফেরত edge-এর index এক ঘর সরে যাওয়া, ফেরত edge-এর capacity 0-এর বদলে মূল capacity দেওয়া, বা bipartite matching-এর DFS-এ seen তালিকা প্রতিবার নতুন না করা — এগুলো compile হয়, অনেক ইনপুটে ঠিক উত্তর দেয়, আর কোনো এক অদ্ভুত ইনপুটে ভুল বা অসম্ভব ধীর হয়। আর মডেলিং-এর প্রশ্নে (“এই সমস্যাকে flow-তে রূপান্তর করো”) AI প্রায়ই একটি capacity ভুল বসায়, বা প্রমাণ ছাড়াই একটি greedy প্রস্তাব করে।
Network flow-এর কোডে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| ফেরত edge নেই, বা প্রবাহ পাঠানোর পর তার capacity বাড়ানো হয়নি | অংশ ২-এর চার-vertex নেটওয়ার্কে পথের ক্রম জোর করে s→a→b→t দিন — উত্তর ২০-এর বদলে ১০ আসবে |
Adjacency list-এ ফেরত edge-এর index ভুল (e ^ 1 কৌশলে edge জোড়ায় জোড়ায় না রাখা) | প্রতিটি edge e-এর জন্য assert করুন: rev(rev(e)) == e, আর দুইয়ের capacity-র যোগফল মূল capacity |
| DFS-ভিত্তিক Ford-Fulkerson-কে “O(VE²)” বা “O(V²E)” বলা | সঠিক সীমা চান: DFS-এ O(E·|f*|); O(VE²) শুধু BFS (Edmonds-Karp)-এ, O(V²E) Dinic-এ |
| “Min cut = সবচেয়ে ছোট capacity-র edge” | একটি cut-এর সব edge সরিয়ে দেখুন s থেকে t বিচ্ছিন্ন হয় কি না, আর capacity-র যোগফল max flow-এর সমান কি না |
| Kuhn-এ seen তালিকা প্রতিটি শিক্ষার্থীর জন্য নতুন না করা, বা আদৌ না রাখা | উত্তর কমে যাবে বা DFS অসীম লুপে পড়বে; ছোট এলোমেলো গ্রাফে brute force-এর সাথে মেলান |
| সাধারণ (bipartite নয়) গ্রাফে Kuhn/Hopcroft-Karp চালানো | একটি বিজোড় cycle (যেমন ত্রিভুজ) দিন; সাধারণ গ্রাফে matching-এর জন্য Edmonds-এর blossom অ্যালগরিদম লাগে |
Project selection-এ ∞ হিসেবে ছোট সংখ্যা, বা int-এ যোগফল | সব লাভের যোগফলের চেয়ে বড় ∞ দিন, long long ব্যবহার করুন, আর ছোট ইনপুটে সব উপসেট brute force করে মেলান |
AI-কে কোড লিখে দিতে না বলে, আপনার কোড, আপনার trace আর আপনার মডেলিং পরীক্ষা করতে বলুন — আর প্রতিটি দাবির যুক্তি চান:
Flow-এর কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Max flow-এর জন্ম একটি স্নায়ুযুদ্ধের রেলপথ প্রতিবেদনে — প্রশ্নটি আসলে প্রবাহ নিয়ে ছিল, নাকি কাটা নিয়ে?
১৯৫০-এর দশকের মাঝামাঝি যুক্তরাষ্ট্রের বিমানবাহিনী জানতে চাইত, যুদ্ধ বাধলে সোভিয়েত ইউনিয়ন রেলপথে পূর্ব ইউরোপে কতটা সৈন্য আর রসদ পাঠাতে পারবে — আর কোন রেললাইনগুলো বোমা মেরে অচল করলে সেই সরবরাহ সবচেয়ে কম খরচে বন্ধ হবে। RAND Corporation-এর Theodore Harris আর জেনারেল Frank Ross ১৯৫৫ সালে এ নিয়ে একটি গোপন প্রতিবেদন লেখেন। তাঁরা রেল নেটওয়ার্ককে অঞ্চল (vertex) আর তাদের সংযোগ (edge) হিসেবে মডেল করেন, প্রতিটি সংযোগের দৈনিক ধারণক্ষমতাসহ, আর হাতে-কলমে একটি “bottleneck” খুঁজে বের করেন — আজকের ভাষায় একটি min cut.
লক্ষ করুন, সামরিক প্রশ্নটি ছিল মূলত কাটা নিয়ে: কোথায় আঘাত করলে প্রবাহ বন্ধ হয়। কিন্তু সবচেয়ে সস্তা কাটা খুঁজতে গেলে আগে জানতে হয় সর্বোচ্চ প্রবাহ কত — দুটি প্রশ্ন একই মুদ্রার দুই পিঠ। Harris প্রশ্নটি RAND-এর L. R. Ford Jr. আর D. R. Fulkerson-এর কাছে নিয়ে যান। তাঁরা দেখান যে সর্বোচ্চ প্রবাহ আর সবচেয়ে ছোট cut সবসময় সমান, আর augmenting path-এর পদ্ধতি দেন। ১৯৫৬ সালে প্রকাশিত তাঁদের প্রবন্ধটি পরে ১৯৬২ সালের বই Flows in Networks-এ বিস্তৃত হয় — combinatorial optimization বিষয়টির অন্যতম ভিত্তি-গ্রন্থ।
Harris–Ross প্রতিবেদন বহু দশক গোপন ছিল; ১৯৯৯ সালে প্রকাশ্যে আসার পর ডাচ গণিতবিদ Alexander Schrijver ২০০২ সালে “On the history of the transportation and maximum flow problems” প্রবন্ধে গল্পটি বিস্তারিত তুলে ধরেন। সেখানে আরও একটি চমক: পরিবহন-সমস্যার কাছাকাছি প্রশ্ন সোভিয়েত ইউনিয়নেই ১৯৩০-এর দশকে রেলপথে মাল পাঠানোর পরিকল্পনা নিয়ে কাজ হয়েছিল — অর্থাৎ একই প্রশ্ন দুই প্রতিপক্ষের টেবিলেই ছিল।
আজ একই “কাটা” প্রশ্ন শান্তিপূর্ণ কাজে লাগে: বিদ্যুৎ গ্রিডের কোন লাইনগুলো সবচেয়ে ঝুঁকিপূর্ণ (একসাথে বিকল হলে শহর বিচ্ছিন্ন), ইন্টারনেট ব্যাকবোনে কয়টি কেবল কাটা পড়লে একটি দেশ বিচ্ছিন্ন — বাংলাদেশের মতো দেশে, যেখানে আন্তর্জাতিক সংযোগ অল্প কয়েকটি সাবমেরিন কেবলে নির্ভর করে, প্রশ্নটি খুব বাস্তব। Min cut-এর capacity যত বড়, নেটওয়ার্ক তত মজবুত।
মূল উপলব্ধি: max flow আর min cut একই প্রশ্নের দুই রূপ — একটি বলে “কতটা যেতে পারে”, অন্যটি বলে “কোথায় আটকায়”. একটির উত্তর পেলেই অন্যটির প্রমাণ মেলে।
প্র ০২Ford-Fulkerson সঠিক, তবু কখনো থামে না — এটি কীভাবে সম্ভব, আর Edmonds ও Karp কীভাবে সমস্যাটি সারালেন?
Ford-Fulkerson-এর প্রমাণ বলে: অ্যালগরিদম যদি থামে, তবে প্রবাহ সর্বোচ্চ। কিন্তু “যদি থামে” কথাটিই ফাঁদ। Integer capacity-তে প্রতিটি ধাপ অন্তত ১ বাড়ায়, তাই থামা নিশ্চিত — তবে সর্বোচ্চ |f*| ধাপে, যা capacity-র মান-এর সমানুপাতিক। ইনপুটে একটি সংখ্যা 109 লিখতে লাগে মাত্র ১০টি অঙ্ক, কিন্তু ধাপ লাগতে পারে 109 টি। একে বলে pseudo-polynomial: সময় ইনপুটের দৈর্ঘ্যে নয়, সংখ্যার আকারে বহুপদী — knapsack-এর DP-তে (পর্ব ৫৩) একই ধারণা দেখবেন।
Capacity অমূলদ হলে অবস্থা আরও অদ্ভুত। Ford আর Fulkerson নিজেই দেখান যে এমন নেটওয়ার্ক আছে; আজ সবচেয়ে প্রচলিত উদাহরণে কয়েকটি capacity-তে r = (√5 − 1)/2 ব্যবহার হয়, আর augmenting path-এর একটি নির্দিষ্ট চক্রাকার ক্রমে প্রতিটি পথের bottleneck আগেরটির একটি নির্দিষ্ট অনুপাত — একটি জ্যামিতিক ধারার মতো ছোট হতে থাকে। প্রবাহ বাড়তেই থাকে, কিন্তু অসীম ধাপেও একটি সীমার দিকে এগোয়, যা প্রকৃত সর্বোচ্চের চেয়ে কম। পরে Uri Zwick ১৯৯৫ সালে এমন আচরণের সবচেয়ে ছোট নেটওয়ার্কগুলো বিশ্লেষণ করেন। বাস্তবে কম্পিউটারে অমূলদ সংখ্যা থাকে না, কিন্তু floating-point capacity-তে rounding-এর কারণে প্রায় একই রকম “অনন্ত ক্ষুদ্র ধাপ” দেখা দিতে পারে।
১৯৭২ সালে Jack Edmonds আর Richard Karp Journal of the ACM-এ দুটি সমাধান প্রস্তাব করেন। প্রথমটি এই পর্বের Edmonds-Karp: সবচেয়ে কম edge-এর পথ নিলে augmentation-এর সংখ্যা O(VE), capacity-র মান যা-ই হোক — একটি strongly polynomial সীমা, যা শুধু V আর E-এর ওপর নির্ভর করে। (Dinitz-এর কাজ ততদিনে সোভিয়েত ইউনিয়নে প্রকাশিত, কিন্তু পশ্চিমে প্রায় অজানা ছিল।) দ্বিতীয়টি: সবচেয়ে “চওড়া” পথ, অর্থাৎ যে পথের bottleneck সবচেয়ে বড় — তাতে প্রতিটি ধাপ বাকি প্রবাহের একটি নির্দিষ্ট ভগ্নাংশ মেটায়, ফলে ধাপের সংখ্যা মোটামুটি E গুণ capacity-র মানের log — মানের ওপর নির্ভরতা থাকে, কিন্তু অনেক কম। এই ধারণা থেকেই পরে capacity scaling কৌশল জনপ্রিয় হয়: প্রথমে শুধু বড় জায়গার edge নিয়ে কাজ, তারপর ধাপে ধাপে সীমা অর্ধেক।
Edmonds এর আগেই ১৯৬৫ সালের “Paths, trees, and flowers” প্রবন্ধে প্রস্তাব করেছিলেন যে “দক্ষ অ্যালগরিদম” মানে বহুপদী সময়ের অ্যালগরিদম — আজকের P শ্রেণির ধারণার একটি উৎস (পর্ব ৬৪)। Karp ঠিক ১৯৭২ সালেই আরেকটি প্রবন্ধে ২১টি NP-complete সমস্যার বিখ্যাত তালিকা দেন। অর্থাৎ max flow-এর এই সংশোধনটি জটিলতা-তত্ত্বের জন্মলগ্নের দুই স্থপতির হাতে।
মূল উপলব্ধি: একটি অ্যালগরিদম “সঠিক” হওয়া আর “দ্রুত” হওয়া আলাদা প্রশ্ন। Ford-Fulkerson-এ কোন পথ বাছবেন — এই আপাত-তুচ্ছ সিদ্ধান্তটিই অসীম আর বহুপদীর পার্থক্য গড়ে।
প্র ০৩একটি ক্রিকেট লিগে আপনার দল কি গাণিতিকভাবে বাদ পড়ে গেছে? পয়েন্ট-টেবিল দেখে নয়, max flow দিয়ে কীভাবে উত্তর দেবেন?
ধরুন একটি ঘরোয়া ক্রিকেট লিগে প্রতিটি জয়ে ১ পয়েন্ট, ড্র নেই, আর সবচেয়ে বেশি জয়ের দল (বা যৌথভাবে) চ্যাম্পিয়ন। মৌসুমের মাঝপথে প্রশ্ন: দল X কি এখনো চ্যাম্পিয়ন হওয়ার সম্ভাবনা রাখে? সহজ পরীক্ষা: X-এর বর্তমান জয় + বাকি সব ম্যাচে জয় যদি কোনো দলের বর্তমান জয়ের চেয়ে কম হয়, X বাদ। কিন্তু এর চেয়ে সূক্ষ্ম ক্ষেত্রও আছে: হয়তো A আর B দুজনেই X-এর সম্ভাব্য সর্বোচ্চের কাছাকাছি, আর তাদের নিজেদের মধ্যে অনেক ম্যাচ বাকি — যে-ই জিতুক, কেউ একজন X-কে ছাড়িয়ে যাবে। পয়েন্ট-টেবিল দেখে এটি চোখে পড়ে না।
১৯৬৬ সালে Benjamin Schwartz মার্কিন বেসবল লিগের জন্য এই প্রশ্নকে max flow-এ রূপান্তর করেন — “baseball elimination” সমস্যা নামে পাঠ্যবইয়ে ক্লাসিক। ধরে নিন X তার বাকি সব ম্যাচ জিতবে, তাতে তার জয় হবে W. এখন বাকি দলগুলোর মধ্যের প্রতিটি অবশিষ্ট ম্যাচ-জোড়া (i, j)-এর জন্য একটি vertex: s → (i, j), capacity = তাদের মধ্যে বাকি ম্যাচ-সংখ্যা। (i, j) → i আর (i, j) → j, capacity ∞ — প্রতিটি ম্যাচের জয় দুই দলের কোনো একজন পাবে। আর প্রতিটি দল i → t, capacity = W − (i-এর বর্তমান জয়) — i সর্বোচ্চ এতগুলো ম্যাচ আর জিততে পারবে, নইলে X-কে ছাড়িয়ে যাবে।
এখন max flow যদি s থেকে বের হওয়া সব edge ভরায় — অর্থাৎ বাকি সব ম্যাচের ফল এমনভাবে বণ্টন করা যায় যে কেউ W ছাড়ায় না — তাহলে X এখনো টিকে আছে, আর integrality theorem বলে সেই বণ্টন integer, তাই প্রতিটি ম্যাচের একটি সত্যিকারের বিজয়ী দিয়ে বাস্তবায়নযোগ্য। না ভরালে X বাদ। আরও সুন্দর: min cut-এর s-পাশে পড়া দলগুলোর সেট R একটি “প্রমাণ” দেয় — R-এর দলগুলোর বর্তমান জয় আর নিজেদের মধ্যে বাকি ম্যাচ মিলিয়ে গড় জয় W-এর বেশি, তাই তাদের কেউ একজন অবশ্যই X-কে ছাড়াবে। দর্শককে বোঝানোর মতো এক লাইনের যুক্তি।
এই উদাহরণের শিক্ষা হলো মডেলিং: সমস্যায় কোথাও “পাইপ” বা “রাস্তা” নেই, তবু “ম্যাচের জয় দলগুলোর মধ্যে বণ্টন” একটি প্রবাহ। Capacity দিয়ে সীমা (কতগুলো ম্যাচ, কতগুলো জয় সর্বোচ্চ), ∞ দিয়ে “যেকোনো একজনকে দিতেই হবে”, আর integrality দিয়ে “ভগ্নাংশ ম্যাচ হয় না” — তিনটি ধারণা মিলে একটি দক্ষ সমাধান। দল n টি হলে ম্যাচ-জোড়া O(n²), তাই নেটওয়ার্কও ছোট।
মূল উপলব্ধি: flow-এর আসল শক্তি অ্যালগরিদমে নয়, মডেলিং-এ — “কিছু একটা কয়েকটি জায়গায় সীমা মেনে ভাগ করে দিতে হবে” ধরনের প্রায় যেকোনো প্রশ্ন max flow হতে পারে, আর min cut তখন “কেন অসম্ভব” তার প্রমাণ দেয়।
প্র ০৪ছবি থেকে মানুষটিকে আলাদা করা (background remove) — এর সাথে min cut-এর কী সম্পর্ক?
একটি ছবির প্রতিটি pixel হয় সামনের বস্তুর (foreground), নয় পেছনের (background). প্রতিটি pixel-এর রং দেখে একটি অনুমান করা যায় — “এই pixel-টি চামড়ার রঙের, সম্ভবত মানুষ” — কিন্তু শুধু রং দেখে সিদ্ধান্ত নিলে ফল হয় দানাদার, ছিটেফোঁটা: মানুষের মাঝে পেছনের রঙের একটি দাগ, আকাশে মানুষের রঙের একটি বিন্দু। আমরা চাই সিদ্ধান্ত “মসৃণ” হোক — পাশাপাশি pixel সাধারণত একই দিকে।
১৯৮৯ সালে D. M. Greig, B. T. Porteous আর A. H. Seheult দেখান যে এমন সাদা-কালো ছবির সর্বোত্তম পরিষ্কার করা ঠিক একটি min cut. নির্মাণ: প্রতিটি pixel একটি vertex; s মানে “foreground”, t মানে “background”. s → pixel edge-এর capacity = pixel-টিকে background বললে যত “জরিমানা” (রং যত বেশি foreground-এর মতো, তত বেশি); pixel → t একইভাবে উল্টো। আর পাশাপাশি দুটি pixel-এর মধ্যে দুই দিকে edge, capacity = তাদের আলাদা পাশে রাখার জরিমানা (রং যত কাছাকাছি, তত বেশি)। যেকোনো s-t cut মানে প্রতিটি pixel-কে একটি পাশে দেওয়া, আর cut-এর capacity = মোট জরিমানা। তাই min cut = সবচেয়ে কম মোট জরিমানার ভাগাভাগি — এক ধাপে সর্বোত্তম, কোনো আন্দাজ নয়।
এরপর কম্পিউটার ভিশনে “graph cuts” একটি বড় ধারা হয়ে ওঠে। Yuri Boykov, Olga Veksler, Ramin Zabih আর Vladimir Kolmogorov-এর মতো গবেষকরা ১৯৯০-এর শেষ আর ২০০০-এর দশকে দেখান কোন ধরনের জরিমানা-ফাংশন min cut দিয়ে সমাধান করা যায়, আর ছবির গ্রিড-গ্রাফের জন্য বিশেষভাবে দ্রুত max-flow অ্যালগরিদম তৈরি করেন (Boykov–Kolmogorov অ্যালগরিদম)। Microsoft Research-এর “GrabCut” (২০০৪) — ব্যবহারকারী বস্তুর চারপাশে একটি আয়তক্ষেত্র আঁকেন, বাকিটা min cut আর রঙের মডেল মিলে করে — এই ধারার একটি পরিচিত ফল। আজ deep learning এই কাজের বড় অংশ নিয়ে নিয়েছে, তবু মেডিক্যাল ইমেজে (যেমন MRI-তে একটি অঙ্গের সীমানা) graph cut এখনো ব্যবহার হয়, কারণ এটি সর্বোত্তমতার নিশ্চয়তা দেয় আর প্রশিক্ষণের ডেটা লাগে না।
এটি আসলে অংশ ৮-এর project selection-এর আত্মীয়: প্রতিটি উপাদানকে দুই দলের একটিতে দিতে হবে, প্রতিটি সিদ্ধান্তের একটি নিজস্ব খরচ, আর জোড়ায় জোড়ায় “একসাথে থাকলে ভালো” শর্ত। এই কাঠামোর সমস্যাগুলো — দুই অবস্থার মধ্যে বাছাই, জোড়ার জরিমানা অঋণাত্মক — সবই একটি min cut.
মূল উপলব্ধি: min cut একটি “দুই ভাগে সবচেয়ে কম খরচে ভাগ করো” যন্ত্র। যেখানেই হাজারো ছোট হ্যাঁ/না সিদ্ধান্ত আর তাদের মধ্যে জোড়ার শর্ত আছে, সেখানে এটি সর্বোত্তম উত্তর দিতে পারে।
প্র ০৫একজন সোভিয়েত ছাত্রের ক্লাসের অনুশীলনী থেকে কনটেস্টের সবচেয়ে প্রিয় flow অ্যালগরিদম — Dinic-এর গল্প, আর তারপর কত দ্রুত হলো max flow?
Yefim Dinitz ১৯৬০-এর দশকের শেষে মস্কোতে Georgy Adelson-Velsky-র (AVL tree-এর “A”, পর্ব ২৮) অ্যালগরিদমের ক্লাসে পড়তেন। Dinitz পরে লিখেছেন, অ্যালগরিদমটি তিনি তৈরি করেন একটি ক্লাসের অনুশীলনীর জবাবে — Ford-Fulkerson-কে কীভাবে দ্রুত করা যায়। ১৯৭০ সালে Soviet Mathematics Doklady-তে প্রকাশিত হয় — Edmonds আর Karp-এর প্রবন্ধের দুই বছর আগে, আর তাতে শুধু সবচেয়ে ছোট পথ নয়, level graph আর blocking flow-এর পুরো ধারণা ছিল। স্নায়ুযুদ্ধের দেয়ালের কারণে পশ্চিমে কাজটি দেরিতে পৌঁছায়; Shimon Even আর Alon Itai-এর ব্যাখ্যার মাধ্যমে এটি জনপ্রিয় হয়, আর নামের বানান হয়ে যায় “Dinic”. Dinitz ২০০৬ সালে একটি প্রবন্ধে নিজের আসল সংস্করণ আর Even-এর সংস্করণের পার্থক্য লেখেন।
Max flow-এর গতির দৌড় এরপর থামেনি। ১৯৮৮ সালে Andrew Goldberg আর Robert Tarjan সম্পূর্ণ ভিন্ন একটি ধারণা দেন — push-relabel: পথ না খুঁজে প্রতিটি vertex-এ “বাড়তি” প্রবাহ জমতে দিন, তারপর উচ্চতার নিয়ম মেনে তা নিচের দিকে ঠেলে দিন, পানি যেমন ঢালু বেয়ে নামে। এর ভালো বাস্তবায়ন বড় সাধারণ গ্রাফে প্রায়ই Dinic-এর চেয়েও দ্রুত। ২০১৩ সালে James Orlin O(VE) সময়ের অ্যালগরিদম দেন। আর ২০২২ সালে Li Chen, Rasmus Kyng, Yang Liu, Richard Peng, Maximilian Probst Gutenberg আর Sushant Sachdeva দেখান যে max flow (এমনকি min-cost flow) প্রায় রৈখিক সময়ে — m1+o(1) — সমাধান করা যায়; প্রবন্ধটি সে বছর FOCS-এর শ্রেষ্ঠ প্রবন্ধের পুরস্কার পায়। তবে সেই অ্যালগরিদম এত জটিল যে কনটেস্ট বা সাধারণ সফটওয়্যারে লেখা হয় না।
তাহলে কনটেস্টে Dinic কেন? তিনটি কারণ। প্রথমত, ৪০–৫০ লাইনে লেখা যায়, আর ভুলের জায়গা কম। দ্বিতীয়ত, তাত্ত্বিক O(V²E) সীমাটি খুবই নিরাশাবাদী — বাস্তবের বেশিরভাগ নেটওয়ার্কে phase অল্প কয়েকটি। তৃতীয়ত, unit-capacity নেটওয়ার্কে এটি আরও দ্রুত, আর bipartite matching-এর নেটওয়ার্কে এর সীমা O(E√V), অর্থাৎ একই কোড দিয়ে Hopcroft-Karp-এর গতি মেলে। ICPC বা Codeforces-এর অনেক flow সমস্যায় আসল কঠিন অংশ অ্যালগরিদম নয়, মডেলিং — কোন vertex, কোন edge, কোন capacity — তাই একটি পরীক্ষিত Dinic-এর টেমপ্লেট হাতে রাখা আর মডেলিং-এ মন দেওয়াই কৌশল।
মূল উপলব্ধি: একটি ক্লাসের অনুশীলনী থেকে আসা ধারণা পঞ্চাশ বছর পরেও প্রতিদিনের হাতিয়ার — কারণ এটি সরল, দ্রুত আর বিশ্লেষণযোগ্য। তাত্ত্বিকভাবে দ্রুততম আর বাস্তবে সবচেয়ে কাজের অ্যালগরিদম সবসময় এক নয়।
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।
-
হাতে trace করুন: নেটওয়ার্ক: s → a (3), s → b (2), a → b (1), a → t (2), b → t (3). Edmonds-Karp চালান (BFS প্রতিবেশী দেখে a, b, t ক্রমে)। প্রতিটি augmenting path আর bottleneck লিখুন, তারপর max flow আর একটি min cut.
পথ ১: BFS s থেকে a, b পায়; a থেকে t. s → a → t, bottleneck min(3, 2) = 2. পথ ২: এখন s → a-তে 1, a → t ভরা। BFS: a, b; a থেকে b আগেই দেখা; b থেকে t. s → b → t, bottleneck min(2, 3) = 2. পথ ৩: s → b ভরা; s → a (1) → b (a → b-তে 1) → t (b → t-তে 1): s → a → b → t, bottleneck 1. মোট ২ + ২ + ১ = ৫.
শেষ BFS: s থেকে দুটি edge-ই ভরা, তাই S = {s}. Min cut {s} | {a, b, t}, capacity 3 + 2 = ৫ = max flow. পথের দৈর্ঘ্য 2, 2, 3 — কখনো কমেনি।
-
ভেবে বলুন: মূল উদাহরণে (বন্দর থেকে ঢাকা) min cut হিসেবে {S, F} পাওয়া গেল। (ক) দেখান যে {S, F, Ch}-ও একটি min cut. (খ) শেষ residual graph-এ t-এর দিক থেকে উল্টো খোঁজ করলে (কোন vertex থেকে t-তে পৌঁছানো যায়) কোন cut পাবেন? (গ) কোনো edge-এর capacity ১ বাড়ালে max flow বাড়বে, এমন একটি edge বলুন।
(ক) {S, F, Ch} থেকে বাকিদের দিকে যাওয়া edge: F → Cu (6), Ch → Cu (2), Ch → M (5) = ১৩ = max flow; weak duality বলে কোনো cut ১৩-এর কম হতে পারে না, তাই এটিও min cut. (S → Ch আর F → Ch এখন দুই পাশের ভেতরে, গোনা হয় না।)
(খ) শেষ residual-এ t-তে পৌঁছায়: M (M → T-তে 5 জায়গা), Cu (Cu → M-এ 4), আর Ch? Ch → M ভরা, Ch → Cu ভরা — না। তাই T-পাশ = {Cu, M, T}, অর্থাৎ cut {S, F, Ch} | {Cu, M, T} — t-এর সবচেয়ে কাছের min cut. দুই খোঁজ মিলে দেখায় min cut একক নয়।
(গ) একটি edge বাড়ালে max flow বাড়ে কেবল তখন, যখন সেটি প্রতিটি min cut-এ আছে। F → Cu দুটি min cut-েই আছে: capacity 7 করলে residual-এ S → F (1) → Cu (1) → M (4) → T পথ খোলে, max flow ১৪. কিন্তু S → Ch বাড়ালে কিছু হয় না — {S, F, Ch} cut-এ সেটি নেই, তাই সেই cut তখনও ১৩-এ আটকে রাখে।
-
মডেল করুন: একটি হাসপাতালে ৩ জন ডাক্তার আর এক সপ্তাহে ৫টি রাতের শিফট। প্রত্যেক ডাক্তার কিছু নির্দিষ্ট রাতে পারেন, আর কেউ সপ্তাহে ২টির বেশি শিফট করবেন না; প্রতিটি শিফটে ঠিক একজন লাগবে। সব শিফট পূরণ করা যায় কি না — flow নেটওয়ার্কটি বানান, আর বলুন উত্তর কীভাবে পড়বেন।
Vertex: s, তিনজন ডাক্তার, পাঁচটি শিফট, t. Edge: s → ডাক্তার, capacity 2 (সপ্তাহে সর্বোচ্চ ২টি); ডাক্তার → সেই শিফট যেটিতে তিনি পারেন, capacity 1 (একই শিফট দুবার নয়); শিফট → t, capacity 1 (একটি শিফটে একজন)। Max flow = ৫ হলে সব শিফট পূরণ সম্ভব; কম হলে নয়।
Integrality theorem বলে একটি integer max flow আছে — প্রতিটি ডাক্তার → শিফট edge-এ ০ বা ১ — আর প্রবাহ ১-এর edge-গুলোই রোস্টার। সর্বোচ্চ সম্ভাব্য মান min(3 × 2, 5) = 5; ডাক্তারদের মোট ৬টি শিফটের ক্ষমতা আছে বলেই হবে, এমন নয় — কে কোন রাতে পারেন, তা min cut দিয়ে বাধা তৈরি করতে পারে (যেমন দুটি শিফট শুধু একজন ডাক্তারই পারেন, আর তাঁর সীমা ২ — তাহলে ঠিক আছে; তিনটি হলে নয়)। এটি bipartite matching-এর সাধারণ রূপ, যেখানে বাঁ পাশের vertex একাধিকবার জোড়া পেতে পারে।
-
কোড লিখুন: একটি directed গ্রাফে vertex 0 থেকে 5 পর্যন্ত কয়টি edge-disjoint পথ আছে, তা বের করুন। Edges: 0→1, 0→2, 0→3, 1→4, 2→4, 2→5, 3→5, 4→5. ওপরের Edmonds-Karp কোড ব্যবহার করুন; হাতেও উত্তর মেলান, আর একটি সবচেয়ে ছোট “কাটার সেট” বলুন।
প্রতিটি edge-এর capacity 1 দিয়ে max flow — integrality-র কারণে প্রতিটি edge-এ প্রবাহ ০ বা ১, আর মান = edge-disjoint পথের সংখ্যা (Menger). Edmonds-Karp কোডে শুধু নেটওয়ার্ক বদলান:
// replace the network in the Edmonds-Karp example vector<string> names = {"0", "1", "2", "3", "4", "5"}; vector<tuple<int, int, int>> edges = {{0, 1, 1}, {0, 2, 1}, {0, 3, 1}, {1, 4, 1}, {2, 4, 1}, {2, 5, 1}, {3, 5, 1}, {4, 5, 1}}; // s = 0, t = 5 as before; max flow = number of edge-disjoint paths // prints: max flow = 3, min cut: 0->1 (1), 0->2 (1), 0->3 (1)হাতে: তিনটি পথ 0→1→4→5, 0→2→5, 0→3→5 — কোনো edge ভাগাভাগি নেই। 0 থেকে বের হওয়া edge মাত্র ৩টি, তাই ৩-এর বেশি অসম্ভব — cut {0}-এর capacity ৩. 5-এ ঢোকা edge-ও ৩টি (2→5, 3→5, 4→5) — সেটিও একটি min cut. BFS-এর শেষ খোঁজ কোনটি দেবে? প্রবাহ শেষে 0 থেকে বের হওয়া তিনটিই ভরা, তাই S = {0}, কোড ছাপবে
0->1, 0->2, 0->3. সময় O(VE²), কিন্তু unit capacity-তে augmentation সর্বোচ্চ deg(s) টি, তাই আসলে O(E · deg(s)).
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমEdmonds-Karp-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- প্রতিটি edge-এর residual capacity = মূল capacity, ফেরত edge 0, মোট প্রবাহ 0
- Residual graph-এ শুধু ধনাত্মক capacity-র edge ধরে s থেকে BFS চালানো
- t-তে পৌঁছানো না গেলে থামা — BFS যাদের ছুঁয়েছে তারাই min cut-এর s-পাশ
- পাওয়া পথের সবচেয়ে কম residual capacity (bottleneck) বের করা
- পথের প্রতিটি এগোনো edge-এ bottleneck কমানো, ফেরত edge-এ বাড়ানো, মোট প্রবাহে যোগ করে আবার BFS
একটি flow network-এ edge (u, v)-এর capacity 7 আর বর্তমান প্রবাহ 5. Residual graph-এ u → v আর v → u-এর residual capacity কত?
- u → v: 2, v → u: 7
- u → v: 2, v → u: 5
- u → v: 5, v → u: 2
- u → v: 2, v → u: 0 (ফেরত edge থাকে না)
Ford-Fulkerson পদ্ধতিতে augmenting path সবসময় BFS দিয়ে (সবচেয়ে কম edge-এর পথ) নিলে, V টি vertex আর E টি edge-এর নেটওয়ার্কে worst-case সময় কত?
- O(E · |f*|), যেখানে |f*| সর্বোচ্চ প্রবাহ
- O(V²E)
- O(VE²)
- O(E√V)
একটি flow network-এ একটি বৈধ flow f-এর মান 12, আর একটি s-t cut (S, T)-এর capacity 12. নিচের কোনটি অবশ্যই সত্য?
- f একটি maximum flow আর (S, T) একটি minimum cut
- f সর্বোচ্চ কি না বলা যায় না; আরও augmenting path থাকতে পারে
- (S, T)-ই একমাত্র minimum cut
- নেটওয়ার্কের সবচেয়ে ছোট capacity-র edge অবশ্যই (S, T)-তে আছে
একটি bipartite matching সমস্যাকে max flow-তে রূপান্তর করতে s → বাঁ পাশ, বাঁ → ডান (যেখানে জোড়া সম্ভব), আর ডান → t edge দেওয়া হলো। Capacity কী হলে max flow = maximum matching?
- s → বাঁ: ∞, বাঁ → ডান: 1, ডান → t: 1
- s → বাঁ: 1, বাঁ → ডান: 1, ডান → t: ∞
- সবগুলো edge-এ বাঁ পাশের vertex-সংখ্যা
- সব edge-এ 1 (বাঁ → ডান-এ ∞ দিলেও মান একই থাকে)
একটি bipartite গ্রাফে maximum matching-এর আকার 6. ওই গ্রাফের minimum vertex cover-এর আকার কত?
- 5
- 6
- 12
- বলা যায় না — vertex cover NP-hard
Dinic-এর অ্যালগরিদমের একটি phase-এ কী ঘটে?
- একটি BFS দিয়ে একটিমাত্র সবচেয়ে ছোট augmenting path খুঁজে প্রবাহ পাঠানো হয়
- সবচেয়ে বড় bottleneck-এর একটি পথ বেছে নেওয়া হয়
- BFS দিয়ে level graph বানিয়ে তাতে DFS দিয়ে একটি blocking flow পাঠানো হয়
- প্রতিটি vertex-এ বাড়তি প্রবাহ জমিয়ে তা উচ্চতা অনুযায়ী নিচে ঠেলে দেওয়া হয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Flow = প্রতিটি edge-এ 0 ≤ f ≤ c, আর s, t ছাড়া প্রতিটি vertex-এ ঢোকা = বের হওয়া; max flow = s থেকে t-তে সবচেয়ে বড় মোট প্রবাহ।
- Residual graph: এগোনো edge-এ c − f, ফেরত edge-এ f; s থেকে t-এর যেকোনো পথ একটি augmenting path, আর ফেরত edge আগের ভুল বাতিল করে।
- Ford-Fulkerson O(E·|f*|); Edmonds-Karp (BFS) O(VE²); Dinic (level graph + blocking flow) O(V²E), bipartite matching-এর নেটওয়ার্কে O(E√V).
- Max flow = min cut; শেষ residual-এ s থেকে পৌঁছানো vertex-ই min cut-এর s-পাশ। Integer capacity → integer max flow.
- প্রয়োগ: bipartite matching (সব capacity 1; Kuhn O(VE), Hopcroft-Karp O(E√V)), König (matching = vertex cover), Menger (edge-disjoint path), project selection (লাভ − min cut).
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৮ · Tree-এর কৌশল — LCA, binary lifting আর diameterপরবর্তী পর্বমডিউল ৬-এর শেষ: tree-তে দ্রুত প্রশ্নের উত্তর।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ৪০ · Breadth-first search (BFS)আগের ভিত্তিEdmonds-Karp আর Dinic-এর level graph — দুটোর ভেতরেই BFS.
- C++ প্রোগ্রামিং · vector, deque আর listপাশের ট্র্যাক
vector,dequeআর adjacency list বানানোর তৈরি কাঠামো। - Python প্রোগ্রামিং · অ্যালগরিদম ও কনটেস্টে Pythonপাশের ট্র্যাক
deque, recursion-এর সীমা আর কনটেস্টে Python-এর কৌশল।