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

Greedy অ্যালগরিদম আর exchange argument

Greedy Algorithms — Exchange Arguments, Staying Ahead, Interval Scheduling, Knapsack, Coin Change, Deadlines & Matroids

Taught in Bangla, this lesson explains when greedy choices are correct, using the greedy-choice property and optimal substructure. It proves activity selection with exchange arguments and greedy stays ahead, then covers interval partitioning, fractional knapsack, minimising lateness and job sequencing, shows why 0/1 knapsack and some coin systems defeat greedy, links MST, Dijkstra and Huffman to matroids, and stress-tests greedy code against brute force in C++ and Python.

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

  • Greedy অ্যালগরিদম কী, আর greedy-choice property ও optimal substructure দিয়ে কখন তা ঠিক উত্তর দেয়
  • Exchange argument আর “greedy stays ahead” দিয়ে greedy-র সম্পূর্ণ প্রমাণ লেখা, আর counterexample দিয়ে ভুল greedy বাতিল করা
  • Activity selection, interval partitioning, fractional knapsack, minimising lateness আর job sequencing হাতে trace করা ও তাদের complexity বলা
  • কেন 0/1 knapsack আর কিছু coin system-এ greedy ব্যর্থ, আর canonical coin system মানে কী
  • MST, Dijkstra আর Huffman-কে greedy হিসেবে চেনা, matroid-এর ধারণা পাওয়া, আর brute force দিয়ে greedy-র stress test করা

১ · Greedy-র মূল ধারণা — এই মুহূর্তের সেরা পছন্দ

উপমা — স্টেশনের রিকশাওয়ালা। কমলাপুর স্টেশনের সামনে একজন রিকশাওয়ালা সারাদিন যাত্রী টানেন। তাঁর নিয়ম সরল: প্রতিবার যাত্রী নামিয়ে আশেপাশে সবচেয়ে কাছের যাত্রীকে তোলেন। সারা দিনের পরিকল্পনা তিনি করেন না, আগের সিদ্ধান্ত নিয়ে আফসোসও করেন না। বেশিরভাগ দিন এতে ভালোই চলে। কিন্তু কোনো দিন হয়তো কাছের যাত্রীটি তাঁকে এমন গলিতে নিয়ে যায়, যেখান থেকে খালি রিকশা নিয়ে অনেক দূর ফিরতে হয় — অথচ একটু দূরের যাত্রী নিলে সারা দিনে বেশি আয় হতো। এই “এখনকার সেরাটা নাও” কৌশলই greedy, আর এর বিপদও ঠিক এখানেই।

একটি greedy অ্যালগরিদম একটি optimization problem-এর সমাধান ধাপে ধাপে বানায়। প্রতিটি ধাপে কয়েকটি বিকল্পের মধ্যে একটি সরল নিয়মে “এখন সবচেয়ে ভালো” বিকল্পটি বেছে নেয়, আর সেই সিদ্ধান্ত আর কখনো বদলায় না। পর্ব ৫৬-এর backtracking-এর মতো ফিরে গিয়ে অন্য পথ চেষ্টা করে না, পর্ব ৫২-এর dynamic programming-এর মতো সব উপসমস্যার উত্তরও মনে রাখে না। তাই greedy সাধারণত খুব দ্রুত — প্রায়ই একটি সর্ট আর একটি লুপ, মোট O(n log n).

প্রশ্ন একটাই: স্থানীয়ভাবে সেরা পছন্দগুলো জুড়ে কি সামগ্রিকভাবে সেরা উত্তর হয়? সব সমস্যায় হয় না। দুটি ধর্ম থাকলে হয়:

দুটি শর্ত

১. Greedy-choice property: greedy যে প্রথম পছন্দটি করে, এমন অন্তত একটি সর্বোত্তম সমাধান আছে যাতে সেই পছন্দটি আছে। অর্থাৎ প্রথম পছন্দটি “ভুল দরজা” নয়।

২. Optimal substructure: প্রথম পছন্দের পর যা বাকি থাকে, তা একই ধরনের একটি ছোট সমস্যা, আর পুরো সমস্যার সর্বোত্তম সমাধান = প্রথম পছন্দ + বাকি সমস্যার সর্বোত্তম সমাধান।

দুটি একসাথে থাকলে induction দিয়ে (পর্ব ৩) প্রমাণ শেষ: প্রথম ধাপ নিরাপদ, বাকিটা ছোট সমস্যা, আর ছোট সমস্যায় greedy ঠিক — induction hypothesis.

Optimal substructure dynamic programming-এরও ভিত্তি। পার্থক্য হলো, DP সব সম্ভাব্য প্রথম পছন্দ চেষ্টা করে সেরাটি নেয়; greedy শুধু একটি নেয়, কারণ greedy-choice property বলে ওটিই যথেষ্ট। তাই greedy-র আসল কাজ হলো প্রমাণ: কেন ঠিক ওই একটি পছন্দই নিরাপদ।

Greedy নকশার তিন ধাপ

১. প্রার্থী নিয়ম

“কোন ক্রমে সাজিয়ে কোনটি আগে নেব?” — কয়েকটি স্বাভাবিক নিয়ম লিখুন: সবচেয়ে ছোট, সবচেয়ে আগে, সবচেয়ে দামি, সবচেয়ে কম সংঘাত…

২. Counterexample খুঁজুন

প্রতিটি নিয়মকে ২–৩টি উপাদানের ইনপুটে ভাঙার চেষ্টা করুন। একটি counterexample-ই একটি নিয়মকে চিরতরে বাতিল করে।

৩. প্রমাণ করুন

যে নিয়ম টিকে গেল, তার জন্য exchange argument বা stays ahead লিখুন। প্রমাণ না দাঁড়ালে সন্দেহ করুন — হয়তো counterexample আরও বড় ইনপুটে লুকিয়ে আছে।

২ · Activity selection — এক রুমে সবচেয়ে বেশি ক্লাস

একটি কোচিং সেন্টারের একটিমাত্র সেমিনার রুম। আটজন শিক্ষক আজকের জন্য রুমটি চেয়েছেন, প্রত্যেকের নির্দিষ্ট শুরু আর শেষের সময় আছে। দুটি ক্লাস একসাথে চলতে পারে না, তবে একটি ১০টায় শেষ হলে আরেকটি ঠিক ১০টায় শুরু হতে পারে। প্রশ্ন: সবচেয়ে বেশি কয়টি ক্লাস রাখা যায়? এটিই বিখ্যাত activity selection বা interval scheduling সমস্যা। প্রতিটি ক্লাস একটি half-open interval [s, f) — শুরুর মুহূর্ত ভেতরে, শেষের মুহূর্ত বাইরে। দুটি ক্লাস মেলে না (compatible) যদি একটির f ≤ অন্যটির s.

ক্লাসABCDEFGH
সময় [s, f)৮–১০৯–১১১১–১২১২–১৩১০–১৪১৩–১৫১৪–১৬১৫–১৭

চারটি স্বাভাবিক নিয়ম — তিনটিই ভুল

নিয়মছোট counterexampleফল
যে আগে শুরু হয়৮–১৭ একটি লম্বা ক্লাস, আর ৯–১০, ১০–১১, ১১–১২greedy ১টি, সেরা ৩টি
যে সবচেয়ে ছোট৯–১৩, ১২–১৪, ১৩–১৭: ছোট ১২–১৪ দুই পাশের দুটিকেই আটকায়greedy ১টি, সেরা ২টি
যার সংঘাত সবচেয়ে কমএকটু বড় ইনপুট লাগে (Kleinberg ও Tardos-এর Algorithm Design বইয়ে এর একটি পরিচিত উদাহরণ আছে)ভুল
যে সবচেয়ে আগে শেষ হয়কোনো counterexample নেই — নিচে প্রমাণসবসময় সর্বোত্তম

আমাদের আট ক্লাসেও “আগে শুরু” নিয়মটি ভোগায়: A (৮টা) নেয়, তারপর E (১০–১৪) — চার ঘণ্টার এই ক্লাস C আর D দুটোকেই খেয়ে ফেলে — তারপর G. মোট ৩টি। অথচ পাঁচটি সম্ভব। জয়ী নিয়মটি হলো earliest finish time first: সব ক্লাস শেষের সময় অনুযায়ী সাজান; যে ক্লাস শেষ নেওয়া ক্লাসের পরে শুরু হয়, তা নিন। স্বজ্ঞা: যে ক্লাস সবচেয়ে আগে শেষ হয়, সে রুমটি সবচেয়ে আগে খালি করে দেয় — বাকিদের জন্য সবচেয়ে বেশি সময় রেখে যায়।

Pseudocode — activity selection, earliest finish time (0-based)

SelectActivities(a[0..n-1]):          // a[i] = (s, f), half-open [s, f)
    sort a by finish time f            // O(n log n)
    chosen = [ ];  last = -infinity    // finish time of the last chosen one
    for each (s, f) in sorted order:
        if s >= last:                  // starts after the last chosen ends
            append (s, f) to chosen;  last = f
    return chosen
সময়রেখা (ঘণ্টা), [ ) = ক্লাসসবুজ = নেওয়া, লাল = বাদ

            
প্রতিটি ধাপের পর
এই ধাপের ক্লাস
শর্ত s ≥ last
নেওয়া ক্লাস
last
  1. সাজানো. আটটি ক্লাস শেষের সময়ের ক্রমে: A (১০), B (১১), C (১২), D (১৩), E (১৪), F (১৫), G (১৬), H (১৭). খেয়াল করুন, E আগে শুরু হলেও (১০টা) তালিকায় পাঁচ নম্বরে, কারণ সে দেরিতে শেষ হয়। last = ০, অর্থাৎ রুম পুরো খালি।
  2. A নিন. সবচেয়ে আগে শেষ হয় A. ৮ ≥ ০, তাই নিন; রুম এখন ১০টা পর্যন্ত ব্যস্ত, last = ১০.
  3. B বাদ. B শুরু হয় ৯টায়, কিন্তু রুম ১০টা পর্যন্ত A-এর দখলে। ৯ < ১০ — সংঘাত, বাদ।
  4. C নিন. C ১১টায় শুরু, ১১ ≥ ১০. নিন; last = ১২. ১০ থেকে ১১টা রুম খালি থাকল — তাতে ক্ষতি নেই।
  5. D নিন. D ঠিক ১২টায় শুরু, যখন C শেষ হয়। Half-open interval-এ এটি বৈধ (১২ ≥ ১২). last = ১৩.
  6. E বাদ. E-এর শুরু ১০টা, কিন্তু last = ১৩. এই লম্বা ক্লাসটিই “আগে শুরু” নিয়মকে ডুবিয়েছিল; এখানে সে নিজেই বাদ পড়ল।
  7. F নিন. ১৩ ≥ ১৩, নিন; last = ১৫.
  8. G বাদ. ১৪ < ১৫ — F তখনো চলছে।
  9. H নিন. ১৫ ≥ ১৫, নিন। মোট ৫টি ক্লাস: A, C, D, F, H. প্রতিটি ক্লাস একবার দেখা হলো, কোনো সিদ্ধান্ত বদলানো হলো না।

