পাঠ ৩৮ · ৫৭-এর মধ্যে · মডিউল ৮
Home / Courses / Design and Analysis of Algorithms / ব্রাঞ্চ-অ্যান্ড-বাউন্ড

ব্রাঞ্চ-অ্যান্ড-বাউন্ড প্যারাডাইম

The branch-and-bound paradigm
১১ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ব্রাঞ্চ-অ্যান্ড-বাউন্ড-এর সাধারণ কাঠামো — best-found ট্র্যাক করা, প্রতিটি নোডে একটি বাউন্ড হিসেব করা, বাউন্ড তুলনা করে প্রুন করা
  • এই কৌশল প্লেইন ব্যাকট্র্যাকিং থেকে ঠিক কোথায় ভিন্ন — দুই ধরনের প্রুনিং শর্তের স্পষ্ট পার্থক্য
  • একটি বাউন্ডিং ফাংশন কেমন হতে হয় (অপটিমিস্টিক/অ্যাডমিসিবল — কখনো প্রকৃত সেরার চেয়ে খারাপ অনুমান করে না)
  • একটি সত্যিকারের কোড উদাহরণে দুই ধরনের প্রুনিং আলাদাভাবে গোনা এবং ব্রুট-ফোর্সের সাথে ফলাফল যাচাই

১ · ব্যাকট্র্যাকিং বনাম ব্রাঞ্চ-অ্যান্ড-বাউন্ড — একটি মূল পার্থক্য

L36-L37-এ আমরা যে ব্যাকট্র্যাকিং দেখেছি, তার প্রুনিং শর্তগুলো ছিল সব ফিজিবিলিটি-ভিত্তিক: N-Queens-এ কলাম/ডায়াগোনাল কনফ্লিক্ট, সাবসেট সামে যোগফল সম্ভব কি না, হ্যামিল্টোনিয়ান সাইকেলে এজ ও ভিজিট-স্ট্যাটাস। এই প্রতিটি চেক একটিই প্রশ্নের উত্তর দেয়: এই আংশিক সমাধান কি এখনো কোনো বৈধ পূর্ণ সমাধানে পরিণত হতে পারে?

কিন্তু যখন সমস্যাটি শুধু "একটি বৈধ সমাধান খোঁজা" নয়, বরং "সেরা সমাধান খোঁজা" (যেমন সর্বোচ্চ মূল্য, ন্যূনতম দূরত্ব), তখন আরেকটি শক্তিশালী প্রশ্ন করা সম্ভব হয়: এই শাখা কি বর্তমান পর্যন্ত পাওয়া সেরা সমাধানের চেয়ে ভালো হতে পারে? এই দ্বিতীয় প্রশ্নের উত্তর "না" হলে, সেই শাখায় হয়তো একাধিক সম্পূর্ণ বৈধ সমাধান থাকলেও, সেগুলোর কোনোটিই আমাদের চূড়ান্ত উত্তর বদলাতে পারবে না — তাই পুরো শাখাটি নিরাপদে প্রুন করা যায়।

ফিজিবিলিটি প্রুনিং (ব্যাকট্র্যাকিং)
প্রশ্ন: এই শাখায় কোনো বৈধ সমাধান থাকা সম্ভব কি? না হলে প্রুন।
বাউন্ড প্রুনিং (ব্রাঞ্চ-অ্যান্ড-বাউন্ড)
প্রশ্ন: এই শাখার সেরা সম্ভাব্য ফলাফলও কি বর্তমান সেরাকে হারাতে পারবে? না হলে প্রুন — যদিও শাখায় বৈধ সমাধান থাকতে পারে।

২ · সাধারণ কাঠামো ও অ্যাডমিসিবল বাউন্ড

