সরাসরি পর্বে যান
পর্ব ৫৭ · ৭২-এর মধ্যে · মডিউল ৭

Branch and bound আর traveling salesman

Branch and Bound — Bounds, Incumbents & Pruning, FIFO / Depth-first / Best-first Search, 0/1 Knapsack, TSP (Reduced Cost Matrix, 1-trees, Held–Karp Bound) & Job Assignment

Taught in Bangla, this lesson extends backtracking to optimisation with bounds. It explains optimistic bounds, incumbents and pruning rules for minimisation and maximisation, FIFO, depth-first and best-first search, 0/1 knapsack with a fractional-knapsack upper bound, the textbook TSP reduced cost matrix with code checked against brute force, cheaper-edge, MST and 1-tree bounds, the job assignment problem, and how real solvers use these ideas.

এই পর্বে যা শিখবেন

  • Optimisation-এর সমাধান-গাছ, bound, incumbent আর live/E/dead node — পরিভাষা আর সঠিকতার প্রমাণ
  • 0/1 knapsack-কে fractional knapsack-এর upper bound দিয়ে best-first branch and bound-এ সমাধান
  • FIFO, depth-first আর best-first কৌশলের তুলনা — কোনটি কখন, কত node, কত মেমরি
  • TSP-এর reduced cost matrix পদ্ধতি হাতে-কলমে, আর দুই সস্তা edge, MST ও 1-tree bound
  • Job assignment problem, ভালো শুরুর উত্তরের গুরুত্ব, আর বাস্তবের solver (branch and cut) কীভাবে কাজ করে

১ · Backtracking-এর পরের ধাপ — “সবচেয়ে ভালো” উত্তরের খোঁজ

পর্ব ৫৬-এ backtracking দেখেছেন: সিদ্ধান্ত একটি একটি করে নিন, কোনো শর্ত ভাঙলেই ফিরে আসুন। N-queens বা subset sum-এ প্রশ্ন ছিল “একটি বৈধ উত্তর আছে কি?” কিন্তু অনেক সমস্যায় বৈধ উত্তর অগণিত, আর চাই সবচেয়ে ভালোটি — সবচেয়ে কম খরচ বা সবচেয়ে বেশি লাভ। এগুলো optimization problem.

ধরুন একজন কুরিয়ার ভ্যানচালক মিরপুরের অফিস থেকে বেরিয়ে চারটি এলাকায় পার্সেল দিয়ে আবার অফিসে ফিরবেন। প্রতিটি এলাকায় ঠিক একবার যাবেন, আর মোট সময় সবচেয়ে কম চান — এটাই traveling salesman problem (TSP). চারটি এলাকা হলে সম্ভাব্য ক্রম ৪! = ২৪টি, হাতে গোনা যায়। কিন্তু ১৯টি এলাকা হলে ১৯! ≈ ১.২ × ১০১৭ — প্রতি সেকেন্ডে একশো কোটি ক্রম পরীক্ষা করলেও কয়েক বছর লাগবে।

সব ক্রম একে একে দেখা হলো brute force. Branch and bound-এর বুদ্ধি: অর্ধেক বানানো একটি রুট দেখে যদি প্রমাণ করা যায় “বাকিটা যেভাবেই সাজাই, মোট সময় ৯০ মিনিটের কম হবে না”, আর হাতে আগে থেকেই ৮০ মিনিটের একটি রুট থাকে — তাহলে এই অর্ধেক রুটের সব সম্প্রসারণ একসাথে বাদ।

ভর্তি পরীক্ষার খাতা দেখার কথা ভাবুন। মেধাতালিকার কাট-অফ এখন ৭২. একটি খাতার অর্ধেক দেখা হয়েছে, পেয়েছে ৩০; বাকি প্রশ্নে সর্বোচ্চ ৪০ পাওয়া সম্ভব। তাহলে এই খাতার সর্বোচ্চ সম্ভাব্য নম্বর ৭০ — কাট-অফের নিচে, বাকিটা খুঁটিয়ে দেখার দরকার নেই। “সর্বোচ্চ সম্ভাব্য নম্বর” হলো bound, আর কাট-অফ হলো এ পর্যন্ত পাওয়া সেরা উত্তরের মান — incumbent.

সমাধান-গাছ আর তিন রকম node

Backtracking-এর মতোই সিদ্ধান্তগুলো একটি গাছ তৈরি করে — state-space tree. Root-এ এখনো কোনো সিদ্ধান্ত নেই; প্রতিটি স্তরে একটি সিদ্ধান্ত (“জিনিস ৩ নেব কি না”, “এরপর কোন শহর”); আর leaf মানে একটি সম্পূর্ণ সমাধান। একটি node-কে “খোলা” (expand) মানে তার সব সন্তান তৈরি করা — এটাই branch. বিশ্ববিদ্যালয়ের পাঠ্যবইয়ে (যেমন Horowitz–Sahni ধাঁচের বইয়ে) তিনটি নাম খুব চলে:

  • Live node: তৈরি হয়েছে, কিন্তু এখনো খোলা হয়নি — অপেক্ষার তালিকায় আছে।
  • E-node (expanding node): এই মুহূর্তে যার সন্তান তৈরি হচ্ছে।
  • Dead node: খোলা শেষ, অথবা ছাঁটাই হয়ে বাদ পড়েছে।

Live node-গুলো কোন ডেটা স্ট্রাকচারে রাখবেন, সেটাই ঠিক করে খোঁজের ক্রম — queue, stack, নাকি priority queue. অংশ ৪-এ তিনটির তুলনা।

দিকBacktracking (পর্ব ৫৬)Branch and bound
সাধারণ প্রশ্নবৈধ উত্তর আছে কি? সব বৈধ উত্তর কী?সবচেয়ে ভালো উত্তর কোনটি?
কখন শাখা বাদশর্ত ভেঙেছে (infeasible)শর্ত ভেঙেছে, অথবা bound বলছে এখানে incumbent-এর চেয়ে ভালো কিছু নেই
খোঁজের ক্রমপ্রায় সবসময় depth-first (রিকার্শন)FIFO, depth-first বা best-first — যেকোনোটি
বাড়তি হিসাবনেইপ্রতিটি node-এ একটি bound হিসাব

২ · Bound — আশাবাদী অনুমান আর ছাঁটাইয়ের নিয়ম

পুরো পদ্ধতিটি দাঁড়িয়ে আছে একটি সংখ্যার ওপর। ধরুন সমস্যাটি খরচ কমানোর (minimisation). একটি node x-এর bound b(x) হলো এমন একটি সংখ্যা, যাতে

x-এর নিচের প্রতিটি সম্পূর্ণ সমাধান S-এর জন্য: b(x) ≤ cost(S)

অর্থাৎ b(x) একটি lower bound — x থেকে যতই ভালো সাজান, খরচ এর নিচে নামবে না। একে বলে আশাবাদী (optimistic) অনুমান: এটি সবসময় বাস্তবের চেয়ে ভালো বা সমান ছবি দেখায়, কখনো খারাপ নয়। লাভ বাড়ানোর (maximisation) সমস্যায় সবকিছু উল্টো — bound তখন upper bound, “এর বেশি লাভ অসম্ভব”.

খোঁজের মাঝপথে পাওয়া সেরা সম্পূর্ণ সমাধানের নাম incumbent, তার খরচ best. এবার ছাঁটাইয়ের (pruning) নিয়ম এক লাইনের:

ছাঁটাইয়ের নিয়ম

Minimisation: b(x) ≥ best হলে x-এর পুরো subtree বাদ। Maximisation: b(x) ≤ best হলে বাদ। কারণ x-এর নিচে বড়জোর incumbent-এর সমান কিছু আছে — তার চেয়ে ভালো কিছু নেই।

একটি bound দুটি টানাপোড়েনের মাঝে থাকে। সবচেয়ে সস্তা bound “এ পর্যন্ত যা খরচ হয়েছে” — খরচ ঋণাত্মক না হলে বৈধ, কিন্তু প্রায় কিছুই ছাঁটে না। সবচেয়ে শক্ত bound x-এর নিচের আসল সর্বোত্তম খরচ — কিন্তু সেটি বের করা মানে মূল সমস্যাটাই সমাধান করা! ভালো bound মাঝখানে: দ্রুত হিসাব হয় (সাধারণত polynomial সময়ে), আর আসল মানের কাছাকাছি। প্রায় সব ভালো bound আসে relaxation থেকে: কঠিন একটি শর্ত ঢিলে করে সহজ সমস্যা বানানো, যার সর্বোত্তম মান মূল সমস্যার সর্বোত্তমের চেয়ে খারাপ হতে পারে না।

Pseudocode — best-first branch and bound (minimisation)
BRANCH-AND-BOUND(root):
    best ← cost of a quick heuristic solution   (or ∞ if none)
    Q ← priority queue keyed by bound;  INSERT(Q, root)
    while Q is not empty:
        x ← EXTRACT-MIN(Q)
        if b(x) ≥ best: stop                   // every live node is ≥ best
        for each child y of x:                 // branch
            if y is a complete solution:
                if cost(y) < best: best ← cost(y); remember y
            else if b(y) < best: INSERT(Q, y)  // bound: keep y alive
            // otherwise y is pruned
    return best and the remembered solution

Q-কে সাধারণ queue বা stack বানালে বাকি সবকিছু একই থাকে, শুধু “stop”-এর জায়গায় “এই x বাদ দিয়ে পরেরটি নাও” (continue) লিখতে হয় — কারণ তখন Q-এর সামনের node-টিই সবচেয়ে ছোট bound-এর হবে, এমন কোনো নিশ্চয়তা নেই।

কেন উত্তর ঠিক — একটি invariant

Invariant

প্রতিটি লুপের শুরুতে: হয় incumbent ইতিমধ্যেই সর্বোত্তম, নয়তো কোনো একটি সর্বোত্তম সমাধান Q-এর কোনো live node-এর subtree-তে আছে।

শুরুতে root live, আর সব সমাধানই root-এর নিচে — সত্য। একটি node খুললে x-এর জায়গায় তার সন্তানেরা আসে; x-এর subtree-র প্রতিটি সমাধান ঠিক একটি সন্তানের subtree-তে, তাই কিছু হারায় না। y ছাঁটাই হলে b(y) ≥ best; y-এর নিচের প্রতিটি সমাধানের খরচ ≥ b(y) ≥ best, তাই সেখানে incumbent-এর চেয়ে কম খরচের কিছু নেই — যদি সেখানে কোনো সর্বোত্তম থাকত, তার খরচ best-এর সমান হতো, অর্থাৎ incumbent নিজেই সর্বোত্তম। দুই ক্ষেত্রেই invariant টেকে। শেষে Q খালি, অথবা সবচেয়ে ছোট bound-ও ≥ best — কোনো live node-এর নিচে আরও ভালো কিছু নেই, তাই incumbent সর্বোত্তম। গাছ সসীম, প্রতিটি node বড়জোর একবার খোলা হয়, তাই লুপ থামবেই। ∎

খেয়াল করুন, প্রমাণে লাগেনি যে bound “ভালো” — শুধু লেগেছে যে এটি বৈধ। দুর্বল bound উত্তর ভুল করে না, শুধু ধীর করে। কিন্তু অবৈধ bound — যা কোনো সমাধানের আসল খরচের চেয়ে বেশি বলে — নিঃশব্দে সর্বোত্তম সমাধান ছেঁটে ফেলতে পারে। Branch and bound-এর প্রায় সব বাগ এখানেই।

৩ · 0/1 knapsack — fractional knapsack দিয়ে upper bound

প্রথম পূর্ণ উদাহরণ 0/1 knapsack. একজন হকারের ঝুড়িতে ১০ কেজি ধরে। চারটি জিনিস, প্রতিটি হয় পুরো নেবেন, নয় নেবেন না:

জিনিসওজন (কেজি)মূল্যমূল্য/ওজন
১৪৪০১০
২৭৪২৬
৩৫২৫৫
৪৩১২৪