জটিলতা: সর্ট O(n log n), তারপর একবার স্ক্যান Θ(n) — মোট O(n log n) সময়। ক্লাসগুলো আগে থেকেই শেষের সময়ে সাজানো থাকলে Θ(n). জায়গা: সর্টের জন্য O(log n) থেকে O(n) (লাইব্রেরি অনুযায়ী) আর উত্তরের তালিকা O(n). সময়গুলো ছোট পূর্ণসংখ্যা হলে counting sort (পর্ব ২৪) দিয়ে পুরোটাই রৈখিক।

৩ · প্রমাণ — exchange argument আর “greedy stays ahead”

“আগে যে শেষ হয়” শুনতে যুক্তিসঙ্গত, কিন্তু “আগে শুরু” নিয়মও শুনতে যুক্তিসঙ্গত ছিল। পার্থক্য করে প্রমাণ। দুটি পদ্ধতিতেই একই সত্য প্রমাণ করা যায়; দুটিই শিখে রাখুন, কারণ বিভিন্ন সমস্যায় একটি অন্যটির চেয়ে সহজ হয়।

পদ্ধতি ১ — Exchange argument

লেমা

ধরুন g হলো সব ক্লাসের মধ্যে যে সবচেয়ে আগে শেষ হয়। তাহলে এমন একটি সর্বোত্তম সমাধান আছে, যাতে g আছে।

প্রমাণ (exchange argument): যেকোনো একটি সর্বোত্তম সমাধান OPT নিন, তার ক্লাসগুলো শেষের সময়ে সাজান: o₁, o₂, …, ok. যদি o₁ = g, কাজ শেষ। না হলে OPT′ = OPT − o₁ + g বানান। (১) g সবচেয়ে আগে শেষ হয়, তাই f(g) ≤ f(o₁). (২) o₁ আর o₂ মেলে না, তাই f(o₁) ≤ s(o₂). দুটি মিলে f(g) ≤ s(o₂) — g, o₂-এর সাথে (আর তার পরের সবার সাথে) মেলে না। তাই OPT′ বৈধ, আর তার আকারও k — সেটিও সর্বোত্তম, এবং তাতে g আছে। ∎

এরপর optimal substructure: g নেওয়ার পর বাকি সমস্যা হলো “যেসব ক্লাস f(g)-এর পরে শুরু হয়, তাদের মধ্যে সবচেয়ে বেশি”. Greedy ঠিক এই সমস্যাতেই আবার একই নিয়ম চালায়। ক্লাসের সংখ্যার ওপর induction করলে পুরো অ্যালগরিদম সর্বোত্তম। নিচে আমাদের উদাহরণেই বদলটি দেখুন — এখানে দুটি আলাদা সর্বোত্তম সমাধান আছে।

OPT = একটি সর্বোত্তম সমাধান, GRD = greedyলাল = বের হবে, সবুজ = ঢুকবে

            
প্রতিটি ধাপে
এই ধাপ
দেখার বিষয়
  1. দুটি সমাধান. ধরুন কেউ (হয়তো brute force) আমাদের একটি সর্বোত্তম সমাধান দিল: OPT = {B, C, D, F, H}. Greedy পেয়েছে {A, C, D, F, H}. দুটিতেই ৫টি ক্লাস — প্রশ্ন হলো, সবসময় কি এমন হবে?
  2. প্রথমটির তুলনা. OPT-এর প্রথম ক্লাস B, শেষ ১১টায়। Greedy-র প্রথম A, শেষ ১০টায় — সব ক্লাসের মধ্যে সবচেয়ে আগে। তাই f(A) ≤ f(B) — সবসময় সত্য, কারণ A-কে ঠিক এভাবেই বাছা হয়েছে।
  3. বদল. B বের করে A ঢোকান। OPT-এর দ্বিতীয় ক্লাস C শুরু হয় ১১টায়, আর A শেষ হয় ১০টায় — কোনো সংঘাত নেই। নতুন সেট OPT′-এ (ছবিতে NEW) আবার ৫টি ক্লাস, অর্থাৎ সেটিও সর্বোত্তম। এখানে এক বদলেই OPT′ = greedy; সাধারণভাবে বাকি অংশে একই যুক্তি বারবার (induction).
  4. Stays ahead. একই ঘটনা অন্যভাবে: greedy-র শেষের সময়গুলো ১০, ১২, ১৩, ১৫, ১৭ আর OPT-এর ১১, ১২, ১৩, ১৫, ১৭. প্রতিটি অবস্থানে greedy-র ক্লাস আগে বা একই সময়ে শেষ — greedy কখনো পিছিয়ে পড়ে না। এটিই পরের প্রমাণের মূল কথা।

পদ্ধতি ২ — Greedy stays ahead

Greedy-র বাছাই g₁, …, gm আর যেকোনো সর্বোত্তম সমাধান o₁, …, ok, দুটিই শেষের সময়ে সাজানো। লক্ষ্য m = k. দাবি (greedy stays ahead): প্রতিটি r ≤ m-এর জন্য f(gr) ≤ f(or).

  1. Base case, r = 1: g₁ সব ক্লাসের মধ্যে সবচেয়ে আগে শেষ হয়, তাই f(g₁) ≤ f(o₁).
  2. Inductive step: ধরুন f(gr−1) ≤ f(or−1). or শুরু হয় or−1 শেষ হওয়ার পর: s(or) ≥ f(or−1) ≥ f(gr−1). অর্থাৎ greedy যখন r-তম ক্লাস বাছছিল, or তখন বৈধ প্রার্থী ছিল। Greedy সবচেয়ে আগে শেষ হওয়া বৈধ প্রার্থীকে নেয়, তাই f(gr) ≤ f(or).
  3. উপসংহার: ধরুন k > m. তাহলে om+1 আছে, আর s(om+1) ≥ f(om) ≥ f(gm) — greedy শেষ হওয়ার পরেও একটি বৈধ ক্লাস বাকি ছিল, অথচ greedy থেমে গেছে। বিরোধ। তাই k = m. ∎

Exchange argument

সর্বোত্তম সমাধান থেকে শুরু করে একটি একটি করে উপাদান greedy-র উপাদানে বদলান, প্রতিবার দেখান খরচ বাড়ে না। MST-এর cut property (পর্ব ৪৫) আর পরে EDF-এর প্রমাণ এই ধাঁচের।

Greedy stays ahead

একটি মাপকাঠি বাছুন (এখানে শেষের সময়) আর induction দিয়ে দেখান প্রতিটি ধাপে greedy এগিয়ে বা সমান। Dijkstra-র প্রমাণ (পর্ব ৪৩) এই ধাঁচের।

Counterexample

উল্টো দিক: একটি নির্দিষ্ট ইনপুট, যেখানে নিয়মটি হারে। প্রমাণ লিখতে বসে কোথায় আটকে যাচ্ছেন, সেখান থেকেই প্রায়ই counterexample-এর সূত্র মেলে।

৪ · কোডে activity selection — আর brute force দিয়ে যাচাই

প্রমাণ থাকলেও কোডে ভুল হতে পারে: ভুল key দিয়ে সর্ট, > আর >=-এর গোলমাল। তাই নিচের প্রোগ্রাম দুটি কাজ করে। প্রথমে greedy চালিয়ে প্রতিটি ক্লাসের জন্য take বা skip ছাপে। তারপর brute force: আটটি ক্লাসের সব 28 = ২৫৬টি উপসেট দেখে, সংঘাতহীন উপসেটগুলোর মধ্যে সবচেয়ে বড়টির আকার বের করে। দুটি উত্তর মিললে ভরসা বাড়ে। ক্লাসগুলো ইচ্ছা করে এলোমেলো ক্রমে দেওয়া, যাতে সর্টটিও পরীক্ষিত হয়।

#include <algorithm>
#include <bit>
#include <cstdio>
#include <vector>
using namespace std;
struct Class { char name; int s, f; };                 // half-open [s, f)

vector<char> greedy(vector<Class> a) {                 // earliest finish time first
    sort(a.begin(), a.end(), [](const Class& x, const Class& y) { return x.f < y.f; });
    vector<char> chosen; int last = 0;                 // finish of the last chosen class
    for (const Class& c : a) {
        bool ok = c.s >= last;                         // touching is fine
        printf("  %s %c [%d,%d)\n", ok ? "take" : "skip", c.name, c.s, c.f);
        if (ok) { chosen.push_back(c.name); last = c.f; }
    }
    return chosen;
}
int brute(const vector<Class>& a, int& ways) {         // try all 2^n subsets
    int n = a.size(), best = 0; ways = 0;
    for (unsigned m = 0; m < (1u << n); ++m) {
        bool ok = true;
        for (int i = 0; i < n; ++i) for (int j = i + 1; j < n; ++j)
            if ((m >> i & 1) && (m >> j & 1) && a[i].s < a[j].f && a[j].s < a[i].f) ok = false;
        int k = popcount(m);
        if (ok && k > best) { best = k; ways = 1; } else if (ok && k == best) ++ways;
    }
    return best;
}
int main() {
    vector<Class> a = {{'E', 10, 14}, {'A', 8, 10}, {'G', 14, 16}, {'C', 11, 12},
                       {'H', 15, 17}, {'B', 9, 11}, {'F', 13, 15}, {'D', 12, 13}};
    vector<char> g = greedy(a);
    int ways, best = brute(a, ways);
    printf("greedy picks %zu:", g.size());
    for (char c : g) printf(" %c", c);
    printf("\nbrute force over %d subsets: best = %d (%d optimal sets)\n", 1 << a.size(), best, ways);
    puts((int)g.size() == best ? "match: yes" : "match: NO");
}

আউটপুট:

  take A [8,10)
  skip B [9,11)
  take C [11,12)
  take D [12,13)
  skip E [10,14)
  take F [13,15)
  skip G [14,16)
  take H [15,17)
greedy picks 5: A C D F H
brute force over 256 subsets: best = 5 (2 optimal sets)
match: yes

Take/skip-এর আটটি লাইন অংশ ২-এর ডায়াগ্রামের ধাপগুলোর সাথে হুবহু মেলে। brute প্রতিটি bitmask m-কে একটি উপসেট ধরে: bit i = 1 মানে ক্লাস i আছে। দুটি half-open interval মেলে (overlap করে) ঠিক তখন, যখন a[i].s < a[j].f && a[j].s < a[i].f. popcount (C++20, <bit>) উপসেটের আকার গোনে। “2 optimal sets” দেখায় সর্বোত্তম সমাধান একটিই নয় — {A, C, D, F, H} আর {B, C, D, F, H}, ঠিক অংশ ৩-এর দুটি। Greedy এদের একটি খুঁজে দেয়; প্রমাণ শুধু আকার নিয়ে, কোন সেট তা নিয়ে নয়। Brute force-এর সময় Θ(2n · n²) — n = 8-এ মুহূর্তে শেষ, n = 30-এ অচল। তাই এটি শুধু ছোট ইনপুটে যাচাইয়ের হাতিয়ার।

