বেলম্যান-ফোর্ড অ্যালগরিদম#
নেগেটিভ ওয়েট এজ সহ সিঙ্গেল সোর্স শর্টেস্ট পাথ
ধরা যাক, আমাদের কাছে $n$ টি ভার্টেক্স ও $m$ টি এজ বিশিষ্ট একটি ওয়েটেড ডিরেক্টেড গ্রাফ $G$ এবং একটি নির্দিষ্ট ভার্টেক্স $v$ দেওয়া আছে। আপনি ভার্টেক্স $v$ থেকে অন্য প্রতিটি ভার্টেক্সে শর্টেস্ট পাথের দৈর্ঘ্য বের করতে চান।
ডায়াক্সট্রা অ্যালগরিদমের বিপরীতে, এই অ্যালগরিদম নেগেটিভ ওয়েট এজ বিশিষ্ট গ্রাফেও প্রয়োগ করা যায়। তবে, গ্রাফে যদি নেগেটিভ সাইকেল থাকে, তাহলে স্পষ্টতই কিছু ভার্টেক্সে শর্টেস্ট পাথ নাও থাকতে পারে (কারণ শর্টেস্ট পাথের ওয়েট মাইনাস ইনফিনিটির সমান হওয়া উচিত); তবে এই অ্যালগরিদম পরিবর্তন করে নেগেটিভ ওয়েট সাইকেলের উপস্থিতি সংকেত দেওয়া, এমনকি সেই সাইকেলটি বের করাও সম্ভব।
এই অ্যালগরিদমটি দুইজন আমেরিকান বিজ্ঞানীর নামে পরিচিত: রিচার্ড বেলম্যান এবং লেস্টার ফোর্ড। ফোর্ড মূলত ১৯৫৬ সালে অন্য একটি গাণিতিক সমস্যা অধ্যয়নের সময় এই অ্যালগরিদম আবিষ্কার করেন, যেটি শেষ পর্যন্ত গ্রাফে শর্টেস্ট পাথ খুঁজে বের করার একটি সাব-প্রবলেমে পরিণত হয়েছিল, এবং ফোর্ড এই সমস্যা সমাধানের জন্য অ্যালগরিদমের একটি রূপরেখা দেন। বেলম্যান ১৯৫৮ সালে শর্টেস্ট পাথ খোঁজার সমস্যা নিয়ে বিশেষভাবে একটি নিবন্ধ প্রকাশ করেন, এবং এই নিবন্ধে তিনি অ্যালগরিদমটি স্পষ্টভাবে সেই রূপে উপস্থাপন করেন যে রূপে আমরা এখন এটি জানি।
অ্যালগরিদমের বর্ণনা#
আমরা ধরে নিই যে গ্রাফে কোনো নেগেটিভ ওয়েট সাইকেল নেই। নেগেটিভ ওয়েট সাইকেলের উপস্থিতির ক্ষেত্রে নিচে একটি আলাদা অংশে আলোচনা করা হবে।
আমরা একটি দূরত্বের অ্যারে $d[0 \ldots n-1]$ তৈরি করব, যেটি অ্যালগরিদম সম্পাদনের পর সমস্যার উত্তর ধারণ করবে। শুরুতে আমরা এটি এভাবে পূরণ করি: $d[v] = 0$, এবং বাকি সব উপাদান $d[ ]$ অসীম $\infty$ এর সমান।
অ্যালগরিদমটি কয়েকটি ফেজে গঠিত। প্রতিটি ফেজে গ্রাফের সমস্ত এজ স্ক্যান করা হয়, এবং অ্যালগরিদম $c$ ওয়েট বিশিষ্ট প্রতিটি এজ $(a,b)$ বরাবর রিলাক্সেশন করার চেষ্টা করে। এজ বরাবর রিলাক্সেশন হলো $d[a] + c$ মান ব্যবহার করে $d[b]$ এর মান উন্নত করার একটি প্রচেষ্টা। আসলে, এর মানে হলো আমরা এজ $(a,b)$ এবং ভার্টেক্স $a$ এর বর্তমান উত্তর ব্যবহার করে এই ভার্টেক্সের উত্তর উন্নত করার চেষ্টা করছি।
এটি দাবি করা হয় যে অ্যালগরিদমের $n-1$ টি ফেজ গ্রাফের সমস্ত শর্টেস্ট পাথের দৈর্ঘ্য সঠিকভাবে গণনা করতে যথেষ্ট (আবারও, আমরা বিশ্বাস করি যে নেগেটিভ ওয়েট সাইকেল নেই)। অগম্য ভার্টেক্সগুলোর জন্য দূরত্ব $d[ ]$ অসীম $\infty$ এর সমান থাকবে।
ইমপ্লিমেন্টেশন#
অন্যান্য অনেক গ্রাফ অ্যালগরিদমের বিপরীতে, বেলম্যান-ফোর্ড অ্যালগরিদমের জন্য গ্রাফটি সমস্ত এজের একটি একক তালিকা হিসেবে উপস্থাপন করা বেশি সুবিধাজনক ($n$ টি ভার্টেক্সের প্রতিটি থেকে এজের $n$ টি তালিকার পরিবর্তে)। আমরা এজ উপস্থাপনের জন্য $\rm edge$ স্ট্রাকচার দিয়ে ইমপ্লিমেন্টেশন শুরু করি। অ্যালগরিদমের ইনপুট হলো সংখ্যা $n$, $m$, এজের তালিকা $e$ এবং শুরুর ভার্টেক্স $v$। সমস্ত ভার্টেক্স $0$ থেকে $n - 1$ পর্যন্ত নম্বরযুক্ত।
সরলতম ইমপ্লিমেন্টেশন#
ধ্রুবক $\rm INF$ “অসীম” সংখ্যাটি নির্দেশ করে — এটি এমনভাবে নির্বাচন করতে হবে যাতে এটি সমস্ত সম্ভাব্য পাথ দৈর্ঘ্যের চেয়ে বেশি হয়।
struct Edge {
int a, b, cost;
};
int n, m, v;
vector<Edge> edges;
const int INF = 1000000000;
void solve()
{
vector<int> d(n, INF);
d[v] = 0;
for (int i = 0; i < n - 1; ++i)
for (Edge e : edges)
if (d[e.a] < INF)
d[e.b] = min(d[e.b], d[e.a] + e.cost);
// display d, for example, on the screen
}if (d[e.a] < INF) চেকটি শুধুমাত্র তখনই প্রয়োজন যখন গ্রাফে নেগেটিভ ওয়েট এজ থাকে: এরকম যাচাই না থাকলে এমন ভার্টেক্স থেকে রিলাক্সেশন হবে যেখানে এখনো পাথ খুঁজে পাওয়া যায়নি, এবং $\infty - 1$, $\infty - 2$ ইত্যাদি ধরনের ভুল দূরত্ব তৈরি হবে।
একটি উন্নত ইমপ্লিমেন্টেশন#
এই অ্যালগরিদমটি কিছুটা দ্রুত করা সম্ভব: প্রায়শই আমরা কয়েকটি ফেজেই উত্তর পেয়ে যাই এবং বাকি ফেজগুলোতে কোনো কার্যকর কাজ হয় না, শুধু সমস্ত এজ পরিদর্শনে সময় নষ্ট হয়। তাই, বর্তমান ফেজে কিছু পরিবর্তন হয়েছে কিনা তা জানাতে একটি ফ্ল্যাগ রাখা যাক, এবং যদি কোনো ফেজে কিছুই পরিবর্তন না হয়, অ্যালগরিদম বন্ধ করা যেতে পারে। (এই অপটিমাইজেশন অ্যাসিম্পটোটিক আচরণ উন্নত করে না, অর্থাৎ কিছু গ্রাফে এখনো সব $n-1$ টি ফেজ প্রয়োজন হবে, কিন্তু “গড়ে” অর্থাৎ র্যান্ডম গ্রাফে অ্যালগরিদমের আচরণ উল্লেখযোগ্যভাবে দ্রুত করে।)
এই অপটিমাইজেশনের সাথে, সাধারণত অ্যালগরিদমের ফেজ সংখ্যা ম্যানুয়ালি $n-1$ এ সীমাবদ্ধ করার প্রয়োজন হয় না — অ্যালগরিদম প্রয়োজনীয় সংখ্যক ফেজের পর নিজেই থেমে যাবে।
void solve()
{
vector<int> d(n, INF);
d[v] = 0;
for (;;) {
bool any = false;
for (Edge e : edges)
if (d[e.a] < INF)
if (d[e.b] > d[e.a] + e.cost) {
d[e.b] = d[e.a] + e.cost;
any = true;
}
if (!any)
break;
}
// display d, for example, on the screen
}পাথ পুনরুদ্ধার#
এখন দেখা যাক কীভাবে অ্যালগরিদমটি পরিবর্তন করা যায় যাতে এটি শুধু শর্টেস্ট পাথের দৈর্ঘ্যই খুঁজে না, বরং শর্টেস্ট পাথগুলো পুনর্গঠনও করতে পারে।
এর জন্য, আরেকটি অ্যারে $p[0 \ldots n-1]$ তৈরি করি, যেখানে প্রতিটি ভার্টেক্সের জন্য তার “পূর্বসূরি” সংরক্ষণ করি, অর্থাৎ সেই ভার্টেক্সে যাওয়ার শর্টেস্ট পাথের শেষ থেকে দ্বিতীয় ভার্টেক্স। আসলে, যেকোনো ভার্টেক্স $a$ এর শর্টেস্ট পাথ হলো কোনো ভার্টেক্স $p[a]$ পর্যন্ত একটি শর্টেস্ট পাথ, যার সাথে আমরা পাথের শেষে $a$ যোগ করেছি।
লক্ষ্য করুন যে অ্যালগরিদম একই যুক্তিতে কাজ করে: এটি ধরে নেয় যে একটি ভার্টেক্সের শর্টেস্ট দূরত্ব ইতিমধ্যে গণনা করা হয়েছে, এবং সেই ভার্টেক্স থেকে অন্যান্য ভার্টেক্সের শর্টেস্ট দূরত্ব উন্নত করার চেষ্টা করে। অতএব, উন্নতির সময় আমাদের কেবল $p[ ]$ মনে রাখতে হবে, অর্থাৎ কোন ভার্টেক্স থেকে এই উন্নতি ঘটেছে।
নিচে একটি নির্দিষ্ট নোড $t$ পর্যন্ত শর্টেস্ট পাথ পুনরুদ্ধারসহ বেলম্যান-ফোর্ডের একটি ইমপ্লিমেন্টেশন দেওয়া হলো:
void solve()
{
vector<int> d(n, INF);
d[v] = 0;
vector<int> p(n, -1);
for (;;) {
bool any = false;
for (Edge e : edges)
if (d[e.a] < INF)
if (d[e.b] > d[e.a] + e.cost) {
d[e.b] = d[e.a] + e.cost;
p[e.b] = e.a;
any = true;
}
if (!any)
break;
}
if (d[t] == INF)
cout << "No path from " << v << " to " << t << ".";
else {
vector<int> path;
for (int cur = t; cur != -1; cur = p[cur])
path.push_back(cur);
reverse(path.begin(), path.end());
cout << "Path from " << v << " to " << t << ": ";
for (int u : path)
cout << u << ' ';
}
}এখানে ভার্টেক্স $t$ থেকে শুরু করে, আমরা পূর্বসূরিদের মধ্য দিয়ে যাই যতক্ষণ না পূর্বসূরিবিহীন শুরুর ভার্টেক্সে পৌঁছাই, এবং পাথের সমস্ত ভার্টেক্স $\rm path$ তালিকায় সংরক্ষণ করি। এই তালিকাটি $v$ থেকে $t$ পর্যন্ত একটি শর্টেস্ট পাথ, কিন্তু বিপরীত ক্রমে, তাই আমরা $\rm path$ এর উপর $\rm reverse()$ ফাংশন কল করি এবং তারপর পাথটি আউটপুট করি।
অ্যালগরিদমের প্রমাণ#
প্রথমত, লক্ষ্য করুন যে সমস্ত অগম্য ভার্টেক্স $u$ এর জন্য অ্যালগরিদম সঠিকভাবে কাজ করবে, লেবেল $d[u]$ অসীমের সমান থাকবে (কারণ বেলম্যান-ফোর্ড অ্যালগরিদম শুরুর ভার্টেক্স $v$ থেকে সমস্ত গম্য ভার্টেক্সে কোনো না কোনো পাথ খুঁজে পাবে, এবং অবশিষ্ট সমস্ত ভার্টেক্সের জন্য রিলাক্সেশন কখনো ঘটবে না)।
এখন নিম্নলিখিত দাবিটি প্রমাণ করি: $i$-তম ফেজ সম্পাদনের পর, বেলম্যান-ফোর্ড অ্যালগরিদম সঠিকভাবে সেই সমস্ত শর্টেস্ট পাথ খুঁজে পায় যাদের এজ সংখ্যা $i$ এর বেশি নয়।
অন্যভাবে বললে, যেকোনো ভার্টেক্স $a$ এর জন্য ধরা যাক $k$ হলো তার শর্টেস্ট পাথের এজ সংখ্যা (যদি একাধিক এমন পাথ থাকে, যেকোনো একটি নেওয়া যায়)। এই দাবি অনুসারে, অ্যালগরিদম নিশ্চিত করে যে $k$-তম ফেজের পর ভার্টেক্স $a$ এর শর্টেস্ট পাথ পাওয়া যাবে।
প্রমাণ: একটি ইচ্ছামতো ভার্টেক্স $a$ বিবেচনা করুন যেখানে শুরুর ভার্টেক্স $v$ থেকে একটি পাথ আছে, এবং এটির একটি শর্টেস্ট পাথ $(p_0=v, p_1, \ldots, p_k=a)$ বিবেচনা করুন। প্রথম ফেজের আগে, ভার্টেক্স $p_0 = v$ এর শর্টেস্ট পাথ সঠিকভাবে পাওয়া গিয়েছিল। প্রথম ফেজে, এজ $(p_0,p_1)$ অ্যালগরিদম দ্বারা পরীক্ষিত হয়েছে, এবং তাই প্রথম ফেজের পর ভার্টেক্স $p_1$ এর দূরত্ব সঠিকভাবে গণনা করা হয়েছে। এই বিবৃতি $k$ বার পুনরাবৃত্তি করলে, আমরা দেখতে পাই যে $k$-তম ফেজের পর ভার্টেক্স $p_k = a$ এর দূরত্ব সঠিকভাবে গণনা করা হয়, যা আমরা প্রমাণ করতে চেয়েছিলাম।
সর্বশেষ যে বিষয়টি লক্ষ্য করতে হবে তা হলো যেকোনো শর্টেস্ট পাথে $n - 1$ এর বেশি এজ থাকতে পারে না। অতএব, অ্যালগরিদম $(n-1)$-তম ফেজ পর্যন্ত চলাই যথেষ্ট। এর পরে, নিশ্চিত করা যায় যে কোনো রিলাক্সেশন কোনো ভার্টেক্সের দূরত্ব উন্নত করবে না।
নেগেটিভ সাইকেলের ক্ষেত্রে#
উপরে সর্বত্র আমরা ধরে নিয়েছিলাম যে গ্রাফে কোনো নেগেটিভ সাইকেল নেই (সুনির্দিষ্টভাবে, আমরা শুরুর ভার্টেক্স $v$ থেকে গম্য নেগেটিভ সাইকেলে আগ্রহী, এবং অগম্য সাইকেলের জন্য উপরের অ্যালগরিদমে কিছু পরিবর্তন হয় না)। নেগেটিভ সাইকেলের উপস্থিতিতে, এই সাইকেলের সমস্ত ভার্টেক্সের দূরত্ব, সেইসাথে এই সাইকেল থেকে গম্য ভার্টেক্সগুলোর দূরত্ব সংজ্ঞায়িত নয় — এগুলো মাইনাস ইনফিনিটি $(- \infty)$ এর সমান হওয়া উচিত, যা সম্পর্কিত আরও জটিলতা তৈরি করে।
এটি দেখা সহজ যে বেলম্যান-ফোর্ড অ্যালগরিদম এই সাইকেলের সমস্ত ভার্টেক্স এবং এটি থেকে গম্য ভার্টেক্সগুলোর মধ্যে অবিরামভাবে রিলাক্সেশন করতে পারে। অতএব, আপনি যদি ফেজ সংখ্যা $n - 1$ এ সীমাবদ্ধ না করেন, অ্যালগরিদম অনির্দিষ্টকালের জন্য চলবে, ক্রমাগত এই ভার্টেক্সগুলো থেকে দূরত্ব উন্নত করতে থাকবে।
তাই আমরা সোর্স ভার্টেক্স $v$ থেকে গম্য নেগেটিভ ওয়েট সাইকেলের উপস্থিতির মানদণ্ড পাই: $(n-1)$-তম ফেজের পর, যদি আমরা আরও এক ফেজ অ্যালগরিদম চালাই, এবং এটি কমপক্ষে আরও একটি রিলাক্সেশন করে, তাহলে গ্রাফে $v$ থেকে গম্য একটি নেগেটিভ ওয়েট সাইকেল আছে; অন্যথায়, এমন কোনো সাইকেল নেই।
তদুপরি, যদি এমন সাইকেল পাওয়া যায়, বেলম্যান-ফোর্ড অ্যালগরিদম পরিবর্তন করা যেতে পারে যাতে এটি সেই সাইকেলটি ভার্টেক্সের একটি ক্রম হিসেবে পুনরুদ্ধার করে। এর জন্য, $n$-তম ফেজে যে শেষ ভার্টেক্স $x$ এ রিলাক্সেশন হয়েছে তা মনে রাখাই যথেষ্ট। এই ভার্টেক্সটি হয় নেগেটিভ ওয়েট সাইকেলে থাকবে, অথবা এটি থেকে গম্য হবে। নেগেটিভ সাইকেলে নিশ্চিতভাবে থাকা ভার্টেক্সগুলো পেতে, ভার্টেক্স $x$ থেকে শুরু করে পূর্বসূরিদের মধ্য দিয়ে $n$ বার যান। এভাবে, আমরা ভার্টেক্স $y$ তে পৌঁছাব, যেটি নিশ্চিতভাবে নেগেটিভ সাইকেলে আছে। আমাদের এই ভার্টেক্স থেকে পূর্বসূরিদের মধ্য দিয়ে যেতে হবে যতক্ষণ না আমরা একই ভার্টেক্স $y$ তে ফিরে আসি (এবং এটি ঘটবে, কারণ নেগেটিভ ওয়েট সাইকেলে রিলাক্সেশন বৃত্তাকারভাবে ঘটে)।
ইমপ্লিমেন্টেশন:#
void solve()
{
vector<int> d(n, INF);
d[v] = 0;
vector<int> p(n, -1);
int x;
for (int i = 0; i < n; ++i) {
x = -1;
for (Edge e : edges)
if (d[e.a] < INF)
if (d[e.b] > d[e.a] + e.cost) {
d[e.b] = max(-INF, d[e.a] + e.cost);
p[e.b] = e.a;
x = e.b;
}
}
if (x == -1)
cout << "No negative cycle from " << v;
else {
int y = x;
for (int i = 0; i < n; ++i)
y = p[y];
vector<int> path;
for (int cur = y;; cur = p[cur]) {
path.push_back(cur);
if (cur == y && path.size() > 1)
break;
}
reverse(path.begin(), path.end());
cout << "Negative cycle: ";
for (int u : path)
cout << u << ' ';
}
}নেগেটিভ সাইকেলের উপস্থিতির কারণে, অ্যালগরিদমের $n$ ইটারেশনে দূরত্ব নেগেটিভ রেঞ্জে অনেক দূর চলে যেতে পারে ($-n m W$ এর অর্ডারে নেগেটিভ সংখ্যায়, যেখানে $W$ হলো গ্রাফের যেকোনো ওয়েটের সর্বাধিক পরম মান)। তাই কোডে, আমরা ইন্টিজার ওভারফ্লোর বিরুদ্ধে অতিরিক্ত ব্যবস্থা নিম্নরূপ গ্রহণ করেছি:
d[e.b] = max(-INF, d[e.a] + e.cost);উপরের ইমপ্লিমেন্টেশন কোনো শুরুর ভার্টেক্স $v$ থেকে গম্য নেগেটিভ সাইকেল খোঁজে; তবে, অ্যালগরিদমটি গ্রাফে যেকোনো নেগেটিভ সাইকেল খুঁজতে পরিবর্তন করা যায়। এর জন্য আমাদের সমস্ত দূরত্ব $d[i]$ শূন্যে রাখতে হবে, অসীমে নয় — যেন আমরা একই সাথে সমস্ত ভার্টেক্স থেকে শর্টেস্ট পাথ খুঁজছি; নেগেটিভ সাইকেল সনাক্তকরণের বৈধতা এতে প্রভাবিত হয় না।
এই বিষয়ে আরও জানতে আলাদা নিবন্ধ দেখুন, গ্রাফে নেগেটিভ সাইকেল খোঁজা।
শর্টেস্ট পাথ ফাস্টার অ্যালগরিদম (SPFA)#
SPFA হলো বেলম্যান-ফোর্ড অ্যালগরিদমের একটি উন্নতি যা এই সুবিধা নেয় যে সব রিলাক্সেশন প্রচেষ্টা সফল হয় না। মূল ধারণাটি হলো একটি কিউ তৈরি করা যেটিতে শুধু সেই ভার্টেক্সগুলো থাকবে যেগুলো রিলাক্স হয়েছে কিন্তু এখনো তাদের প্রতিবেশীদের আরও রিলাক্স করতে পারে। এবং যখনই আপনি কোনো প্রতিবেশীকে রিলাক্স করতে পারেন, আপনার তাকে কিউতে রাখা উচিত। এই অ্যালগরিদম বেলম্যান-ফোর্ডের মতো নেগেটিভ সাইকেল সনাক্ত করতেও ব্যবহার করা যেতে পারে।
এই অ্যালগরিদমের ওয়ার্স্ট কেস বেলম্যান-ফোর্ডের $O(n m)$ এর সমান, কিন্তু বাস্তবে এটি অনেক দ্রুত কাজ করে এবং কিছু লোক দাবি করেন যে এটি গড়ে $O(m)$ তে কাজ করে। তবে সতর্ক থাকুন, কারণ এই অ্যালগরিদম ডিটারমিনিস্টিক এবং এমন কাউন্টার-এক্সাম্পল তৈরি করা সহজ যা অ্যালগরিদমকে $O(n m)$ তে চালায়।
ইমপ্লিমেন্টেশনে কিছু সতর্কতা অবলম্বন করতে হয়, যেমন নেগেটিভ সাইকেল থাকলে অ্যালগরিদম চিরকালের জন্য চলতে থাকে। এটি এড়াতে, একটি কাউন্টার তৈরি করা সম্ভব যেটি সংরক্ষণ করে একটি ভার্টেক্স কতবার রিলাক্স হয়েছে এবং কোনো ভার্টেক্স $n$-তম বারের জন্য রিলাক্স হওয়ার সাথে সাথে অ্যালগরিদম বন্ধ করা যায়। এছাড়াও লক্ষ্য করুন, কোনো ভার্টেক্স ইতিমধ্যে কিউতে থাকলে তাকে আবার কিউতে রাখার কোনো কারণ নেই।
const int INF = 1000000000;
vector<vector<pair<int, int>>> adj;
bool spfa(int s, vector<int>& d) {
int n = adj.size();
d.assign(n, INF);
vector<int> cnt(n, 0);
vector<bool> inqueue(n, false);
queue<int> q;
d[s] = 0;
q.push(s);
inqueue[s] = true;
while (!q.empty()) {
int v = q.front();
q.pop();
inqueue[v] = false;
for (auto edge : adj[v]) {
int to = edge.first;
int len = edge.second;
if (d[v] + len < d[to]) {
d[to] = d[v] + len;
if (!inqueue[to]) {
q.push(to);
inqueue[to] = true;
cnt[to]++;
if (cnt[to] > n)
return false; // negative cycle
}
}
}
}
return true;
}অনলাইন জাজে সম্পর্কিত সমস্যা#
বেলম্যান-ফোর্ড অ্যালগরিদম ব্যবহার করে সমাধান করা যায় এমন সমস্যাগুলোর একটি তালিকা:
- E-OLYMP #1453 “Ford-Bellman” [difficulty: low]
- UVA #423 “MPI Maelstrom” [difficulty: low]
- UVA #534 “Frogger” [difficulty: medium]
- UVA #10099 “The Tourist Guide” [difficulty: medium]
- UVA #515 “King” [difficulty: medium]
- UVA 12519 - The Farnsworth Parabox
গ্রাফে নেগেটিভ সাইকেল খোঁজা নিবন্ধের সমস্যা তালিকাও দেখুন।