জিনিসগুলো আগেই মূল্য/ওজন অনুপাতের বড় থেকে ছোট ক্রমে সাজানো — এটা জরুরি, একটু পরেই বুঝবেন। গাছের স্তর k-তে সিদ্ধান্ত: জিনিস k নেব (১) নাকি নেব না (০)। একটি node-এর নাম তার সিদ্ধান্তের সারি — যেমন [10] মানে “জিনিস ১ নিয়েছি, ২ নিইনি, বাকিগুলো এখনো ঠিক হয়নি”.

Bound: ভগ্নাংশ নেওয়া গেলে কত পেতাম?

এটি লাভ বাড়ানোর সমস্যা, তাই চাই upper bound. Relaxation: “পুরো নাও বা নিও না” শর্তটি ঢিলে করে বলি, জিনিসের ভগ্নাংশও নেওয়া যায় — চাল বা ডালের মতো, যতটুকু জায়গা আছে ততটুকু। এটাই fractional knapsack, আর পর্ব ৫০-এ দেখেছেন এটি greedy দিয়েই নিখুঁত সমাধান হয়: অনুপাতের ক্রমে পুরো জিনিস নিতে থাকুন, যেটি আর পুরো ধরে না তার যতটুকু ধরে ততটুকু নিন, ব্যস।

কেন এটি বৈধ upper bound: node x-এর নিচের প্রতিটি 0/1 সমাধান একই সাথে x-এর সিদ্ধান্ত মেনে চলা একটি fractional সমাধানও (০ আর ১-ও তো ভগ্নাংশ)। Fractional সমস্যার সর্বোত্তম মান তাই প্রতিটি 0/1 সমাধানের মানের ≥. আর greedy সেই সর্বোত্তম মানটিই দেয় (পর্ব ৫০-এর exchange argument). মূল্যগুলো পূর্ণসংখ্যা, তাই 0/1 উত্তরও পূর্ণসংখ্যা — bound-কে নিচের দিকে পূর্ণসংখ্যায় নামালেও (floor) বৈধ থাকে, আর একটু শক্ত হয়।

Root-এ হিসাব: জিনিস ১ পুরো (৪ কেজি, ৪০), বাকি জায়গা ৬ কেজি; জিনিস ২-এর ওজন ৭, পুরো ধরে না, তাই তার ৬/৭ অংশ: ৬ × ৪২/৭ = ৩৬. Upper bound = ৪০ + ৩৬ = ৭৬. কোনো 0/1 বাছাই ৭৬-এর বেশি দিতে পারবে না। এবার best-first খোঁজ: প্রতিবার সবচেয়ে বড় upper bound-এর live node খুলুন।

[পথ] = নেওয়া (1) / না-নেওয়া (0), v = মূল্য, w = ওজন, ub = upper boundসবুজ = সেরা, লাল = বাদ

            
প্রতিটি ধাপে
এই ধাপ
incumbent (best)
live node (ub বড় আগে)
  1. Root. কিছুই ঠিক হয়নি: v = ০, w = ০. Fractional bound ৪০ + ৩৬ = ৭৬. কোনো সম্পূর্ণ সমাধান এখনো নেই, তবে “কিছুই না নেওয়া” নিজেই বৈধ, তাই incumbent = ০.
  2. Root খোলা — জিনিস ১. নিলে [1]: v = ৪০, w = ৪, bound আগের মতোই ৭৬ (greedy তো এটিই নিচ্ছিল)। ৪০ > ০, তাই incumbent এখন ৪০. না নিলে [0]: greedy শুরু হয় জিনিস ২ থেকে — ২ পুরো (৭ কেজি, ৪২), বাকি ৩ কেজিতে জিনিস ৩-এর ৩/৫ অংশ = ১৫, bound ৫৭. দুটিই ৪০-এর বেশি, তাই দুটিই live.
  3. [1] খোলা — জিনিস ২. সবচেয়ে বড় bound ৭৬, তাই [1]. নিলে [11]: ওজন ৪ + ৭ = ১১ > ১০ — infeasible, বাদ (এটি backtracking-এর ছাঁটাই)। না নিলে [10]: জিনিস ৩ পুরো (মোট ৯ কেজি, ৬৫), বাকি ১ কেজিতে জিনিস ৪-এর ১/৩ = ৪, bound ৬৯.
  4. [10] খোলা — জিনিস ৩. নিলে [101]: v = ৬৫, w = ৯, bound ৬৯. ৬৫ > ৪০, তাই নতুন incumbent ৬৫. না নিলে [100]: v = ৪০, বাকি ৬ কেজিতে জিনিস ৪ পুরো, মোট ৫২ — bound ৫২ ≤ ৬৫, তাই ছাঁটাই: এই শাখার কোনো বাছাই ৬৫-কে হারাতে পারবে না। এটিই branch and bound-এর নিজস্ব ছাঁটাই।
  5. [101] খোলা — জিনিস ৪. নিলে ওজন ১২ > ১০, infeasible. না নিলে [1010]: v = ৬৫, bound ৬৫ ≤ ৬৫ — incumbent-কে হারাতে পারে না, ছাঁটাই (সমান হওয়া যথেষ্ট নয়, আমরা ভালো চাই)।
  6. [0] বের করা হলো. Bound ৫৭ ≤ ৬৫: জিনিস ১ বাদ দিয়ে কোনোভাবেই ৬৫ ছোঁয়া যাবে না — পুরো ডান অর্ধেক একবারে বাদ। Queue খালি, উত্তর ৬৫: জিনিস ১ আর ৩. মাত্র ৪টি node খোলা হলো; পূর্ণ গাছে ৩১টি node থাকত।

কোডে — best-first knapsack, DP দিয়ে যাচাই

প্রোগ্রামটি ঠিক ওপরের খোঁজটি চালায়, প্রতিটি সিদ্ধান্ত ছাপে, আর শেষে পর্ব ৫৩-এর knapsack DP দিয়ে উত্তর মেলায়। Priority queue-তে max-heap দরকার; C++ আর Python দুই জায়গাতেই min-heap-এ -ub রেখে কাজটি সারা হয়েছে, যাতে দুটির ক্রম হুবহু এক থাকে।

#include <algorithm>
#include <cstdio>
#include <initializer_list>
#include <queue>
#include <string>
#include <tuple>
#include <vector>
using namespace std;
const int W = 10;
const vector<pair<int, int>> items = {{4, 40}, {7, 42}, {5, 25}, {3, 12}};  // (weight, value)
int bound(size_t k, int w, int v) {            // fractional knapsack on items k.. (upper bound)
    for (; k < items.size(); k++) {
        auto [wi, vi] = items[k];
        if (w + wi > W) return v + (W - w) * vi / wi;   // floor: still valid for integers
        w += wi; v += vi; }
    return v;
}
int main() {
    int best = 0, expanded = 0, pruned = 0; string bestPath;
    using Node = tuple<int, string, int, int>;            // (-ub, path, weight, value)
    priority_queue<Node, vector<Node>, greater<Node>> pq;  // smallest -ub = largest ub first
    pq.push({-bound(0, 0, 0), "", 0, 0});
    while (!pq.empty()) {
        auto [nub, path, w, v] = pq.top(); pq.pop();
        if (-nub <= best) { printf("pop [%s] ub=%d <= best %d: pruned\n", path.c_str(), -nub, best); pruned++; continue; }
        expanded++;
        printf("expand [%s] w=%d v=%d ub=%d\n", path.c_str(), w, v, -nub);
        auto [wi, vi] = items[path.size()];
        for (auto [bit, nw, nv] : {tuple{'1', w + wi, v + vi}, tuple{'0', w, v}}) {
            string p = path + bit;
            if (nw > W) { printf("  [%s] infeasible\n", p.c_str()); continue; }
            int ub = bound(p.size(), nw, nv);
            if (nv > best) { best = nv; bestPath = p; }
            if (ub <= best) { printf("  [%s] v=%d ub=%d pruned\n", p.c_str(), nv, ub); pruned++; }
            else { printf("  [%s] v=%d ub=%d queued\n", p.c_str(), nv, ub); pq.push({-ub, p, nw, nv}); }
        }
    }
    vector<int> dp(W + 1, 0);                              // 0/1 knapsack DP to check
    for (auto [wi, vi] : items) for (int c = W; c >= wi; c--) dp[c] = max(dp[c], dp[c - wi] + vi);
    printf("B&B best = %d take [%s] | expanded %d, pruned %d\n", best, bestPath.c_str(), expanded, pruned);
    printf("DP best = %d | match: %s\n", dp[W], dp[W] == best ? "yes" : "no");
}

আউটপুট:

expand [] w=0 v=0 ub=76
  [1] v=40 ub=76 queued
  [0] v=0 ub=57 queued
expand [1] w=4 v=40 ub=76
  [11] infeasible
  [10] v=40 ub=69 queued
expand [10] w=4 v=40 ub=69
  [101] v=65 ub=69 queued
  [100] v=40 ub=52 pruned
expand [101] w=9 v=65 ub=69
  [1011] infeasible
  [1010] v=65 ub=65 pruned
pop [0] ub=57 <= best 65: pruned
B&B best = 65 take [101] | expanded 4, pruned 3
DP best = 65 | match: yes

প্রতিটি লাইন স্টেপারের একটি ধাপের সাথে মেলে। bound হলো fractional greedy: পুরো জিনিস নিতে থাকে, প্রথম যেটি ধরে না তার ভগ্নাংশ যোগ করে ফেরে; (W - w) * vi / wi পূর্ণসংখ্যার ভাগ, অর্থাৎ floor. items আগে থেকেই অনুপাতের ক্রমে সাজানো — এলোমেলো ইনপুটে আগে O(n log n)-এ সাজাতে হবে, নইলে bound আর বৈধ থাকে না। pop [0] ... pruned দেখায় দ্বিতীয় ছাঁটাই: ঢোকার সময় [0]-এর bound ৫৭ ছিল incumbent ৪০-এর বেশি, কিন্তু বের হওয়ার আগে incumbent ৬৫ হয়ে গেছে।

import heapq
W = 10
items = [(4, 40), (7, 42), (5, 25), (3, 12)]    # (weight, value), best value/weight first

def bound(k, w, v):                 # fractional knapsack on items k.. (an upper bound)
    for wi, vi in items[k:]:
        if w + wi > W: return v + (W - w) * vi // wi   # floor: still valid for integers
        w, v = w + wi, v + vi
    return v

best, best_path, expanded, pruned = 0, "", 0, 0
heap = [(-bound(0, 0, 0), "", 0, 0)]             # (-ub, path, weight, value): max ub first
while heap:
    nub, path, w, v = heapq.heappop(heap)
    if -nub <= best:                              # cannot beat the incumbent
        print(f"pop [{path}] ub={-nub} <= best {best}: pruned"); pruned += 1; continue
    expanded += 1
    print(f"expand [{path}] w={w} v={v} ub={-nub}")
    wi, vi = items[len(path)]
    for bit, nw, nv in (("1", w + wi, v + vi), ("0", w, v)):
        if nw > W: print(f"  [{path + bit}] infeasible"); continue
        ub = bound(len(path) + 1, nw, nv)
        if nv > best: best, best_path = nv, path + bit
        if ub <= best: print(f"  [{path + bit}] v={nv} ub={ub} pruned"); pruned += 1
        else: print(f"  [{path + bit}] v={nv} ub={ub} queued"); heapq.heappush(heap, (-ub, path + bit, nw, nv))

dp = [0] * (W + 1)                                  # 0/1 knapsack DP to check
for wi, vi in items:
    for c in range(W, wi - 1, -1): dp[c] = max(dp[c], dp[c - wi] + vi)
print(f"B&B best = {best} take [{best_path}] | expanded {expanded}, pruned {pruned}")
print(f"DP best = {dp[W]} | match: {'yes' if dp[W] == best else 'no'}")

আউটপুট (C++-এর সাথে হুবহু এক):

expand [] w=0 v=0 ub=76
  [1] v=40 ub=76 queued
  [0] v=0 ub=57 queued
expand [1] w=4 v=40 ub=76
  [11] infeasible
  [10] v=40 ub=69 queued
expand [10] w=4 v=40 ub=69
  [101] v=65 ub=69 queued
  [100] v=40 ub=52 pruned