from itertools import combinations

def greedy(classes):                          # earliest finish time first
    chosen, last = [], 0                      # last = finish of the last chosen class
    for name, s, f in sorted(classes, key=lambda c: c[2]):
        ok = s >= last                        # half-open [s, f): touching is fine
        print(f"  {'take' if ok else 'skip'} {name} [{s},{f})")
        if ok:
            chosen.append(name)
            last = f
    return chosen

def brute(classes):                           # try all 2^n subsets
    best, ways = 0, 0
    for k in range(len(classes) + 1):
        for sub in combinations(classes, k):
            if all(a[1] >= b[2] or b[1] >= a[2] for a, b in combinations(sub, 2)):
                if k > best: best, ways = k, 1
                elif k == best: ways += 1
    return best, ways

classes = [("E", 10, 14), ("A", 8, 10), ("G", 14, 16), ("C", 11, 12),
           ("H", 15, 17), ("B", 9, 11), ("F", 13, 15), ("D", 12, 13)]
g = greedy(classes)
best, ways = brute(classes)
print(f"greedy picks {len(g)}:", " ".join(g))
print(f"brute force over {2 ** len(classes)} subsets: best = {best} ({ways} optimal sets)")
print("match: yes" if len(g) == best else "match: NO")

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

  take A [8,10)
  skip B [9,11)
  take C [11,12)
  take D [12,13)
  skip E [10,14)
  take F [13,15)
  skip G [14,16)
  take H [15,17)
greedy picks 5: A C D F H
brute force over 256 subsets: best = 5 (2 optimal sets)
match: yes

sorted(..., key=lambda c: c[2]) শেষের সময় দিয়ে সাজায়; Python-এর সর্ট stable, তাই সমান শেষের সময়ে মূল ক্রম থাকে (এখানে সমতা নেই)। Brute force-এ itertools.combinations ছোট থেকে বড় আকারের উপসেট বানায় — bitmask-এর বদলে আরও পড়তে সহজ রূপ। শর্ত a[1] >= b[2] or b[1] >= a[2] মানে “একটি অন্যটি শেষ হওয়ার পর শুরু” — overlap না করার শর্ত। দুই ভাষার এই মিল নিজেই একটি পরীক্ষা: একই ইনপুটে আলাদা দুটি বাস্তবায়ন একই উত্তর দিচ্ছে।

Stress testing — একটি ইনপুট যথেষ্ট নয়

একটি ইনপুটে মিল মানে শুধু ওই ইনপুটে ঠিক। আসল অভ্যাস হলো stress testing: একটি লুপে হাজার হাজার এলোমেলো ছোট ইনপুট (৬–১০টি interval, সময় ০–২০) বানান, প্রতিটিতে greedy আর brute force চালান, অমিল পেলেই ইনপুটটি ছেপে থামুন। ভুল greedy সাধারণত কয়েক সেকেন্ডেই ধরা পড়ে — “আগে শুরু” নিয়মটি প্রথম কয়েকটি ইনপুটেই হারবে। ছোট ইনপুট বাছার কারণ দুটি: brute force দ্রুত চলে, আর ভুল ধরা পড়লে ইনপুটটি হাতে trace করা যায়। কনটেস্টে wrong answer পেলে, বা AI-এর দেওয়া কোনো greedy-তে সন্দেহ হলে, এটিই প্রথম হাতিয়ার।

৫ · Interval partitioning — সব ক্লাসের জন্য কয়টি রুম লাগবে

এবার প্রশ্ন উল্টো। কোনো ক্লাস বাদ দেওয়া যাবে না — সবগুলো রাখতে হবে; প্রশ্ন হলো সবচেয়ে কম কয়টি রুম লাগবে? রেলস্টেশনে ট্রেনের জন্য প্ল্যাটফর্ম, হাসপাতালের অপারেশন থিয়েটার, পরীক্ষার হল — একই সমস্যা। একে বলে interval partitioning.

নিম্নসীমা

ক্লাসগুলোর depth d = কোনো একটি মুহূর্তে একসাথে চলা ক্লাসের সর্বোচ্চ সংখ্যা। যেকোনো সমাধানে অন্তত d টি রুম লাগে — সেই মুহূর্তের d টি ক্লাসকে আলাদা রুমে বসাতেই হবে। চমকের কথা: greedy ঠিক d টি রুমেই কাজ সারে, তাই উত্তর = depth.

অ্যালগরিদম: ক্লাসগুলো শুরুর সময়ে সাজান। প্রতিটি ক্লাসের জন্য দেখুন, এমন কোনো রুম কি আছে যা এর শুরুর মধ্যে খালি হয়ে যায়? থাকলে সেখানে দিন; না থাকলে নতুন রুম খুলুন। “কোন রুম সবচেয়ে আগে খালি হয়” দ্রুত জানতে একটি min-heap (পর্ব ২৩) রাখুন, যাতে প্রতিটি রুমের (খালি হওয়ার সময়, রুম নম্বর) থাকে। নিচের উদাহরণে সাতটি ক্লাস, নম্বর ১–৭.

প্রতিটি সারি একটি রুম (R1, R2, R3)সবুজ = এইমাত্র বসানো ক্লাস

            
প্রতিটি ধাপের পর
এই ধাপের ক্লাস
সবচেয়ে আগে খালি
সিদ্ধান্ত
heap (খালি হওয়ার সময়, রুম)
  1. শুরু. সাতটি ক্লাস শুরুর ক্রমে: ১ [৯,১১), ২ [৯,১২), ৩ [১০,১৩), ৪ [১১,১৪), ৫ [১২,১৩), ৬ [১৩,১৫), ৭ [১৪,১৬). এখনো কোনো রুম খোলা হয়নি, heap খালি।
  2. ক্লাস ১. কোনো রুম নেই, তাই R1 খুলুন। R1 খালি হবে ১১টায়।
  3. ক্লাস ২. ৯টায় শুরু, কিন্তু সবচেয়ে আগে খালি হওয়া R1-ও ১১টার আগে নয়। নতুন রুম R2.
  4. ক্লাস ৩. ১০টায় শুরু; heap-এর সবচেয়ে ছোট এখনো ১১. নতুন রুম R3. এই মুহূর্তে (১০–১১টা) ক্লাস ১, ২, ৩ একসাথে চলছে — depth অন্তত ৩.
  5. ক্লাস ৪. ১১টায় শুরু, R1 ঠিক ১১টায় খালি। Half-open, তাই R1-এ বসান; R1 এখন ১৪টা পর্যন্ত ব্যস্ত।
  6. ক্লাস ৫. ১২টায় শুরু, R2 ১২টায় খালি — R2. এখন (১২–১৩টা) ৩, ৪, ৫ একসাথে চলছে; আবার তিনটি।
  7. ক্লাস ৬. ১৩টায় শুরু। R2 আর R3 দুটোই ১৩টায় খালি; সমতায় ছোট নম্বরের রুম, R2.
  8. ক্লাস ৭. ১৪টায় শুরু, R3 ১৩টা থেকেই খালি — R3. মোট ৩টি রুম, আর depth-ও ৩ — নিম্নসীমা ছুঁয়ে ফেলেছে, তাই এর চেয়ে কম সম্ভব নয়।

কেন ঠিক: ধরুন greedy মোট d টি রুম খুলেছে। শেষ (d-তম) রুমটি খোলা হয়েছিল একটি ক্লাস x-এর জন্য, কারণ তখন বাকি d − 1 টি রুমের প্রতিটি ব্যস্ত ছিল — প্রতিটিতে এমন একটি ক্লাস চলছিল যা x-এর আগে বা একই সময়ে শুরু হয়েছে (শুরুর ক্রমে সাজানো) আর s(x)-এর পরে শেষ হবে। তাহলে ঠিক s(x) মুহূর্তে x আর ওই d − 1 টি ক্লাস, মোট d টি ক্লাস একসাথে চলছে: depth ≥ d. আর যেকোনো সমাধানে লাগে ≥ depth. তাই greedy সর্বোত্তম। ∎ লক্ষ করুন, এই প্রমাণটি একটি তৃতীয় ধাঁচের: “একটি কাঠামোগত নিম্নসীমা খুঁজে দেখানো যে greedy সেটি ছুঁয়ে ফেলে”.

জটিলতা: সর্ট O(n log n), প্রতিটি ক্লাসে heap-এ একটি push আর সর্বোচ্চ একটি pop, প্রতিটি O(log n) — মোট O(n log n), জায়গা O(n). শুধু রুমের সংখ্যা দরকার হলে আরও সহজ একটি sweep line চলে: প্রতিটি ক্লাসকে দুটি ঘটনা বানান — শুরুতে +১, শেষে −১ — সময় অনুযায়ী সাজান, আর চলমান যোগফলের সর্বোচ্চ মানই উত্তর। সতর্কতা: একই সময়ে একটি শেষ আর একটি শুরু থাকলে half-open interval-এ −১ আগে প্রক্রিয়া করুন; না হলে উত্তর একটি বেশি আসবে — একটি ক্লাসিক off-by-one বাগ।

৬ · যেখানে greedy হারে — knapsack আর coin change

Fractional knapsack — ভাঙা গেলে greedy

চকবাজারের একজন মশলা ব্যবসায়ী ৫০ কেজির একটি বস্তা নিয়ে পাইকারি বাজারে গেছেন। তিন রকম মশলা পাওয়া যাচ্ছে, প্রতিটির পরিমাণ সীমিত। প্রশ্ন হলো, বস্তায় কোন মশলা কতটুকু ভরলে মোট দাম সবচেয়ে বেশি হয়। এটিই knapsack সমস্যা. মশলা ওজন করে যেকোনো পরিমাণে নেওয়া যায়, তাই এখানে রূপটি fractional knapsack.

মশলামোট ওজনমোট দাম (হাজার টাকা)প্রতি কেজি (হাজার টাকা)
এলাচ১০ কেজি৬০৬
জিরা২০ কেজি১০০৫
গোলমরিচ৩০ কেজি১২০৪

Greedy: প্রতিটি জিনিসের value density, অর্থাৎ প্রতি কেজির দাম, বের করুন; বড় থেকে ছোট ক্রমে সাজান; যতক্ষণ জায়গা আছে পুরোটা নিন, শেষেরটির যতটুকু ধরে ততটুকু। এখানে পুরো এলাচ (১০ কেজি, ৬০), পুরো জিরা (২০ কেজি, ১০০), তারপর বাকি ২০ কেজি জায়গায় গোলমরিচের ২০/৩০ অংশ (৮০) — মোট ২৪০ হাজার টাকা. সময় O(n log n) (সর্ট), জায়গা O(n).

