এই পর্বে যা শিখবেন
- Weighted graph, দূরত্ব δ(s, v), optimal substructure আর triangle inequality-র নির্ভুল সংজ্ঞা ও প্রমাণ
- Negative cycle থাকলে shortest path কেন অর্থহীন, আর না থাকলে কেন simple পথই যথেষ্ট
- Dijkstra-র algorithm-এর সম্পূর্ণ সঠিকতার প্রমাণ, আর ঋণাত্মক edge-এ তার ব্যর্থতার পাল্টা-উদাহরণ
- Bellman–Ford-এর n − 1 round-এর প্রমাণ, negative cycle ধরা, DAG-এর এক-পাস পদ্ধতি আর Floyd–Warshall-এর recurrence
- Traveling salesperson problem-এর গণনা, কঠিনতা, MST-ভিত্তিক 2-approximation আর nearest neighbour-এর দুর্বলতা
১ · Weighted graph আর দূরত্ব — সমস্যাটি নির্ভুলভাবে
বাজারঘরের ডেলিভারি রাইডার মিরপুরের গুদাম থেকে যাত্রাবাড়ীতে একটি পার্সেল নিয়ে যাবেন। মানচিত্রে সরাসরি রাস্তা আছে, কিন্তু সেটি সবসময় দ্রুততম নয়; ধানমন্ডি আর মতিঝিল ঘুরে গেলে হয়তো কম সময় লাগে। “কোন পথে সবচেয়ে কম সময়?” প্রশ্নটি সহজ শোনায়, কিন্তু উত্তর যে সত্যিই সবচেয়ে কম, তা প্রমাণ করতে হলে আগে প্রশ্নটি নির্ভুলভাবে লিখতে হয়।
ঢাকার রিকশাচালক অভিজ্ঞতা থেকে জানেন কোন গলি দিয়ে গেলে দ্রুত হয়। কিন্তু নতুন রাইডারকে নিয়ম শেখাতে হলে “অভিজ্ঞতা” যথেষ্ট নয়; দরকার এমন একটি পদ্ধতি, যা প্রতিবার সঠিক উত্তর দেয়, আর তার পক্ষে একটি যুক্তি। এই পর্বের কাজ ঠিক সেই যুক্তি দেওয়া। Algorithm-এর কোড আর জটিলতা DSA কোর্সে আছে; এখানে প্রশ্ন: কেন কাজ করে, আর কখন কাজ করে না।
পর্ব ৫৭ “গ্রাফ — সংজ্ঞা, degree আর handshake theorem”-এ graph-এর সংজ্ঞা এসেছে। এখানে আমরা সাধারণত directed graph নিয়ে কথা বলব; undirected graph-এর প্রতিটি edge {u, v}-কে দুই দিকের দুটি তীর (u, v) আর (v, u) ধরলেই সব ফল সেখানেও খাটে (তবে ঋণাত্মক weight-এর undirected edge নিজেই একটি ঋণাত্মক cycle u → v → u হয়ে যায়, তাই §২-এর ফলগুলো undirected graph-এ শুধু অঋণাত্মক weight-এর জন্য প্রয়োগ করুন)।
একটি weighted graph হলো G = (V, E) আর একটি ফাংশন w : E → ℝ; সংখ্যা w(u, v)-কে বলে edge (u, v)-এর weight (সময়, দূরত্ব, খরচ)। একটি walk P = (v0, v1, …, vk)-এর weight
w(P) = w(v0, v1) + w(v1, v2) + ⋯ + w(vk−1, vk)
আর s থেকে v-এর দূরত্ব δ(s, v) = s থেকে v-এর সব walk-এর weight-এর infimum (সবচেয়ে বড় নিম্নসীমা); কোনো walk না থাকলে δ(s, v) = ∞, আর walk-এর weight যত খুশি ছোট করা গেলে δ(s, v) = −∞. যে walk-এর weight ঠিক δ(s, v), সেটিই একটি shortest path.
দুটি সূক্ষ্ম কথা। প্রথমত, সংজ্ঞায় walk নেওয়া হয়েছে (vertex পুনরাবৃত্ত হতে পারে), কারণ algorithm কোন পথ খুঁজবে তা আগে থেকে জানে না; §২-এ দেখব কখন walk-কে simple path (কোনো vertex দুবার নয়) দিয়ে বদলানো যায়। দ্বিতীয়ত, edge না থাকলে weight ∞ ধরা সুবিধাজনক। এক উৎস s থেকে সব vertex-এর দূরত্ব বের করার কাজকে বলে single-source shortest path, আর সব জোড়ার জন্য all-pairs shortest path.
বাজারঘরের ৬টি হাব: রাস্তা আর মিনিটে সময় (কাল্পনিক সংখ্যা)
প্রতিটি রেখা দুই দিকের রাস্তা, সংখ্যাটি মিনিটে সময়। সবুজ মোটা রেখা মিরপুর (M) থেকে shortest path tree; ধূসর রেখা কোনো সবচেয়ে ছোট পথে লাগে না। দূরত্ব: M 0, D 9, U 12, G 13, T 19, J 24.
ছবিতে M থেকে J-এর সরাসরি কোনো রাস্তা নেই; M → D → J পথে 9 + 20 = 29 মিনিট, কিন্তু M → D → G → T → J পথে 9 + 4 + 6 + 5 = 24 মিনিট। বেশি হাব ঘুরলেও সময় কম। পরের উপপাদ্যটি সব shortest path algorithm-এর ভিত্তি।
ধরুন P = (v0, …, vk) হলো v0 থেকে vk-এর একটি shortest path, আর 0 ≤ i ≤ j ≤ k. তাহলে P-এর অংশ Pij = (vi, …, vj) হলো vi থেকে vj-এর একটি shortest path. এই ধর্মকে বলে optimal substructure.
- P-কে তিন টুকরো লিখি: P = P0i · Pij · Pjk, তাই w(P) = w(P0i) + w(Pij) + w(Pjk). — walk-এর weight-এর সংজ্ঞা
- উল্টোটা ধরি: vi থেকে vj-এ এমন একটি walk Q আছে, যার w(Q) < w(Pij). — contradiction-এর অনুমান
- Pij-এর জায়গায় Q বসাই (“কেটে জোড়া”): P′ = P0i · Q · Pjk একটি v0 থেকে vk-এর walk. — দুটি walk-এর শেষ আর শুরু মেলে
- w(P′) = w(P0i) + w(Q) + w(Pjk) < w(P), অথচ P সবচেয়ে ছোট। বিরোধ, তাই Pij সবচেয়ে ছোট। ∎ — ধাপ ১, ২
প্রমাণে কোথাও weight অঋণাত্মক লাগেনি, আর P′ walk হলেই চলে। শেষের কথাটি গুরুত্বপূর্ণ: “সবচেয়ে লম্বা simple path”-এ একই যুক্তি চালালে P′ simple না-ও থাকতে পারে, আর সেখানে উপপাদ্যটি সত্যিই ভাঙে (অনুশীলন ৪)।
যেকোনো edge (u, v)-এর জন্য δ(s, v) ≤ δ(s, u) + w(u, v).
প্রমাণ। s থেকে u-এর যেকোনো walk-এর শেষে edge (u, v) জুড়ে দিলে s থেকে v-এর একটি walk পাওয়া যায়, যার weight সেই walk-এর weight + w(u, v). তাই δ(s, v) ≤ w(walk) + w(u, v) প্রতিটি walk-এর জন্য; ডান দিকের infimum নিলে ফলটি আসে (∞ আর −∞-এর স্বাভাবিক নিয়মে)। ∎ এই triangle inequality-ই পরের অংশের relaxation-এর পেছনের যুক্তি: “u হয়ে v-তে গেলে যা লাগে, v-এর দূরত্ব তার চেয়ে বেশি হতে পারে না”।
২ · ঋণাত্মক edge আর negative cycle — কখন প্রশ্নটিই অর্থহীন
সময় বা দূরত্ব ঋণাত্মক হয় না, কিন্তু খরচ হতে পারে। ধরুন বাজারঘরের রাইডার একটি রুটে ফেরার পথে আরেকটি পার্সেল তুলে বোনাস পান, তাহলে সেই রুটের “নিট খরচ” ঋণাত্মক। এখানে একটি নতুন বিপদ আসে।
একটি cycle C = (v0, v1, …, vk = v0) হলো negative cycle, যদি w(C) < 0. যে edge-এর weight ঋণাত্মক, তাকে বলে negative-weight edge.
(ক) s থেকে v-এর কোনো walk-এর ওপর এমন একটি negative cycle থাকলে, যা s থেকে পৌঁছানো যায় আর যেখান থেকে v-তে পৌঁছানো যায়, δ(s, v) = −∞.
(খ) s থেকে পৌঁছানো যায় এমন কোনো negative cycle না থাকলে, s থেকে পৌঁছানো যায় এমন প্রতিটি v-এর জন্য একটি simple shortest path আছে, যার edge-সংখ্যা ≤ n − 1 (n = |V|)।
(ক)-এর প্রমাণ।
- ধরুন s থেকে C-এর একটি vertex x-এ walk A, আর x থেকে v-তে walk B. C-কে m বার ঘুরলে walk A · Cm · B পাওয়া যায়। — walk-এ vertex পুনরাবৃত্ত হতে পারে
- তার weight w(A) + m · w(C) + w(B). w(C) < 0, তাই m বাড়ালে এটি যত খুশি ছোট হয়। — রৈখিক রাশি, ঋণাত্মক ঢাল
- অতএব কোনো সসীম নিম্নসীমা নেই, δ(s, v) = −∞, আর “সবচেয়ে ছোট পথ” বলে কিছু নেই। ∎
(খ)-এর প্রমাণ।
- যেকোনো s থেকে v-এর walk-এ কোনো vertex দুবার থাকলে, দুই উপস্থিতির মাঝের অংশটি একটি cycle. — pigeonhole-এর সরল রূপ
- সেই cycle s থেকে পৌঁছানো যায়, তাই অনুমান অনুযায়ী তার weight ≥ 0; cycle-টি কেটে দিলে walk-এর weight বাড়ে না, আর edge-সংখ্যা কমে। — অনুমান
- বারবার কাটলে একটি simple path পাওয়া যায়, যার weight মূল walk-এর ≤. — edge-সংখ্যা কমতে কমতে থামে
- তাই δ(s, v) = সব simple path-এর weight-এর minimum. Simple path সসীম সংখ্যক, তাই minimum অর্জিত হয়; আর simple path-এ সর্বোচ্চ n টি vertex, অর্থাৎ ≤ n − 1টি edge. ∎ — সসীম সেটের minimum থাকে
এই উপপাদ্যের (খ) অংশই Bellman–Ford-এর “n − 1 round” আর আমাদের brute force-এর (সব simple path দেখা) বৈধতার ভিত্তি। তবে brute force দ্রুত অসম্ভব হয়ে পড়ে: n = 10 vertex-এর complete graph-এ দুটি নির্দিষ্ট vertex-এর মধ্যে simple path-এর সংখ্যা ∑k=08 8!/(8 − k)! = 109601 (মাঝের k টি vertex ক্রমসহ বাছাই, পর্ব ৩৩-এর r-permutation দিয়ে গোনা)।
Simple path-এ সীমিত করলেই সমস্যা মেটে না: negative cycle থাকলে কেউ বলতে পারেন “শুধু simple path-এর মধ্যে সবচেয়ে ছোটটি চাই”। এটি সংজ্ঞায়িত, কিন্তু তখন সমস্যাটি সবচেয়ে লম্বা simple path-এর মতোই কঠিন (সব weight-কে −1 দিলে “সবচেয়ে ছোট simple path” মানে সবচেয়ে বেশি edge-এর path, আর তা থেকে Hamilton path প্রশ্নের উত্তর মেলে)। পর্ব ৬০ “Euler আর Hamilton path — Königsberg থেকে NP-কঠিনতা” দেখায় সেটি NP-complete.
৩ · Relaxation আর Dijkstra-র algorithm
সব shortest path algorithm একটি অনুমান-সারণি d রাখে: d(v) হলো এ পর্যন্ত জানা s থেকে v-এর সবচেয়ে ভালো পথের weight-এর মান। শুরুতে d(s) = 0 আর বাকি সব ∞. তারপর একটিই মৌলিক অপারেশন বারবার চলে।
Edge (u, v)-কে relax করা মানে: যদি d(u) + w(u, v) < d(v), তাহলে d(v) ← d(u) + w(u, v) আর v-এর আগের vertex (parent) হিসেবে u লিখে রাখা।
শুরু থেকে যেকোনো ক্রমে যতবারই relaxation চলুক, প্রতিটি মুহূর্তে প্রতিটি v-এর জন্য d(v) ≥ δ(s, v); আর d(v) < ∞ হলে d(v) হলো s থেকে v-এর কোনো একটি walk-এর weight.
প্রমাণ (relaxation-এর সংখ্যার ওপর পর্ব ১০-এর induction):
- ভিত্তি: শুরুতে d(s) = 0 হলো খালি walk (s)-এর weight, আর বাকি সব ∞; দুটি দাবিই সত্য (খালি walk-এর weight 0, তাই δ(s, s) ≤ 0 সত্য)। — শুরুর মান
- ধাপ: একটি relaxation শুধু d(v) বদলায়, নতুন মান d(u) + w(u, v). Induction hypothesis অনুযায়ী d(u) হলো কোনো walk W-এর weight, তাই নতুন d(v) হলো walk W · (u, v)-এর weight. — hypothesis
- যেকোনো walk-এর weight ≥ δ(s, v) (δ হলো infimum), তাই নতুন d(v) ≥ δ(s, v). ∎ — δ-এর সংজ্ঞা
অর্থাৎ d কখনো “খুব ছোট” হয় না; algorithm-এর কাজ শুধু তাকে নামিয়ে ঠিক δ পর্যন্ত আনা। একবার d(v) = δ(s, v) হলে আর কমতে পারে না, কারণ lemma অনুযায়ী তার নিচে যাওয়া অসম্ভব।
Dijkstra-র algorithm (সব weight ≥ 0 ধরে): একটি settled সেট S রাখুন, শুরুতে খালি। প্রতি ধাপে S-এর বাইরের যে vertex u-এর d(u) সবচেয়ে ছোট, তাকে S-এ তুলুন (“settle”), তারপর u থেকে বেরোনো প্রতিটি edge relax করুন। সবাই settle হলে থামুন। সবচেয়ে ছোট d খুঁজতে বাস্তবে priority queue (heap) ব্যবহার হয়; কোড আর O((n + m) log n) বিশ্লেষণ DSA কোর্সের পর্ব ৪৩-এ আছে। নিচের stepper ছবির graph-এ M থেকে algorithm-টি হাতে চালায়।
1d(M) = 0, বাকি ∞2settle M, relax3settle D, relax4settle U, relax5settle G, relax6settle T, relax7settle J: শেষ
| S | |
| বাকি d | |
| বদল |
- শুরু। d(M) = 0, বাকি পাঁচটি হাবের d = ∞. কেউ এখনো settled নয়।
- M settle. S-এর বাইরে সবচেয়ে ছোট d হলো M-এর 0. M-এর তিনটি রাস্তা relax: d(D) = 9, d(U) = 12, d(G) = 15.
- D settle. বাকিদের মধ্যে সবচেয়ে ছোট D = 9. Relax: G পায় 9 + 4 = 13 < 15, তাই 15 থেকে 13; T পায় 9 + 11 = 20; J পায় 9 + 20 = 29.
- U settle. সবচেয়ে ছোট এখন U = 12. U থেকে G: 12 + 8 = 20, যা 13-এর চেয়ে বড়, তাই কিছু বদলায় না।
- G settle. G = 13. Relax: T পায় 13 + 6 = 19 < 20, অর্থাৎ D হয়ে সরাসরি T-তে যাওয়ার 20 মিনিটের পথ হেরে গেল; J পায় 13 + 14 = 27 < 29.
- T settle. T = 19. Relax: J পায় 19 + 5 = 24 < 27.
- J settle, শেষ। J = 24, পথ M → D → G → T → J. প্রতিটি vertex-এর শেষ parent (D-এর M, U-এর M, G-এর D, T-এর G, J-এর T) মিলে ছবির সবুজ shortest path tree.
এবার একই হিসাব Python-এ, settled সেটের ক্রমসহ; সঙ্গে প্রতিটি গন্তব্যের জন্য সব simple path তালিকা করে brute force-এ সবচেয়ে ছোটটি মিলিয়ে দেখা হয়েছে (উপপাদ্য ২.২(খ) অনুযায়ী simple path দেখাই যথেষ্ট)। প্রথমবার চালালে ব্রাউজারে Python (Pyodide) নামতে কয়েক সেকেন্ড লাগে; পরের ব্লকগুলো দ্রুত চলে।
import heapq
E = {('M','U'):12, ('M','D'):9, ('M','G'):15, ('U','G'):8, ('D','G'):4,
('D','T'):11, ('G','T'):6, ('G','J'):14, ('T','J'):5, ('D','J'):20}
G = {} # undirected adjacency dict
for (a, b), w in E.items():
G.setdefault(a, {})[b] = w
G.setdefault(b, {})[a] = w
def dijkstra(G, s):
d, done, pq = {s: 0}, [], [(0, s)]
while pq:
du, u = heapq.heappop(pq)
if u in done:
continue # stale queue entry
done.append(u)
print(f"settle {u} d={du:2} S={''.join(done)}")
for v, w in G[u].items():
if v not in done and du + w < d.get(v, float('inf')):
d[v] = du + w # relaxation
heapq.heappush(pq, (d[v], v))
return d
def all_simple_paths(G, u, t, seen):
if u == t:
yield 0, [t]
return
for v, w in G[u].items():
if v not in seen:
for L, p in all_simple_paths(G, v, t, seen | {v}):
yield L + w, [u] + p
d = dijkstra(G, 'M')
for t in 'UDGTJ':
paths = sorted(all_simple_paths(G, 'M', t, {'M'}))
best = paths[0]
print(t, d[t], best[0], '-'.join(best[1]), f"({len(paths)} simple paths)")আসল আউটপুট (ব্রাউজারের Python):
settle M d= 0 S=M
settle D d= 9 S=MD
settle U d=12 S=MDU
settle G d=13 S=MDUG
settle T d=19 S=MDUGT
settle J d=24 S=MDUGTJ
U 12 12 M-U (7 simple paths)
D 9 9 M-D (11 simple paths)
G 13 13 M-D-G (7 simple paths)
T 19 19 M-D-G-T (15 simple paths)
J 24 24 M-D-G-T-J (15 simple paths)
প্রথম ছয় লাইন stepper-এর settle-ক্রম হুবহু মেলায়, আর লক্ষ করুন settle-এর সময় d-এর মান কখনো কমে না: 0, 9, 12, 13, 19, 24 (অনুশীলন ১-এ এটি প্রমাণ করবেন)। শেষ পাঁচ লাইনে প্রতিটি গন্তব্যের জন্য Dijkstra-র দূরত্ব, brute force-এর সবচেয়ে ছোট weight, সেই পথ আর মোট simple path-এর সংখ্যা। দুই কলাম সবখানে মিলেছে। এখানে heap-এ একই vertex একাধিকবার ঢুকতে পারে (“lazy” রূপ), তাই পুরোনো entry বাদ দিতে if u in done লাগে; গাণিতিকভাবে এটি §৩-এর algorithm-ই।
৪ · Dijkstra-র সঠিকতার প্রমাণ
Dijkstra-র algorithm greedy: প্রতি ধাপে সেই মুহূর্তের সবচেয়ে ছোট d-টি নিয়ে আর কখনো ফিরে দেখে না। Greedy algorithm প্রায়ই ভুল করে, তাই প্রমাণ জরুরি। প্রমাণের কেন্দ্রে একটি loop invariant (পর্ব ১৩): S-এর প্রতিটি vertex-এর d চূড়ান্ত, অর্থাৎ d(v) = δ(s, v), এই দাবি প্রতিটি ধাপে সত্য থাকে। নিচের ছবিটি মূল যুক্তি দেখায়।
মূল lemma-র ছবি: shortest path S থেকে বেরোয় প্রথম কোথায়
নীল অঞ্চল settled সেট S. u হলো S-এর বাইরের সবচেয়ে ছোট d-এর vertex. s থেকে u-এর যেকোনো পথ কোথাও প্রথমবার S ছাড়ে: edge x → y. পথের বাকি অংশ y ⇝ u-এর weight ≥ 0.
G একটি directed (বা undirected) weighted graph, যার প্রতিটি edge weight ≥ 0, আর s একটি উৎস। তাহলে Dijkstra-র algorithm যখন কোনো vertex u-কে S-এ তোলে, তখন d(u) = δ(s, u). ফলে শেষে প্রতিটি vertex-এর জন্য d(v) = δ(s, v).
প্রমাণ (S-এর আকারের ওপর induction; invariant হলো S-এর প্রতিটি v-এর জন্য d(v) = δ(s, v) সমতা)।
- ভিত্তি: প্রথম settle হয় s, কারণ d(s) = 0 আর বাকি সব ∞. δ(s, s) = 0, কারণ খালি পথের weight 0 আর কোনো cycle-এর weight ঋণাত্মক নয়। — weight ≥ 0
- ধাপ: ধরুন invariant এখন সত্য, আর পরের বাছাই u (S-এর বাইরে সবচেয়ে ছোট d). u-তে পৌঁছানো না গেলে δ(s, u) = ∞ = d(u) (lemma ৩.২), কাজ শেষ। তাই ধরি পৌঁছানো যায়, আর P একটি s থেকে u-এর shortest path. — induction hypothesis
- উল্টোটা ধরি: d(u) > δ(s, u). P শুরু হয় S-এর ভেতরে (s ∈ S) আর শেষ হয় বাইরে (u ∉ S), তাই P-এ প্রথম একটি edge x → y আছে, যেখানে x ∈ S আর y ∉ S (y = u-ও হতে পারে)। — পথ কোথাও প্রথমবার সীমানা পার হয়
- x যখন settle হয়েছিল, তখন d(x) = δ(s, x) (hypothesis), আর edge (x, y) relax হয়েছিল; তাই d(y) ≤ δ(s, x) + w(x, y). — relaxation-এর পর d কখনো বাড়ে না
- P-এর s থেকে y পর্যন্ত অংশটি shortest (উপপাদ্য ১.২), আর তার weight δ(s, x) + w(x, y) = δ(s, y). তাই d(y) ≤ δ(s, y), আর lemma ৩.২ অনুযায়ী d(y) = δ(s, y). — optimal substructure
- P-এর y থেকে u পর্যন্ত বাকি অংশের weight ≥ 0, তাই δ(s, y) ≤ δ(s, u). এখানেই অঋণাত্মক weight লাগে। — সব weight ≥ 0
- মিলিয়ে: d(y) = δ(s, y) ≤ δ(s, u) < d(u). কিন্তু y ∉ S, আর u-কে বাছা হয়েছিল কারণ S-এর বাইরে d(u) সবচেয়ে ছোট, তাই d(u) ≤ d(y). বিরোধ। অতএব d(u) = δ(s, u), invariant রক্ষা পেল। ∎ — বাছাইয়ের নিয়ম
প্রমাণের প্রতিটি ধাপে কোন তথ্য লেগেছে, তা ডান পাশে লেখা। ধাপ ৬ ছাড়া অন্য কোথাও weight ≥ 0 লাগেনি (আর ধাপ ১-এ শুধু δ(s, s) = 0 নিশ্চিত করতে)। নিচের stepper ঠিক G-কে settle করার আগের মুহূর্তে যুক্তিটি সংখ্যা দিয়ে দেখায়।
1S = {M, D, U}, পরে G2ধরি P: M ⇝ G3প্রথম বাইরের y4d(y) ≤ δ(M,x) + w(x,y)5w(P) ≥ d(y) ≥ d(G)6d(G) = δ(M, G) = 13
| পথ P | |
| x → y | |
| হিসাব |
- মুহূর্তটি। M, D, U settled; বাইরে d(G) = 13, d(T) = 20, d(J) = 29. Algorithm G-কে বাছবে। প্রশ্ন: পরে কি G-তে 13-এর চেয়ে ছোট কোনো পথ বেরোতে পারে?
- একটি প্রতিদ্বন্দ্বী পথ। উদাহরণ হিসেবে P = M → D → T → G নিন, weight 9 + 11 + 6 = 26. প্রমাণ যেকোনো P-এর জন্য একই যুক্তি দেয়; এটি শুধু সংখ্যা দেখানোর জন্য।
- প্রথম সীমানা পার। P শুরু হয় S-এর ভেতরে M-এ। প্রথম যে vertex S-এর বাইরে, তা T; তার আগেরটি x = D ∈ S. সব পথকেই কোথাও না কোথাও এভাবে S ছাড়তে হয়।
- Relaxation-এর স্মৃতি। D settle হওয়ার সময় (D, T) relax হয়েছিল, তাই d(T) ≤ d(D) + w(D, T) = 9 + 11 = 20; আসলে d(T) = 20.
- বাকি অংশ অঋণাত্মক। P-এর T থেকে G অংশের weight 6 ≥ 0, তাই w(P) ≥ d(T) = 20. আর G-কে বাছা হয়েছে বলে d(G) = 13 ≤ d(T). তাই w(P) ≥ 13. ঋণাত্মক weight থাকলে এই ধাপটিই ভাঙত।
- সিদ্ধান্ত। যেকোনো পথ S ছাড়ার মুহূর্তেই অন্তত 13 খরচ করে ফেলে, আর পরে খরচ শুধু বাড়ে। তাই δ(M, G) = 13, d(G) চূড়ান্ত। Python ব্লকের brute force-ও G-এর জন্য 13 দিয়েছে।
৫ · ঋণাত্মক edge-এ Dijkstra কেন ভাঙে
প্রমাণের ধাপ ৬ বাদ দিলে কী হয়, একটি ছোট directed graph-এ দেখা যাক: s → a (2), s → b (6), b → a (−5), a → t (3). কোনো cycle নেই, তাই negative cycle-ও নেই, আর উপপাদ্য ২.২ অনুযায়ী সব দূরত্ব সংজ্ঞায়িত: δ(s, a) = 6 − 5 = 1 (পথ s → b → a), δ(s, t) = 4.
Dijkstra চালালে: s settle, d(a) = 2, d(b) = 6. সবচেয়ে ছোট a = 2, তাই a settle হয়, আর d(t) = 5. তারপর t (5), শেষে b (6). b থেকে a-তে 6 − 5 = 1 পাওয়া যায়, কিন্তু a ইতিমধ্যে settled, তাই algorithm তাকে আর ছোঁয় না। ফল d(a) = 2 আর d(t) = 5, দুটিই ভুল। প্রমাণের ভাষায়: a settle হওয়ার সময় shortest path s → b → a প্রথমবার S ছাড়ে s → b edge-এ, d(b) = 6 ≥ d(a) = 2; কিন্তু বাকি অংশ b → a-এর weight −5 < 0, তাই ধাপ ৬ ভাঙে।
একটি জনপ্রিয় “সমাধান”: সবচেয়ে ছোট weight যত, সব edge-এ ততটা (বা তার বেশি) যোগ করে weight অঋণাত্মক বানিয়ে Dijkstra চালানো। নিচের ব্লক দুটি দাবিই পরীক্ষা করে, আর Bellman–Ford-এর ফলও দেখায় (§৬-এ তার প্রমাণ)। শেষে একটি negative cycle-এর graph-এ Bellman–Ford-এর সতর্কসংকেত।
INF = float('inf')
def dijkstra(G, s): # textbook: a settled vertex is final
d, S = {v: INF for v in G}, set()
d[s] = 0
while len(S) < len(G):
u = min((v for v in G if v not in S), key=d.get)
S.add(u)
for v, w in G[u].items():
if v not in S:
d[v] = min(d[v], d[u] + w)
return d
def bellman_ford(G, s):
d = {v: INF for v in G}
d[s] = 0
for _ in range(len(G) - 1): # n - 1 rounds
for u in G:
for v, w in G[u].items():
d[v] = min(d[v], d[u] + w)
bad = any(d[u] + w < d[v] for u in G for v, w in G[u].items())
return d, bad # bad = negative cycle reachable
G = {'s': {'a': 2, 'b': 6}, 'a': {'t': 3}, 'b': {'a': -5}, 't': {}}
print("Dijkstra ", dijkstra(G, 's'))
print("Bellman-Ford ", bellman_ford(G, 's'))
c = 5 # "fix": add 5 to every edge
H = {u: {v: w + c for v, w in G[u].items()} for u in G}
print("shifted+5 ", dijkstra(H, 's'))
K = {'s': {'a': 3}, 'a': {'b': 2}, 'b': {'c': -4}, 'c': {'a': 1}}
print("cycle a-b-c weight", 2 - 4 + 1, bellman_ford(K, 's'))আসল আউটপুট (ব্রাউজারের Python):
Dijkstra {'s': 0, 'a': 2, 'b': 6, 't': 5}
Bellman-Ford ({'s': 0, 'a': 1, 'b': 6, 't': 4}, False)
shifted+5 {'s': 0, 'a': 7, 'b': 11, 't': 15}
cycle a-b-c weight -1 ({'s': 0, 'a': 0, 'b': 3, 'c': -1}, True)
প্রথম লাইনে Dijkstra-র ভুল উত্তর a = 2, t = 5; দ্বিতীয় লাইনে Bellman–Ford-এর সঠিক a = 1, t = 4, আর False মানে কোনো negative cycle নেই। তৃতীয় লাইনটি “+5” কৌশলের ফল: নতুন graph-এ a-এর দূরত্ব 7 = 2 + 5, অর্থাৎ বেছে নেওয়া পথটি একটি edge-এর s → a, যার আসল weight 2; অথচ সঠিক পথ s → b → a, যার আসল weight মাত্র ১। কারণ c যোগ করলে k-edge-এর পথের weight বাড়ে k · c, তাই বেশি edge-এর পথ বেশি শাস্তি পায়: s → b → a এখন 11 + 0 = 11, যা 7-এর চেয়ে বড়। কৌশলটি সবচেয়ে ছোট পথ বদলে দেয়, তাই ভুল। শেষ লাইনে cycle a → b → c → a-এর weight −1; ৩ round পরেও একটি edge relax করা যায়, তাই True.
ঠিক কোন “যোগ” কাজ করে: প্রতিটি vertex-কে একটি সংখ্যা h(v) দিয়ে w′(u, v) = w(u, v) + h(u) − h(v) নিলে যেকোনো s থেকে t-এর পথের নতুন weight = পুরোনো weight + h(s) − h(t); যোগফল telescoping হয়ে মাঝের সব h কেটে যায় (পর্ব ১৭)। তাই সব পথ একই পরিমাণ সরে, আর সবচেয়ে ছোট পথ একই থাকে। h হিসেবে Bellman–Ford-এর দূরত্ব নিলে triangle inequality থেকে w′ ≥ 0 হয়। এটিই Johnson-এর algorithm-এর মূল ধারণা (প্র ০৩)।
৬ · Bellman–Ford — n − 1 round আর negative cycle ধরা
Bellman–Ford কোনো greedy বাছাই করে না: প্রতিটি round-এ সব edge একবার করে relax করে, আর মোট n − 1 round চালায়। বিস্ময়ের ব্যাপার, edge-এর ক্রম যেমনই হোক, এটুকুই যথেষ্ট।
(ক) i round-এর পর প্রতিটি v-এর জন্য d(v) ≤ s থেকে v-এর যেকোনো walk-এর weight, যার edge-সংখ্যা ≤ i.
(খ) s থেকে পৌঁছানো যায় এমন negative cycle না থাকলে n − 1 round-এর পর প্রতিটি v-এর জন্য d(v) = δ(s, v).
(গ) n − 1 round-এর পরও কোনো edge (u, v)-এ d(u) + w(u, v) < d(v) হলে, আর কেবল তখনই, s থেকে পৌঁছানো যায় এমন একটি negative cycle আছে।
(ক)-এর প্রমাণ (i-এর ওপর induction):
- ভিত্তি i = 0: ০-edge-এর walk শুধু (s), weight 0, আর d(s) = 0. অন্য v-তে ০-edge-এর walk নেই, দাবি শূন্যভাবে সত্য। — শুরুর মান
- ধাপ: W একটি s থেকে v-এর walk, ≤ i + 1টি edge, শেষ edge (u, v). W-এর বাকি অংশ W′ হলো s থেকে u-এর ≤ i edge-এর walk, তাই round i-এর পর d(u) ≤ w(W′). — hypothesis
- Round i + 1-এ edge (u, v) relax হয়, আর ততক্ষণে d(u) শুধু কমেছে; তাই তার পর d(v) ≤ d(u) + w(u, v) ≤ w(W′) + w(u, v) = w(W). ∎ — relaxation, d কখনো বাড়ে না
(খ) উপপাদ্য ২.২(খ) অনুযায়ী প্রতিটি পৌঁছানো যায় এমন v-এর একটি shortest path ≤ n − 1 edge-এর; (ক) অনুযায়ী n − 1 round-এর পর d(v) ≤ তার weight = δ(s, v), আর lemma ৩.২ অনুযায়ী d(v) ≥ δ(s, v). পৌঁছানো না গেলে d(v) = ∞ = δ(s, v). ∎
(গ)-এর প্রমাণ।
- Negative cycle না থাকলে (খ) অনুযায়ী d = δ, আর উপপাদ্য ১.৩ অনুযায়ী প্রতিটি edge-এ δ(s, v) ≤ δ(s, u) + w(u, v); তাই কোনো edge relax করা যায় না। — triangle inequality
- উল্টো দিক: ধরুন C = (v0, …, vk = v0) একটি পৌঁছানো যায় এমন negative cycle, অথচ C-এর প্রতিটি edge-এ d(vi) ≤ d(vi−1) + w(vi−1, vi). (C পৌঁছানো যায় বলে n − 1 round-এর পর C-এর সব d সসীম।) — contradiction-এর অনুমান
- i = 1 থেকে k পর্যন্ত যোগ করলে বাঁ দিকে ∑d(vi), ডান দিকে ∑d(vi−1) + w(C). Cycle বলে দুটি ∑ একই সংখ্যাগুলোর যোগ, তাই কেটে যায়: 0 ≤ w(C). — যোগফলে প্রতিটি vertex একবার করে
- কিন্তু w(C) < 0. বিরোধ, তাই C-এর অন্তত একটি edge এখনো relax করা যায়। ∎
নিচের stepper §৫-এর শেষ graph-এ (s → a 3, a → b 2, b → c −4, c → a 1; n = 4) round গুলো দেখায়। প্রতিটি round-এ edge-এর ক্রম s→a, a→b, b→c, c→a.
1d(s) = 0, বাকি ∞2round 13round 24round 3 = n − 15যাচাই: আরেকবার দেখা
| d(a) | |
| d(b) | |
| d(c) |
- শুরু। d(s) = 0, বাকি তিনটি ∞. Cycle a → b → c → a-এর weight 2 − 4 + 1 = −1.
- Round 1. s→a: d(a) = 3. a→b: d(b) = 5. b→c: d(c) = 1. c→a: 1 + 1 = 2 < 3, তাই d(a) = 2. একই round-এ cycle-টি একবার ঘোরা হয়ে গেল।
- Round 2. a→b: 2 + 2 = 4. b→c: 0. c→a: 1. প্রতিটি মান ঠিক 1 কমল, অর্থাৎ cycle-এর weight পরিমাণ।
- Round 3 (n − 1 = 3). d(b) = 3, d(c) = −1, d(a) = 0. Negative cycle না থাকলে এখানেই সব মান চূড়ান্ত হতো (উপপাদ্য ৬.১(খ))।
- যাচাই round. a→b: d(a) + 2 = 2 < d(b) = 3. একটি edge এখনো relax করা যায়, তাই উপপাদ্য ৬.১(গ) অনুযায়ী s থেকে পৌঁছানো যায় এমন negative cycle আছে; a, b, c-এর দূরত্ব −∞. Python ব্লকের শেষ লাইনের
Trueএই কথাই বলে।
খরচ: n − 1 round, প্রতিটিতে m টি edge, মোট O(nm); Dijkstra-র চেয়ে ধীর, কিন্তু ঋণাত্মক weight সামলায়। Network routing-এর distance vector protocol একই relaxation-এর ওপর দাঁড়িয়ে (Computer Networks কোর্সের পর্ব ২৩), আর বিস্তারিত কোড DSA কোর্সের পর্ব ৪৪-এ।
৭ · DAG-এ এক পাস, আর Floyd–Warshall-এর recurrence
DAG-এ shortest path. Graph-এ কোনো directed cycle না থাকলে (একটি DAG) negative cycle-এর প্রশ্নই ওঠে না, ঋণাত্মক weight থাকলেও। পর্ব ৫৬-এ দেখেছেন প্রতিটি DAG-এর একটি topological ক্রম আছে: v1, …, vn, যাতে প্রতিটি edge সামনের দিকে যায়। এই ক্রমে vertex নিয়ে প্রতিটির বেরোনো edge একবার relax করলেই হয়।
G একটি DAG (weight যেকোনো বাস্তব সংখ্যা), আর v1, …, vn একটি topological ক্রম, v1 = s. ক্রম অনুযায়ী প্রতিটি vertex-এর বেরোনো edge relax করলে, vj-কে প্রক্রিয়া করার মুহূর্তে d(vj) = δ(s, vj).
- j-এর ওপর strong induction (পর্ব ১১)। j = 1: d(s) = 0 = δ(s, s), কারণ DAG-এ কোনো cycle নেই। — ভিত্তি
- ধরুন vj পৌঁছানো যায়, আর P একটি shortest path, শেষ edge (vi, vj). Topological ক্রমে i < j, তাই vi আগে প্রক্রিয়া হয়েছে, তখন d(vi) = δ(s, vi) (hypothesis), আর (vi, vj) relax হয়েছে। — সব edge সামনের দিকে
- তাই d(vj) ≤ δ(s, vi) + w(vi, vj) = δ(s, vj) (উপপাদ্য ১.২); lemma ৩.২ উল্টো অসমতা দেয়। ∎
একটি হাতের হিসাব। ক্রম A, B, C, D, E; edge A→B 3, A→C 6, B→C −2, B→D 5, C→D 1, C→E 4, D→E −3. A প্রক্রিয়ার পর B = 3, C = 6. B-এর পর C = min(6, 3 − 2) = 1, D = 8. C-এর পর D = min(8, 1 + 1) = 2, E = 5. D-এর পর E = min(5, 2 − 3) = −1. ফল A 0, B 3, C 1, D 2, E −1, খরচ O(n + m) (scratch-এ মেলানো)। Weight-এর চিহ্ন উল্টে দিলে একই পদ্ধতি DAG-এ সবচেয়ে লম্বা পথ দেয়, যা প্রকল্পের critical path হিসাবের ভিত্তি (DSA কোর্সের পর্ব ৪২)।
সব জোড়ার জন্য: Floyd–Warshall. পর্ব ৫৪-এ Warshall-এর algorithm “শুধু 1..k-কে মাঝের স্টপ ধরে i থেকে j-তে যাওয়া যায়?” প্রশ্নের উত্তর k বাড়িয়ে বাড়িয়ে বের করেছে। ∨ আর ∧-এর জায়গায় min আর + বসালেই Floyd–Warshall.
Vertex 1, …, n, কোনো negative cycle নেই। D(k)[i][j] দিয়ে বোঝাই i থেকে j-এর সেসব পথের সবচেয়ে ছোট weight, যাদের প্রতিটি interior vertex (মাঝের vertex) {1, …, k}-এ। তাহলে D(0)[i][j] = w(i, j) (i = j হলে 0, edge না থাকলে ∞), আর
D(k)[i][j] = min( D(k−1)[i][j], D(k−1)[i][k] + D(k−1)[k][j] )
প্রমাণ। উপপাদ্য ২.২(খ) অনুযায়ী একটি simple shortest path P নেওয়া যায় (মাঝের vertex {1..k}-এ সীমিত রেখেও একই কাটার যুক্তি চলে)। দুটি ক্ষেত্র। k যদি P-এর মাঝের vertex না হয়, তাহলে P-এর সব মাঝের vertex {1..k − 1}-এ, তাই w(P) = D(k−1)[i][j]. k যদি মাঝে থাকে, simple বলে ঠিক একবার; P-কে k-তে কাটলে দুটি টুকরো, যাদের মাঝের vertex {1..k − 1}-এ, আর optimal substructure অনুযায়ী দুটিই সবচেয়ে ছোট, তাই w(P) = D(k−1)[i][k] + D(k−1)[k][j]. দুটি রাশিই কোনো বৈধ walk-এর weight, তাই সবচেয়ে ছোট মানটি সঠিক। ∎ Negative cycle থাকলে কোনো D[i][i] শেষে ঋণাত্মক হয়, এভাবেই তা ধরা যায় (অনুশীলন ৩)। n = |V| বার k বাড়ে, প্রতিবার n2টি ঘর: মোট Θ(n3), Warshall-এর মতোই।
নিচের ব্লক দুটি উপপাদ্য এলোমেলো ছোট graph-এ brute force-এর বিরুদ্ধে পরীক্ষা করে। DAG-এ edge শুধু ছোট থেকে বড় নম্বরে, তাই 0..n−1 নিজেই topological ক্রম, আর weight −5 থেকে 9. Floyd–Warshall-এর জন্য পেছনের দিকের edge যোগ করা হয়েছে, কিন্তু weight ≥ 25, যাতে কোনো negative cycle না হয় (n ≤ 6-এ সামনের দিকের অংশ সর্বোচ্চ −25 কমাতে পারে)।
import random
from itertools import combinations, permutations, product
INF = float('inf')
random.seed(61)
def brute(n, W, i, j): # min over every simple path i -> j
best = W[i][j] if i != j else 0
others = [k for k in range(n) if k not in (i, j)]
for r in range(1, len(others) + 1):
for mid in permutations(others, r):
best = min(best, sum(W[a][b] for a, b in zip((i, *mid), (*mid, j))))
return best
def dag_sp(n, W, s): # 0..n-1 is already a topological order
d = [INF] * n; d[s] = 0
for u, v in combinations(range(s, n), 2): # edges u -> v, u in order
d[v] = min(d[v], d[u] + W[u][v])
return d
def floyd_warshall(n, W):
D = [[0 if i == j else W[i][j] for j in range(n)] for i in range(n)]
for k in range(n): # now k may be an interior vertex
for i, j in product(range(n), repeat=2):
D[i][j] = min(D[i][j], D[i][k] + D[k][j])
return D
ok_dag = ok_fw = 0
for trial in range(200):
n = random.randint(3, 6)
W = [[INF] * n for _ in range(n)]
for i, j in permutations(range(n), 2): # i < j: DAG edge, may be negative
if i < j and random.random() < 0.6: W[i][j] = random.randint(-5, 9)
ok_dag += all(dag_sp(n, W, 0)[j] == brute(n, W, 0, j) for j in range(n))
for i, j in permutations(range(n), 2): # back edges >= 25: no negative cycle
if i > j and random.random() < 0.4: W[i][j] = random.randint(25, 30)
D = floyd_warshall(n, W)
ok_fw += all(D[i][j] == brute(n, W, i, j) for i, j in product(range(n), repeat=2))
print("DAG order DP == brute force:", ok_dag, "/ 200")
print("Floyd-Warshall == brute force:", ok_fw, "/ 200")আসল আউটপুট (ব্রাউজারের Python):
DAG order DP == brute force: 200 / 200
Floyd-Warshall == brute force: 200 / 200
২০০টি এলোমেলো graph-এর প্রতিটিতে দুই পদ্ধতির উত্তর সব জোড়ায় brute force-এর সঙ্গে মিলেছে। combinations(range(s, n), 2) জোড়াগুলো অভিধান-ক্রমে দেয়: আগে 0-এর সব edge, তারপর 1-এর, …; তাই u-এর বেরোনো edge relax হওয়ার আগে u-তে ঢোকা সব edge (ছোট নম্বর থেকে) ইতিমধ্যে relax হয়ে গেছে, ঠিক উপপাদ্য ৭.১-এর শর্ত। মনে রাখবেন, ২০০টি পরীক্ষা প্রমাণ নয়; কিন্তু প্রমাণে কোনো শর্ত বাদ পড়লে (যেমন back edge-এর weight ছোট রাখলে negative cycle) এমন পরীক্ষা সাধারণত সঙ্গে সঙ্গে তা ধরে ফেলে।
একটি পরীক্ষার অঙ্ক। Vertex 1..4, edge 1→2 4, 1→4 9, 2→3 −2, 2→4 7, 3→4 1, 3→1 5, 4→2 3. D(0)-এ 2 থেকে 1-এ কোনো edge নেই (∞). k = 3 খোলার পর 2 → 3 → 1 = −2 + 5 = 3 আর 2 → 3 → 4 = −2 + 1 = −1 পাওয়া যায়; k = 4 খোলার পর 3 → 4 → 2 = 1 + 3 = 4 (আগের সেরা 3 → 1 → 2 = 9-এর চেয়ে ছোট)। চূড়ান্ত matrix-এর সারি: 1: (0, 4, 2, 3); 2: (3, 0, −2, −1); 3: (5, 4, 0, 1); 4: (6, 3, 1, 0). কর্ণে সব 0, তাই negative cycle নেই (প্রতিটি D(k) scratch-এ মেলানো)।
৮ · Traveling salesperson — গণনা, কঠিনতা আর 2-approximation
বাজারঘরের একজন রাইডার সকালে গুদাম থেকে বেরিয়ে n − 1টি হাবে একবার করে পার্সেল দিয়ে গুদামে ফিরবেন। কোন ক্রমে গেলে মোট সময় সবচেয়ে কম? এটি shortest path-এর মতো শোনায়, কিন্তু একটি শর্ত সব বদলে দেয়: প্রতিটি হাবে ঠিক একবার।
n ≥ 3টি শহর আর প্রতিটি জোড়ার জন্য দূরত্ব D(i, j) ≥ 0 (complete graph Kn-এর edge weight) দেওয়া আছে। একটি tour হলো সব শহর ঠিক একবার ঘুরে শুরুতে ফেরা, অর্থাৎ একটি Hamilton cycle. Traveling salesperson problem (TSP) হলো সবচেয়ে ছোট মোট দূরত্বের tour খোঁজা। D(i, j) = D(j, i) হলে symmetric TSP; সঙ্গে D(i, k) ≤ D(i, j) + D(j, k) সব i, j, k-এর জন্য সত্য হলে metric TSP.
কয়টি tour? শহরগুলোর একটি ক্রম (permutation) একটি tour দেয়। n!টি ক্রমের মধ্যে কোন শহর থেকে শুরু (n রকম) আর কোন দিকে ঘোরা (২ রকম) tour বদলায় না, আর n ≥ 3-এ এই 2n টি ক্রম সব আলাদা। তাই পর্ব ৩১-এর ভাগের নিয়ম অনুযায়ী symmetric tour-এর সংখ্যা
n! / (2n) = (n − 1)! / 2
n = 5-এ 12, n = 10-এ 181440, n = 20-এ প্রায় 6.08 × 1016. সব tour দেখা (brute force) ছোট n-এই থেমে যায়। Dynamic programming দিয়ে O(n2 2n) সময়ে করা যায় (Held–Karp, ১৯৬২), যা এখনো exponential.
কঠিনতা। “দূরত্ব ≤ L এমন tour আছে কি?” প্রশ্নটি NP-complete: সব weight 1 আর অ-edge-এর weight 2 দিলে, দৈর্ঘ্য n-এর tour আছে কি না মানে graph-এ Hamilton cycle আছে কি না, আর Hamilton cycle প্রশ্নটি Richard Karp-এর ১৯৭২ সালের বিখ্যাত তালিকায় NP-complete হিসেবে আছে বলে জানা যায় (পর্ব ৬০)। তাই সবচেয়ে ছোট tour খোঁজা NP-hard: P = NP প্রমাণ না হলে কোনো polynomial-time algorithm সব ক্ষেত্রে সঠিক উত্তর দেয় না বলে বিশ্বাস করা হয়। DSA কোর্সের পর্ব ৬৪-এ reduction-এর ভাষা আছে।
তাহলে আশা কোথায়? নিখুঁত উত্তর না পেলেও প্রমাণিত-ভাবে “কাছাকাছি” উত্তর। এখানে পর্ব ৬৭ “Spanning tree — DFS/BFS, minimum spanning tree আর Kirchhoff-এর গণনা”-এর minimum spanning tree (MST) কাজে আসে: সব vertex জোড়া লাগানো, cycle-হীন, সবচেয়ে কম মোট weight-এর edge-সেট। MST দ্রুত বের করা যায় (Prim বা Kruskal, DSA কোর্সের পর্ব ৪৫)।
Metric TSP-এ OPT সবচেয়ে ছোট tour-এর দৈর্ঘ্য আর T একটি MST হলে: (ক) w(T) ≤ OPT; (খ) T-এর DFS ক্রমে শহরগুলো ঘুরে (আগে দেখা শহর বাদ দিয়ে) শুরুতে ফিরলে যে tour পাওয়া যায়, তার দৈর্ঘ্য ≤ 2 · w(T) ≤ 2 · OPT.
প্রমাণের রূপরেখা।
- (ক) সবচেয়ে ছোট tour থেকে যেকোনো একটি edge বাদ দিলে একটি Hamilton path থাকে, যা সব শহরকে জোড়ে আর cycle-হীন, অর্থাৎ একটি spanning tree. তার weight ≤ OPT (বাদ দেওয়া edge ≥ 0), আর MST সব spanning tree-র মধ্যে সবচেয়ে হালকা, তাই w(T) ≤ OPT. — D ≥ 0
- (খ) T-এর প্রতিটি edge দুবার নিলে (যাওয়া আর ফেরা, DFS-এর মতো) একটি closed walk পাওয়া যায়, যা প্রতিটি শহর অন্তত একবার ছোঁয়, দৈর্ঘ্য ঠিক 2 · w(T). — প্রতিটি tree edge-এ একবার নামা, একবার ওঠা
- Walk-এ আগে দেখা শহর এলে তা বাদ দিয়ে সরাসরি পরের নতুন শহরে যান; একে বলে shortcut নেওয়া। Triangle inequality অনুযায়ী D(a, c) ≤ D(a, b) + D(b, c), তাই প্রতিটি shortcut দৈর্ঘ্য বাড়ায় না। — metric শর্ত এখানেই
- ফলে tour-এর দৈর্ঘ্য ≤ 2 · w(T) ≤ 2 · OPT. ∎ — ধাপ ১, ২, ৩
এমন algorithm-কে বলে 2-approximation: ফল কখনো সেরা উত্তরের দ্বিগুণের বেশি নয়। Triangle inequality ছাড়া এমন কোনো ধ্রুব নিশ্চয়তা পাওয়া যায় না, যদি না P = NP (Sahni ও Gonzalez, ১৯৭৬, বলে উল্লেখ করা হয়)। বিস্তারিত DSA কোর্সের পর্ব ৬৬-এ।
Nearest neighbour heuristic. সবচেয়ে সহজ greedy নিয়ম: যেখানে আছেন, তার সবচেয়ে কাছের না-দেখা শহরে যান। নিচের ব্লক এলোমেলো সমতল বিন্দুতে (Euclidean দূরত্ব, যা metric) brute force-এর OPT, nearest neighbour-এর ফল আর MST-এর weight তুলনা করে, আর প্রতিটি ক্ষেত্রে w(T) ≤ OPT ≤ 2 · w(T) যাচাই করে (OPT ≤ 2 · w(T) আসে উপপাদ্য ৮.২(খ) থেকে)। শেষে একটি metric নয় এমন ৪-শহরের ফাঁদ।
import random
from itertools import permutations
from math import dist, factorial
random.seed(2026)
def tour_len(D, t):
return sum(D[a][b] for a, b in zip(t, t[1:] + t[:1]))
def brute_tsp(D): # fix city 0, try (n-1)! orders
n = len(D)
return min(tour_len(D, (0,) + p) for p in permutations(range(1, n)))
def nearest_neighbour(D):
t, left = [0], set(range(1, len(D)))
while left:
nxt = min(left, key=lambda v: D[t[-1]][v])
t.append(nxt); left.remove(nxt)
return tour_len(D, tuple(t))
def mst_weight(D): # Prim, O(n^2)
n, inside, w = len(D), {0}, 0
while len(inside) < n:
c, v = min((D[u][v], v) for u in inside for v in range(n) if v not in inside)
inside.add(v); w += c
return w
for n in (5, 7, 9):
worst = 1.0
for _ in range(6):
P = [(random.randint(0, 99), random.randint(0, 99)) for _ in range(n)]
D = [[dist(p, q) for q in P] for p in P]
opt, nn, m = brute_tsp(D), nearest_neighbour(D), mst_weight(D)
assert m <= opt <= 2 * m # MST <= OPT <= 2 MST (metric)
worst = max(worst, nn / opt)
print(f"n={n}: {factorial(n-1)//2} tours, worst NN/OPT = {worst:.3f}")
M = 1000 # non-metric trap for the greedy rule
D = [[0, 1, 2, M], [1, 0, 1, 2], [2, 1, 0, 1], [M, 2, 1, 0]]
print("trap: NN =", nearest_neighbour(D), " OPT =", brute_tsp(D))আসল আউটপুট (ব্রাউজারের Python):
n=5: 12 tours, worst NN/OPT = 1.095
n=7: 360 tours, worst NN/OPT = 1.071
n=9: 20160 tours, worst NN/OPT = 1.194
trap: NN = 1003 OPT = 6
প্রতিটি n-এ ৬টি এলোমেলো instance, আর assert কোথাও ভাঙেনি: MST ≤ OPT ≤ 2 · MST সবখানে সত্য। Tour-এর সংখ্যা (n − 1)!/2 সূত্রের সঙ্গে মেলে (n = 9-এ 20160; কোড আসলে 8! = 40320টি ক্রম দেখে, কারণ প্রতিটি tour দুই দিকে দুবার আসে)। এলোমেলো বিন্দুতে nearest neighbour খারাপ করেনি, সবচেয়ে খারাপ ক্ষেত্রেও OPT-এর প্রায় ১.১৯ গুণ। কিন্তু শেষ লাইনের ফাঁদে লোভী নিয়ম 0 → 1 → 2 → 3 পর্যন্ত প্রতিবার 1 খরচে এগোয়, তারপর ফিরতে 1000 লাগে: 1003, যেখানে সেরা tour 0 → 1 → 3 → 2 → 0-এর দৈর্ঘ্য 6. M বাড়ালে অনুপাত যত খুশি বড় হয়। Metric ক্ষেত্রেও nearest neighbour-এর জন্য এমন instance জানা আছে, যেখানে অনুপাত শহরের সংখ্যার log-এর হারে বাড়ে (Rosenkrantz, Stearns ও Lewis, ১৯৭৭, বলে উল্লেখ করা হয়)। ছোট এলোমেলো পরীক্ষা খারাপ ক্ষেত্র খুঁজে পায় না; সেজন্যই প্রমাণ লাগে।
৯ · এক নজরে তুলনা, আরও গভীরে আর পরীক্ষার প্রশ্ন
| পদ্ধতি | শর্ত | মূল প্রমাণের হাতিয়ার | খরচ |
|---|---|---|---|
| Dijkstra | সব weight ≥ 0 | settled সেটের ওপর induction, সীমানার প্রথম edge | O((n + m) log n) heap-এ |
| Bellman–Ford | যেকোনো weight; negative cycle ধরে | ≤ i edge-এর walk-এর ওপর induction | O(nm) |
| DAG এক পাস | directed cycle নেই | topological ক্রমে strong induction | O(n + m) |
| Floyd–Warshall | negative cycle নেই (থাকলে কর্ণে ধরা পড়ে) | মাঝের vertex {1..k}-এর recurrence | Θ(n3) |
| TSP brute force | যেকোনো D ≥ 0 | সব (n − 1)!/2 tour | exponential |
| MST দ্বিগুণ | metric D | w(T) ≤ OPT আর shortcut | polynomial, ফল ≤ 2 · OPT |
আরও গভীরে: Metric TSP-এ Nicos Christofides ১৯৭৬ সালে MST-এর সঙ্গে বিজোড় degree-র vertex-গুলোর একটি minimum perfect matching (পর্ব ৬৪ “Matching — bipartite graph, Hall-এর উপপাদ্য আর stable matching”) জুড়ে 3/2-approximation দেন, আর প্রায় একই সময়ে Anatoliy Serdyukov স্বাধীনভাবে একই ফল পান বলে জানা যায়। প্রায় ৪৫ বছর পর ২০২০ সালে Karlin, Klein ও Oveis Gharan একটি randomized algorithm-এ 3/2-এর চেয়ে অতি সামান্য ভালো অনুপাত প্রমাণ করেন বলে উল্লেখ করা হয়। Shortest path-এ বাস্তবের রাস্তার মানচিত্রে A* (Hart, Nilsson ও Raphael, ১৯৬৮) আর পূর্ব-গণনাভিত্তিক কৌশল Dijkstra-কে অনেক দ্রুত করে, কিন্তু প্রত্যেকটির সঠিকতার প্রমাণ এই পর্বের lemma-গুলোর ওপরই দাঁড়িয়ে।
পরীক্ষা ও ইন্টারভিউতে: বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন প্রায়ই আসে:
- একটি ৬-vertex graph-এ Dijkstra চালিয়ে প্রতিটি ধাপের সারণি দেখান। §৩-এর stepper-এর মতো: প্রতি সারিতে settle হওয়া vertex আর বাকিদের d.
- Dijkstra-র প্রমাণে weight ≥ 0 কোথায় লাগে? পথের বাকি অংশ y ⇝ u-এর weight ≥ 0, তাই δ(s, y) ≤ δ(s, u).
- Bellman–Ford-এ n − 1 round কেন যথেষ্ট? Negative cycle না থাকলে একটি simple shortest path আছে, যার edge ≤ n − 1টি।
- ৬টি শহরের symmetric TSP-এ কয়টি আলাদা tour? 5!/2 = 60.
- সব edge-এ একই ধ্রুবক যোগ করলে কি shortest path একই থাকে? না; k-edge-এর পথ k · c বাড়ে। ধনাত্মক ধ্রুবক দিয়ে গুণ করলে থাকে।
১০ · AI-এর যুগে — shortest path-এর প্রমাণ আর হিসাব যাচাই
Graph algorithm-এর প্রশ্নে AI সহকারী সাধারণত কোড ভালো লেখে, কিন্তু প্রমাণে আর শর্তে আত্মবিশ্বাসী ভুল করে। কয়েকটি নিয়মিত ভুল আর ধরার উপায়:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| “Negative cycle না থাকলে Dijkstra ঋণাত্মক edge-এও কাজ করে” | §৫-এর ৪-vertex graph-এ চালিয়ে Bellman–Ford-এর সঙ্গে মেলান |
| Dijkstra-র প্রমাণ, যেখানে weight ≥ 0 কোথাও ব্যবহারই হয়নি | প্রমাণে সেই ধাপটি খুঁজুন; না পেলে প্রমাণটি অসম্পূর্ণ (বা ভুল) |
| “সব weight-এ ধ্রুবক যোগ করে Dijkstra চালান” | দুই পথ নিন, ভিন্ন edge-সংখ্যার; যোগের পর ক্রম বদলায় কি না দেখুন |
| Bellman–Ford-এ n round, বা যাচাই round ভুলে যাওয়া | একটি negative cycle-এর ছোট graph-এ ফল দেখুন |
| TSP-এর tour গোনা n! বা (n − 1)! | n = 4-এ সব ক্রম তালিকা করে আলাদা cycle গুনুন (৩টি) |
| “Nearest neighbour সবসময় 2-approximation” বা উৎসহীন সাল আর নাম | non-metric ফাঁদ চালান; উদ্ধৃতি পাঠ্যবইয়ে মেলান |
AI-এর কাছে graph algorithm-এর প্রমাণ চাইলে এমন কাঠামো দিন, যাতে প্রতিটি শর্ত আর ধাপ আলাদা করে যাচাই করা যায়:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
Shortest path-এর সুবিধা হলো ছোট graph-এ সব simple path গুনে ফেলা যায়, তাই যেকোনো দাবি মিনিটের মধ্যে পরীক্ষা করা চলে। ভুল প্রমাণ সাধারণত একটি শর্ত চুপচাপ বাদ দেয়; সেই শর্ত ভাঙা একটি ৩-৪ vertex-এর graph প্রায় সবসময় ভুলটি ধরিয়ে দেয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Dijkstra-র algorithm কীভাবে আবিষ্কৃত হলো, আর ষাট বছরেরও বেশি পরে এটি কেন এখনো পাঠ্যবইয়ের কেন্দ্রে?
ডাচ কম্পিউটার বিজ্ঞানী Edsger W. Dijkstra ১৯৫৬ সালে algorithm-টি ভাবেন, আর ১৯৫৯ সালে Numerische Mathematik পত্রিকায় “A note on two problems in connexion with graphs” নামে তিন পৃষ্ঠার একটি প্রবন্ধে প্রকাশ করেন। একই প্রবন্ধের অন্য সমস্যাটি (প্রবন্ধে আসলে প্রথমটি) ছিল minimum spanning tree. পরবর্তী এক সাক্ষাৎকারে তিনি বলেছেন বলে জানা যায় যে, আমস্টারডামে একটি ক্যাফেতে বসে কাগজ-কলম ছাড়াই প্রায় ২০ মিনিটে তিনি ধারণাটি পান, আর উদ্দেশ্য ছিল নতুন ARMAC কম্পিউটারের ক্ষমতা সাধারণ মানুষকে দেখানো: নেদারল্যান্ডসের কয়েকটি শহরের মধ্যে সবচেয়ে ছোট পথ।
মূল প্রবন্ধে priority queue ছিল না; সবচেয়ে ছোট d খোঁজা হতো সরাসরি তালিকা দেখে, ফলে O(n2) সময় (§৫-এর Python-এর dijkstra ঠিক এই রূপ)। পরে binary heap দিয়ে O((n + m) log n), আর Michael Fredman ও Robert Tarjan-এর Fibonacci heap (১৯৮০-এর দশকের মাঝামাঝি) দিয়ে O(m + n log n) পাওয়া যায় বলে উল্লেখ করা হয়। লক্ষ করুন, এই সব উন্নতিতে গণিত একটুও বদলায়নি: §৪-এর প্রমাণ শুধু বলে “S-এর বাইরের সবচেয়ে ছোট d বাছুন”; কীভাবে বাছবেন, তা ডেটা স্ট্রাকচারের প্রশ্ন।
এর দীর্ঘ জনপ্রিয়তার কারণ তিনটি। এক, এটি একটি সুন্দর সাধারণ নীতির উদাহরণ: greedy বাছাই সঠিক হয় যখন একটি invariant দেখানো যায় যে বাছাই করা জিনিস আর কখনো উন্নত হবে না। দুই, বাস্তব প্রয়োগে এটি ভিত্তি: ইন্টারনেটের link state routing protocol (যেমন OSPF) প্রতিটি router-এ Dijkstra চালায় (Computer Networks কোর্সের পর্ব ২৪), আর মানচিত্রের দ্রুত পদ্ধতিগুলো এর ওপর গড়া। তিন, এর ব্যর্থতাও শিক্ষণীয়: ঋণাত্মক edge-এ একটি মাত্র শর্ত ভাঙলে পুরো যুক্তি ভেঙে যায়, যা প্রমাণে শর্তের ভূমিকা বোঝার আদর্শ উদাহরণ।
Dijkstra নিজে প্রোগ্রামের সঠিকতা প্রমাণ নিয়ে আজীবন সোচ্চার ছিলেন; “testing দিয়ে bug-এর উপস্থিতি দেখানো যায়, অনুপস্থিতি নয়” কথাটি তাঁর নামে বহুল উদ্ধৃত। এই পর্বের Python পরীক্ষা আর প্রমাণের যুগল ঠিক সেই দর্শন মানে।
মূল উপলব্ধি: Dijkstra-র algorithm-এর গণিত ১৯৫৯ থেকে অপরিবর্তিত; গতি বেড়েছে ডেটা স্ট্রাকচারে, কিন্তু সঠিকতা আজও সেই এক invariant আর weight ≥ 0 শর্তের ওপর দাঁড়িয়ে।
প্র ০২Bellman–Ford নামে দুজন, অথচ আরও কয়েকজন একই ধারণা পেয়েছিলেন। Network routing-এ এই algorithm কী সমস্যা তৈরি করে?
“সব edge বারবার relax করা” ধারণাটি ১৯৫০-এর দশকে কয়েকজন প্রায় একসঙ্গে পান বলে ইতিহাসবিদেরা লেখেন: Alfonso Shimbel (১৯৫৫), Lester Ford Jr. (১৯৫৬, RAND কর্পোরেশনের একটি প্রতিবেদনে), Edward F. Moore (১৯৫৭ সালের একটি সম্মেলনে, প্রকাশ ১৯৫৯) আর Richard Bellman (১৯৫৮)। তাই কিছু বইয়ে নামটি Bellman–Ford–Moore লেখা হয়। Bellman-এর দৃষ্টিভঙ্গি ছিল dynamic programming: di(v) = “≤ i edge-এ v-তে পৌঁছানোর সবচেয়ে কম খরচ” আর di+1(v) = min(di(v), minu di(u) + w(u, v)), যা ঠিক উপপাদ্য ৬.১(ক)-এর বক্তব্য।
এই algorithm-এর একটি বড় সুবিধা হলো এটি বিকেন্দ্রীকৃতভাবে চলে: প্রতিটি node শুধু প্রতিবেশীর কাছ থেকে তাদের d শোনে আর নিজের d হালনাগাদ করে, পুরো মানচিত্র জানার দরকার নেই। এজন্যই ইন্টারনেটের শুরুর দিকের distance vector routing protocol (যেমন RIP) এর ওপর গড়া। কিন্তু §৬-এর stepper-এ যে ঘটনা দেখেছেন, তার একটি আত্মীয় সেখানে সমস্যা করে: কোনো link কেটে গেলে দুটি router একে অন্যকে পুরোনো পথের খবর দিয়ে দূরত্ব ধীরে ধীরে বাড়াতে থাকে, যাকে বলে count-to-infinity. এটি আসলে relaxation-এর উল্টো দিক: d শুধু কমতে পারে বলে খারাপ খবর ধীরে ছড়ায়। RIP-এ “16 মানে অসীম” ধরে সমস্যাটি সীমিত রাখা হয় বলে জানা যায় (Computer Networks কোর্সের পর্ব ২৩)।
গণিতের দিক থেকে উপপাদ্য ৬.১(গ)-এর negative cycle ধরার ক্ষমতা এর আরেকটি শক্তি; Dijkstra সেটি পারে না। দাম হলো সময়: O(nm), যেখানে Dijkstra প্রায় রৈখিক। বাস্তবে একটি round-এ কিছুই না বদলালে আগেভাগে থামা যায়, আর কেবল যেসব vertex-এর d বদলেছে তাদের edge আবার দেখার কৌশল (SPFA নামে পরিচিত) গড়ে ভালো চলে, যদিও worst case একই থাকে।
মূল উপলব্ধি: Bellman–Ford আসলে dynamic programming: “≤ i edge”-এর উত্তর থেকে “≤ i + 1 edge”-এর উত্তর; এটি বিকেন্দ্রীকৃত আর negative cycle ধরে, কিন্তু ধীর, আর network-এ খারাপ খবর ধীরে ছড়ানোর সমস্যা আনে।
প্র ০৩বাস্তবে ঋণাত্মক weight আর negative cycle কোথায় আসে? মুদ্রা বিনিময়ের arbitrage আর Johnson-এর reweighting কীভাবে কাজ করে?
সবচেয়ে পরিচিত উদাহরণ মুদ্রা বিনিময়। ধরুন টাকা → ডলার → ইউরো → টাকা বিনিময়ের হার r1, r2, r3. গুণফল r1 r2 r3 > 1 হলে চক্রটি ঘুরে লাভ, যাকে বলে arbitrage. গুণফল নিয়ে shortest path চলে না, কিন্তু log নিলে গুণ যোগ হয়ে যায়। প্রতিটি edge-এর weight w = −log r নিলে চক্রের weight = −log(r1 r2 r3), যা ঋণাত্মক ঠিক তখন, যখন গুণফল > 1. তাই “arbitrage আছে কি?” প্রশ্নটি “negative cycle আছে কি?”, আর Bellman–Ford-এর যাচাই round উত্তর দেয় (উপপাদ্য ৬.১(গ))। এটি একটি গাণিতিক মডেল মাত্র; বাস্তব বাজারে কমিশন, দামের পার্থক্য আর সময় এমন সুযোগকে প্রায় সঙ্গে সঙ্গে মুছে দেয়।
অন্য উদাহরণ: বোনাস বা ভর্তুকিসহ খরচ, রাসায়নিক বিক্রিয়ায় শক্তির লাভ-ক্ষতি, আর scheduling-এর “difference constraints”: xj − xi ≤ c ধরনের অসমতার একটি ব্যবস্থার সমাধান আছে কি না, তা একটি graph-এ negative cycle আছে কি না প্রশ্নের সমান, আর থাকলে Bellman–Ford-এর দূরত্বই একটি সমাধান দেয়।
সব জোড়ার দূরত্ব দরকার হলে n বার Bellman–Ford চালানো O(n2m), যা ধীর। Donald B. Johnson ১৯৭৭ সালে একটি কৌশল প্রকাশ করেন বলে জানা যায়: একবার Bellman–Ford চালিয়ে প্রতিটি vertex-এর জন্য একটি “উচ্চতা” h(v) বের করুন (একটি নতুন উৎস থেকে সবার দিকে 0-weight edge দিয়ে), তারপর w′(u, v) = w(u, v) + h(u) − h(v). Triangle inequality h(v) ≤ h(u) + w(u, v) থেকে w′ ≥ 0, আর যেকোনো পথে নতুন weight = পুরোনো + h(শুরু) − h(শেষ), কারণ মাঝের h-গুলো telescoping হয়ে কেটে যায়। তাই সবচেয়ে ছোট পথ একই থাকে, আর এখন প্রতিটি উৎস থেকে Dijkstra চালানো বৈধ।
§৫-এর “সব edge-এ +5” কৌশলের সঙ্গে পার্থক্যটিই শিক্ষণীয়: ধ্রুবক যোগ edge-সংখ্যার ওপর নির্ভর করে, তাই পথের ক্রম বদলায়; vertex-ভিত্তিক potential শুধু দুই প্রান্তের ওপর নির্ভর করে, তাই বদলায় না। পদার্থবিজ্ঞানের ভাষায় h একটি potential, আর w′ হলো “উচ্চতা বিবেচনায় নেওয়া খরচ”।
মূল উপলব্ধি: ঋণাত্মক weight বাস্তব মডেলে আসে, আর negative cycle-এর প্রশ্ন প্রায়ই “লাভের চক্র আছে কি” বা “অসমতার ব্যবস্থা সমাধানযোগ্য কি”; vertex potential দিয়ে reweighting পথের ক্রম রক্ষা করে, ধ্রুবক যোগ করে না।
প্র ০৪TSP কতটা পুরোনো সমস্যা, আর NP-hard হওয়া সত্ত্বেও কয়েক হাজার শহরের instance কীভাবে নিখুঁতভাবে সমাধান হয়েছে?
“সব শহর একবার ঘুরে ফেরা” ধাঁধার একটি পূর্বসূরি William Rowan Hamilton-এর ১৮৫০-এর দশকের icosian game, যেখানে একটি dodecahedron-এর ২০টি কোণ ঘুরে আসতে হয় (পর্ব ৬০)। দূরত্বসহ সংস্করণটি Karl Menger ১৯৩০ সালের দিকে “messenger problem” নামে আলোচনা করেন বলে উল্লেখ করা হয়, আর ১৯৩০-৪০-এর দশকে প্রিন্সটন আর RAND কর্পোরেশনের গণিতবিদদের মধ্যে “traveling salesman problem” নামটি ছড়ায়।
১৯৫৪ সালে George Dantzig, Ray Fulkerson আর Selmer Johnson যুক্তরাষ্ট্রের ৪৯টি শহরের (হিসাবের সুবিধায় ৪২টিতে নামানো) একটি instance-এর সেরা tour প্রমাণসহ বের করেন বলে জানা যায়। তাঁদের কৌশল linear programming আর “cutting plane”: আগে শর্ত ঢিলে করে একটি সহজ সমস্যা সমাধান, তারপর যে শর্ত ভাঙল (যেমন tour-এর জায়গায় কয়েকটি আলাদা ছোট চক্র) তা নিষেধ করে নতুন অসমতা যোগ, আবার সমাধান। এই ধারণা পরে branch-and-cut নামে বিকশিত হয়।
NP-hard মানে “সব instance-এ দ্রুত সমাধানের কোনো জানা পদ্ধতি নেই”, “কোনো instance সমাধান করা যায় না” নয়। David Applegate, Robert Bixby, Vašek Chvátal আর William Cook-এর Concorde প্রোগ্রাম ২০০৬ সালের দিকে ৮৫,৯০০ বিন্দুর একটি circuit-board instance নিখুঁতভাবে সমাধান করে বলে উল্লেখ করা হয়, বহু CPU-বছরের হিসাবে। নিখুঁত উত্তরের প্রমাণ আসে একটি নিম্নসীমা থেকে: LP relaxation দেখায় কোনো tour একটি নির্দিষ্ট মানের কম হতে পারে না, আর পাওয়া tour সেই মানে পৌঁছালে সেটিই সেরা। §৮-এর w(T) ≤ OPT একই ধরনের, তবে অনেক দুর্বল, নিম্নসীমা।
বাস্তবের ডেলিভারি পরিকল্পনায় সমস্যা আরও জটিল: একাধিক গাড়ি, ধারণক্ষমতা, সময়ের জানালা; একে বলে vehicle routing problem, যা TSP-এর বড় আত্মীয়। সেখানে সাধারণত heuristic আর local search (যেমন 2-opt: দুটি edge সরিয়ে উল্টো করে জোড়া) ব্যবহার হয়, আর সেরা উত্তরের সঙ্গে দূরত্ব মাপতে নিম্নসীমা।
মূল উপলব্ধি: TSP-এর worst case কঠিন, কিন্তু নিম্নসীমা (MST, LP) আর চতুর অনুসন্ধান মিলিয়ে বাস্তবের বড় instance-ও নিখুঁতভাবে সমাধান হয়েছে; NP-hard একটি সতর্কবার্তা, হতাশার ঘোষণা নয়।
প্র ০৫সবচেয়ে ছোট পথ খোঁজা সহজ, অথচ সবচেয়ে লম্বা simple path খোঁজা কঠিন। এই অসমতার গাণিতিক কারণ কী?
প্রথম দেখায় দুটি সমস্যা প্রতিসম মনে হয়: weight-এর চিহ্ন উল্টালেই তো লম্বা পথ ছোট হয়ে যায়। কিন্তু চিহ্ন উল্টালে সাধারণ graph-এ negative cycle তৈরি হয় (প্রতিটি ধনাত্মক cycle ঋণাত্মক হয়ে যায়), আর তখন §২-এর উপপাদ্য অনুযায়ী walk-এর ওপর সবচেয়ে ছোট মান −∞. “শুধু simple path” শর্ত যোগ করলে প্রশ্নটি সংজ্ঞায়িত হয়, কিন্তু তখন আমাদের সব হাতিয়ার হারিয়ে যায়।
কারণটি optimal substructure-এ। উপপাদ্য ১.২-এর প্রমাণে “কেটে জোড়া” দেওয়া P′ শুধু walk হলেই চলেছিল, কারণ সংজ্ঞাটি walk-এর ওপর। Simple path-এর জগতে কেটে জোড়া দেওয়া পথ simple না-ও থাকতে পারে, তাই যুক্তি ভাঙে, আর সত্যিই উপপাদ্যটি মিথ্যা হয়ে যায় (অনুশীলন ৪-এর ৪-cycle তার উদাহরণ)। Dynamic programming আর relaxation দুটিই এই substructure-এর ওপর দাঁড়িয়ে; এটি না থাকলে ছোট উপ-সমস্যার উত্তর থেকে বড়টি গড়া যায় না।
কঠিনতার প্রমাণ সরাসরি: n vertex-এর graph-এ দৈর্ঘ্য n − 1-এর simple path আছে কি না মানে Hamilton path আছে কি না, যা NP-complete (পর্ব ৬০)। তাই সবচেয়ে লম্বা simple path-এর দ্রুত algorithm পেলে Hamilton path-এর প্রশ্নও দ্রুত মিটত। ব্যতিক্রম DAG: সেখানে কোনো cycle নেই, প্রতিটি walk-ই simple, আর §৭-এর topological পদ্ধতি চিহ্ন উল্টে সবচেয়ে লম্বা পথ দেয়। প্রকল্প পরিকল্পনার critical path method ঠিক এটিই করে।
একই নকশা TSP-তে: Hamilton cycle-এর শর্ত (“প্রতিটি vertex ঠিক একবার”) একটি বৈশ্বিক শর্ত, যা স্থানীয় relaxation দিয়ে রক্ষা করা যায় না। Shortest path-এ প্রতিটি vertex শুধু “এ পর্যন্ত সেরা” একটি সংখ্যা মনে রাখলেই চলে; TSP-এর Held–Karp dynamic programming-এ মনে রাখতে হয় “কোন কোন শহর দেখা হয়েছে”-এর পুরো সেট, যা 2n রকম, আর সেখান থেকেই exponential সময়।
মূল উপলব্ধি: সহজ আর কঠিন সমস্যার সীমানা প্রায়ই একটি গাণিতিক ধর্মে: optimal substructure থাকলে স্থানীয় হিসাব যথেষ্ট, আর “simple” বা “ঠিক একবার”-এর মতো বৈশ্বিক শর্ত সেই ধর্ম ভেঙে কঠিনতা আনে।
অনুশীলন
আগে খাতায় করুন, তারপর উত্তর খুলে মেলান।
-
প্রমাণ: সব edge weight ≥ 0 হলে প্রমাণ করুন, Dijkstra-র algorithm যে ক্রমে vertex settle করে, তাদের d-এর মান সেই ক্রমে কখনো কমে না: u-এর পরে v settle হলে d(u) ≤ d(v).
- যথেষ্ট দেখানো: পরপর দুটি settle u তারপর v হলে d(u) ≤ d(v) (তারপর অসমতার শৃঙ্খল)। — transitive
- u বাছাইয়ের মুহূর্তে v S-এর বাইরে, তাই তখন d(u) ≤ d(v)পুরোনো. — বাছাইয়ের নিয়ম
- u settle হওয়ার পর শুধু u-এর edge relax হয়; v-এর d বদলালে নতুন মান d(u) + w(u, v) ≥ d(u), কারণ w ≥ 0. না বদলালে d(v) = পুরোনো মান ≥ d(u). — weight ≥ 0
- v বাছাইয়ের সময় তার d এই মানই, তাই d(u) ≤ d(v). ∎
Python ব্লকের settle-ক্রম 0, 9, 12, 13, 19, 24 এর একটি উদাহরণ। ঋণাত্মক edge-এ এটি ভাঙে: §৫-এর graph-এ b (6) settle হওয়ার পর a-এর জন্য 1 পাওয়া যায়, যা আগে settle হওয়া a-এর 2-এর চেয়ে ছোট।
-
হিসাব: বাজারঘরের ৫টি হাব A, B, C, D, E একটি গ্রিডে, দূরত্ব (রাস্তা ধরে, কাল্পনিক কিলোমিটারে): AB 2, AC 5, AD 4, AE 2, BC 3, BD 4, BE 4, CD 3, CE 3, DE 2. (ক) কয়টি আলাদা tour? (খ) MST-এর weight কত, আর তা থেকে OPT-এর কোন সীমা পাওয়া যায়? (গ) সব tour দেখে OPT বের করুন।
(ক) (5 − 1)!/2 = 12টি।
(খ) Prim, A থেকে: AB 2, AE 2, ED 2, তারপর C-এর সবচেয়ে সস্তা সংযোগ 3, যা BC, CD বা CE যেকোনোটি হতে পারে। MST weight = 2 + 2 + 2 + 3 = 9. দূরত্বগুলো Manhattan দূরত্ব (A(0,0), B(2,0), C(3,2), D(1,3), E(0,2)), তাই metric; উপপাদ্য ৮.২ অনুযায়ী 9 ≤ OPT ≤ 18.
(গ) ১২টি tour-এর দৈর্ঘ্য 12 থেকে 20; সবচেয়ে ছোট A → B → C → D → E → A = 2 + 3 + 3 + 2 + 2 = 12, একমাত্র; পরের সবচেয়ে ছোট 14. অনুপাত OPT/MST = 12/9 ≈ 1.33, সীমার মধ্যে (scratch-এ সব tour তালিকা করে মেলানো)।
-
আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে, আর ফলটি graph সম্পর্কে কী বলে। তারপর যেকোনো Python-এ চালিয়ে মেলান।
INF = float('inf') W = [[0, 1, INF], [INF, 0, 1], [-3, INF, 0]] for k in range(3): for i in range(3): for j in range(3): W[i][j] = min(W[i][j], W[i][k] + W[k][j]) print([W[i][i] for i in range(3)])[-1, -1, -2]Graph-এ cycle 0 → 1 → 2 → 0-এর weight 1 + 1 − 3 = −1, একটি negative cycle. Floyd–Warshall-এর কর্ণ শুরুতে 0, কিন্তু cycle-টি খোলার সঙ্গে সঙ্গে D[i][i] ঋণাত্মক হয়: কর্ণে ঋণাত্মক মান মানেই negative cycle. মানগুলো (−1, −1, −2) নিজে কোনো অর্থপূর্ণ “দূরত্ব” নয়; cycle থাকলে আসল δ = −∞, আর in-place হালনাগাদে কোন ঘর কতবার cycle “দেখল” তার ওপর সংখ্যা নির্ভর করে (যেমন k = 2-এ W[2][2] + W[2][2] = −1 + (−1) = −2 হিসাব হয়, অর্থাৎ cycle-টি দুবার ঘোরা)। আউটপুট CPython 3.14-এ চালিয়ে মেলানো।
-
প্রমাণের ভুল ধরুন: একজন লিখলেন: “সবচেয়ে লম্বা simple path-এরও optimal substructure আছে: P যদি u থেকে v-এর সবচেয়ে লম্বা simple path হয়, তার প্রতিটি অংশও সবচেয়ে লম্বা; নইলে লম্বা অংশটি কেটে বসালে P-এর চেয়ে লম্বা পথ পাওয়া যেত।” Edge-weight 1-এর ৪-cycle a – b – c – d – a দিয়ে দাবিটি পরীক্ষা করুন। ভুল কোথায়?
- হিসাব: a থেকে c-এর simple path দুটি: a-b-c আর a-d-c, দুটিরই দৈর্ঘ্য 2, তাই P = a-b-c সবচেয়ে লম্বা। তার অংশ a-b (দৈর্ঘ্য 1). কিন্তু a থেকে b-এর সবচেয়ে লম্বা simple path a-d-c-b, দৈর্ঘ্য 3. তাই অংশটি সবচেয়ে লম্বা নয়: দাবি মিথ্যা।
- ভুল ধাপ: “কেটে বসানো”। a-d-c-b বসিয়ে বাকি অংশ b-c জুড়লে পাই a-d-c-b-c, যেখানে c দুবার: এটি simple path নয়, তাই P-এর সঙ্গে তুলনা চলে না। উপপাদ্য ১.২-এর প্রমাণে P′ walk হলেই চলত, কারণ shortest path-এর সংজ্ঞা walk-এর ওপর আর negative cycle না থাকলে walk আর simple path-এর minimum একই। Longest-এর ক্ষেত্রে এই সমতা নেই।
- শিক্ষা: কেটে জোড়া যুক্তিতে সবসময় দেখুন জোড়া দেওয়া বস্তুটি একই শ্রেণির কি না (scratch-এ সব simple path তালিকা করে মেলানো)।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমDijkstra-র algorithm-এর সঠিকতার প্রমাণের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Invariant: settled সেট S-এর প্রতিটি v-এর d(v) = δ(s, v); শুরুতে S = {s}, d(s) = 0
- পরের বাছাই u-এর জন্য উল্টোটা ধরা: d(u) > δ(s, u), আর P একটি s থেকে u-এর shortest path
- P-এর প্রথম edge x → y নেওয়া, যেখানে x ∈ S আর y ∉ S
- x settle হওয়ার সময়ের relaxation থেকে d(y) = δ(s, y)
- Weight ≥ 0 বলে δ(s, y) ≤ δ(s, u) < d(u) ≤ d(y): বিরোধ, তাই d(u) = δ(s, u)
একটি directed graph-এ edge: s → a (2), s → b (6), b → a (−5), a → t (3). প্রতিটি settled vertex-কে চূড়ান্ত ধরে এমন textbook Dijkstra s থেকে চালালে d(t)-এর মান কত আসে, আর আসল δ(s, t) কত?
- Dijkstra 4, আসল 4
- Dijkstra 5, আসল 4
- Dijkstra 5, আসল −∞
- Dijkstra 9, আসল 4
n টি vertex-এর একটি weighted directed graph-এ s থেকে পৌঁছানো যায় এমন কোনো negative cycle নেই। Bellman–Ford-এ n − 1 round relaxation যথেষ্ট কেন?
- কারণ edge-গুলো topological ক্রমে relax করা হয়
- কারণ সব weight অঋণাত্মক ধরে নেওয়া হয়
- কারণ একটি simple shortest path আছে, যাতে সর্বোচ্চ n − 1টি edge
- কারণ প্রতি round-এ ঠিক একটি vertex চূড়ান্ত হয়
৬টি শহরের একটি symmetric traveling salesperson problem-এ (D(i, j) = D(j, i)) কয়টি আলাদা tour আছে?
- 60
- 720
- 120
- 15
একটি weighted graph-এ কিছু edge ঋণাত্মক। কেউ সব edge-এর weight-এ একই ধনাত্মক ধ্রুবক c যোগ করে সব weight অঋণাত্মক করলেন। নিচের কোন কথাটি সঠিক?
- সব পথ একই পরিমাণ বাড়ে, তাই shortest path একই থাকে
- Negative cycle না থাকলে shortest path একই থাকে
- নতুন graph-এ বেশি edge-এর পথ সুবিধা পায়
- k-edge-এর পথ k · c বাড়ে, তাই সবচেয়ে ছোট পথ বদলে যেতে পারে
Floyd–Warshall algorithm শেষ করার পর কীভাবে বোঝা যায় যে graph-এ একটি negative cycle আছে?
- কোনো একটি ঘর D[i][j] ঋণাত্মক হলে
- কোনো কর্ণের ঘর D[i][i] ঋণাত্মক হলে
- কোনো ঘর ∞ থেকে গেলে
- শেষ ধাপে (k = n) কোনো মান বদলালে
Metric TSP-এর MST-ভিত্তিক 2-approximation-এর প্রমাণে “MST-এর weight ≤ সবচেয়ে ছোট tour-এর দৈর্ঘ্য” অংশটি কোন যুক্তিতে আসে?
- Triangle inequality দিয়ে shortcut নেওয়া থেকে
- MST-এর প্রতিটি edge দুবার নিয়ে closed walk বানানো থেকে
- সেরা tour থেকে একটি edge সরালে একটি spanning tree থাকে বলে
- Nearest neighbour tour-এর সঙ্গে তুলনা করে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- সংজ্ঞা আর অস্তিত্ব: δ(s, v) = walk-এর weight-এর infimum; পৌঁছানো যায় এমন negative cycle পথে থাকলে −∞, না থাকলে একটি simple shortest path আছে, ≤ n − 1 edge-এর।
- দুই ভিত্তি: shortest path-এর প্রতিটি অংশ shortest (কেটে জোড়ার যুক্তি), আর δ(s, v) ≤ δ(s, u) + w(u, v); relaxation কখনো d-কে δ-এর নিচে নামায় না।
- Dijkstra: settled সেটের ওপর induction; পথ প্রথম যে edge-এ S ছাড়ে, সেখানে d(y) = δ(s, y), আর weight ≥ 0 বলে বাকি অংশ দূরত্ব কমাতে পারে না। একটি ঋণাত্মক edge-ই প্রমাণ ভাঙে, আর ধ্রুবক যোগ সমাধান নয়।
- Bellman–Ford, DAG, Floyd–Warshall: i round-এর পর ≤ i edge-এর সব walk ধরা হয়, n − 1 যথেষ্ট, আরেকবার কিছু কমলে negative cycle; DAG-এ topological ক্রমে এক পাস; D(k) = min(D(k−1)[i][j], D(k−1)[i][k] + D(k−1)[k][j]).
- TSP: (n − 1)!/2টি tour, NP-hard; metric ক্ষেত্রে MST ≤ OPT ≤ 2 · MST থেকে MST দ্বিগুণ করে 2-approximation, আর nearest neighbour খারাপ হতে পারে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৬২ · Planar graph — Euler-এর সূত্র আর Kuratowskiপরবর্তী পর্বকাটাকাটি ছাড়া আঁকা গ্রাফ, Euler-এর সূত্র আর Kuratowski-র উপপাদ্য।
- পর্ব ৫৪ · Relation-এর closure আর Warshall-এর algorithmপুনরালোচনাএকই “মাঝের vertex 1..k” invariant, reachability-র জন্য।
- পর্ব ৫৬ · Partial order, Hasse diagram, topological sort আর latticeপুনরালোচনাTopological ক্রমের অস্তিত্ব, DAG-এর এক-পাস পদ্ধতির ভিত্তি।
- ডিসক্রিট ম্যাথমেটিক্স · কোর্স হোম৮৩টি পর্বপুরো কোর্সের মানচিত্র আর ১৪টি মডিউল।
- DSA · পর্ব ৪৩ · Dijkstra — সবচেয়ে ছোট পথকোডে প্রয়োগHeap-ভিত্তিক বাস্তবায়ন আর জটিলতার বিশ্লেষণ।
- DSA · পর্ব ৪৪ · Bellman-Ford আর Floyd-Warshallকোডে প্রয়োগNegative cycle ধরা আর সব জোড়ার দূরত্বের কোড।
- DSA · পর্ব ৬৬ · Approximation অ্যালগরিদম — vertex cover, set cover আর metric TSPআরও গভীরেTSP-এর approximation algorithm-এর বিস্তারিত।