একটি ব্রাঞ্চ-অ্যান্ড-বাউন্ড অ্যালগরিদম প্রতিটি নোডে একটি বাউন্ডিং ফাংশন $B(\text{node})$ হিসেব করে, যা সেই নোডের সাবট্রি থেকে সম্ভাব্য সর্বোত্তম ফলাফলের একটি আশাবাদী (optimistic) অনুমান দেয় — একে অ্যাডমিসিবল বাউন্ড বলে যদি এটি কখনো প্রকৃত সর্বোত্তম ফলাফলের চেয়ে খারাপ অনুমান না করে (ম্যাক্সিমাইজেশনে বাউন্ড $\ge$ প্রকৃত সর্বোচ্চ সম্ভাব্য মান)। যদি

$$B(\text{node}) \le \text{best\_found}$$

হয় (ম্যাক্সিমাইজেশন সমস্যায়), তাহলে এই নোডের সাবট্রি থেকে best_found-এর চেয়ে ভালো কিছু আসা অসম্ভব — নিরাপদে প্রুন করা যায়। বাউন্ড যদি অ্যাডমিসিবল না হয় (অর্থাৎ প্রকৃত সর্বোচ্চের চেয়ে কম অনুমান করে ফেলে), তাহলে এই প্রুনিং ভুলভাবে প্রকৃত অপটিমাম বাদ দিয়ে দিতে পারে — এটি ব্রাঞ্চ-অ্যান্ড-বাউন্ডের সবচেয়ে গুরুত্বপূর্ণ ও সবচেয়ে সহজে ভুল হওয়ার মতো শর্ত, এবং L39-এ TSP-তে আমরা এটি সতর্কভাবে যাচাই করব।

শুরু (root) আইটেম ১ নাও আইটেম ১ বাদ দাও weight=9, value=17 weight=0, value=0 আইটেম ২ নাও আইটেম ২ বাদ দাও প্রুন: অসম্ভব weight > capacity চলতে থাকে… (আরও শাখা নিচে) আইটেম ২ নাও আইটেম ২ বাদ দাও চলতে থাকে… (আরও শাখা নিচে) প্রুন: বাউন্ড ≤ সেরা (বৈধ হতে পারত, তবু লাভ নেই) B(node) ≤ best_found লাল/ড্যাশড = ফিজিবিলিটি প্রুনিং (L36-L37 স্টাইল) অ্যাম্বার/ডটেড = বাউন্ড প্রুনিং (এই পাঠের নতুন অংশ)
বাম দিকের শাখাটি অসম্ভব (ওজন ক্যাপাসিটি পার হয়ে যায়) বলে প্রুন হয়েছে — এটি L36-L37-এর মতো ফিজিবিলিটি প্রুনিং। ডান দিকের শাখাটি সম্পূর্ণ বৈধ হতে পারত, কিন্তু তার নিজস্ব সর্বোচ্চ সম্ভাব্য মান (বাউন্ড) ইতিমধ্যে পাওয়া সেরা সমাধানকে হারাতে পারে না বলে প্রুন হয়েছে — এটিই ব্রাঞ্চ-অ্যান্ড-বাউন্ডের নতুন সংযোজন।

৩ · একটি সত্যিকারের উদাহরণ — 0/1 ন্যাপস্যাকে ব্রাঞ্চ-অ্যান্ড-বাউন্ড

0/1 ন্যাপস্যাকের DP সমাধান ইতিমধ্যে L25-এ কভার করা হয়েছে। এখানে আমরা একই সমস্যা ব্রাঞ্চ-অ্যান্ড-বাউন্ড দিয়ে সমাধান করছি শুধু কৌশলটি concrete ভাবে দেখানোর জন্য। বাউন্ড হিসেবে ব্যবহার করা হচ্ছে ফ্র্যাকশনাল রিল্যাক্সেশন — অর্থাৎ বাকি আইটেমগুলো আংশিকভাবে (ভগ্নাংশে) নেওয়ার অনুমতি দিলে সর্বোচ্চ কত মূল্য পাওয়া সম্ভব, ঠিক L22-এর গ্রিডি কৌশল ব্যবহার করে। এই বাউন্ড সবসময় অ্যাডমিসিবল, কারণ 0/1 সমস্যার প্রকৃত অপটিমাম কখনোই তার নিজস্ব ফ্র্যাকশনাল রিল্যাক্সেশনের চেয়ে বেশি হতে পারে না (রিল্যাক্সেশন সবসময় আসল সমস্যার চেয়ে বেশি স্বাধীনতা দেয়)।