প্রমাণ (exchange): ধরুন একটি সর্বোত্তম ভরাট আছে, যাতে কোনো কম-ঘনত্বের জিনিস j-এর কিছু অংশ আছে, অথচ বেশি-ঘনত্বের জিনিস i পুরোটা নেওয়া হয়নি। j-এর ε কেজি সরিয়ে i-এর ε কেজি ভরুন: ওজন একই, দাম বদলায় ε · (ঘনত্বi − ঘনত্বj) ≥ ০. এভাবে বদলাতে বদলাতে ঠিক greedy-র ভরাটে পৌঁছানো যায়, দাম কখনো কমে না — তাই greedy-ও সর্বোত্তম। ∎ (George Dantzig ১৯৫৭ সালে knapsack-এর এই ভগ্নাংশ রূপের জন্য ঠিক এই ক্রমের সমাধান দেখান।)

0/1 knapsack — ভাঙা না গেলে greedy ভুল

এবার ধরুন মশলাগুলো সিলমোহর করা বস্তায় — হয় পুরো বস্তা নেবেন, নয়তো নেবেন না। এটি 0/1 knapsack. একই greedy প্রথমে এলাচ (১০ কেজি) আর জিরা (২০ কেজি) নেয় — ৩০ কেজি, ১৬০ হাজার। গোলমরিচের ৩০ কেজি আর ধরে না, তাই শেষ: ১৬০. অথচ জিরা + গোলমরিচ = ঠিক ৫০ কেজি, দাম ২২০. একটি counterexample — নিয়মটি বাতিল। “সবচেয়ে দামি আগে” বা “সবচেয়ে হালকা আগে” নিয়মেরও সহজ counterexample আছে। আসলে 0/1 knapsack NP-hard; কোনো দ্রুত greedy আশা করাই ভুল। সমাধান dynamic programming দিয়ে O(n · W) সময়ে (W = ধারণক্ষমতা) — একটি pseudo-polynomial অ্যালগরিদম, বিস্তারিত পর্ব ৫৩-এ।

কেন exchange প্রমাণটি এখানে ভেঙে পড়ে? প্রমাণে আমরা “j-এর ε কেজি সরিয়ে i-এর ε কেজি” ভরেছিলাম। জিনিস ভাঙা না গেলে ε-এর বদলে পুরো বস্তা বদলাতে হয়, আর তখন ওজন মেলে না — খালি জায়গা নষ্ট হয়। প্রমাণ কোথায় আটকায় তা দেখলেই বোঝা যায়, counterexample কোথায় খুঁজতে হবে।

Coin change — টাকার নোটে greedy, অদ্ভুত কয়েনে নয়

দোকানদার ১,৭৮৯ টাকা ফেরত দেবেন, যত কম সংখ্যক নোট ও কয়েনে সম্ভব। বাংলাদেশের মানগুলো ১, ২, ৫, ১০, ২০, ৫০, ১০০, ২০০, ৫০০ আর ১০০০ টাকা। প্রত্যেক দোকানদার না ভেবেই greedy চালান: যে সবচেয়ে বড় নোট ধরে, সেটি দিন, বাকিটার জন্য আবার একই কাজ। ১০০০ + ৫০০ + ২০০ + ৫০ + ২০ + ১০ + ৫ + ২ + ২ — মোট ৯টি। এটিই coin change সমস্যা। বাংলাদেশের টাকায় greedy সবসময় সবচেয়ে কম সংখ্যক নোট দেয় — যে coin system-এ এমন হয়, তাকে বলে canonical coin system.

কিন্তু ধরুন কোনো দেশে কয়েন শুধু ১, ৩ আর ৪ টাকার, আর ফেরত দিতে হবে ৬ টাকা। Greedy: ৪ + ১ + ১ = ৩টি কয়েন। অথচ ৩ + ৩ = মাত্র ২টি। এই system canonical নয়। সঠিক উত্তরের জন্য DP: dp[x] = x টাকা বানাতে সবচেয়ে কম কয়েন, আর dp[x] = ১ + min(dp[x − c]), যেখানে c প্রতিটি কয়েন ≤ x. সময় O(n · A) (n = কয়েনের রকম, A = অঙ্ক), জায়গা O(A).

dp[x] = x বানাতে সবচেয়ে কম কয়েনpick[x] = শেষ কয়েনটি, সবুজ = এই ধাপে

            
প্রতিটি ধাপে
হিসাব
ফল
  1. Greedy. ৬-এর মধ্যে সবচেয়ে বড় কয়েন ৪; বাকি ২-এ ধরে শুধু ১, তারপর আবার ১. মোট ৩টি কয়েন। প্রথম পছন্দ “৪” নেওয়ার পর আর ফেরার উপায় নেই — greedy-choice property এখানে খাটে না।
  2. dp[0] = ০. শূন্য টাকা বানাতে কোনো কয়েন লাগে না — DP-র ভিত্তি।
  3. dp[1] = ১. শুধু ১-এর কয়েন ধরে: dp[0] + ১.
  4. dp[2] = ২. আবার শুধু ১ ধরে: dp[1] + ১ = ২ (১ + ১).
  5. dp[3] = ১. দুটি বিকল্প: শেষ কয়েন ৩ (dp[0] + ১ = ১) বা ১ (dp[2] + ১ = ৩). ছোটটি ১, pick = ৩.
  6. dp[4] = ১. শেষ কয়েন ৪ নিলে dp[0] + ১ = ১ — একটিই কয়েন।
  7. dp[5] = ২. ৪ (dp[1] + ১ = ২), ৩ (dp[2] + ১ = ৩), ১ (dp[4] + ১ = ২). সবচেয়ে কম ২; প্রথম পাওয়া pick = ৪, অর্থাৎ ৪ + ১.
  8. dp[6] = ২. ৪ নিলে dp[2] + ১ = ৩ — ঠিক greedy-র পথ। ৩ নিলে dp[3] + ১ = ২ — ভালো! ১ নিলে dp[5] + ১ = ৩. DP তিনটিই দেখে, তাই ২ পায়; greedy শুধু প্রথমটি দেখেছিল।
  9. উত্তর পুনর্গঠন. pick[6] = ৩, বাকি ৩; pick[3] = ৩, বাকি ০. সর্বোত্তম ৩ + ৩, ২টি কয়েন — greedy-র চেয়ে একটি কম।

নিচের প্রোগ্রাম দুটি coin system-এ প্রতিটি অঙ্কের জন্য greedy আর DP-র কয়েন-সংখ্যা মেলায়, আর প্রথম যে অঙ্কে greedy হারে তা দেখায়।

#include <cstdio>
#include <vector>
using namespace std;
const int INF = 1 << 30;

vector<int> greedy(const vector<int>& coins, int x) {      // coins sorted big -> small
    vector<int> used;
    for (int v : coins) while (x >= v) { x -= v; used.push_back(v); }
    return used;
}
void dpCoins(const vector<int>& coins, int A, vector<int>& dp, vector<int>& pick) {
    dp.assign(A + 1, INF); pick.assign(A + 1, 0); dp[0] = 0;   // dp[x] = fewest coins for x
    for (int x = 1; x <= A; ++x)
        for (int v : coins)
            if (v <= x && dp[x - v] + 1 < dp[x]) { dp[x] = dp[x - v] + 1; pick[x] = v; }
}
void check(const char* name, const vector<int>& coins, int A) {
    vector<int> dp, pick, bad;
    dpCoins(coins, A, dp, pick);
    for (int x = 1; x <= A; ++x) if ((int)greedy(coins, x).size() != dp[x]) bad.push_back(x);
    printf("%s amounts 1..%d: greedy wrong for %zu amounts\n", name, A, bad.size());
    if (bad.empty()) return;
    int x = bad[0];
    vector<int> g = greedy(coins, x);
    printf("  first failure %d: greedy", x);
    for (int v : g) printf(" %d", v);
    printf(" (%zu coins), optimal", g.size());
    for (int y = x; y > 0; y -= pick[y]) printf(" %d", pick[y]);
    printf(" (%d coins)\n", dp[x]);
}
int main() {
    vector<int> taka = {1000, 500, 200, 100, 50, 20, 10, 5, 2, 1};
    vector<int> used = greedy(taka, 1789);
    printf("1789 taka -> %zu notes:", used.size());
    for (int v : used) printf(" %d", v);
    puts("");
    check("taka", taka, 2000);
    check("{1,3,4}", {4, 3, 1}, 20);
}

আউটপুট:

1789 taka -> 9 notes: 1000 500 200 50 20 10 5 2 2
taka amounts 1..2000: greedy wrong for 0 amounts
{1,3,4} amounts 1..20: greedy wrong for 4 amounts
  first failure 6: greedy 4 1 1 (3 coins), optimal 3 3 (2 coins)

টাকার নোটে ১ থেকে ২০০০ পর্যন্ত কোনো অঙ্কেই greedy হারেনি। {1, 3, 4}-এ ২০-এর মধ্যে চারটি অঙ্কে হেরেছে — ৬, ১০, ১৪ আর ১৮ (প্রতিটিতে ৪ + ১ + ১-এর বদলে ৩ + ৩ ভালো)। dpCoins শুধু সংখ্যা নয়, pick-এ শেষ কয়েনটিও রাখে, তাই for (int y = x; y > 0; y -= pick[y]) লুপে আসল কয়েনগুলো ফিরে পাওয়া যায় — DP-তে উত্তর পুনর্গঠনের সাধারণ কৌশল। INF = 1 << 30 “অসম্ভব” বোঝায়; এখানে ১ টাকার কয়েন আছে বলে সব অঙ্কই সম্ভব। Greedy-র সময় O(n + উত্তরের কয়েন-সংখ্যা), DP-র O(n · A).

INF = float("inf")

def greedy(coins, x):                         # coins sorted big -> small
    used = []
    for v in coins:
        while x >= v:
            x -= v
            used.append(v)
    return used

def dp_coins(coins, A):                       # dp[x] = fewest coins that make x
    dp, pick = [0] + [INF] * A, [0] * (A + 1)
    for x in range(1, A + 1):
        for v in coins:
            if v <= x and dp[x - v] + 1 < dp[x]:
                dp[x], pick[x] = dp[x - v] + 1, v
    return dp, pick

def check(name, coins, A):
    dp, pick = dp_coins(coins, A)
    bad = [x for x in range(1, A + 1) if len(greedy(coins, x)) != dp[x]]
    print(f"{name} amounts 1..{A}: greedy wrong for {len(bad)} amounts")
    if bad:
        x, best = bad[0], []
        y = x
        while y:
            best.append(pick[y]); y -= pick[y]
        g = greedy(coins, x)
        print(f"  first failure {x}: greedy", *g, f"({len(g)} coins), optimal", *best, f"({len(best)} coins)")