expand [101] w=9 v=65 ub=69
  [1011] infeasible
  [1010] v=65 ub=65 pruned
pop [0] ub=57 <= best 65: pruned
B&B best = 65 take [101] | expanded 4, pruned 3
DP best = 65 | match: yes

heapq min-heap, তাই tuple-এর প্রথম উপাদান -ub: সবচেয়ে বড় bound আগে বের হয়। Bound সমান হলে পরের উপাদান path দিয়ে ক্রম ঠিক হয় — C++-এর tuple তুলনাও একই নিয়ম মানে, তাই দুই ভাষার আউটপুট মেলে। শেষের DP O(nW) সময়ের; W যদি ১০১২ হয়, DP অচল, অথচ branch and bound-এর খরচ W-এর ওপর নির্ভর করে না।

জটিলতা: গাছে বড়জোর 2n+1 − 1 টি node, প্রতিটির bound O(n) (prefix sum আর binary search দিয়ে O(log n)), heap-এর প্রতিটি কাজ O(log 2ⁿ) = O(n). তাই worst case সময় O(n · 2ⁿ), শুরুর সর্ট O(n log n). Best-first-এর queue-তে worst case-এ O(2ⁿ) টি node, প্রতিটিতে O(n) লম্বা path — জায়গা O(n · 2ⁿ); depth-first-এ মাত্র O(n). বাস্তবে fractional bound খুব শক্ত, তবে নিশ্চয়তা নেই — knapsack NP-hard.

৪ · খোঁজের তিন কৌশল — FIFO, depth-first আর best-first

একই bound, একই ছাঁটাইয়ের নিয়ম — শুধু live node রাখার পাত্র বদলালে খোঁজের চেহারা পুরো বদলে যায়। ট্রেনের টিকিটের লাইনের কথা ভাবুন: যে আগে এসেছে সে আগে (queue); যে সবশেষে এসেছে সে আগে (stack); নাকি যার প্রয়োজন সবচেয়ে জরুরি সে আগে (হাসপাতালের জরুরি বিভাগের মতো — priority queue).

FIFO branch and bound

FIFO B&B: live node সাধারণ queue-তে, স্তর ধরে ধরে খোলা — BFS-এর মতো। হাতে আঁকতে সবচেয়ে সহজ, তাই অনেক পাঠ্যবই এটি দিয়ে শুরু করে। কিন্তু সম্পূর্ণ সমাধান আসে শেষ স্তরে, তাই incumbent দেরিতে আসে, আর queue বিশাল হয়।

queue · স্তর ধরে

Depth-first (LIFO) B&B

Depth-first B&B: stack বা রিকার্শন — আসলে bound-যুক্ত backtracking. দ্রুত একটি leaf-এ পৌঁছে incumbent পায়; মেমরি শুধু গভীরতার সমানুপাতিক। দুর্বলতা: শুরুতে ভুল দিকে গেলে খারাপ শাখায় সময় নষ্ট।

stack · মেমরি O(গভীরতা)

Best-first (LC) B&B

Best-first বা least-cost (LC) search: priority queue, সবচেয়ে আশাব্যঞ্জক bound আগে। একই bound দিয়ে প্রায় সবচেয়ে কম node খোলে। দুর্বলতা: শেষের আগে হয়তো কোনো সম্পূর্ণ সমাধান হাতে নেই, আর queue-তে exponential সংখ্যক node জমতে পারে।

priority queue · সবচেয়ে কম node

অংশ ৫-এর পাঁচ শহরের TSP-তে (reduced cost matrix bound) তিনটি কৌশল চালিয়ে গোনা হয়েছে — কতটি node খোলা হলো, কতটি সন্তান তৈরি হলো, আর queue/stack সবচেয়ে বড় কত হলো। দুইভাবে: শুরুতে কোনো incumbent ছাড়া (best = ∞), আর nearest neighbour-এর দেওয়া ৩৬ খরচের রুট দিয়ে শুরু করে (অংশ ৯)। সম্পূর্ণ tour তৈরি হওয়ামাত্র incumbent আপডেট হয়।

কৌশলশুরু best = ∞: খোলা / তৈরি / সর্বোচ্চ queueশুরু best = ৩৬: খোলা / তৈরি / সর্বোচ্চ queue
FIFO২০ / ৪৩ / ২৪৬ / ১৬ / ৩
Depth-first৯ / ১৯ / ৭৫ / ১৩ / ৩
Best-first৪ / ১০ / ৭৪ / ১০ / ৩

তিনটি শিক্ষা। এক, best-first সবচেয়ে কম node খোলে। দুই, FIFO-র সবচেয়ে বড় শত্রু incumbent-এর অভাব — ভালো শুরুর উত্তরেই ২০ থেকে ৬. তিন, best-first-এ শুরুর উত্তর খোলা node কমায়নি, কিন্তু queue ৭ থেকে ৩ — খারাপ সন্তান তৈরি হওয়ামাত্র ছাঁটা পড়ে। বড় সমস্যায় এই মেমরির পার্থক্যই ঠিক করে প্রোগ্রাম শেষ হবে, নাকি মেমরি ফুরিয়ে থেমে যাবে।

Best-first কেন সবচেয়ে কম node খোলে

ধরুন bound monotone: সন্তানের bound কখনো মা-বাবার চেয়ে কম নয় (এই পর্বের সব bound-ই এমন)। সর্বোত্তম খরচ OPT. দাবি ১: b(x) < OPT এমন প্রতিটি node যেকোনো সঠিক branch and bound-কে খুলতেই হবে, যদি তার পূর্বপুরুষেরা খোলা হয় — কারণ best কখনো OPT-এর নিচে নামে না, তাই b(x) < OPT ≤ best, ছাঁটাইয়ের শর্ত খাটে না। দাবি ২: best-first কখনো b(x) > OPT এমন node খোলে না — সর্বোত্তম সমাধানের পথের প্রতিটি node-এর bound ≤ OPT, তাদের কেউ না কেউ সবসময় queue-তে থাকে আর আগে বের হয়, আর সর্বোত্তম leaf পাওয়ামাত্র best = OPT হয়ে যায়। ফলে best-first শুধু bound < OPT-এর node (আর কিছু bound = OPT-এর node) খোলে — যা না খুলে উপায় নেই। পর্ব ৪৩-এর A*-এর সর্বোত্তমতার যুক্তিও হুবহু এটাই: A* আসলে shortest path-এর ওপর best-first branch and bound, যেখানে admissible heuristic হলো lower bound.

পরীক্ষা ও ইন্টারভিউতে — এই প্রশ্নগুলো প্রায়ই আসে:

  • Backtracking আর branch and bound-এর পার্থক্য? Backtracking শুধু শর্ত ভাঙলে ছাঁটে; B&B optimisation-এ bound আর incumbent তুলনা করেও ছাঁটে, আর যেকোনো ক্রমে (FIFO/LIFO/LC) খুঁজতে পারে।
  • LC search কী? Live node-গুলোর মধ্যে সবচেয়ে কম আনুমানিক খরচের (bound) node-কে পরের E-node বানানো — priority queue দিয়ে।
  • 0/1 knapsack-এর B&B-তে bound কী? বাকি জিনিসে fractional knapsack-এর greedy মান — একটি upper bound; ub ≤ best হলে ছাঁটাই।
  • TSP-এর root-এর reduced cost কীভাবে? প্রতিটি সারির minimum বিয়োগ, তারপর প্রতিটি কলামের minimum বিয়োগ; বিয়োগফলের যোগফল = lower bound.
  • B&B-এর worst-case জটিলতা? Exponential — knapsack-এ O(n · 2ⁿ), TSP-এ (n−1)!-এর ক্রমে node; উন্নতি শুধু গড় বা বাস্তব ইনপুটে, নিশ্চয়তা নেই।

৫ · TSP-এর reduced cost matrix — পাঠ্যবইয়ের ক্লাসিক bound

এবার কুরিয়ার ভ্যানের সমস্যায় ফিরি। পাঁচটি জায়গা, ১ হলো অফিস। Cᵢⱼ = i থেকে j যেতে মিনিট — ঢাকার একমুখী রাস্তা আর জ্যামের কারণে i → j আর j → i সমান নয় (asymmetric TSP). নিজের কাছে যাওয়া নিষেধ, তাই কর্ণে ∞. পর্ব ৫৫-এ এই সমস্যার Held–Karp DP দেখেছেন — O(n² 2ⁿ) সময়, কিন্তু O(n 2ⁿ) মেমরিও লাগে; পর্ব ৫৬-এ Hamiltonian cycle-এর backtracking. এখন branch and bound.

কেন সারি-কলাম বিয়োগ একটি lower bound

মূল পর্যবেক্ষণ: প্রতিটি tour প্রতিটি শহর থেকে ঠিক একবার বের হয়, আর প্রতিটি শহরে ঠিক একবার ঢোকে। অর্থাৎ ম্যাট্রিক্সের প্রতিটি সারি থেকে ঠিক একটি আর প্রতিটি কলাম থেকে ঠিক একটি ঘর ব্যবহার করে। এখন সারি i-এর প্রতিটি ঘর থেকে একই সংখ্যা r বিয়োগ করলে প্রতিটি tour-এর খরচ ঠিক r কমে — কারণ প্রতিটি tour সারি i-এর ঠিক একটি ঘর নেয়। কলামের বেলায়ও তাই। সারির minimum বিয়োগ করলে কোনো ঘর ঋণাত্মক হয় না।

তাই সব সারি আর কলাম থেকে মোট R বিয়োগ করে পাওয়া ম্যাট্রিক্সে (reduced cost matrix) প্রতিটি tour-এর খরচ = আসল খরচ − R, আর এটি ≥ ০ (সব ঘর অঋণাত্মক)। সুতরাং আসল খরচ ≥ R — প্রতিটি tour-এর জন্য। R-ই root-এর lower bound. নিচের স্টেপারে পুরো হিসাব, তারপর root-এর চারটি সন্তানের bound.

সারি = থেকে, কলাম = দিকে; ∞ = যাওয়া নিষেধসবুজ = এই ধাপে বদলেছে

            
প্রতিটি ধাপে
এই ধাপ
bound
  1. আসল ম্যাট্রিক্স। প্রতিটি সারির সবচেয়ে ছোট মান ডানে: ১০, ২, ২, ৩, ৪. যেকোনো tour শহর ১ থেকে বের হতে অন্তত ১০ মিনিট খরচ করবেই, শহর ২ থেকে অন্তত ২ — এভাবে মোট অন্তত ২১.
  2. সারি বিয়োগ। প্রতিটি সারি থেকে তার minimum বিয়োগ করা হলো; প্রতিটি সারিতে এখন অন্তত একটি ০. কিন্তু কলাম ১-এ সবচেয়ে ছোট ১, কলাম ৩-এ ৩ — অর্থাৎ শহর ১-এ ঢুকতে আরও অন্তত ১, শহর ৩-এ অন্তত ৩ লাগবে। কলাম মোট ৪.
  3. কলাম বিয়োগ = root. কলাম ১ থেকে ১, কলাম ৩ থেকে ৩ বিয়োগ। এখন প্রতিটি সারি আর প্রতিটি কলামে অন্তত একটি ০. Root-এর bound = ২১ + ৪ = ২৫: কোনো tour ২৫ মিনিটের কম হতে পারে না।
  4. সন্তান 1→2. নিয়ম: সারি ১ (১ থেকে আর কোথাও নয়) আর কলাম ২ (২-এ আর কেউ ঢুকবে না) পুরো ∞; আর ঘর (২, ১) ∞, যাতে ২ থেকে সরাসরি অফিসে ফিরে ছোট একটি চক্র না হয়। তারপর আবার reduce. এখানে প্রতিটি সারি-কলামে ০ আছে, বিয়োগ ০. Bound = মা-বাবার ২৫ + reduced ম্যাট্রিক্সে ঘর (১, ২)-এর মান ১০ + নতুন বিয়োগ ০ = ৩৫.
  5. সন্তান 1→3. সারি ১, কলাম ৩ আর ঘর (৩, ১) ∞. কলাম ১-এর একমাত্র ০ ছিল (৩, ১)-এ — সেটি গেল, তাই কলাম ১-এর নতুন minimum ১১, বিয়োগ ১১. Bound = ২৫ + ১৭ + ১১ = ৫৩.
  6. সন্তান 1→4. ঘর (১, ৪)-এর মান ০, আর ∞ বসানোর পরেও সব সারি-কলামে ০ আছে। Bound = ২৫ + ০ + ০ = ২৫ — root-এর সমান, সবচেয়ে আশাব্যঞ্জক।
  7. সন্তান 1→5. ঘর (১, ৫) = ১. কলাম ৫ বন্ধ হওয়ায় সারি ২ আর সারি ৪-এর ০ হারিয়েছে: সারি ২ থেকে ২, সারি ৪ থেকে ৩ বিয়োগ, মোট ৫. Bound = ২৫ + ১ + ৫ = ৩১. Best-first এবার 1→4 খুলবে।