Python
import itertools

def knapsack_branch_and_bound(weights, values, capacity):
    n = len(weights)
    # value/weight অনুপাত অনুযায়ী সাজানো -- ফ্র্যাকশনাল রিল্যাক্সেশন বাউন্ডের জন্য দরকার (L22-এর গ্রিডি)
    order = sorted(range(n), key=lambda i: values[i] / weights[i], reverse=True)
    w = [weights[i] for i in order]
    v = [values[i] for i in order]

    best_value = 0
    nodes_explored = 0
    pruned_infeasible = 0
    pruned_bound = 0

    def bound(index, current_weight, current_value):
        # বাকি ক্যাপাসিটি আইটেমগুলো ভগ্নাংশে নেওয়ার অনুমতি দিলে সর্বোচ্চ কত মূল্য সম্ভব -- একটি আশাবাদী (upper) বাউন্ড
        if current_weight > capacity:
            return 0
        result = current_value
        remaining = capacity - current_weight
        i = index
        while i < n and w[i] <= remaining:
            remaining -= w[i]
            result += v[i]
            i += 1
        if i < n:
            result += v[i] * remaining / w[i]  # শেষ আইটেমের একটি ভগ্নাংশ
        return result

    def backtrack(index, current_weight, current_value):
        nonlocal best_value, nodes_explored, pruned_infeasible, pruned_bound
        nodes_explored += 1
        if current_weight > capacity:
            pruned_infeasible += 1  # প্রুন ধরন ১: ফিজিবিলিটি (L36-L37 স্টাইল)
            return
        best_value = max(best_value, current_value)
        if index == n:
            return
        b = bound(index, current_weight, current_value)
        if b <= best_value:
            pruned_bound += 1        # প্রুন ধরন ২: এই নতুন, বাউন্ড-ভিত্তিক প্রুনিং
            return
        backtrack(index + 1, current_weight + w[index], current_value + v[index])  # আইটেম নাও
        backtrack(index + 1, current_weight, current_value)                        # আইটেম বাদ দাও

    backtrack(0, 0, 0)
    return best_value, nodes_explored, pruned_infeasible, pruned_bound


def brute_force_knapsack(weights, values, capacity):
    n = len(weights)
    best = 0
    for r in range(n + 1):
        for combo in itertools.combinations(range(n), r):
            tw = sum(weights[i] for i in combo)
            tv = sum(values[i] for i in combo)
            if tw <= capacity and tv > best:
                best = tv
    return best


weights = [2, 3, 4, 5, 9, 7, 1, 6]
values = [3, 4, 5, 8, 10, 6, 2, 9]
capacity = 15
n = len(weights)

bb_value, nodes, pruned_infeasible, pruned_bound = knapsack_branch_and_bound(weights, values, capacity)
bf_value = brute_force_knapsack(weights, values, capacity)

print("ব্রুট-ফোর্স অপটিমাম:      ", bf_value)
print("ব্রাঞ্চ-অ্যান্ড-বাউন্ড অপটিমাম:", bb_value)
print("দুটো মিলল কিনা:            ", bb_value == bf_value)
print()
print("সার্চ ট্রি-র সম্পূর্ণ (বাইনারি) নোড সংখ্যা:", 2 ** (n + 1) - 1)
print("প্রকৃতপক্ষে এক্সপ্লোর করা নোড:            ", nodes)
print("  -- ফিজিবিলিটির কারণে প্রুন হয়েছে:       ", pruned_infeasible)
print("  -- বাউন্ড তুলনার কারণে প্রুন হয়েছে:      ", pruned_bound)

    
$8$টি আইটেমের এই ইনস্ট্যান্সে সম্পূর্ণ বাইনারি ডিসিশন ট্রি-তে $2^9 - 1 = 511$টি নোড থাকত, কিন্তু ব্রাঞ্চ-অ্যান্ড-বাউন্ড মাত্র $21$টি নোড এক্সপ্লোর করে সঠিক অপটিমাম ($23$) খুঁজে পায় — যা ব্রুট-ফোর্সের সাথে হুবহু মেলে। এর মধ্যে $4$টি শাখা ফিজিবিলিটির কারণে (ওজন ক্যাপাসিটি পার হয়ে যাওয়ায়) এবং $6$টি শাখা শুধু বাউন্ড তুলনার কারণে প্রুন হয়েছে — এই $6$টি শাখায় হয়তো বৈধ সমাধান ছিল, কিন্তু তাদের সর্বোচ্চ সম্ভাব্য মূল্যও ইতিমধ্যে পাওয়া সেরাকে ছাড়িয়ে যেতে পারত না।
মূল কথা · Key takeaway