taka = [1000, 500, 200, 100, 50, 20, 10, 5, 2, 1]
used = greedy(taka, 1789)
print(f"1789 taka -> {len(used)} notes:", *used)
check("taka", taka, 2000)
check("{1,3,4}", [4, 3, 1], 20)

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

1789 taka -> 9 notes: 1000 500 200 50 20 10 5 2 2
taka amounts 1..2000: greedy wrong for 0 amounts
{1,3,4} amounts 1..20: greedy wrong for 4 amounts
  first failure 6: greedy 4 1 1 (3 coins), optimal 3 3 (2 coins)

float("inf") দিয়ে শুরুর মান রাখা Python-এ স্বাভাবিক, কারণ যেকোনো পূর্ণসংখ্যা তার চেয়ে ছোট। print(..., *g, ...) তালিকার উপাদানগুলো ফাঁকা দিয়ে ছাপে, তাই আউটপুট C++-এর লুপের মতোই। DP-র ভেতরের লুপে কয়েনের ক্রম [4, 3, 1] আর তুলনা কঠোর (<), তাই সমতায় প্রথম পাওয়া কয়েনটি pick-এ থাকে — দুই ভাষায় একই নিয়ম, তাই পুনর্গঠিত উত্তরও এক।

কতদূর পরীক্ষা করলে যথেষ্ট? ১ থেকে ২০০০ পরীক্ষা করে কি “সব অঙ্কে” বলা যায়? যায়, একটি জানা ফলের জোরে: Kozen ও Zaks (১৯৯৪) দেখান, কোনো coin system canonical না হলে সবচেয়ে ছোট counterexample অঙ্কটি সবচেয়ে বড় দুটি কয়েনের যোগফলের চেয়ে ছোট। টাকায় সেটি ১০০০ + ৫০০ = ১৫০০, তাই ২০০০ পর্যন্ত মিল পুরো system-এর প্রমাণ। {1, 3, 4}-এ সীমা ৪ + ৩ = ৭, আর ৬ সেই সীমার ভেতরেই। পরে David Pearson (২০০৫) একটি coin system canonical কি না তা O(n³) সময়ে যাচাইয়ের অ্যালগরিদম দেন। আর কয়েনের মানগুলোই যদি ইনপুটের অংশ হয়, তবে সাধারণ change-making সমস্যা NP-hard — DP-র O(n · A) বড় A-তে দ্রুত নয়।

৭ · Deadline-এর সমস্যা — lateness কমানো আর লাভের কাজ বাছাই

Minimising lateness — earliest deadline first

একটি ছাপাখানায় একটিই মেশিন। চারটি অর্ডার এসেছে; প্রতিটির ছাপার সময় t আর ডেলিভারির deadline d জানা (এখন থেকে ঘণ্টায়)। সব কাজ করতেই হবে, একটির পর একটি। যে কাজ deadline-এর পরে শেষ হয়, তার lateness = শেষ হওয়ার সময় − d (আগে শেষ হলে ০)। লক্ষ্য: সবচেয়ে বেশি lateness যতটা সম্ভব কম রাখা — কোনো গ্রাহককেই যেন বেশিক্ষণ অপেক্ষা না করতে হয়।

কাজ (EDF ক্রমে)tdচলেlateness
X২৩০–২০
W৩৫২–৫০
Y১৭৫–৬০
Z৪৮৬–১০২

নিয়মটি হলো earliest deadline first (EDF): deadline-এর ক্রমে কাজ করুন, কাজের দৈর্ঘ্য দেখবেন না। উপরের সময়সূচিতে সর্বোচ্চ lateness ২, আর এর চেয়ে ভালো অসম্ভব: মোট কাজ ১০ ঘণ্টার, তাই শেষ কাজটি ১০-এ শেষ হবেই, আর কোনো deadline ৮-এর বেশি নয়। দুটি “স্বাভাবিক” বিকল্প নিয়ম ভুল: ছোট কাজ আগে — (t = ১, d = ১০০) আর (t = ১০, d = ১০): ছোটটি আগে করলে বড়টি ১ ঘণ্টা দেরি, EDF-এ কেউ দেরি নয়। সবচেয়ে কম ঢিল (d − t) আগে — (t = ১, d = ২) আর (t = ১০, d = ১০): ঢিল-নিয়মে বড়টি আগে, ছোটটি ৯ ঘণ্টা দেরি; EDF-এ মাত্র ১.

প্রমাণ (inversion দিয়ে exchange): একটি সময়সূচিতে inversion মানে এমন দুটি কাজ i আর j, যেখানে di < dj অথচ j আগে চলে। মাঝে কোনো ফাঁকা সময় নেই এমন একটি সর্বোত্তম সময়সূচি নিন।

  1. Inversion থাকলে অন্তত একটি পাশাপাশি inversion আছে: পরপর দুটি কাজ j, i, যেখানে di < dj. (পরপর প্রতিটি জোড়া deadline-এর ক্রমে থাকলে পুরো ক্রমটাই সাজানো হতো।)
  2. দুটির জায়গা বদলান। বাকি সব কাজ একই সময়ে শেষ হয়। i আগে শেষ হয়, তার lateness বাড়ে না।
  3. j এখন শেষ হয় ঠিক সেই সময়ে f, যখন আগে i শেষ হতো। তাই j-এর নতুন lateness = f − dj < f − di = i-এর পুরোনো lateness. সর্বোচ্চ lateness তাই বাড়ে না।
  4. প্রতি বদলে inversion ঠিক একটি কমে; সর্বোচ্চ n(n − 1)/2 বদলে কোনো inversion থাকে না — সেটিই EDF-এর সময়সূচি (সমান deadline-এর কাজগুলোর নিজেদের ক্রম সর্বোচ্চ lateness বদলায় না)। ∎

সময় O(n log n) — শুধু deadline দিয়ে সর্ট। এই নিয়মটি J. R. Jackson-এর নামে “Jackson-এর নিয়ম” বলেও পরিচিত (১৯৫৫)।

Job sequencing with deadlines — লাভ সবচেয়ে বেশি

অন্য রূপ: প্রতিটি কাজ ঠিক এক দিন নেয়, প্রতিটির একটি deadline (দিন) আর লাভ আছে; deadline-এর মধ্যে শেষ হলে লাভ মেলে, না হলে কাজটি নেওয়ারই দরকার নেই। এটি job sequencing with deadlines. Greedy: লাভের বড় থেকে ছোট ক্রমে কাজ দেখুন; প্রতিটিকে তার deadline-এর মধ্যে সবচেয়ে শেষের খালি দিনে বসান, খালি দিন না থাকলে বাদ। শেষের দিনে বসানোর কারণ: আগের দিনগুলো খালি রাখা, যাতে ছোট deadline-এর কাজগুলো জায়গা পায়।

কাজ (লাভের ক্রমে)লাভ (হাজার টাকা)deadlineসিদ্ধান্ত
a১০০২দিন ২-এ বসল
c২৭২দিন ২ ভরা → দিন ১-এ
d২৫১দিন ১ ভরা → বাদ
b১৯১বাদ
e১৫৩দিন ৩-এ বসল

ফল: দিন ১-এ c, দিন ২-এ a, দিন ৩-এ e — মোট লাভ ১৪২. প্রতিটি কাজের জন্য পেছনে হেঁটে খালি দিন খুঁজলে O(n log n + n · D) (D = সবচেয়ে বড় deadline); “প্রতিটি দিনের আগের সবচেয়ে কাছের খালি দিন” DSU-তে (পর্ব ৩৩) রাখলে প্রায় O(n log n). এই greedy কেন ঠিক, তার সবচেয়ে পরিষ্কার ব্যাখ্যা matroid — পরের অংশে।

৮ · Greedy-র বড় পরিবার — MST, Dijkstra, Huffman আর matroid

এই কোর্সে আপনি আগেই কয়েকটি বিখ্যাত greedy দেখেছেন, হয়তো সেভাবে না ভেবে:

অ্যালগরিদমgreedy পছন্দপ্রমাণের ধাঁচ
Kruskal / Prim (পর্ব ৪৫)cycle না বানানো সবচেয়ে সস্তা edge / tree থেকে বের হওয়া সবচেয়ে সস্তা edgeExchange: cut property, T − f + e
Dijkstra (পর্ব ৪৩)এখনো চূড়ান্ত হয়নি এমন vertex-গুলোর মধ্যে সবচেয়ে কাছেরটিStays ahead: চূড়ান্ত দূরত্ব কখনো কমে না (ঋণাত্মক edge না থাকলে)
Huffman coding (পর্ব ৫১)সবচেয়ে কম ঘনঘন আসা দুটি প্রতীক জোড়া লাগানোExchange: সবচেয়ে বিরল দুটি প্রতীক গভীরতম পাতায়
Activity selection, EDF, fractional knapsackআগে শেষ / আগে deadline / বেশি ঘনত্বStays ahead / inversion / exchange

লক্ষ করুন, Dijkstra-ও ঋণাত্মক edge-এ ভুল করে — greedy-র নিশ্চয়তা সবসময় কিছু অনুমানের ওপর দাঁড়িয়ে (এখানে: edge-এর ওজন ঋণাত্মক নয়)। প্রমাণের কোন বাক্যে অনুমানটি লাগছে, তা জানলেই বোঝা যায় কোথায় greedy ভাঙবে।

আরও গভীরে — matroid: একটি matroid হলো একটি সেট E আর তার কিছু “স্বাধীন” উপসেটের পরিবার, যেখানে (১) স্বাধীন সেটের যেকোনো উপসেটও স্বাধীন, আর (২) A, B স্বাধীন এবং |A| < |B| হলে B-এর এমন একটি x আছে যাতে A + x-ও স্বাধীন (exchange ধর্ম)। Rado (১৯৫৭) আর Edmonds (১৯৭১)-এর ফল: “ওজন অনুযায়ী সাজিয়ে, স্বাধীনতা না ভেঙে নাও” greedy প্রতিটি ওজন-ফাংশনে সর্বোচ্চ ওজনের স্বাধীন সেট দেয় — ঠিক তখন, যখন কাঠামোটি matroid. গ্রাফের forest-গুলো একটি matroid (graphic matroid) — তাই Kruskal কাজ করে; “সবাই deadline-এর মধ্যে শেষ করা যায়” এমন কাজের সেটগুলোও একটি matroid — তাই job sequencing কাজ করে। 0/1 knapsack-এর “ব্যাগে ধরে” সেটগুলো matroid নয় (exchange ধর্ম ভাঙে), তাই সেখানে greedy হারে।

Matroid একটি সুন্দর তত্ত্ব, কিন্তু সব greedy-কে ব্যাখ্যা করে না — যেমন activity selection-এর সংঘাতহীন সেটগুলো matroid নয়, তবু নির্দিষ্ট “আগে শেষ” নিয়মে greedy ঠিক। তাই বাস্তবে প্রতিটি নতুন greedy-র জন্য সরাসরি exchange বা stays ahead প্রমাণই সবচেয়ে নির্ভরযোগ্য পথ।