সন্তানের নিয়মটি কেন বৈধ: মা-বাবার reduced ম্যাট্রিক্স M-এ প্রতিটি tour-এর খরচ = মা-বাবার bound + M-এ tour-এর ঘরগুলোর যোগফল (ওপরের যুক্তিতে)। i → j ধরে নিলে tour-এর একটি ঘর M[i][j]; বাকি ঘরগুলো সারি i বা কলাম j-তে থাকতে পারে না, আর (j, ১)-ও নয় (শেষ ধাপ ছাড়া)। তাই ওগুলো ∞ করলে কোনো বৈধ tour হারায় না, আর নতুন ম্যাট্রিক্সকে আবার reduce করলে আরও r নিশ্চিত হয়। মোট: b(সন্তান) = b(মা-বাবা) + M[i][j] + r ≥ b(মা-বাবা) — monotone. Little, Murty, Sweeney ও Karel-এর ১৯৬৩ সালের মূল পদ্ধতি একটু অন্যভাবে branch করত — প্রতিটি ধাপে একটি edge “নাও” আর “নিও না” এই দুই ভাগে — কিন্তু bound-এর চিন্তাটি এই সারি-কলাম বিয়োগই।

পুরো খোঁজ — best-first, nearest neighbour দিয়ে শুরু

শুরুর incumbent হিসেবে একটি দ্রুত আনুমানিক রুট নিই: অফিস থেকে প্রতিবার সবচেয়ে কাছের না-যাওয়া জায়গায় যাওয়া — 1 → 4 (১০) → 5 (৩) → 2 (৪) → 3 (১৬) → 1 (৩), মোট ৩৬ (অংশ ৯-এ বিস্তারিত)। এবার গাছ:

পথ আর bound (reduced cost matrix)সবুজ = সম্পূর্ণ tour, লাল = ছাঁটাই

            
প্রতিটি ধাপে
E-node
incumbent
live node (bound ছোট আগে)
  1. শুরু। Root-এর bound ২৫, incumbent ৩৬. আসল উত্তর ২৫ থেকে ৩৬-এর মধ্যে কোথাও — এই ফাঁকটিই optimality gap. লক্ষ্য: হয় ভালো রুট খুঁজে incumbent নামানো, নয়তো প্রমাণ করা যে এর চেয়ে ভালো নেই।
  2. Root খোলা। আগের স্টেপারের চার সন্তান: ৩৫, ৫৩, ২৫, ৩১. 1-3-এর bound ৫৩ ≥ ৩৬ — তৈরি হওয়ামাত্র ছাঁটাই, queue-তে ঢোকেই না। বাকি তিনটি live.
  3. 1-4 খোলা (bound ২৫). সন্তান 1-4-2 = ২৮, 1-4-3 = ৫০, 1-4-5 = ৩৬. ৫০ আর ৩৬ দুটিই ≥ ৩৬, ছাঁটাই (৩৬ সমান — incumbent-এর চেয়ে ভালো হতে পারে না)।
  4. 1-4-2 খোলা (২৮). সন্তান 1-4-2-3 = ৫২ — ছাঁটাই; 1-4-2-5 = ২৮ — live. এখন সবচেয়ে ছোট bound আবার ২৮.
  5. 1-4-2-5 খোলা (২৮). বাকি শুধু শহর ৩, তারপর অফিস। সম্পূর্ণ tour 1-4-2-5-3-1, আসল ম্যাট্রিক্স থেকে খরচ ১০ + ৬ + ২ + ৭ + ৩ = ২৮ — leaf-এর bound-ও ঠিক ২৮. Incumbent ৩৬ থেকে নেমে ২৮.
  6. থামা। Queue-এর সবচেয়ে ছোট bound ৩১ ≥ ২৮. 1-5 আর 1-2-এর নিচে কোনো tour ৩১ ও ৩৫-এর কম নয়, তাই ২৮-কে হারাতে পারে না — দুটিই না খুলে বাদ। মাত্র ৪টি node খুলে, ২৪টি tour-এর বেশিরভাগ না দেখেই প্রমাণ: ২৮ সর্বোত্তম।

জটিলতা: গভীরতা k-তে node সংখ্যা (n−1)(n−2)…(n−k), মোট ≤ e · (n−1)!. প্রতিটি node-এ ম্যাট্রিক্স কপি আর reduce O(n²), heap-এর কাজ O(log((n−1)!)) = O(n log n). তাই worst case সময় O(n² · (n−1)!) = O(n · n!) — brute force-এর মতোই খারাপ; উন্নতি শুধু bound যতটা ছাঁটে। জায়গা: best-first-এ প্রতিটি live node নিজের n × n ম্যাট্রিক্স রাখে, worst case O(n² · (n−1)!); depth-first-এ শুধু পথের n টি ম্যাট্রিক্স, O(n³).

৬ · কোডে TSP branch and bound — brute force দিয়ে যাচাই

প্রোগ্রামটি প্রথমে nearest neighbour দিয়ে incumbent বানায়, তারপর ওপরের best-first খোঁজ চালায়: priority queue-তে (bound, পথ, reduced ম্যাট্রিক্স)। শেষে ২৪টি tour-এর brute force দিয়ে উত্তর মেলায় — ছোট ইনপুটে এমন যাচাই প্রতিটি branch and bound কোডের সাথে থাকা উচিত।

#include <algorithm>
#include <cstdio>
#include <queue>
#include <string>
#include <tuple>
#include <vector>
using namespace std;
using Mat = vector<vector<int>>; const int INF = 1e9, n = 5;
const Mat C = {{INF, 20, 30, 10, 11}, {15, INF, 16, 4, 2}, {3, 5, INF, 2, 4}, {19, 6, 18, INF, 3}, {16, 4, 7, 16, INF}};
int cost(const vector<int>& t) { int s = 0; for (int k = 0; k < n; k++) s += C[t[k]][t[(k + 1) % n]]; return s; }
string fmt(const vector<int>& p) { string s; for (int c : p) s += (s.empty() ? "" : "-") + to_string(c + 1); return s; }
int reduce(Mat& M) {                              // subtract row minima, then column minima
    int total = 0;
    for (auto& row : M) { int m = ranges::min(row); if (m > 0 && m < INF) { total += m; for (int& x : row) if (x < INF) x -= m; } }
    for (int j = 0; j < n; j++) { int m = INF; for (auto& row : M) m = min(m, row[j]);
        if (m > 0 && m < INF) { total += m; for (auto& row : M) if (row[j] < INF) row[j] -= m; } }
    return total;
}
int main() {
    vector<int> t = {0};                          // nearest neighbour gives the first incumbent
    while ((int)t.size() < n) { int nx = -1;
        for (int j = 0; j < n; j++) if (ranges::find(t, j) == t.end() && (nx < 0 || C[t.back()][j] < C[t.back()][nx])) nx = j;
        t.push_back(nx); }
    int best = cost(t), expanded = 0; vector<int> tour = t;
    printf("nearest neighbour %s-1 cost %d\n", fmt(t).c_str(), best);
    using Node = tuple<int, vector<int>, Mat>;    // (bound, path, reduced matrix), min-heap
    priority_queue<Node, vector<Node>, greater<Node>> pq; Mat M0 = C; pq.push({reduce(M0), {0}, M0});
    while (!pq.empty() && get<0>(pq.top()) < best) {   // smallest bound >= best: stop
        auto [b, path, M] = pq.top(); pq.pop();
        expanded++; printf("expand %s bound %d\n", fmt(path).c_str(), b);
        for (int i = path.back(), j = 0; j < n; j++) if (M[i][j] < INF) {
            Mat M2 = M; vector<int> p = path; p.push_back(j);
            for (int k = 0; k < n; k++) M2[i][k] = M2[k][j] = INF;
            if ((int)p.size() < n) M2[j][0] = INF;        // no early return to city 1
            int nb = b + M[i][j] + reduce(M2);
            if (nb >= best) printf("  %s bound %d pruned\n", fmt(p).c_str(), nb);
            else if ((int)p.size() == n) { best = nb; tour = p; printf("  %s-1 complete, new best %d\n", fmt(p).c_str(), nb); }
            else { printf("  %s bound %d queued\n", fmt(p).c_str(), nb); pq.push({nb, p, M2}); }
        }
    }
    printf("best %s-1 cost %d | expanded %d, left in queue %zu\n", fmt(tour).c_str(), best, expanded, pq.size());
    vector<int> perm = {0, 1, 2, 3, 4}; int bf = INF;   // brute force: city 1 fixed, 4! orders
    do bf = min(bf, cost(perm)); while (next_permutation(perm.begin() + 1, perm.end()));
    printf("brute force over all 24 tours: %d | match: %s\n", bf, bf == best ? "yes" : "no");
}

আউটপুট:

nearest neighbour 1-4-5-2-3-1 cost 36
expand 1 bound 25
  1-2 bound 35 queued
  1-3 bound 53 pruned
  1-4 bound 25 queued
  1-5 bound 31 queued
expand 1-4 bound 25
  1-4-2 bound 28 queued
  1-4-3 bound 50 pruned
  1-4-5 bound 36 pruned
expand 1-4-2 bound 28
  1-4-2-3 bound 52 pruned
  1-4-2-5 bound 28 queued
expand 1-4-2-5 bound 28
  1-4-2-5-3-1 complete, new best 28
best 1-4-2-5-3-1 cost 28 | expanded 4, left in queue 2
brute force over all 24 tours: 28 | match: yes

দুই স্টেপারের সাথে প্রতিটি সংখ্যা মেলে। reduce ম্যাট্রিক্সটি জায়গায় বদলায় আর মোট বিয়োগফল ফেরত দেয়; INF ঘর কখনো বিয়োগ হয় না, আর পুরো ∞ সারি বা কলাম বাদ যায়। M2[j][0] = INF বসে শুধু শেষ শহরের আগে পর্যন্ত — শেষ শহরে অফিসে ফেরার ঘর খোলা থাকে, তাই leaf-এর bound ঠিক আসল খরচ (২৮)। লুপের শর্ত get<0>(pq.top()) < best হলো best-first-এর থামার নিয়ম। প্রতিটি node নিজের Mat কপি রাখে — n = ৩০-এ এটিই মেমরি খেয়ে ফেলে।

import heapq, itertools
INF = 10**9
C = [[INF, 20, 30, 10, 11], [15, INF, 16, 4, 2], [3, 5, INF, 2, 4],
     [19, 6, 18, INF, 3], [16, 4, 7, 16, INF]]
n = len(C)
cost = lambda t: sum(C[t[k]][t[(k + 1) % n]] for k in range(n))
fmt = lambda p: "-".join(str(c + 1) for c in p)

def reduce(M):                                  # subtract row minima, then column minima
    total = 0
    for row in M:
        m = min(row)
        if 0 < m < INF: total += m; row[:] = [x - m if x < INF else INF for x in row]
    for j in range(n):
        m = min(row[j] for row in M)
        if 0 < m < INF:
            total += m
            for row in M: row[j] = row[j] - m if row[j] < INF else INF
    return total