ব্রাঞ্চ-অ্যান্ড-বাউন্ড হলো ব্যাকট্র্যাকিং-এর একটি বিশেষায়িত রূপ, যা অপটিমাইজেশন সমস্যায় একটি অতিরিক্ত অস্ত্র যোগ করে: শুধু "এটা কি সম্ভব?" নয়, বরং "এটা কি লাভজনক হতে পারে?" প্রশ্নের ভিত্তিতেও প্রুনিং। এই কৌশলের কার্যকারিতা পুরোপুরি নির্ভর করে বাউন্ডিং ফাংশনের মানের উপর — যত টাইট (প্রকৃত মানের যত কাছাকাছি) বাউন্ড, তত বেশি শাখা তাড়াতাড়ি প্রুন করা যায়। L39-এ আমরা এই কৌশল ট্র্যাভেলিং সেলসম্যান সমস্যায় প্রয়োগ করব, এবং কঠোরভাবে যাচাই করব যে বাউন্ডটি সত্যিই অ্যাডমিসিবল — অন্যথায় এটি ভুলভাবে প্রকৃত অপটিমাম বাদ দিয়ে দিতে পারত।

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

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

প্র ০১ যদি বাউন্ডিং ফাংশন সবসময় $+\infty$ রিটার্ন করত, তাহলে ব্রাঞ্চ-অ্যান্ড-বাউন্ড অ্যালগরিদমটির আচরণ কেমন হতো?

বাউন্ড প্রুনিং শর্ত ($B(\text{node}) \le \text{best\_found}$) কখনোই সত্যি হতো না, কারণ কোনো সসীম best_found-ই $+\infty$-এর চেয়ে বড় হতে পারে না। ফলে pruned_bound সবসময় $0$ থাকত এবং অ্যালগরিদমটি কার্যত প্লেইন ব্যাকট্র্যাকিং-এ পরিণত হতো — শুধু ফিজিবিলিটির ভিত্তিতেই প্রুন করত, যা এই লেসনের মূল পার্থক্যটিকেই অকার্যকর করে দিত। এটাই দেখায় বাউন্ডের "টাইটনেস" (কতটা প্রকৃত মানের কাছাকাছি) সরাসরি প্রুনিং-এর কার্যকারিতা নির্ধারণ করে।

প্র ০২ উপরের কোডে bound() ফাংশন ফ্র্যাকশনাল (ভগ্নাংশ-অনুমোদিত) রিল্যাক্সেশন ব্যবহার করে। এটি সবসময় প্রকৃত 0/1 অপটিমামের চেয়ে বেশি বা সমান কেন?