পরীক্ষা ও ইন্টারভিউতে:

  • Activity selection-এ কোন নিয়ম, আর কেন “ছোট আগে” নয়? — আগে শেষ হওয়া আগে; ৯–১৩, ১২–১৪, ১৩–১৭-এ ছোটটি দুটিকে আটকায়।
  • Fractional আর 0/1 knapsack-এর পার্থক্য? — প্রথমটিতে ঘনত্ব-greedy সর্বোত্তম, O(n log n); দ্বিতীয়টি NP-hard, DP-তে O(n · W).
  • Greedy coin change কখন ভুল? — non-canonical system-এ; {1, 3, 4}-এ ৬ = ৪ + ১ + ১ (৩টি), অথচ ৩ + ৩ (২টি)।
  • Minimum কয়টি প্ল্যাটফর্ম লাগবে? — কোনো মুহূর্তে একসাথে থাকা ট্রেনের সর্বোচ্চ সংখ্যা (depth); sweep বা min-heap, O(n log n).
  • Exchange argument কী? — সর্বোত্তম সমাধানের একটি উপাদান greedy-র উপাদানে বদলে দেখানো যে সমাধান বৈধ থাকে আর খরচ বাড়ে না; বারবার করে greedy-তে পৌঁছানো।

৯ · AI-এর যুগে greedy — শুনতে ঠিক, প্রমাণ ছাড়া বিপজ্জনক

Greedy AI-এর সবচেয়ে প্রিয় ফাঁদ। “সাজাও আর লুপ চালাও” ধরনের সমাধান লেখা সহজ, পড়তেও বিশ্বাসযোগ্য, আর ছোট উদাহরণে প্রায়ই ঠিক উত্তর দেয়। AI সহকারীরা প্রায়ই একটি greedy প্রস্তাব করে, সাথে আত্মবিশ্বাসী একটি “প্রমাণ”-ও দেয়, যার কোনো একটি বাক্য আসলে মিথ্যা। কনটেস্টে এর ফল wrong answer, ইন্টারভিউতে “why is this correct?” প্রশ্নে আটকে যাওয়া।

Greedy-তে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
0/1 knapsack-এ ঘনত্ব-greedy, “এটি সর্বোত্তম” দাবিসহ(৬০, ১০), (১০০, ২০), (১২০, ৩০), W = ৫০: greedy ১৬০, সঠিক ২২০
যেকোনো কয়েনে greedy coin change{1, 3, 4}, অঙ্ক ৬ দিয়ে চালান; DP-র সাথে ১ থেকে cn + cn−1 পর্যন্ত মেলান
Interval-এ শুরুর সময় বা দৈর্ঘ্য দিয়ে সর্টঅংশ ২-এর তিন-interval counterexample-গুলো দিন
সমান সময়ে শেষ আর শুরু থাকলে ভুল tie-break — রুম একটি বেশি[৯, ১০) আর [১০, ১১) দিন; উত্তর ১ রুম হওয়া উচিত
“Greedy, তাই O(n)” — সর্টের খরচ বাদপ্রতিটি ধাপের খরচ আলাদা করে লিখতে বলুন: সর্ট, heap, লুপ
প্রমাণে “স্পষ্টতই সেরা” বা “স্বাভাবিকভাবেই” — কোনো exchange নেইবলুন: কোন সর্বোত্তম সমাধান থেকে কোন উপাদান বদলাচ্ছ, আর কেন বৈধ থাকে?

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

আমি DSA শিখছি, এখন greedy অ্যালগরিদম। নিচে একটি সমস্যা, আমার greedy নিয়ম, আর আমার লেখা প্রমাণের খসড়া দিলাম: [এখানে সমস্যা, নিয়ম আর প্রমাণ পেস্ট করুন] পুরো সমাধান লিখে দিও না। শুধু এগুলো করো: ১. আমার নিয়মের জন্য ৫টির কম উপাদানের একটি counterexample খোঁজো; না পেলে বলো কোন ধরনের ইনপুট তুমি চেষ্টা করেছ। ২. আমার প্রমাণের প্রতিটি বাক্য আলাদা করে যাচাই করো — কোন বাক্যটি দুর্বল বা অপ্রমাণিত? ৩. এটি exchange argument নাকি greedy stays ahead ধাঁচে লেখা ভালো? কাঠামোটি দেখাও, পূর্ণ প্রমাণ নয়। ৪. আমার কোডের time আর space complexity কত — সর্ট, heap আর লুপ আলাদা করে হিসাব করো। ৫. greedy আর brute force মেলানোর একটি stress test-এর পরিকল্পনা দাও: কত বড় ইনপুট, কোন সীমার মান, কোন edge case.

Greedy যাচাইয়ের চেকলিস্ট

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

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

প্র ০১“Greedy” নামটি কোথা থেকে এল, আর matroid তত্ত্ব কেন বলে যে greedy ঠিক এক ধরনের কাঠামোতেই সব সময় জেতে?

Greedy ধরনের পদ্ধতি অনেক পুরোনো। ১৯২৬ সালে Borůvka-র MST, ১৯৫০-এর দশকে Kruskal আর Prim — সবই আসলে greedy, কিন্তু তখন এদের এক নামে ডাকা হতো না। “Greedy algorithm” নামটি জনপ্রিয় হয় মূলত Jack Edmonds-এর ১৯৭১ সালের প্রবন্ধ “Matroids and the greedy algorithm” থেকে। Edmonds তখন combinatorial optimization-এর একজন অগ্রদূত; তাঁর ১৯৬৫ সালের matching-এর কাজ “polynomial সময় = দক্ষ অ্যালগরিদম” ধারণাটিও প্রতিষ্ঠিত করে।

Matroid ধারণাটি এসেছিল আরও আগে, ১৯৩৫ সালে Hassler Whitney-র কাছ থেকে। তিনি লক্ষ করেন, matrix-এর linearly independent কলামগুলো আর গ্রাফের cycle-হীন edge-সেটগুলো একই নিয়ম মানে: স্বাধীন সেটের উপসেটও স্বাধীন, আর ছোট স্বাধীন সেটকে বড়টি থেকে একটি উপাদান ধার করে বাড়ানো যায়। এই দুটি ধর্মই matroid-এর সংজ্ঞা।

Richard Rado ১৯৫৭ সালে আর পরে Edmonds (১৯৭১) দেখান: একটি স্বাধীনতা-কাঠামোয় “সবচেয়ে ভারী থেকে শুরু করে, স্বাধীনতা না ভেঙে যা নেওয়া যায় নাও” greedy প্রতিটি সম্ভাব্য ওজন-বণ্টনে সর্বোত্তম — ঠিক তখন এবং কেবল তখন, যখন কাঠামোটি matroid. “কেবল তখন” অংশটি শক্তিশালী: কাঠামো matroid না হলে এমন কোনো ওজন আছে, যেখানে greedy হারবে। প্রমাণের ভেতরে আছে exchange ধর্ম — ঠিক আমাদের exchange argument-এর বিমূর্ত রূপ।

এই তত্ত্ব এক ধাক্কায় অনেক কিছু ব্যাখ্যা করে: Kruskal (graphic matroid), unit-time job sequencing (scheduling matroid), linear algebra-য় সবচেয়ে হালকা basis খোঁজা। আবার দেখায় কেন 0/1 knapsack-এ আশা নেই। পরে দুটি matroid-এর ছেদে সর্বোচ্চ সেট (matroid intersection) — যার একটি বিশেষ রূপ bipartite matching — polynomial সময়ে সমাধানযোগ্য প্রমাণিত হয়, যদিও সেখানে সরল greedy আর যথেষ্ট নয়।

মূল উপলব্ধি: Greedy-র সাফল্য কোনো ভাগ্য নয়; তার পেছনে একটি নির্দিষ্ট গাণিতিক কাঠামো — exchange ধর্ম — কাজ করে। প্রতিটি exchange argument আসলে সেই কাঠামোর একটি ছোট প্রতিফলন।

প্র ০২বাংলাদেশের টাকায় দোকানদারের greedy সবসময় ঠিক, অথচ পুরোনো ব্রিটিশ মুদ্রায় ঠিক ছিল না — একটি দেশ তার নোট-কয়েনের মান কীভাবে বাছে, আর greedy-র সাথে তার সম্পর্ক কী?

বেশিরভাগ আধুনিক মুদ্রা ১-২-৫ ধাঁচ মানে: ১, ২, ৫, ১০, ২০, ৫০, ১০০… বাংলাদেশের টাকা এর সাথে ২০০, ৫০০ আর ১০০০ যোগ করেছে। এই ধাঁচের একটি সুবিধা হলো, যেকোনো অঙ্ক অল্প কয়েকটি নোটে বানানো যায়। আরেকটি সুবিধা — যেটি এই পর্বের বিষয় — greedy সবসময় সবচেয়ে কম নোট দেয়, তাই দোকানদারকে হিসাব করতে হয় না। কোড থেকে আমরা দেখেছি, ১ থেকে ২০০০ পর্যন্ত কোনো অঙ্কেই greedy হারে না, আর Kozen–Zaks-এর সীমা (সবচেয়ে বড় দুটি মানের যোগফল) বলে এটুকু পরীক্ষাই যথেষ্ট।

১৯৭১ সালের দশমিক পদ্ধতির আগের ব্রিটিশ মুদ্রার কয়েকটি প্রচলিত কয়েন দেখুন: পেনি (১), থ্রিপেন্স (৩), সিক্সপেন্স (৬), শিলিং (১২), ফ্লোরিন (২৪) আর হাফ-ক্রাউন (৩০ পেন্স)। ৪৮ পেন্স দিতে greedy নেয় হাফ-ক্রাউন + শিলিং + সিক্সপেন্স — ৩টি কয়েন; অথচ দুটি ফ্লোরিন — মাত্র ২টি। ছোট একটি প্রোগ্রাম দিয়ে নিজেই দেখতে পারবেন, এই ছয়টি কয়েনের system-এ সবচেয়ে ছোট counterexample ঠিক ৪৮. মানুষ তখন অভ্যাসেই ঠিক কয়েন বাছত, কিন্তু একটি সরল যন্ত্র বা প্রোগ্রাম greedy চালালে ভুল করত।