t = [0]                                         # nearest neighbour gives the first incumbent
while len(t) < n: t.append(min((j for j in range(n) if j not in t), key=lambda j: C[t[-1]][j]))
best, tour = cost(t), t
print(f"nearest neighbour {fmt(t)}-1 cost {best}")
M = [row[:] for row in C]
heap, expanded = [(reduce(M), [0], M)], 0      # (bound, path, reduced matrix)
while heap and heap[0][0] < best:               # smallest bound >= best: stop
    b, path, M = heapq.heappop(heap)
    expanded += 1; print(f"expand {fmt(path)} bound {b}")
    i = path[-1]
    for j in (j for j in range(n) if M[i][j] < INF):
        M2 = [row[:] for row in M]
        for k in range(n): M2[i][k] = M2[k][j] = INF
        if len(path) + 1 < n: M2[j][0] = INF   # no early return to city 1
        nb, p = b + M[i][j] + reduce(M2), path + [j]
        if nb >= best: print(f"  {fmt(p)} bound {nb} pruned")
        elif len(p) == n: best, tour = nb, p; print(f"  {fmt(p)}-1 complete, new best {nb}")
        else: print(f"  {fmt(p)} bound {nb} queued"); heapq.heappush(heap, (nb, p, M2))
print(f"best {fmt(tour)}-1 cost {best} | expanded {expanded}, left in queue {len(heap)}")
bf = min(cost((0,) + p) for p in itertools.permutations(range(1, n)))
print(f"brute force over all 24 tours: {bf} | match: {'yes' if bf == best else 'no'}")

আউটপুট (C++-এর সাথে হুবহু এক):

nearest neighbour 1-4-5-2-3-1 cost 36
expand 1 bound 25
  1-2 bound 35 queued
  1-3 bound 53 pruned
  1-4 bound 25 queued
  1-5 bound 31 queued
expand 1-4 bound 25
  1-4-2 bound 28 queued
  1-4-3 bound 50 pruned
  1-4-5 bound 36 pruned
expand 1-4-2 bound 28
  1-4-2-3 bound 52 pruned
  1-4-2-5 bound 28 queued
expand 1-4-2-5 bound 28
  1-4-2-5-3-1 complete, new best 28
best 1-4-2-5-3-1 cost 28 | expanded 4, left in queue 2
brute force over all 24 tours: 28 | match: yes

heap[0][0] হলো সবচেয়ে ছোট bound, না সরিয়েই দেখা যায়। Bound সমান হলে path (list) তুলনা হয় — পথগুলো আলাদা, তাই ম্যাট্রিক্স তুলনা পর্যন্ত যেতে হয় না, আর ক্রম C++-এর মতোই। row[:] = [...] সারিটি জায়গায় বদলায়, তাই reduce-এর বদল ডাকার জায়গায় দেখা যায়। itertools.permutations(range(1, n)) শহর ১ স্থির রেখে ৪! = ২৪টি ক্রম দেয়।

৭ · আরও bound — দুই সস্তা edge, MST আর 1-tree

Reduced matrix ছাড়াও TSP-এর জন্য বেশ কিছু bound আছে, বিশেষত symmetric TSP-এ (i → j আর j → i-এর খরচ সমান — যেমন সোজা দূরত্ব)। একটি পাঁচ শহরের উদাহরণ নিই — a, b, c, d, e:

abcde
a—৩১৫৮
b৩—৬৭৯
c১৬—৪২
d৫৭৪—৩
e৮৯২৩—

সর্বোত্তম tour a–b–d–e–c–a: ৩ + ৭ + ৩ + ২ + ১ = ১৬ (১২টি আলাদা tour-এর brute force দিয়ে যাচাই করা)। এবার চারটি bound তুলনা করি।

দুই সস্তা edge-এর যোগফলের অর্ধেক

Tour-এ প্রতিটি শহর ঠিক দুটি edge-এ লাগানো — একটিতে ঢোকে, একটিতে বের হয়। প্রতিটি শহরের জন্য তার দুটি tour-edge যোগ করলে প্রতিটি edge দুবার গোনা হয় (দুই প্রান্তে), তাই যোগফল = ২ × tour. আবার প্রতিটি শহরের দুটি tour-edge-এর যোগফল ≥ সেই শহরের দুটি সবচেয়ে সস্তা edge-এর যোগফল। তাই

tour ≥ ⌈ (প্রতিটি শহরের দুই সস্তা edge-এর যোগফল) / ২ ⌉

এখানে: a (১, ৩), b (৩, ৬), c (১, ২), d (৩, ৪), e (২, ৩) — মোট ২৮, bound ১৪. দূরত্ব পূর্ণসংখ্যা হলে tour-ও পূর্ণসংখ্যা, তাই ceil বৈধ। Branch করার সময় পথে নেওয়া edge-গুলোকে সংশ্লিষ্ট শহরের “দুই edge”-এর মধ্যে জোর করে ধরতে হয়, তাতে গাছের যত গভীরে যাবেন, bound তত শক্ত হয়। আগে থেকে প্রতিটি শহরের দুই minimum রাখলে হিসাব O(n).

MST আর 1-tree

পর্ব ৪৫-এর minimum spanning tree মনে করুন। একটি tour থেকে যেকোনো একটি edge সরালে থাকে সব শহর ছোঁয়া একটি পথ — সেটি একটি spanning tree. তাই tour > tour − (একটি edge) ≥ MST. এখানে MST = a–c ১, c–e ২, a–b ৩, d–e ৩ = ৯ — বৈধ, কিন্তু দুর্বল।

উন্নতি — 1-tree: একটি শহর (ধরুন a) আলাদা করে রাখুন। Tour থেকে a আর তার দুটি edge সরালে বাকি চার শহরে একটি পথ থাকে, যা ওই চার শহরের একটি spanning tree. তাই tour = (a-র দুটি edge) + (বাকিদের একটি spanning tree) ≥ (a-র দুটি সস্তা edge) + (বাকিদের MST). এখানে {b, c, d, e}-এর MST = c–e ২, d–e ৩, b–c ৬ = ১১, আর a-র দুই সস্তা edge ১ + ৩ = ৪; 1-tree bound = ১৫. কোন শহরকে আলাদা রাখবেন তা বদলে সবচেয়ে বড়টি নিতে পারেন — এখানে a বা b নিলে ১৫, অন্যগুলোয় ১৩-১৪.

Boundমানহিসাবের খরচ
শুধু MST৯O(n²) (dense Prim)
দুই সস্তা edge / ২১৪O(n) (আগে থেকে রাখা minimum থেকে)
1-tree (সেরা বিশেষ শহর)১৫প্রতিটি বিশেষ শহরে O(n²)
সর্বোত্তম tour১৬— (যা খুঁজছি)

আরও গভীরে — Held–Karp bound: 1-tree-এ কিছু শহরের degree ২-এর বেশি, কিছুর ১ — tour-এ সবার ঠিক ২. Held ও Karp (১৯৭০, ১৯৭১) প্রতিটি শহর v-তে একটি penalty πv বসালেন: edge (u, v)-এর নতুন খরচ w + πu + πv. প্রতিটি tour-এর খরচ ঠিক ২Σπ বাড়ে (প্রতিটি শহর দুবার), তাই (নতুন খরচে 1-tree) − ২Σπ এখনো একটি বৈধ lower bound — যেকোনো π-তে। যে শহরের degree বেশি তার π বাড়ালে 1-tree তাকে এড়াতে চায়; এভাবে subgradient optimisation দিয়ে π ঠিক করে সবচেয়ে বড় bound-টিই Held–Karp bound. এটি Lagrangian relaxation-এর বিখ্যাত উদাহরণ, আর symmetric TSP-এর subtour LP relaxation-এর মানের সমান; বহু বাস্তব instance-এ আসল উত্তরের প্রায় ১%-এর মধ্যে থাকে। পর্ব ৬৬-এ metric TSP-এর approximation-এও MST আবার আসবে।

৮ · Job assignment — প্রতিটি সারির সবচেয়ে সস্তা কাজ

একটি গার্মেন্টস লাইনে চারজন অপারেটর — a, b, c, d — আর চারটি কাজ: ১ কাটিং, ২ সেলাই, ৩ বোতাম, ৪ প্যাকিং. খরচের ম্যাট্রিক্সে সারি = অপারেটর, কলাম = কাজ, মান = মিনিট। প্রত্যেককে ঠিক একটি কাজ, প্রতিটি কাজ ঠিক একজনকে; মোট মিনিট সবচেয়ে কম চাই — এটাই assignment problem.

কাজ ১কাজ ২কাজ ৩কাজ ৪
a৯২৭৮
b৬৪৩৭
c৫৮১৮
d৭৬৯৪

Bound: যাঁদের কাজ ঠিক হয়েছে তাঁদের খরচ + বাকি প্রত্যেক অপারেটরের, এখনো খালি কাজগুলোর মধ্যে সবচেয়ে সস্তাটি। বৈধ কেন: যেকোনো পূর্ণ বণ্টনে বাকি প্রত্যেক অপারেটর কোনো একটি খালি কাজ পাবেনই, আর তার খরচ সেই সারির minimum-এর ≥. শিথিল করা শর্ত হলো “দুজন একই কাজ পাবেন না” — bound দুজনকে একই সস্তা কাজে বসাতে দেয়। প্রতিটি স্তরে একজন অপারেটরের কাজ ঠিক হয়: স্তর ১-এ a, স্তর ২-এ b, এভাবে।

a→2 মানে অপারেটর a পেলেন কাজ ২সবুজ = সর্বোত্তম, লাল = ছাঁটাই

            
প্রতিটি ধাপে
incumbent
live node (lb ছোট আগে)
  1. Root. কেউ কাজ পাননি। প্রতিটি সারির minimum: a ২, b ৩, c ১, d ৪ — lb = ১০. কোনো বণ্টন ১০ মিনিটের কম হতে পারে না।
  2. স্তর ১ — a-র চার বিকল্প. a→1: ৯ + (b, c, d-র কাজ ২, ৩, ৪-এর মধ্যে minimum: ৩, ১, ৪) = ১৭. a→2: ২ + ৩ + ১ + ৪ = ১০. a→3: ৭ + (b ৪, c ৫, d ৪) = ২০. a→4: ৮ + (b ৩, c ১, d ৬) = ১৮. সবচেয়ে আশাব্যঞ্জক a→2.
  3. a→2 খোলা — b-র তিন বিকল্প. b→1: ২ + ৬ + (c, d-র কাজ ৩, ৪: ১, ৪) = ১৩. b→3: ২ + ৩ + (c, d-র কাজ ১, ৪: ৫, ৪) = ১৪. b→4: ২ + ৭ + (c, d-র কাজ ১, ৩: ১, ৭) = ১৭.
  4. b→1 খোলা. বাকি দুজন, দুটি কাজ — দুটি সম্পূর্ণ বণ্টন: c→3, d→4 খরচ ২ + ৬ + ১ + ৪ = ১৩; c→4, d→3 খরচ ২ + ৬ + ৮ + ৯ = ২৫. প্রথমটি incumbent ১৩, দ্বিতীয়টি ১৩-এর চেয়ে খারাপ বলে বাদ।
  5. বাকি সব ছাঁটাই. Live node-গুলোর lb ১৪, ১৭, ১৭, ১৮, ২০ — সবই ≥ ১৩. কোনোটির নিচে ১৩-এর কম খরচের বণ্টন নেই। উত্তর ১৩: a সেলাই, b কাটিং, c বোতাম, d প্যাকিং. সম্ভাব্য ৪! = ২৪টি বণ্টনের মাত্র দুটি পূর্ণ খরচ হিসাব হলো।

একটি সৎ সতর্কতা: সাধারণ assignment problem কঠিন নয় — Hungarian algorithm (Kuhn, ১৯৫৫; পরের উন্নত রূপে) একে O(n³) সময়ে সমাধান করে। Branch and bound এখানে শেখানোর ভালো উদাহরণ, আর কাজে লাগে বাড়তি শর্ত যোগ হলে (যেমন কাজের ক্ষমতা আলাদা — generalized assignment, যা NP-hard). বাস্তবে আগে দেখুন polynomial অ্যালগরিদম আছে কি না।