কারণ ফ্র্যাকশনাল রিল্যাক্সেশন প্রতিটি 0/1 সমাধানকেও একটি বৈধ সমাধান হিসেবে অন্তর্ভুক্ত করে (একটি আইটেম পুরোপুরি নেওয়া বা পুরোপুরি বাদ দেওয়া, দুটোই ভগ্নাংশ $0$ বা $1$-এর বিশেষ ক্ষেত্র), এবং তার সাথে অতিরিক্ত আরও অনেক সমাধান (ভগ্নাংশ) অনুমোদন করে যা 0/1 সমস্যায় বৈধ নয়। যেহেতু সম্ভাব্য সমাধানের সেট শুধু বড়ই হতে পারে, কখনো ছোট নয়, তাই ফ্র্যাকশনাল রিল্যাক্সেশনের অপটিমাম কখনোই প্রকৃত 0/1 অপটিমামের চেয়ে কম হতে পারে না — এটিই একে একটি বৈধ (অ্যাডমিসিবল) upper bound করে তোলে।

প্র ০৩ L37-এর হ্যামিল্টোনিয়ান সাইকেল সমস্যায় কি ব্রাঞ্চ-অ্যান্ড-বাউন্ড প্রয়োগ করা সম্ভব?

সরাসরি "হ্যামিল্টোনিয়ান সাইকেল আছে কি না" (ফিজিবিলিটি প্রশ্ন) ফরম্যাটে না — কারণ এখানে তুলনা করার মতো কোনো "মূল্য" বা "সেরা এখন পর্যন্ত" নেই, শুধু হ্যাঁ/না উত্তর দরকার। কিন্তু যদি সমস্যাটিকে অপটিমাইজেশনে রূপান্তর করা হয় — যেমন ট্র্যাভেলিং সেলসম্যান সমস্যা (L39), যেখানে প্রতিটি হ্যামিল্টোনিয়ান সাইকেলের একটি খরচ আছে এবং আমরা সর্বনিম্ন খরচেরটি খুঁজছি — তখন ব্রাঞ্চ-অ্যান্ড-বাউন্ড স্বাভাবিকভাবেই প্রযোজ্য হয়ে যায়।

অনুশীলন

  1. চিন্তা করুন: উপরের কোডে capacity-এর মান অনেক বড় (যেমন সব আইটেমের মোট ওজনের চেয়েও বেশি) করলে pruned_infeasible ও pruned_bound-এর মান কেমন হবে বলে আপনার ধারণা?

    pruned_infeasible শূন্যের কাছাকাছি হবে (ক্যাপাসিটি এত বড় যে ওজন কখনো পার হবে না, তাই সব আইটেম নেওয়াই সম্ভব)। pruned_bound-ও কমে যাবে, কারণ ফ্র্যাকশনাল রিল্যাক্সেশন বাউন্ড আর প্রকৃত অপটিমামকে (যা এখানে সব আইটেম নেওয়াই) বেশি "টাইট" করে সীমাবদ্ধ করতে পারবে না — আসলে এই কেসে অপটিমাল সমাধান "সব আইটেম নাও" হওয়ায় অ্যালগরিদমকে প্রায় পুরো ট্রি-ই এক্সপ্লোর করতে হবে।

  2. পরীক্ষা করুন: উপরের কোডে capacity = 15-এর বদলে capacity = 30 (সব আইটেমের মোট ওজন $2+3+4+5+9+7+1+6=37$-এর কাছাকাছি) বসিয়ে Run চেপে দেখুন nodes, pruned_infeasible, ও pruned_bound কীভাবে বদলায়।

    ক্যাপাসিটি বাড়ানোয় বেশিরভাগ কম্বিনেশনই ফিজিবল হয়ে যাবে, তাই pruned_infeasible কমবে। একইসাথে অপটিমাল সমাধান (প্রায় সব আইটেম) বাউন্ডের কাছাকাছি চলে আসায় বাউন্ড-প্রুনিং করার সুযোগও কমে যাবে, ফলে nodes সামগ্রিকভাবে বেড়ে $511$-এর কাছাকাছি চলে যেতে পারে — একটি সুস্পষ্ট প্রমাণ যে প্রুনিং-এর কার্যকারিতা ইনস্ট্যান্সের গঠনের উপর নির্ভরশীল, সবসময় একই রকম বিশাল নয়।

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

আগের পাঠ
সাবসেট সাম ও হ্যামিল্টোনিয়ান সাইকেল ব্যাকট্র্যাকিং