মুদ্রার মান বাছাইয়ে আরও বিষয় কাজ করে: গড়ে কয়টি কয়েন লাগে, ছাপানোর খরচ, মানুষের মাথায় হিসাবের সহজতা, মুদ্রাস্ফীতি। গবেষকরা “গড়ে সবচেয়ে কম কয়েন” লাগে এমন মান খুঁজে দেখেছেন যে সর্বোত্তম system সবসময় canonical হয় না — যেমন Jeffrey Shallit (২০০৩) দেখান, মার্কিন কয়েনে (১, ৫, ১০, ২৫ সেন্ট) একটি ১৮ সেন্টের কয়েন যোগ করলে গড় কয়েন-সংখ্যা কমে, কিন্তু তখন greedy আর সবসময় ঠিক থাকে না (৩৬ = ১৮ + ১৮, অথচ greedy নেয় ২৫ + ১০ + ১)। তখন দোকানদারকে প্রতিবার DP-র মতো ভাবতে হতো — বাস্তবে যা অচল।

অ্যালগরিদমের দিক থেকে: Pearson (২০০৫) দেখান যে একটি system canonical কি না তা O(n³) সময়ে যাচাই করা যায়, অথচ সাধারণ change-making সমস্যা NP-hard. অর্থাৎ “greedy কি এই ইনপুট-পরিবারে সবসময় ঠিক?” — এই প্রশ্নটির উত্তর দেওয়া মূল সমস্যা সমাধানের চেয়ে সহজ।

মূল উপলব্ধি: Greedy ঠিক কি না তা অ্যালগরিদমের নয়, ইনপুটের কাঠামোর ধর্ম। ভালো নকশা (যেমন টাকার মান) সেই কাঠামো তৈরি করে, যাতে সরল নিয়মই সর্বোত্তম হয়।

প্র ০৩১৯৫৫ সালের একটি কারখানার scheduling-এর ফল, EDF, কীভাবে আজকের রিয়েল-টাইম অপারেটিং সিস্টেমে পৌঁছাল — আর সেখানে greedy-র নিশ্চয়তা কতদূর?

১৯৫০-এর দশকে operations research নতুন শাস্ত্র: কারখানায় কোন কাজ কোন মেশিনে কখন — এই প্রশ্নের গাণিতিক উত্তর খোঁজা হচ্ছিল। J. R. Jackson ১৯৫৫ সালে দেখান, এক মেশিনে সর্বোচ্চ lateness কমাতে কাজগুলো deadline-এর ক্রমে (earliest due date) করলেই চলে — আমাদের inversion প্রমাণটি ঠিক এই ফলেরই। একই সময়ে W. E. Smith (১৯৫৬) দেখান, ওজনসহ মোট সমাপ্তি-সময় কমাতে t/w অনুপাতের ক্রমে কাজ করতে হয় — আরেকটি exchange argument.

১৯৭৩ সালে C. L. Liu আর James Layland কম্পিউটারের প্রেক্ষাপটে প্রশ্নটি নেন: একটি প্রসেসরে কয়েকটি পর্যায়ক্রমিক (periodic) কাজ, প্রতিটির নির্দিষ্ট সময় পরপর deadline. তাঁরা দেখান, কাজ মাঝপথে থামানো (preemption) গেলে EDF সর্বোত্তম: মোট ব্যবহার (প্রতিটি কাজের সময় ÷ পর্যায়ের যোগফল) ১-এর বেশি না হলে EDF সব deadline রক্ষা করে। গাড়ির ব্রেক নিয়ন্ত্রণ, বিমানের যন্ত্র, পেসমেকার — যেখানে deadline ভাঙা মানে বিপদ, সেখানে এমন নিশ্চয়তা জরুরি।

তবে নিশ্চয়তার সীমাও আছে। প্রসেসর একাধিক হলে EDF আর সর্বোত্তম থাকে না; আর সিস্টেমে চাপ বেশি হলে (ব্যবহার ১-এর বেশি) EDF-এ একের পর এক কাজ deadline মিস করতে পারে — একটি “domino effect”. তাই অনেক বাস্তব সিস্টেম fixed-priority scheduling (যেমন rate-monotonic) পছন্দ করে, যেখানে চাপের সময় কোন কাজ বলি হবে তা আগে থেকে জানা। Linux kernel-এও SCHED_DEADLINE নামে EDF-ভিত্তিক একটি scheduling নীতি আছে।

লক্ষ করুন, প্রতিটি নিশ্চয়তা একটি নির্দিষ্ট মডেলের ভেতরে: এক মেশিন, জানা কাজের সময়, preemption চলে কি না। মডেল বদলালে একই greedy ভুল হতে পারে — ঠিক যেমন fractional থেকে 0/1 knapsack-এ।

মূল উপলব্ধি: একটি সরল greedy-র প্রমাণ কয়েক দশক পরে জীবন-রক্ষাকারী সিস্টেমের ভিত্তি হয়েছে। কিন্তু প্রমাণের অনুমানগুলো (এক প্রসেসর, চাপ সীমার মধ্যে) না জানলে সেই নিশ্চয়তা বিপজ্জনক ভরসায় পরিণত হয়।

প্র ০৪Greedy যেখানে সর্বোত্তম নয়, সেখানেও কি সে কাজে লাগে? Set cover আর knapsack-এর “প্রায় সঠিক” greedy কতটা ভালো?

NP-hard সমস্যায় (পর্ব ৬৪) দ্রুত সর্বোত্তম অ্যালগরিদম আশা করা যায় না। তখন প্রশ্ন বদলায়: দ্রুত এমন উত্তর কি পাওয়া যায়, যা সর্বোত্তমের একটি নির্দিষ্ট গুণকের মধ্যে থাকে? এখানেও greedy প্রায়ই প্রথম ও সেরা প্রার্থী — এমন অ্যালগরিদমকে বলে approximation algorithm (পর্ব ৬৬)।

Set cover: একটি জেলার সব গ্রামে মোবাইল টাওয়ারের সিগন্যাল পৌঁছাতে হবে; প্রতিটি সম্ভাব্য টাওয়ার কিছু গ্রাম ঢাকে, আর লক্ষ্য সবচেয়ে কম টাওয়ার। Greedy: প্রতিবার সেই টাওয়ার নিন, যা এখনো-না-ঢাকা সবচেয়ে বেশি গ্রাম ঢাকে। ১৯৭০-এর দশকে David Johnson, László Lovász আর Vašek Chvátal দেখান, এই greedy সর্বোত্তমের সর্বোচ্চ প্রায় ln n গুণ টাওয়ার নেয় (n = গ্রামের সংখ্যা)। পরে প্রমাণিত হয়েছে, P ≠ NP হলে polynomial সময়ে এর চেয়ে উল্লেখযোগ্যভাবে ভালো গুণক পাওয়া সম্ভব নয় — অর্থাৎ সরল greedy-ই কার্যত সেরা।

0/1 knapsack: ঘনত্ব-greedy একা যথেচ্ছ খারাপ হতে পারে: একটি ছোট জিনিস (১ কেজি, ২ টাকা) আর একটি বড় (W কেজি, W টাকা) — greedy ছোটটি নিয়ে বড়টি হারায়, আর পায় মাত্র ২, যেখানে সম্ভব W. কিন্তু একটি ছোট সংশোধনে — “greedy-র উত্তর আর একা সবচেয়ে দামি জিনিস, দুটির ভালোটি নিন” — ফল সবসময় সর্বোত্তমের অন্তত অর্ধেক। আরও এগিয়ে, DP আর স্কেলিং মিলিয়ে knapsack-এর FPTAS আছে — যেকোনো ε-এর জন্য (1 − ε) গুণ সঠিক উত্তর, polynomial সময়ে।

বাস্তবে অনেক scheduling, রাউটিং আর বিজ্ঞাপন বরাদ্দের সিস্টেম greedy দিয়ে শুরু করে, তারপর local search দিয়ে উন্নত করে। Greedy-র উত্তর একটি মাপকাঠিও দেয়: আরও জটিল পদ্ধতি তার চেয়ে কতটা ভালো করল।

মূল উপলব্ধি: Greedy-র প্রশ্ন শুধু “সর্বোত্তম কি না” নয়, “কতটা দূরে”. সর্বোত্তম না হলেও প্রমাণিত গুণকের greedy প্রায়ই বাস্তবের সেরা হাতিয়ার।

প্র ০৫রেলস্টেশনের প্ল্যাটফর্ম আর পরীক্ষার হলের সমস্যা গ্রাফের ভাষায় কী — আর ক্লাসে লাভ (ওজন) যোগ হলে কেন greedy আর চলে না?

প্রতিটি ট্রেনের থাকার সময়কে একটি vertex ধরুন, আর দুটি সময় overlap করলে তাদের মধ্যে একটি edge দিন। যে গ্রাফ এভাবে interval থেকে আসে, তাকে বলে interval graph. Interval partitioning তখন হয় graph coloring: প্রতিটি প্ল্যাটফর্ম একটি রং, আর পাশাপাশি vertex-এ একই রং চলবে না। সাধারণ গ্রাফে সবচেয়ে কম রঙে coloring NP-hard, কিন্তু interval graph-এ আমাদের greedy O(n log n)-এ সর্বোত্তম — রঙের সংখ্যা = সবচেয়ে বড় clique (একসাথে overlap করা সবচেয়ে বড় দল) = depth. যে গ্রাফের যেকোনো vertex-দল বেছে নিলে (তাদের মধ্যের edge-সহ) সেই অংশেও রঙের সংখ্যা সবচেয়ে বড় clique-এর সমান, তাদের বলে perfect graph — Claude Berge ১৯৬০-এর দশকে এই ধারণা দেন, আর interval graph তার একটি প্রধান উদাহরণ।

Activity selection-ও গ্রাফের ভাষায় একটি পরিচিত সমস্যা: সবচেয়ে বড় independent set (পরস্পর edge-হীন vertex-এর দল)। সাধারণ গ্রাফে এটিও NP-hard, কিন্তু interval graph-এ “আগে শেষ” greedy যথেষ্ট। অর্থাৎ greedy-র সাফল্যের পেছনে আবার ইনপুটের বিশেষ কাঠামো — interval-গুলো একটি রেখার ওপর সাজানো।

এবার ধরুন প্রতিটি ক্লাসের একটি লাভ আছে (যেমন ফি), আর লক্ষ্য সবচেয়ে বেশি মোট লাভ, ক্লাসের সংখ্যা নয়। “আগে শেষ” নিয়ম ভাঙে: একটি লম্বা, খুব দামি ক্লাস দুটি ছোট সস্তা ক্লাসের চেয়ে ভালো হতে পারে। “বেশি লাভ আগে” নিয়মও ভাঙে: একটি মাঝারি দামি ক্লাস তিনটি প্রায়-সমান দামি ক্লাসকে আটকাতে পারে। এটি weighted interval scheduling, আর এর সমাধান DP: শেষের সময়ে সাজিয়ে opt[j] = max(opt[j − 1], লাভj + opt[p(j)]), যেখানে p(j) হলো j-এর আগে শেষ হওয়া শেষ সংগত ক্লাস — binary search দিয়ে O(n log n). পর্ব ৫২ থেকে DP-র পর্বগুলোতে এ ধরনের recurrence আরও দেখবেন।