৯ · ভালো শুরু, বাস্তবের solver আর একটু ইতিহাস

শুরুর উত্তর কেন এত জরুরি

ছাঁটাইয়ের নিয়মের দুই দিক: bound যত শক্ত, আর incumbent যত ভালো, তত বেশি ছাঁটা যায়। দ্বিতীয়টি সস্তায় পাওয়ার উপায় একটি দ্রুত heuristic. TSP-এর সবচেয়ে সরলটি nearest neighbour: যেখানে আছেন সেখান থেকে সবচেয়ে কাছের না-যাওয়া শহরে যান, O(n²) সময়ে। আমাদের ম্যাট্রিক্সে এটি দিয়েছে ৩৬, সর্বোত্তমের চেয়ে প্রায় ২৯% বেশি — তবু অংশ ৪-এর টেবিলে দেখেছেন, এটুকুতেই FIFO-র খোলা node ২০ থেকে ৬, আর সর্বোচ্চ queue ২৪ থেকে ৩.

Nearest neighbour-এর গুণগত নিশ্চয়তা নেই — triangle inequality মানা ইনপুটেও এটি সর্বোত্তমের log n-এর ক্রমের গুণ খারাপ হতে পারে (Rosenkrantz, Stearns ও Lewis, ১৯৭৭)। তাই এর পরে চালানো হয় local search, যেমন 2-opt: দুটি edge সরিয়ে উল্টো করে জোড়া দিলে tour ছোট হলে বদলান। কয়েক মিলিসেকেন্ডের এই উন্নতি branch and bound-এর ঘণ্টা বাঁচাতে পারে।

বাস্তবের solver — LP relaxation আর branch and cut

শিল্পে branch and bound-এর সবচেয়ে বড় ব্যবহার integer programming-এ: এয়ারলাইনের ক্রু-রোস্টার, বাস-ট্রেনের সময়সূচি, কারখানার উৎপাদন পরিকল্পনা, বিদ্যুৎকেন্দ্র কখন চালু হবে। সমস্যাগুলো লেখা হয় linear শর্ত আর পূর্ণসংখ্যা চলক দিয়ে। Bound আসে LP relaxation থেকে: “পূর্ণসংখ্যা হতে হবে” শর্ত তুলে দিয়ে সাধারণ linear program, যা simplex বা interior-point পদ্ধতিতে দ্রুত সমাধান হয়। LP-র উত্তরে কোনো চলক x = 2.6 এলে branch: একটি শাখায় x ≤ 2, অন্যটিতে x ≥ 3 — Land ও Doig-এর মূল পদ্ধতির আধুনিক রূপ। (Fractional knapsack আসলে knapsack-এর LP relaxation-ই।)

আধুনিক solver (Gurobi, CPLEX, SCIP, HiGHS ইত্যাদি) প্রতিটি node-এ LP-তে নতুন বৈধ শর্ত — cutting plane — যোগ করে, যা প্রতিটি পূর্ণসংখ্যা সমাধান মানে কিন্তু বর্তমান ভগ্নাংশ উত্তরটিকে কেটে বাদ দেয়; bound শক্ত হয়। এই মিশ্রণের নাম branch and cut. Solver চলার সময় incumbent আর সেরা bound-এর ফাঁক (gap) দেখায়, আর “gap ১%-এর নিচে নামলেই থামো” বলা যায় — প্রমাণিত মানের উত্তর, অনেক কম সময়ে।

TSP-এর চূড়ান্ত উদাহরণ Concorde — David Applegate, Robert Bixby, Vašek Chvátal ও William Cook-এর branch-and-cut প্রোগ্রাম। এটি ২০০৪ সালে সুইডেনের ২৪,৯৭৮টি শহরের আর ২০০৬ সালে একটি চিপ-নকশা থেকে আসা ৮৫,৯০০ বিন্দুর TSP প্রমাণসহ সর্বোত্তম সমাধান করেছে। সম্ভব হয়েছে শক্ত bound (Held–Karp-ধাঁচের relaxation আর হাজারো cut), ভালো শুরুর tour (Lin–Kernighan local search) আর চতুর branching-এর মিলনে।

১৯৫৪ সালে George Dantzig, Ray Fulkerson ও Selmer Johnson cutting plane দিয়ে যুক্তরাষ্ট্রের ৪৯টি শহরের একটি TSP সমাধান করেন — প্রায় হাতে-কলমে। ১৯৬০ সালে লন্ডন স্কুল অব ইকোনমিক্সের Ailsa Land ও Alison Doig Econometrica-তে integer programming-এর জন্য LP relaxation-এর ওপর গাছ-খোঁজার পদ্ধতি প্রকাশ করেন — branch and bound-এর জন্ম, যদিও নামটি তখনো দেওয়া হয়নি। ১৯৬৩ সালে John Little, Katta Murty, Dura Sweeney ও Caroline Karel TSP-এর জন্য reduced matrix-ভিত্তিক অ্যালগরিদম প্রকাশ করেন, আর “branch and bound” নামটি সেখানেই প্রথম আসে বলে প্রচলিত। Knapsack-এর জন্য Kolesar ১৯৬৭ সালে একটি branch and bound অ্যালগরিদম প্রকাশ করেন। Held ও Karp ১৯৬২-তে DP, আর ১৯৭০-৭১-এ 1-tree bound দেন। ১৯৮০-৯০-এর দশকে Padberg ও Rinaldi বড় TSP-তে cutting plane আর branching মিলিয়ে branch and cut-কে জনপ্রিয় করেন, যার উত্তরসূরি Concorde.

১০ · AI-এর যুগে branch and bound — bound-এর বৈধতাই আসল প্রশ্ন

“TSP-এর branch and bound লিখে দাও” — AI চটপট কোড দেবে, আর ৪-৫ শহরের উদাহরণে উত্তরও প্রায়ই ঠিক আসবে। কিন্তু branch and bound-এর বাগ ভুল উত্তর দেয় মাঝেমধ্যে: একটি অবৈধ bound বেশিরভাগ ইনপুটে ক্ষতি করে না, কোনো এক ইনপুটে সর্বোত্তম শাখাটি ছেঁটে দেয়। আর জটিলতার দাবিতে AI প্রায়ই worst case আড়াল করে।

এই বিষয়ে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
Knapsack-এ জিনিস অনুপাতে না সাজিয়েই fractional bound — bound তখন আর upper bound নয়ইনপুট এলোমেলো ক্রমে দিয়ে DP-র সাথে মেলান; ভুল কোড কখনো কখনো কম উত্তর দেবে
“Branch and bound-এর জটিলতা O(n²)” বা “polynomial” বলাজিজ্ঞেস করুন worst case-এ কতটি node; TSP NP-hard, polynomial দাবি মানে P = NP-এর দাবি
Minimisation আর maximisation গুলিয়ে ছাঁটাইয়ের অসমতা উল্টো লেখা — যেমন TSP-এ “b(x) ≤ best হলে ছাঁটাই”হাতে ৩-৪ ধাপ trace করুন; প্রতিটি ছাঁটাইয়ের পাশে লিখুন কোন সংখ্যা কোনটির চেয়ে বড়
“Heuristic” আর “bound” গুলিয়ে ফেলা — যেমন nearest neighbour-এর খরচকে lower bound বলাLower bound সবসময় সর্বোত্তমের ≤; nearest neighbour-এর মান ≥ সর্বোত্তম — এটি incumbent, bound নয়
TSP-এর সন্তান ম্যাট্রিক্সে (j, start) ∞ করতে ভুলে যাওয়া, বা শেষ শহরেও ∞ করে ফেলাপ্রথমটিতে ছোট চক্র ঢোকে, দ্বিতীয়টিতে leaf-এর খরচ ভুল আসে; brute force দিয়ে ২৪টি tour মিলিয়ে দেখুন
Best-first-এ incumbent পাওয়ামাত্র থামা, queue-তে ছোট bound বাকি থাকতেওথামার শর্ত হবে “queue-এর সবচেয়ে ছোট bound ≥ best”; FIFO বা DFS-এ সব live node শেষ না হওয়া পর্যন্ত চলবে

AI-কে সমাধান লিখে দিতে না বলে আপনার bound, trace আর প্রমাণ পরীক্ষা করতে বলুন:

আমি DSA শিখছি, এখন branch and bound. নিচে আমার লেখা TSP-এর (বা knapsack-এর) কোড, আমার ব্যবহার করা bound, আর একটি ছোট ইনপুটে হাতে করা trace দিলাম: [এখানে কোড, bound-এর বর্ণনা আর trace পেস্ট করুন] পুরো সমাধান নতুন করে লিখে দিও না। শুধু এগুলো করো: ১. আমার bound কেন বৈধ (minimisation-এ প্রতিটি সমাধানের খরচের ≤), তার প্রমাণ ধাপে ধাপে লেখো; বৈধ না হলে একটি ছোট প্রতি-উদাহরণ দাও। ২. আমার ইনপুটে প্রতিটি E-node, তার সন্তানদের bound, আর কোনটি কেন ছাঁটা পড়ল — একটি টেবিলে দাও; আমার trace-এর সাথে কোথায় অমিল বলো। ৩. আমার ছাঁটাইয়ের শর্ত আর থামার শর্ত (best-first-এ) ঠিক আছে কি না যাচাই করো। ৪. Worst-case time আর space complexity কত — node সংখ্যা, প্রতিটি node-এর খরচ আর queue-র আকার আলাদা করে হিসাব করো। ৫. এলোমেলো ছোট ইনপুটে brute force বা DP-র সাথে মেলানোর একটি stress test-এর পরিকল্পনা দাও।

Branch and bound কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Worst case-এ branch and bound-ও তো brute force-এর মতো exponential — তাহলে এর আসল মূল্য কোথায়?

প্রশ্নটি ন্যায্য, আর উত্তরের প্রথম অংশ হলো সৎ স্বীকারোক্তি: হ্যাঁ, worst case-এ কোনো উন্নতির নিশ্চয়তা নেই। TSP আর 0/1 knapsack NP-hard (পর্ব ৬৪-৬৫); P ≠ NP হলে কোনো অ্যালগরিদমই এগুলোর সব ইনপুটে polynomial সময়ে নির্ভুল উত্তর দিতে পারবে না। Branch and bound সেই নিয়ম ভাঙে না।

তবু তিনটি জায়গায় এর মূল্য বিশাল। এক, বাস্তবের ইনপুট worst case নয়। কুরিয়ারের রুট, কারখানার সময়সূচি বা চিপের তারের নকশায় গঠন থাকে — কাছের শহর কাছেই, দামি জিনিস দামিই। শক্ত bound এসব ইনপুটে গাছের প্রায় পুরোটাই ছেঁটে ফেলে। Concorde যে ৮৫,৯০০ বিন্দুর TSP সমাধান করেছে, তার সব ক্রম গোনা মহাবিশ্বের বয়সেও সম্ভব নয়; branch and bound ছুঁয়েছে তার অতি ক্ষুদ্র একটি অংশ।

দুই, প্রমাণসহ উত্তর। Heuristic বা local search একটি ভালো উত্তর দেয়, কিন্তু বলতে পারে না সেটি কতটা ভালো। Branch and bound প্রতিটি মুহূর্তে একটি incumbent আর একটি সর্বনিম্ন bound দেয় — gap. মাঝপথে থামালেও আপনি জানেন: “এই রুট সর্বোত্তমের চেয়ে বড়জোর ২% খারাপ।” এয়ারলাইন বা বিদ্যুৎ খাতে ২% মানে কোটি টাকা, তাই এই নিশ্চয়তার দাম আছে।

তিন, n বাড়লে প্রভাব গুণে গুণে। ধরুন bound প্রতিটি স্তরে গড়ে অর্ধেক শাখা ছাঁটে। তাহলে b শাখার গাছ কার্যত b/2 শাখার — ৪০ স্তরে (b/2)৪০ = b৪০/২৪০, প্রায় এক লক্ষ কোটি গুণ কম কাজ। Exponential ভিত্তি ছোট করাও exponential লাভ।

মূল উপলব্ধি: NP-hard মানে “সব ইনপুটে দ্রুত অসম্ভব”, “বাস্তবে অসম্ভব” নয়। Branch and bound সেই ফাঁকে কাজ করে — worst case মেনে নিয়ে, সাধারণ ক্ষেত্রে জেতে, আর সবসময় জানায় কতটা জিতল।

প্র ০২Best-first যদি সবচেয়ে কম node খোলে, তাহলে বাস্তবের solver শুধু best-first চালায় না কেন?

অংশ ৪-এ প্রমাণ দেখেছেন: monotone bound থাকলে best-first শুধু সেই node খোলে যা যেকোনো সঠিক branch and bound-কে খুলতেই হতো। কাগজে এটি সেরা। কিন্তু বাস্তবের তিনটি বাধা আছে।

মেমরি। Best-first-এর queue-তে সব live node একসাথে থাকে, আর সেটি exponential হারে বাড়তে পারে। TSP-এর প্রতিটি node যদি একটি n × n ম্যাট্রিক্স রাখে, n = ৪০-এ প্রতিটি node ১,৬০০টি সংখ্যা; কয়েক কোটি live node মানেই কয়েকশো গিগাবাইট। Depth-first-এর মেমরি শুধু গভীরতার সমান — O(n) node. এজন্যই A*-এর জগতে IDA* (iterative deepening A*) এসেছে: depth-first-এর মেমরিতে best-first-এর মতো আচরণ।

Incumbent দেরিতে আসে। Best-first গাছের ওপরের দিকে চওড়া হয়ে ছড়ায়; একটি সম্পূর্ণ সমাধান হয়তো অনেক পরে পায়। মাঝপথে থামাতে হলে হাতে কিছুই নেই। ব্যবসায় “২ ঘণ্টার মধ্যে যা পাও দাও” খুব সাধারণ চাহিদা — এই anytime আচরণে depth-first ভালো।

Incremental হিসাব। Integer programming-এ মা-বাবা থেকে সন্তানে গেলে LP-তে শুধু একটি শর্ত বদলায়, তাই আগের LP উত্তর থেকে কয়েক ধাপেই নতুনটি পাওয়া যায় (dual simplex-এর warm start). Depth-first-এ পরপর node-গুলো আত্মীয়, এই সুবিধা পুরো পাওয়া যায়; best-first গাছের এক মাথা থেকে আরেক মাথায় লাফায়।

তাই আধুনিক solver মিশ্র কৌশল চালায়: মাঝে মাঝে “diving” — একটি শাখা ধরে দ্রুত গভীরে গিয়ে incumbent খোঁজা — আর বাকি সময় সেরা bound-এর node বাছাই, যাতে gap নামে। কোন node পরে খুলবে তা ঠিক করার নিয়মগুলোও (node selection) বছরের পর বছর গবেষণায় তৈরি।

মূল উপলব্ধি: “সবচেয়ে কম কাজ” আর “সবচেয়ে দ্রুত শেষ” এক জিনিস নয় — মেমরি, ক্যাশ, incremental হিসাব আর মাঝপথে উত্তরের চাহিদা মিলিয়েই আসল নকশা হয়।

প্র ০৩Bound যত শক্ত, তত ভালো — কথাটি কি সবসময় সত্য?

না — মোট সময় = (খোলা node-এর সংখ্যা) × (প্রতিটি node-এর খরচ). শক্ত bound প্রথম গুণককে কমায়, কিন্তু সাধারণত দ্বিতীয়টিকে বাড়ায়। কোনটি জিতবে তা ইনপুট আর আকারের ওপর নির্ভর করে।

অংশ ৭-এর টেবিল আবার দেখুন। “দুই সস্তা edge / ২” প্রতিটি node-এ O(n) — আগে থেকে রাখা minimum থেকে। 1-tree-এ প্রতিটি node-এ একটি MST, O(n²). Held–Karp bound-এ প্রতিটি node-এ বহুবার 1-tree, কারণ penalty বারবার ঠিক করতে হয় — হয়তো O(k · n²), k-টি subgradient ধাপ। আর integer programming-এ LP relaxation বা cut যোগ করা আরও অনেক দামি।

ছোট n-এ সস্তা bound প্রায়ই জেতে: গাছ এমনিতেই ছোট, প্রতিটি node-এ বাড়তি খরচ লাভ খেয়ে ফেলে। বড় n-এ উল্টো: গাছ এত বড় যে প্রতিটি স্তরে কয়েকটি বেশি ছাঁটাই কোটি কোটি node বাঁচায়, তখন প্রতিটি node-এ অনেক হিসাবও সস্তা। Concorde ঠিক এ কারণেই বড় instance-এ root-এ ঘণ্টার পর ঘণ্টা শুধু cut খুঁজে bound শক্ত করে, তারপর branch শুরু করে। কনটেস্টে, যেখানে n ২০-৩০, সাধারণত সস্তা bound আর ভালো ক্রমই (যেমন সবচেয়ে আশাব্যঞ্জক সন্তান আগে) যথেষ্ট।

আরও একটি সূক্ষ্মতা: bound-এর মান নয়, ছাঁটাইয়ের ক্ষমতা গুরুত্বপূর্ণ। দুটি bound-এর গড় পার্থক্য ছোট হলেও যদি একটি ঠিক incumbent-এর সীমার কাছে থাকা node-গুলো ছাঁটতে পারে, তার প্রভাব বিশাল। আর bound-এর পাশাপাশি branching-এর পছন্দও (কোন চলক বা কোন শহর নিয়ে ভাগ) node সংখ্যা কয়েক গুণ বদলাতে পারে।

মূল উপলব্ধি: অ্যালগরিদম নকশায় “সেরা উপাদান” বলে কিছু নেই, আছে ভারসাম্য। মাপুন — কতটি node খুলল, প্রতিটিতে কত সময় — তারপর সিদ্ধান্ত নিন।

প্র ০৪Held–Karp DP (পর্ব ৫৫), backtracking (পর্ব ৫৬) আর branch and bound — একই TSP-এর তিন সমাধান। কখন কোনটি?

তিনটিই নির্ভুল উত্তর দেয়, কিন্তু খরচের চেহারা আলাদা।

  • Backtracking (সব ক্রম, শুধু শর্তে ছাঁটাই): সময় Θ(n!)-এর কাছাকাছি, মেমরি O(n). n ≈ ১০-১১ পর্যন্ত চলে। কোড সবচেয়ে সহজ, আর ছোট ইনপুটে brute force যাচাইয়ের হাতিয়ার।
  • Held–Karp DP: সময় Θ(n² 2ⁿ), মেমরি Θ(n 2ⁿ) — প্রতিটি ইনপুটে, ভালো-খারাপ নির্বিশেষে। n = ২০-এ প্রায় ৪০ কোটি ধাপ আর ২ কোটি ঘরের টেবিল — চলে; n = ৩০-এ টেবিলেই কয়েক হাজার কোটি ঘর, অচল। কনটেস্টে “n ≤ 16” বা “n ≤ 20” দেখলে এটিই প্রথম ভাবনা, কারণ এর সময় অনুমানযোগ্য।
  • Branch and bound: worst case n!-এর ক্রমে, কিন্তু বাস্তব ইনপুটে প্রায়ই অনেক কম; মেমরি কৌশলের ওপর নির্ভর করে। ভালো গঠনের ইনপুটে সরল bound দিয়েও প্রায়ই কয়েক ডজন শহর পর্যন্ত যায়, আর branch and cut দিয়ে হাজার হাজার শহর। কিন্তু সময়ের কোনো নিশ্চয়তা নেই — খারাপ ইনপুটে আটকে যেতে পারে।

সম্পর্কটাও সুন্দর। DP একটি state (S, v)-কে একবারই হিসাব করে — একই সেট ঘুরে একই শহরে শেষ হওয়া দুটি পথের মধ্যে শুধু ছোটটি রাখে। একে branch and bound-এর ভাষায় বলে dominance pruning: “এই আংশিক পথ অন্য একটির চেয়ে খারাপ, অথচ বাকি সম্ভাবনা একই — বাদ।” অনেক বাস্তব B&B কোড এই কৌশলটি মেশায়, যেমন শেষ কয়েকটি শহরের জন্য ছোট DP টেবিল। আর A* (পর্ব ৪৩) হলো best-first B&B, যেখানে graph-এর vertex-ই state — consistent heuristic থাকলে DP-র মতো একই state দুবার খোলে না।

মূল উপলব্ধি: পদ্ধতি বাছাইয়ের প্রশ্ন “কোনটি দ্রুত?” নয়, “কোন নিশ্চয়তা চাই?” — অনুমানযোগ্য সময় (DP), কম মেমরি (backtracking), নাকি বাস্তব ইনপুটে বড় n আর প্রমাণসহ উত্তর (B&B).

প্র ০৫Land ও Doig-এর ১৯৬০ সালের প্রবন্ধ থেকে Concorde — এই ইতিহাস কী শেখায়?

Ailsa Land আর Alison Doig যখন ১৯৬০ সালে integer programming-এর পদ্ধতি প্রকাশ করেন, কম্পিউটার তখন বিরল আর ধীর। তাঁদের চিন্তা ছিল সরল: LP relaxation সমাধান করুন; কোনো চলক ভগ্নাংশ হলে দুই ভাগে ভাগ করুন; যে ভাগের LP মান ইতিমধ্যে পাওয়া পূর্ণসংখ্যা উত্তরের চেয়ে খারাপ, সেটি আর দেখবেন না। নাম ছিল না, কিন্তু branch আর bound দুটোই ছিল। চিন্তাটি এতই সাধারণ যে পরের দশকগুলোতে প্রায় প্রতিটি কঠিন optimisation সমস্যায় এর কোনো না কোনো রূপ ব্যবহার হয়েছে।

তিন বছর পর Little, Murty, Sweeney ও Karel TSP-তে একই দর্শন প্রয়োগ করে নামটি দিলেন। লক্ষ করার মতো ব্যাপার: পদ্ধতিটি জন্মেছে operations research-এ — পরিবহন, উৎপাদন আর সময়সূচির মতো বাস্তব সমস্যার চর্চা থেকে — তাত্ত্বিক কম্পিউটার বিজ্ঞানের কোনো উপপাদ্য থেকে নয়। তত্ত্ব (১৯৭০-এর দশকে NP-completeness) পরে এসে বলল, এই সমস্যাগুলো মৌলিকভাবে কঠিন। তবু চর্চা থামেনি।

পরের পঞ্চাশ বছরে অগ্রগতি এসেছে প্রায় পুরোটাই bound-কে শক্ত করে: Held–Karp-এর 1-tree আর penalty, তারপর Padberg–Rinaldi আর Grötschel-দের polyhedral তত্ত্ব থেকে আসা cut, তারপর Concorde-এর হাজারো ধরনের cut আর দক্ষ LP solver. Dantzig, Fulkerson ও Johnson-এর ৪৯ শহর থেকে ২০০৬ সালের ৮৫,৯০০ — হার্ডওয়্যারের গতি বেড়েছে বহু লক্ষ গুণ, কিন্তু n!-এর বৃদ্ধির কাছে সেটি তুচ্ছ; আসল কৃতিত্ব গাণিতিক bound-এর।

আরও একটি শিক্ষা: ভালো ইঞ্জিনিয়ারিং তত্ত্বের নেতিবাচক ফল মেনে নিয়েও এগোয়। “NP-hard, তাই হাল ছাড়ো” নয় — “NP-hard, তাই worst case-এ নিশ্চয়তা নেই; তবু বাস্তব ইনপুটের গঠন কাজে লাগাই, আর প্রতিটি উত্তরের মানের প্রমাণ দিই।” আজকের integer programming solver দিয়ে প্রতিদিন বিমান, ট্রেন, বিদ্যুৎ আর সরবরাহ শৃঙ্খলের সিদ্ধান্ত হয় — সেই ১৯৬০-এর গাছ-খোঁজার উত্তরসূরি দিয়ে।

মূল উপলব্ধি: একটি সরল ধারণা — “আশাবাদী হিসাব দিয়ে হতাশ শাখা বাদ দাও” — ছয় দশক ধরে আরও শক্ত গণিতে শান দিয়ে অসম্ভবকে প্রতিদিনের হাতিয়ার বানিয়েছে।