মূল উপলব্ধি: একই কাঠামোয় (interval) সংখ্যা গুনলে greedy যথেষ্ট, ওজন যোগ হলে DP লাগে। সমস্যার ছোট একটি বদলে greedy-choice property হারিয়ে যায় — তাই প্রতিবার নতুন করে প্রমাণ খুঁজতে হয়।

অনুশীলন

খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।

  1. হাতে trace করুন: আটটি ক্লাস: P [১, ৪), Q [৩, ৫), R [০, ৬), S [৫, ৭), T [৩, ৯), U [৫, ৯), V [৬, ১০), X [৮, ১১). Earliest finish time চালিয়ে প্রতিটি ক্লাসের জন্য নিন না বাদ লিখুন। সবচেয়ে বেশি কয়টি ক্লাস রাখা যায়?

    শেষের ক্রমে: P (৪), Q (৫), R (৬), S (৭), T (৯), U (৯), V (১০), X (১১). P নিন, last = ৪. Q: ৩ < ৪, বাদ। R: ০ < ৪, বাদ। S: ৫ ≥ ৪, নিন, last = ৭. T: ৩ < ৭, বাদ। U: ৫ < ৭, বাদ। V: ৬ < ৭, বাদ। X: ৮ ≥ ৭, নিন, last = ১১.

    ফল: P, S, X — ৩টি ক্লাস. T আর U-এর শেষ সমান (৯) — tie-break যা-ই হোক, দুটিই বাদ পড়ে, তাই উত্তর বদলায় না। চারটি সম্ভব নয় — অংশ ৩-এর প্রমাণ বলে greedy যতগুলো নেয়, সেটিই সর্বোচ্চ। চাইলে অংশ ৪-এর brute force-এ এই আটটি ক্লাস বসিয়ে মিলিয়ে নিন।

  2. Counterexample খুঁজুন: কয়েনের মান {1, 5, 6, 9}. সবচেয়ে ছোট কোন অঙ্কে greedy সবচেয়ে কম কয়েন দেয় না? Kozen–Zaks-এর সীমা অনুযায়ী কত পর্যন্ত খুঁজলে যথেষ্ট?

    সীমা: সবচেয়ে বড় দুটির যোগফল ৯ + ৬ = ১৫, তাই ১৫-এর নিচে খুঁজলেই চলে। ১ থেকে ১০ পর্যন্ত greedy ঠিক (যেমন ১০ = ৯ + ১, দুটি কয়েন; ৫ + ৫-ও দুটি)। ১১-তে greedy নেয় ৯ + ১ + ১ = ৩টি কয়েন, অথচ ৬ + ৫ = ২টি। তাই উত্তর ১১.

    পরের কয়েকটিও দেখুন: ১২-তে greedy ৯ + ১ + ১ + ১ (৪টি), সেরা ৬ + ৬ (২টি); ১৩-তে greedy ৯ + ১ + ১ + ১ + ১ (৫টি), সেরা ৬ + ৬ + ১ (৩টি)। হাতে এমন হিসাবে ভুল সহজেই হয়, তাই ছোট একটি DP লুপ দিয়ে মিলিয়ে নেওয়াই নিরাপদ — উপরের coins প্রোগ্রামে শুধু কয়েনের তালিকাটি বদলে দিন।

  3. রুম গুনুন: সাতটি পরীক্ষা: ১ [৮, ১০), ২ [৮, ৯), ৩ [৯, ১১), ৪ [৯, ১০), ৫ [১০, ১২), ৬ [১০, ১১), ৭ [১১, ১২). (ক) Depth কত, কোন মুহূর্তে? (খ) শুরুর ক্রমে min-heap greedy চালিয়ে প্রতিটি পরীক্ষা কোন হলে যায় লিখুন (সমতায় ছোট নম্বরের হল)।

    (ক) ৯–১০টায় ১, ৩, ৪ একসাথে চলে; ১০–১১টায় ৩, ৫, ৬. Depth = ৩, তাই অন্তত ৩টি হল।

    (খ) ১ → H1 (খালি ১০-এ)। ২: সবচেয়ে আগে খালি H1, ১০ > ৮ → H2 (খালি ৯-এ)। ৩: (৯, H2) ≤ ৯ → H2, এখন খালি ১১-এ। ৪: সবচেয়ে আগে (১০, H1) > ৯ → নতুন H3 (খালি ১০-এ)। ৫: (১০, H1) → H1 (খালি ১২-এ)। ৬: (১০, H3) → H3 (খালি ১১-এ)। ৭: (১১, H2) আর (১১, H3) সমান, ছোট নম্বর → H2. ফল: H1 = {১, ৫}, H2 = {২, ৩, ৭}, H3 = {৪, ৬} — ৩টি হল, depth-এর সমান।

  4. কোড লিখুন: n টি কাজের (t, d) দেওয়া আছে। EDF ক্রমে চালিয়ে সর্বোচ্চ lateness ফেরত দিন। অংশ ৭-এর চারটি কাজ (W, X, Y, Z) দিয়ে হাতে উত্তর মেলান।

    Deadline দিয়ে সর্ট, তারপর একটি চলমান সময় রেখে প্রতিটি কাজের শেষ হওয়ার মুহূর্ত বের করুন:

    // jobs as (t, d); returns the minimum possible maximum lateness
    long long maxLatenessEDF(vector<pair<long long, long long>> jobs) {
        sort(jobs.begin(), jobs.end(), [](auto& a, auto& b) { return a.second < b.second; });
        long long now = 0, worst = 0;
        for (auto [t, d] : jobs) {
            now += t;                              // this job finishes at time now
            worst = max(worst, now - d);           // lateness = max(0, now - d)
        }
        return worst;
    }

    হাতে: সাজানো ক্রম X (২, ৩), W (৩, ৫), Y (১, ৭), Z (৪, ৮). now = ২, ৫, ৬, ১০; now − d = −১, ০, −১, ২. worst শুরু ০ থেকে, তাই ঋণাত্মক মান আপনা থেকেই বাদ — উত্তর ২. সময় O(n log n), জায়গা O(1) বাড়তি (সর্ট বাদে)। বড় t-এর যোগফলে উপচে পড়া এড়াতে long long.

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

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

ক্রমএকটি নতুন সমস্যার greedy সমাধান তৈরির ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. কয়েকটি সম্ভাব্য greedy নিয়ম লেখা (কোন key দিয়ে সাজিয়ে কোনটি আগে)
  2. প্রতিটি নিয়মকে ছোট ইনপুটে counterexample দিয়ে ভাঙার চেষ্টা করা
  3. টিকে থাকা নিয়মের জন্য exchange argument বা stays ahead প্রমাণ লেখা
  4. সর্ট আর একটি লুপ দিয়ে কোড লেখা, complexity হিসাব করা
  5. এলোমেলো ছোট ইনপুটে brute force-এর সাথে stress test করা

একটি রুমে পরস্পর সংঘাতহীন সবচেয়ে বেশি সংখ্যক interval বাছতে কোন greedy নিয়ম সবসময় সর্বোত্তম?

  • যে interval সবচেয়ে আগে শুরু হয়
  • যে interval সবচেয়ে ছোট
  • যে interval সবচেয়ে আগে শেষ হয়
  • যে interval-এর সাথে সবচেয়ে কম interval-এর সংঘাত

কয়েনের মান 1, 3 আর 4. “সবচেয়ে বড় যে কয়েন ধরে, সেটি নাও” greedy দিয়ে 6 বানালে কয়টি কয়েন লাগে, আর সবচেয়ে কম কয়টি সম্ভব?

  • greedy 2, সর্বোত্তম 2
  • greedy 3, সর্বোত্তম 2
  • greedy 3, সর্বোত্তম 3
  • greedy 4, সর্বোত্তম 2

ব্যাগের ধারণক্ষমতা 50 কেজি; তিনটি জিনিস (দাম, ওজন): (60, 10), (100, 20), (120, 30). জিনিসের ভগ্নাংশ নেওয়া গেলে সর্বোচ্চ মোট দাম কত?

  • 160
  • 220
  • 280
  • 240

একদল interval-এর সবগুলোকে রুমে বসাতে হবে, একই রুমে দুটি একসাথে নয়। সবচেয়ে কম কয়টি রুম লাগে?

  • কোনো একটি মুহূর্তে একসাথে চলা interval-এর সর্বোচ্চ সংখ্যা
  • মোট interval-এর সংখ্যা
  • একটি রুমে রাখা যায় এমন সংঘাতহীন interval-এর সর্বোচ্চ সংখ্যা
  • যে interval-এর সংঘাত সবচেয়ে বেশি, তার সংঘাতের সংখ্যা + 1

এক মেশিনে সব কাজ পরপর করতে হবে; প্রতিটি কাজের সময় t আর deadline d আছে। সর্বোচ্চ lateness সবচেয়ে কম রাখতে কোন ক্রম সবসময় সর্বোত্তম?

  • ছোট কাজ আগে (t-এর ক্রমে)
  • আগের deadline আগে (d-এর ক্রমে)
  • সবচেয়ে কম ঢিল (d − t) আগে
  • বড় কাজ আগে (t-এর উল্টো ক্রমে)

n টি interval এলোমেলো ক্রমে দেওয়া আছে। Earliest finish time নিয়মে activity selection-এর মোট time complexity কত?

  • Θ(n)
  • O(n²)
  • O(n log n)
  • O(2ⁿ)

শব্দকোষ

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

মনে রাখুন

  1. Greedy = প্রতি ধাপে স্থানীয়ভাবে সেরা পছন্দ, ফিরে তাকানো নেই; ঠিক হয় যখন greedy-choice property আর optimal substructure দুটোই থাকে।
  2. প্রমাণ: exchange argument (সর্বোত্তম সমাধানকে বদলে greedy-তে আনা) বা greedy stays ahead (induction); বাতিল: একটি counterexample.
  3. Activity selection = আগে শেষ হওয়া আগে, O(n log n); interval partitioning = রুম = depth, শুরুর ক্রম + min-heap.
  4. Fractional knapsack-এ ঘনত্ব-greedy সর্বোত্তম, 0/1-এ নয় (DP, পর্ব ৫৩); টাকার নোট canonical, {1, 3, 4} নয় (৬ টাকা)।
  5. EDF সর্বোচ্চ lateness কমায় (inversion প্রমাণ); MST, Dijkstra, Huffman-ও greedy; matroid-এ greedy সব ওজনে সর্বোত্তম; সন্দেহ হলে stress test.

পরের পর্বে: একটি ফাইলে কিছু অক্ষর বারবার আসে, কিছু কালেভদ্রে। সবার জন্য সমান দৈর্ঘ্যের কোড না দিয়ে কীভাবে ফাইল ছোট করা যায়? Huffman coding — একটি greedy, একটি priority queue আর একটি সুন্দর exchange প্রমাণ।

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

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