অনুশীলন

প্রথম দুটি হাতে trace করার, তৃতীয়টি কোড লেখার, চতুর্থটি প্রমাণ আর হিসাবের।

  1. হাতে trace — knapsack: W = ৫, জিনিস (ওজন, মূল্য): (১, ১০), (৩, ২৪), (৪, ২০) — অনুপাতের ক্রমে সাজানো। Fractional upper bound দিয়ে best-first branch and bound চালান। প্রতিটি node-এর ub লিখুন, কোনগুলো ছাঁটা পড়ে বলুন, আর কতটি node খোলা হয়?

    Root: জিনিস ১ আর ২ পুরো (৪ কেজি, ৩৪), বাকি ১ কেজিতে জিনিস ৩-এর ১/৪ = ৫ → ub ৩৯. খুললে [1]: v ১০, ub ৩৯ (incumbent ১০); [0]: জিনিস ২ পুরো (৩ কেজি, ২৪), বাকি ২ কেজিতে জিনিস ৩-এর অর্ধেক ১০ → ub ৩৪.

    [1] খোলা: [11]: v ৩৪, w ৪, ub ৩৯ → incumbent ৩৪; [10]: v ১০, তারপর জিনিস ৩ পুরো (মোট ৫ কেজি) → ub ৩০ ≤ ৩৪, ছাঁটাই।

    [11] খোলা: [111]: ওজন ৮ > ৫, infeasible; [110]: ub ৩৪ ≤ ৩৪, ছাঁটাই। [0] বের হয়: ub ৩৪ ≤ ৩৪, ছাঁটাই। খোলা node ৩টি, উত্তর ৩৪ (জিনিস ১ আর ২)। খেয়াল করুন, [0]-এর bound incumbent-এর ঠিক সমান — “≤ হলে বাদ” নিয়মে বাদ, কারণ সমান মানের আরেকটি উত্তর খুঁজে লাভ নেই।

  2. হাতে trace — reduced cost matrix: চার শহরের ম্যাট্রিক্স (সারি = থেকে): ১: [∞, 10, 15, 20], ২: [5, ∞, 9, 10], ৩: [6, 13, ∞, 12], ৪: [8, 8, 9, ∞]. Root-এর reduced matrix আর bound বের করুন, তারপর 1→2, 1→3, 1→4-এর bound. Best-first চালিয়ে সর্বোত্তম tour কী?

    সারি বিয়োগ: minimum ১০, ৫, ৬, ৮ — মোট ২৯. সারিগুলো হয় [∞, 0, 5, 10], [0, ∞, 4, 5], [0, 7, ∞, 6], [0, 0, 1, ∞]. কলাম বিয়োগ: কলাম ৩-এর minimum ১, কলাম ৪-এর ৫ — মোট ৬. Root matrix: [∞, 0, 4, 5], [0, ∞, 3, 0], [0, 7, ∞, 1], [0, 0, 0, ∞]; bound = ২৯ + ৬ = ৩৫.

    সন্তান: 1→2 = ৩৫ + ০ + ০ = ৩৫; 1→3 = ৩৫ + ৪ + ১ = ৪০; 1→4 = ৩৫ + ৫ + ০ = ৪০. 1→2 খুললে 1-2-3 = ৩৯, 1-2-4 = ৩৫; 1-2-4 খুললে বাকি শুধু ৩: tour 1-2-4-3-1 = ১০ + ১০ + ৯ + ৬ = ৩৫. বাকি সব bound (৩৯, ৪০, ৪০) ≥ ৩৫, তাই থামুন। Root-এর bound নিজেই সর্বোত্তমের সমান — bound একেবারে নিখুঁত হলে best-first সোজা পথ ধরে নেমে যায়।

  3. কোড লিখুন — depth-first knapsack: একটি রিকার্সিভ ফাংশন dfs(k, w, v) লিখুন, যা জিনিস k-এর জন্য আগে “নাও”, তারপর “নিও না” চেষ্টা করে, আর fractional bound ≤ best হলে ফিরে আসে। ওপরের চার জিনিসের উদাহরণে (W = ১০) উত্তর ৬৫ আসে কি না দেখুন, আর কতবার ফাংশন ডাকা হলো গুনুন।

    একটি সম্ভাব্য সমাধান (অংশ ৩-এর bound আর items ধরে নিয়ে):

    int best = 0, calls = 0;
    void dfs(size_t k, int w, int v) {
        calls++;
        best = max(best, v);                    // every node is a feasible packing
        if (k == items.size() || bound(k, w, v) <= best) return;   // prune
        auto [wi, vi] = items[k];
        if (w + wi <= W) dfs(k + 1, w + wi, v + vi);   // take item k first
        dfs(k + 1, w, v);                             // then skip it
    }

    dfs(0, 0, 0) ডাকলে best = ৬৫. “নাও” আগে চেষ্টা করায় প্রথম leaf-এই একটি ভালো incumbent আসে (৬৫), তারপর বেশিরভাগ শাখা bound পরীক্ষাতেই ফেরে। Infeasible সন্তানকে ডাকার আগেই বাদ দেওয়া হয়েছে। মেমরি শুধু রিকার্শনের গভীরতা, O(n) — best-first-এর queue-এর তুলনায় নগণ্য। এই উদাহরণে ফাংশনটি মোট ৭ বার ডাকা হয়: root, (১, ৪, ৪০), (২, ৪, ৪০), (৩, ৯, ৬৫), (৪, ৯, ৬৫), (৩, ৪, ৪০) আর (১, ০, ০) — শেষ দুটি bound ৫২ আর ৫৭ দেখেই ফিরে যায়। Best-first-এর ৪টি খোলা node-এর সাথে তুলনা করুন: depth-first কয়েকটি বেশি node ছোঁয়, কিন্তু queue বা heap লাগে না।

  4. প্রমাণ ও হিসাব — bound যখন সার্টিফিকেট: চার শহরের symmetric গ্রাফ: a–b ২, a–c ৯, a–d ৩, b–c ৪, b–d ৭, c–d ৫. “দুই সস্তা edge / ২” bound আর a-কে বিশেষ শহর ধরে 1-tree bound বের করুন। তারপর tour a–b–c–d–a-এর খরচ বের করে দেখান যে এটি সর্বোত্তম — কোনো branching ছাড়াই।

    দুই সস্তা edge: a (২, ৩) = ৫, b (২, ৪) = ৬, c (৪, ৫) = ৯, d (৩, ৫) = ৮ — মোট ২৮, bound ১৪. 1-tree: {b, c, d}-এর MST = b–c ৪, c–d ৫ = ৯; a-র দুই সস্তা edge ২ + ৩ = ৫; bound ১৪.

    Tour a–b–c–d–a = ২ + ৪ + ৫ + ৩ = ১৪. যেকোনো tour ≥ ১৪ (bound), আর এই tour ঠিক ১৪ — তাই এটি সর্বোত্তম। এটাই branch and bound-এর থামার নিয়মের সবচেয়ে ছোট রূপ: incumbent = সেরা bound হলে gap শূন্য, আর কিছু খোঁজার নেই। Concorde-এর প্রতিটি “প্রমাণিত সর্বোত্তম” দাবির পেছনেও এই একই যুক্তি, শুধু অনেক বড় মাপে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমMinimisation-এর best-first branch and bound-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. একটি দ্রুত heuristic দিয়ে শুরুর incumbent বানানো
  2. Root-এর lower bound হিসাব করে priority queue-তে রাখা
  3. সবচেয়ে ছোট bound-এর live node বের করা; সেটি ≥ best হলে থামা
  4. সেই node-এর প্রতিটি সন্তান তৈরি করে তার bound হিসাব করা
  5. সম্পূর্ণ সমাধান ভালো হলে incumbent বদলানো, bound < best হলে queue-তে রাখা, নইলে ছাঁটাই

খরচ কমানোর একটি সমস্যায় branch and bound চলছে। Incumbent-এর খরচ ৫০. একটি node x-এর lower bound ৫০. কী করা উচিত?

  • x খুলতেই হবে, কারণ সেখানে ৫০ খরচের সমাধান থাকতে পারে
  • x ছাঁটাই করা যায়
  • x-এর bound-কেই নতুন incumbent বানানো
  • Incumbent বাড়িয়ে ৫১ করা

0/1 knapsack-এর branch and bound-এ fractional knapsack-এর মানকে upper bound হিসেবে ব্যবহার করা বৈধ কেন?

  • কারণ fractional আর 0/1 knapsack-এর উত্তর সবসময় সমান
  • কারণ greedy 0/1 knapsack-ও নিখুঁতভাবে সমাধান করে
  • কারণ প্রতিটি 0/1 সমাধান একটি fractional সমাধানও, আর greedy fractional-এর সর্বোত্তম দেয়
  • কারণ ওজনগুলো পূর্ণসংখ্যা

TSP-এর খরচের ম্যাট্রিক্সের প্রতিটি সারি থেকে তার minimum বিয়োগ করলে প্রতিটি tour-এর খরচ কীভাবে বদলায়?

  • প্রতিটি tour ঠিক সারি-minimum-গুলোর যোগফল পরিমাণ কমে
  • বিভিন্ন tour বিভিন্ন পরিমাণ কমে, তাই সর্বোত্তম tour বদলে যেতে পারে
  • কোনো tour-এর খরচ বদলায় না
  • প্রতিটি tour-এর খরচ বাড়ে

Branch and bound-এর কোন কৌশল সাধারণত সবচেয়ে কম মেমরি ব্যবহার করে আর দ্রুত প্রথম সম্পূর্ণ সমাধান পায়?

  • FIFO branch and bound
  • Best-first (LC) branch and bound
  • Brute force
  • Depth-first (LIFO) branch and bound

Symmetric TSP-এ 1-tree bound-এর যুক্তি কোনটি?

  • Tour সবসময় একটি MST
  • Tour থেকে একটি শহর আর তার দুটি edge সরালে বাকিদের একটি spanning tree থাকে
  • Nearest neighbour tour-এর খরচ সবসময় সর্বোত্তমের ≤
  • 1-tree-এ প্রতিটি শহরের degree ঠিক ২

n শহরের TSP-এ branch and bound-এর worst-case জটিলতা সম্পর্কে কোন কথাটি সঠিক?

  • ভালো bound থাকলে এটি O(n³)
  • সবসময় O(n² 2ⁿ), Held–Karp-এর সমান
  • Worst case-এ exponential (প্রায় n!-এর ক্রমে), উন্নতি শুধু বাস্তব ইনপুটে, নিশ্চিত নয়
  • Best-first কৌশলে worst case O(n log n)

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Branch and bound = সমাধান-গাছে branch + প্রতিটি node-এ আশাবাদী bound; minimisation-এ lower bound ≥ incumbent হলে ছাঁটাই (maximisation-এ upper bound ≤ incumbent).
  2. সঠিকতার শর্ত শুধু একটি: bound বৈধ হতে হবে; দুর্বল bound ধীর করে, অবৈধ bound উত্তর ভুল করে। ভালো bound আসে relaxation থেকে।
  3. FIFO (queue, সহজ, মেমরি বেশি), depth-first (stack, মেমরি O(গভীরতা), দ্রুত incumbent), best-first/LC (priority queue, সবচেয়ে কম node).
  4. Knapsack: fractional greedy upper bound; TSP: reduced cost matrix (সারি + কলাম minimum), দুই সস্তা edge / ২, 1-tree, Held–Karp; assignment: সারির minimum.
  5. Worst case exponential; ভালো শুরুর উত্তর (nearest neighbour, 2-opt) আর শক্ত bound বাস্তবে জেতায় — integer programming solver আর Concorde-এর branch and cut.

পরের পর্বে: এবার অন্য জগৎ — লম্বা লেখার ভেতরে একটি শব্দ খোঁজা। সরল খোঁজ O(nm), কিন্তু Rabin-Karp-এর hash, KMP-এর failure function আর Z algorithm একই কাজ O(n + m)-এ করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান