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

Stack — শেষে ঢোকে, আগে বের হয়

Stacks — LIFO, Array & Linked-List Implementations, Expression Parsing & the Monotonic Stack

Taught in Bangla, this lesson presents the stack as an abstract data type and implements it with arrays and linked lists, then uses std::stack and Python lists. It covers balanced brackets, infix to postfix with the Shunting-yard algorithm, postfix evaluation, undo and redo, browser history and iterative DFS, and solves next greater element, largest rectangle in a histogram and min-stack with monotonic stacks.

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

  • Stack-কে একটি abstract data type হিসেবে চেনা, আর array ও linked list দিয়ে নিজে বানানো
  • C++-এর std::stack আর Python list-কে stack হিসেবে নিরাপদে ব্যবহার করা
  • বন্ধনী মেলানো, infix থেকে postfix (Shunting-yard) আর postfix-এর হিসাব হাতে trace করা
  • Undo/redo, ব্রাউজারের back বোতাম আর explicit stack দিয়ে DFS-এর নকশা বোঝা
  • Monotonic stack দিয়ে O(n)-এ সমস্যা সমাধান, আর amortized যুক্তিতে সেই O(n) প্রমাণ করা

১ · Stack কী — বিয়েবাড়ির থালার স্তূপ

বিয়েবাড়ির খাবারের টেবিলের পাশে ধোয়া থালা একটির ওপর আরেকটি সাজিয়ে রাখা হয়। নতুন ধোয়া থালা এলে রাখা হয় সবার ওপরে। অতিথি থালা নিতে এলে নেন সবার ওপরেরটি। নিচের থালা টেনে বের করার চেষ্টা করলে পুরো স্তূপ ভেঙে পড়বে। ফল হলো একটি সরল নিয়ম: সবার শেষে যে থালাটি রাখা হয়েছে, সেটিই সবার আগে উঠবে।

এই নিয়মে চলা ডেটা স্ট্রাকচারের নাম stack (স্ট্যাক), আর নিয়মটির নাম LIFO — Last In, First Out. Stack-এ আপনি শুধু একটি প্রান্ত ছুঁতে পারেন, যার নাম top. মাঝের বা নিচের উপাদান সরাসরি দেখা যায় না। শুনতে সীমাবদ্ধতা মনে হয়, কিন্তু এই সীমাবদ্ধতাই stack-কে এত দ্রুত আর এত কাজের বানায়।

Stack একটি abstract data type (ADT): এটি বলে কী কাজ করা যায়, কীভাবে বানানো হবে তা বলে না। কাজগুলো মাত্র চারটি (সাথে প্রায়ই একটি পঞ্চম):

অপারেশনকাজসময়থালার উপমায়
push(x)x-কে ওপরে রাখাO(1)ধোয়া থালা স্তূপের ওপরে রাখা
pop()ওপরের উপাদান সরানোO(1)ওপরের থালাটি তুলে নেওয়া
top() বা peek()ওপরের উপাদান দেখা, না সরিয়েO(1)ওপরের থালাটি পরিষ্কার কি না দেখা
empty()stack খালি কি নাO(1)টেবিলে কোনো থালা আছে কি?
size()কয়টি উপাদান আছেO(1)স্তূপে কয়টি থালা

খেয়াল করুন, “৫ নম্বর উপাদানটি দেখাও” বা “৪২ কোথায় আছে খোঁজো” — এমন কাজ stack-এর তালিকায় নেই। দরকার হলে সেটি অন্য ডেটা স্ট্রাকচারের কাজ (অ্যারে, পর্ব ১০; hash table, পর্ব ৩২)। Stack যা কথা দেয়, তা খুব কম — কিন্তু সেগুলো সবই ধ্রুবক সময়ে, ইনপুট যত বড়ই হোক।

দুটি ভুল অবস্থা। খালি stack থেকে pop বা top করার চেষ্টাকে বলে stack underflow — যে থালা নেই, তা তোলার চেষ্টা। আর নির্দিষ্ট মাপের অ্যারেতে বানানো stack ভরে গেলে আরেকটি push হলো overflow. ভালো কোড প্রতিটি pop/top-এর আগে empty() যাচাই করে। কনটেস্টের wrong answer আর runtime error-এর বড় একটি অংশ আসে ঠিক এই একটি যাচাই ভুলে যাওয়া থেকে।

আরও কয়েকটি চেনা stack: পরীক্ষার হলে জমা দেওয়া খাতার স্তূপ — শিক্ষক দেখতে শুরু করেন ওপরের খাতা থেকে, অর্থাৎ যিনি সবার শেষে জমা দিয়েছেন তাঁর খাতা আগে দেখা হয়। লাইব্রেরির টেবিলে ফেরত দেওয়া বইয়ের গাদা। মোবাইলের নোটিফিকেশন, যেখানে সবচেয়ে নতুনটি সবার ওপরে। প্রতিটিতে একই নিয়ম: শেষে এলে আগে যায়।

২ · নিজে বানানো — array/vector আর linked list

ADT একটি চুক্তি; এবার সেটি পূরণ করার দুটি প্রচলিত উপায় দেখি।

উপায় ১: Array বা vector-এর শেষ প্রান্ত = top

একটি অ্যারে আর একটি সংখ্যা top রাখুন, যা বলে ওপরের উপাদান কোন ঘরে আছে (খালি হলে −1). push মানে top এক বাড়িয়ে সেই ঘরে লেখা; pop মানে শুধু top এক কমানো। কোনো উপাদান সরাতে হয় না, কোনো লুপ নেই — তাই প্রতিটি কাজ O(1). নিচে ৪ ঘরের একটি অ্যারেতে কয়েকটি অপারেশন দেখুন:

অ্যারে arr[0..3] আর top (বন্ধনীর মান = পুরোনো, অগ্রাহ্য)
অপারেশন
arr[0] arr[1] arr[2] arr[3]
top (সূচক)
size = top + 1
  1. push(10). শুরুতে top = −1 (খালি). top এক বেড়ে 0 হলো, arr[0]-এ লেখা হলো 10.
  2. push(20). top = 1, arr[1] = 20. একটিই লেখা, একটিই যোগ — O(1).
  3. push(30). top = 2, arr[2] = 30. stack-এ এখন তিনটি উপাদান, ওপরে 30.
  4. pop(). ফেরত দিল arr[2] = 30, তারপর top কমে 1. লক্ষ করুন, 30 মেমরিতে এখনো আছে — কিন্তু top-এর ওপরের সবকিছু “মৃত”, কেউ আর তা পড়বে না। মোছার দরকারই নেই।
  5. top(). শুধু arr[top] = arr[1] = 20 দেখাল। stack বদলায়নি।
  6. push(40). top আবার 2, আর arr[2]-এর পুরোনো 30-এর ওপরেই লেখা হলো 40. অ্যারে এখন ভরতে আর এক ঘর বাকি; সেটিও ভরে গেলে পরের push-এ overflow — যদি না অ্যারেকে বড় করা হয়।

নির্দিষ্ট মাপের অ্যারের সমস্যা হলো overflow. সমাধান dynamic array (C++-এর std::vector, Python-এর list) — জায়গা ফুরোলে দ্বিগুণ মাপের নতুন অ্যারে নিয়ে সব কপি করে (পর্ব ১০)। সেই কপির মুহূর্তে একটি push O(n) নেয়, কিন্তু n টি push-এ মোট কপি হয় 2n-এর কম, তাই গড়ে প্রতিটি push amortized O(1). এই কারণেই বাস্তবে stack মানে প্রায় সবসময় একটি vector, যার শেষ প্রান্তটি top: push_back = push, pop_back = pop, back() = top.

উপায় ২: Linked list-এর মাথা = top

প্রতিটি উপাদান একটি node, যাতে থাকে মান আর তার নিচের node-এর ঠিকানা। linked list-এর মাথায় (head) নতুন node বসানো আর মাথা থেকে node সরানো — দুটোই কয়েকটি pointer বদলের কাজ (পর্ব ১২). তাই এখানে প্রতিটি কাজ worst case-এও O(1), amortized নয়।

// stack on a singly linked list: head = top
struct Node { int val; Node* next; };

struct ListStack {
    Node* head = nullptr;
    void push(int x) { head = new Node{x, head}; }     // new node points to old top
    void pop() { Node* t = head; head = head->next; delete t; }  // caller checks empty()
    int top() const { return head->val; }
    bool empty() const { return head == nullptr; }
    ~ListStack() { while (!empty()) pop(); }           // free every node
};
বিষয়Array / vectorLinked list
push, pop, topO(1); vector-এ push amortized O(1)প্রতিটিই worst case O(1)
প্রতি উপাদানে বাড়তি জায়গানেই (তবে খালি ঘর থাকতে পারে, সর্বোচ্চ প্রায় দ্বিগুণ)একটি pointer (64-bit-এ 8 byte) + প্রতিবার আলাদা মেমরি বরাদ্দ
Cacheউপাদান পাশাপাশি — খুব দ্রুতnode ছড়িয়ে থাকে — ধীর
কখন বেছে নেবেনপ্রায় সবসময়যেখানে কোনো একটি কাজেরও হঠাৎ দেরি চলবে না (real-time), বা একাধিক stack একই node ভাগ করে (persistent stack)

দুটি উপায়েই মোট জায়গা Θ(n), আর কোনো অপারেশনের জন্য বাড়তি auxiliary space লাগে না।

৩ · তৈরি stack — std::stack, Python list আর call stack

কনটেস্ট বা কাজের কোডে নিজে stack বানানোর দরকার কমই পড়ে; ভাষার লাইব্রেরিতে তৈরি আছে। তবে প্রতিটির কিছু ফাঁদ আছে।

#include <stack>
std::stack<int> st;          // default container: std::deque<int>
st.push(10); st.push(20);
int x = st.top();             // 20 -- read first...
st.pop();                     // ...then remove; pop() returns void
if (!st.empty()) { /* top()/pop() only on a non-empty stack */ }
std::stack<int, std::vector<int>> fast;   // vector as the container

std::stack নিজে কোনো ডেটা স্ট্রাকচার নয় — এটি একটি “container adaptor”, ভেতরের একটি container-কে (ডিফল্ট std::deque) শুধু stack-এর কাজগুলো দেখায়। দুটি জিনিস মনে রাখুন: (১) pop() কিছু ফেরত দেয় না, তাই মান লাগলে আগে top() পড়ুন; (২) খালি stack-এ top() বা pop() হলো undefined behavior — কোনো error বার্তা ছাড়াই আবর্জনা মান বা crash. অনেক কনটেস্ট প্রোগ্রামার সরাসরি vector ব্যবহার করেন, কারণ তাতে ভেতরের উপাদানও দেখা যায় (debug-এ সুবিধা)।

st = []                 # a plain list is the standard Python stack
st.append(10)           # push   -- amortized O(1)
st.append(20)
x = st[-1]              # top    -- 20
x = st.pop()            # pop    -- removes AND returns 20
if st:                  # non-empty check
    print(st[-1])
# never: st.pop(0) or st.insert(0, v) -- those shift every element, O(n)

Python-এ আলাদা stack class লাগে না — list-এর শেষ প্রান্তই top. append আর pop() দুটোই amortized O(1). খালি list-এ pop() করলে পরিষ্কার IndexError আসে — C++-এর চেয়ে নিরাপদ। কিন্তু সামনের দিক থেকে (pop(0)) কাজ করলে প্রতিবার সব উপাদান সরাতে হয়, O(n) — এটি নতুনদের সবচেয়ে সাধারণ ধীর-কোডের কারণ। collections.deque-ও stack হিসেবে চলে; একাধিক thread থেকে ব্যবহারের জন্য আছে queue.LifoQueue.

Java-য় যাঁরা লেখেন: পুরোনো java.util.Stack class-টি Vector থেকে তৈরি, প্রতিটি কাজে synchronization-এর বাড়তি খরচ বহন করে। Java-র নিজের documentation-ই এর বদলে Deque interface (যেমন ArrayDeque) ব্যবহারের পরামর্শ দেয়: push, pop, peek.

যে stack আপনি না জেনেই প্রতিদিন ব্যবহার করেন

পর্ব ০৭-এ দেখেছেন, প্রতিটি ফাংশন কল call stack-এ একটি নতুন stack frame push করে, আর ফাংশন শেষ হলে সেটি pop হয়। কেন stack-ই? কারণ ফাংশনগুলো ঠিক LIFO নিয়মে শেষ হয়: main ডাকল f-কে, f ডাকল g-কে — তাহলে g শেষ না হওয়া পর্যন্ত f শেষ হতে পারে না। সবচেয়ে সাম্প্রতিক অসমাপ্ত কাজটিই সবার আগে শেষ হয়। এই বাক্যটি মনে রাখুন; পুরো পর্বে stack-এর প্রতিটি ব্যবহারে এটিই ঘুরে ফিরে আসবে।

মূল ধারণা

কোনো সমস্যায় যদি দেখেন — “যে কাজ সবার শেষে শুরু হয়েছে, সেটি সবার আগে শেষ করতে হবে” বা “সবচেয়ে কাছের অমীমাংসিত জিনিসটির সাথে মেলাতে হবে” — তাহলে সেখানে প্রায় নিশ্চিতভাবেই একটি stack লুকিয়ে আছে। বন্ধনী, ফাংশন কল, undo, ব্রাউজারের ইতিহাস, এমনকি “ডানে প্রথম বড় সংখ্যা” — সবই এই ছাঁচে পড়ে।

৪ · বন্ধনী মেলানো — প্রথম ক্লাসিক প্রয়োগ

Code editor আপনার {-এর জোড়া } কোথায়, তা রং দিয়ে দেখায়; compiler বলে দেয় “expected ‘)’”. এর পেছনে আছে stack. প্রশ্নটি হলো: একটি লেখায় বন্ধনীগুলো কি ঠিকঠাক মিলেছে? [()]{} ঠিক; ([)] ভুল (গোল বন্ধনী খুলে তার ভেতরে বন্ধ হয়েছে চারকোনা — একে অপরকে কেটে গেছে); (( ভুল (বন্ধ হয়নি)।

শুধু গোনা যথেষ্ট নয় — ([)]-এ প্রতিটি ধরনের খোলা আর বন্ধ সংখ্যায় সমান, তবু ভুল। আসল নিয়ম: প্রতিটি বন্ধ বন্ধনী মিলবে সবচেয়ে সাম্প্রতিক এখনো-খোলা বন্ধনীর সাথে। “সবচেয়ে সাম্প্রতিক” শব্দটিই stack-এর ইঙ্গিত। অ্যালগরিদম:

  1. বাঁ থেকে ডানে প্রতিটি অক্ষর পড়ুন। খোলা বন্ধনী (( [ {) হলে push.
  2. বন্ধ বন্ধনী হলে: stack খালি হলে ভুল (বন্ধ করার কিছু নেই)। নইলে top একই ধরনের খোলা বন্ধনী কি না দেখুন — হলে pop, না হলে ভুল।
  3. অন্য অক্ষর (অক্ষর, সংখ্যা, operator) উপেক্ষা করুন।
  4. শেষে stack খালি হলে balanced, নইলে কিছু বন্ধনী খোলা থেকে গেছে।
ইনপুট^ = এখন যে অক্ষর পড়ছি

            
অবস্থা
পড়া অক্ষর
কাজ
stack (নিচ → ওপর)
ফল
  1. [ পড়লাম. খোলা বন্ধনী, তাই push. stack: [.
  2. ( পড়লাম. আবার খোলা, push. stack: [ ( — ওপরে (, অর্থাৎ যেটি সবার শেষে খুলেছে।
  3. ) পড়লাম. বন্ধ বন্ধনী। top হলো ( — একই ধরনের, তাই মিলল, pop. stack: [.
  4. ] পড়লাম. top এখন [ — মিলল, pop. stack খালি। এ পর্যন্ত [()] অংশটি নিখুঁত।
  5. { পড়লাম. push. stack: {.
  6. } পড়লাম. top {, মিলল, pop. stack আবার খালি।
  7. লেখা শেষ. কোনো ভুল পাইনি, আর stack খালি — অর্থাৎ কোনো বন্ধনী খোলা পড়ে নেই। ফল: balanced. যদি ইনপুট হতো ([)], তাহলে ধাপ ৩-এ ) এসে top-এ পেত [ — ধরন মেলে না, সাথে সাথে NOT balanced.

কেন এটি ঠিক — একটি invariant. দাবি: যেকোনো অংশ পড়ার পর stack-এ ঠিক সেই খোলা বন্ধনীগুলো আছে, যেগুলো এখনো বন্ধ হয়নি, যে ক্রমে খুলেছে সেই ক্রমে (ওপরে সবচেয়ে নতুনটি)। শুরুতে এটি সত্য (কিছু পড়িনি, stack খালি)। খোলা বন্ধনী push করলে সত্য থাকে। বন্ধ বন্ধনী আসলে বৈধ লেখায় এটিকে অবশ্যই সবচেয়ে নতুন খোলা বন্ধনীর জোড়া হতে হবে — কারণ তার পরে খোলা কোনোটি বন্ধ না করে আগেরটি বন্ধ করলে দুটি জোড়া একে অপরকে কাটত। তাই top-এর সাথে মেলানোই সঠিক পরীক্ষা, আর pop করলে invariant বজায় থাকে। শেষে লেখা balanced হবে ঠিক তখনই, যখন কোনো বন্ধনী খোলা নেই — অর্থাৎ stack খালি। এটি পর্ব ০৩-এর loop invariant পদ্ধতিরই প্রয়োগ।

বিশ্লেষণ: প্রতিটি অক্ষরে সর্বোচ্চ একটি push বা একটি pop, প্রতিটি O(1) — তাই সময় Θ(n). জায়গা worst case Θ(n), যেমন (((((( ইনপুটে। শুধু এক ধরনের বন্ধনী হলে stack-এর দরকারই নেই — একটি counter যথেষ্ট (কখনো ঋণাত্মক হবে না, শেষে শূন্য)। ধরন একাধিক হলেই stack লাগে, কারণ তখন শুধু সংখ্যা নয়, কোনটি খোলা আছে তা-ও মনে রাখতে হয়।

৫ · Infix থেকে postfix — Dijkstra-র Shunting-yard

আমরা লিখি a + b * (c - d) - e — operator দুটি সংখ্যার মাঝখানে। একে বলে infix notation. মানুষের পড়তে সুবিধা, কিন্তু কম্পিউটারের জন্য ঝামেলা: + দেখেই যোগ করা যায় না, কারণ পরে * আসতে পারে যা আগে হিসাব হবে। বন্ধনী থাকলে আরও জটিল।

বিকল্প হলো postfix notation — operator বসে তার দুটি operand-এর পরে: a b c d - * + e -. একে Reverse Polish Notation (RPN)-ও বলে, কারণ পোলিশ যুক্তিবিদ Jan Łukasiewicz ১৯২০-এর দশকে operator-কে আগে বসানোর (prefix) পদ্ধতি দিয়েছিলেন; postfix হলো তার উল্টো। Postfix-এ বন্ধনী লাগে না, precedence-এর নিয়মও লাগে না — যে ক্রমে লেখা, সেই ক্রমেই হিসাব।

১৯৬০ সালে আমস্টারডামের Mathematisch Centrum-এ Edsger W. Dijkstra আর Jaap Zonneveld ALGOL 60 ভাষার প্রথম দিকের একটি compiler বানান। সেখানে expression অনুবাদের জন্য Dijkstra যে পদ্ধতি ব্যবহার করেন, ১৯৬১ সালের একটি রিপোর্টে তিনি সেটি বর্ণনা করেন। নাম দেন রেলের shunting yard থেকে — যেখানে একটি পার্শ্ব-লাইনে বগি সাময়িকভাবে সরিয়ে রেখে সঠিক ক্রমে ট্রেন সাজানো হয়। Operator-গুলো ঠিক এভাবেই একটি পার্শ্ব-লাইনে (stack) অপেক্ষা করে।

Shunting-yard দুটি নিয়ম মেনে চলে। প্রথম, operator precedence: কোন operator আগে। দ্বিতীয়, associativity: একই precedence-এর দুটি পাশাপাশি থাকলে কোনটি আগে।

OperatorPrecedence (বড় = আগে)Associativityউদাহরণ
^ (ঘাত)3ডান থেকে2 ^ 3 ^ 2 = 2 ^ (3 ^ 2) = 512
* /2বাঁ থেকে8 / 4 / 2 = (8 / 4) / 2 = 1
+ -1বাঁ থেকে8 - 2 - 3 = (8 - 2) - 3 = 3
Pseudocode — Shunting-yard (infix tokens -> postfix list)

for each token t, left to right:
    if t is an operand:      append t to OUTPUT
    else if t == "(":        push t
    else if t == ")":        pop operators to OUTPUT until "(" is on top; pop "(" (discard)
    else (t is an operator):
        while top is an operator o and
              ( prec(o) > prec(t)  or  (prec(o) == prec(t) and t is left-assoc) ):
            pop o to OUTPUT
        push t
at the end: pop every remaining operator to OUTPUT

সহজ ভাষায়: নতুন operator আসার আগে stack থেকে সেই operator-গুলো বের করে দিন, যারা নতুনটির আগে হিসাব হওয়ার কথা — বেশি precedence-এর, অথবা সমান precedence-এর (যদি নতুনটি বাঁ-associative হয়)। ( একটি দেয়াল: এর নিচে কেউ যায় না, যতক্ষণ না ) আসে। এবার a + b * (c - d) - e ধাপে ধাপে দেখুন:

Token, operator stack আর output
পড়া token
operator stack (নিচ → ওপর)
output
  1. a. operand — সরাসরি output-এ। Operand-এর ক্রম কখনো বদলায় না।
  2. +. stack খালি, তুলনার কেউ নেই — push.
  3. b. output: a b.
  4. *. top-এ +, যার precedence (1) *-এর (2) চেয়ে কম। +-কে এখন বের করা যাবে না, কারণ * আগে হিসাব হবে — তাই push. stack: + *.
  5. (. সবসময় push. এটি একটি দেয়াল — এর ওপরের operator-গুলো নিচের + *-এর সাথে তুলনা করবে না।
  6. c. output: a b c.
  7. -. top-এ ( — দেয়াল, তাই কিছু pop না করে push. stack: + * ( -.
  8. d. output: a b c d.
  9. ). ( না পাওয়া পর্যন্ত pop: - output-এ গেল, তারপর ( ফেলে দেওয়া হলো। output: a b c d - — অর্থাৎ (c − d) সম্পূর্ণ।
  10. - (দ্বিতীয়). top-এ * (precedence 2 > 1) — pop. তারপর top-এ +: precedence সমান, আর - বাঁ-associative — তাই +-ও pop. তারপর push -. output: a b c d - * +. এটিই associativity-র কাজ: a + … আগে হবে, তারপর − e.
  11. e. output-এ যোগ হলো।
  12. শেষ. stack-এ বাকি - output-এ। ফল: a b c d - * + e -. মিলিয়ে দেখুন: (a + (b × (c − d))) − e.

বিশ্লেষণ: প্রতিটি token ঠিক একবার output-এ যায় বা একবার push হয়, আর প্রতিটি push করা operator সর্বোচ্চ একবার pop হয়। ভেতরের while লুপ থাকলেও মোট pop ≤ মোট push ≤ n, তাই সময় Θ(n), জায়গা O(n). এই “প্রতিটি উপাদান একবার ঢোকে, একবার বেরোয়” যুক্তিটি অংশ ৮-এ আবার আসবে, আরও জোরালোভাবে।

ঘাতের ফাঁদ। ^ ডান-associative: 2 ^ 3 ^ 2 মানে 29 = 512, (23)2 = 64 নয়। তাই সমান precedence-এ ^ এলে আগের ^-কে pop করবেন না — ওপরের শর্তে “t is left-assoc” অংশটি ঠিক এই কারণে। ফলে 2 ^ 3 ^ 2 → 2 3 2 ^ ^. AI-এর লেখা অনেক কোডে সব operator-কে বাঁ-associative ধরা হয় — ছোট পরীক্ষায় ধরা পড়ে না, ^ এলেই ভুল।

৬ · Postfix-এর হিসাব — একটি stack, একবার পড়া

Postfix পেলে মান বের করা আশ্চর্য রকম সহজ। বাঁ থেকে ডানে পড়ুন: সংখ্যা হলে push; operator হলে ওপরের দুটি সংখ্যা pop করে হিসাব করুন, ফল push. শেষে stack-এ একটিই সংখ্যা থাকবে — সেটিই উত্তর। একটি সূক্ষ্ম বিষয়: প্রথমে যেটি pop হয় সেটি ডান operand. 8 2 --এ আগে বেরোয় 2, তারপর 8 — হিসাব 8 − 2, 2 − 8 নয়।

প্রতিটি token-এর পর
পড়া token
কাজ
stack (নিচ → ওপর)
  1. 5. সংখ্যা — push. stack: 5.
  2. 1. push. stack: 5 1.
  3. 2. push. stack: 5 1 2. তিনটি সংখ্যা অপেক্ষায়, কারণ এখনো কোনো operator আসেনি।
  4. +. pop করে পেলাম 2 (ডান) আর 1 (বাঁ): 1 + 2 = 3, push. stack: 5 3. এটি ছিল infix-এর (1 + 2).
  5. 4. push. stack: 5 3 4.
  6. *. pop 4 আর 3: 3 × 4 = 12, push. stack: 5 12. অর্থাৎ (1 + 2) × 4.
  7. +. pop 12 আর 5: 5 + 12 = 17. stack: 17.
  8. 3. push. stack: 17 3.
  9. -. pop 3 (ডান) আর 17 (বাঁ): 17 − 3 = 14. শেষে stack-এ ঠিক একটি সংখ্যা — উত্তর 14. infix-এ এটি ছিল 5 + (1 + 2) × 4 − 3.

প্রতিটি token-এ O(1) কাজ, তাই সময় Θ(n), জায়গা O(n). শেষে stack-এ একের বেশি সংখ্যা থাকলে, বা কোনো operator-এর সময় দুটি সংখ্যা না পেলে — expression-টিই ভুল লেখা। এবার বন্ধনী মেলানো আর postfix হিসাব একসাথে চালিয়ে দেখুন:

#include <iostream>
#include <stack>
#include <string>
#include <sstream>
#include <cctype>
#include <initializer_list>
using namespace std;
bool balanced(const string& s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') st.push(c);   // opener: wait for its partner
        else if (c == ')' || c == ']' || c == '}') {
            char want = c == ')' ? '(' : c == ']' ? '[' : '{';
            if (st.empty() || st.top() != want) return false; // nothing to close, or wrong kind
            st.pop();
        }
    }
    return st.empty();                                        // leftover openers = unbalanced
}
long long evalPostfix(const string& expr) {
    stack<long long> st;
    istringstream in(expr);
    for (string tok; in >> tok; ) {
        if (isdigit((unsigned char)tok[0])) { st.push(stoll(tok)); continue; }
        long long b = st.top(); st.pop();                     // right operand comes out first
        long long a = st.top(); st.pop();
        st.push(tok == "+" ? a + b : tok == "-" ? a - b : tok == "*" ? a * b : a / b);
    }
    return st.top();
}
int main() {
    for (string s : {"[()]{}", "([)]", "((", "a*(b+c)"})
        cout << s << " -> " << (balanced(s) ? "balanced" : "NOT balanced") << '\n';
    for (string e : {"5 1 2 + 4 * + 3 -", "2 3 + 4 *", "8 2 - 3 -"})
        cout << e << " = " << evalPostfix(e) << '\n';
}

আউটপুট:

[()]{} -> balanced
([)] -> NOT balanced
(( -> NOT balanced
a*(b+c) -> balanced
5 1 2 + 4 * + 3 - = 14
2 3 + 4 * = 20
8 2 - 3 - = 3

([)]-এ তৃতীয় অক্ষর ) এসে top-এ পায় [ — ধরন মেলে না, তাই সাথে সাথে false. ((-এ কোনো ভুল ধাপ নেই, কিন্তু শেষে stack-এ দুটি বন্ধনী থেকে যায়। a*(b+c)-এর অক্ষর আর operator উপেক্ষিত। Postfix-এ 2 3 + 4 * = (2 + 3) × 4 = 20 — infix-এ এর জন্য বন্ধনী লাগত, postfix-এ লাগেনি। আর 8 2 - 3 - = (8 − 2) − 3 = 3: যদি কোডে a আর b-এর pop-এর ক্রম উল্টে যেত, উত্তর আসত −3 বা 9-এর মতো কিছু। isdigit-এ unsigned char cast একটি নিরাপত্তা অভ্যাস — ঋণাত্মক char দিলে সেটি undefined behavior.

PAIR = {')': '(', ']': '[', '}': '{'}

def balanced(s):
    st = []
    for c in s:
        if c in '([{':
            st.append(c)                     # opener: wait for its partner
        elif c in PAIR:
            if not st or st[-1] != PAIR[c]:  # nothing to close, or wrong kind
                return False
            st.pop()
    return not st                            # leftover openers = unbalanced

def eval_postfix(expr):
    st = []
    for tok in expr.split():
        if tok.isdigit():
            st.append(int(tok))
            continue
        b = st.pop()                         # right operand comes out first
        a = st.pop()
        if tok == '+': st.append(a + b)
        elif tok == '-': st.append(a - b)
        elif tok == '*': st.append(a * b)
        else: st.append(int(a / b))          # truncate like C++
    return st[-1]

for s in ["[()]{}", "([)]", "((", "a*(b+c)"]:
    print(f"{s} -> {'balanced' if balanced(s) else 'NOT balanced'}")
for e in ["5 1 2 + 4 * + 3 -", "2 3 + 4 *", "8 2 - 3 -"]:
    print(f"{e} = {eval_postfix(e)}")

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

[()]{} -> balanced
([)] -> NOT balanced
(( -> NOT balanced
a*(b+c) -> balanced
5 1 2 + 4 * + 3 - = 14
2 3 + 4 * = 20
8 2 - 3 - = 3

dictionary PAIR প্রতিটি বন্ধ বন্ধনীর জোড়া বলে দেয়, তাই তিনটি আলাদা if লাগে না। return not st মানে “stack খালি হলে True”. ভাগের লাইনে খেয়াল করুন: Python-এর // নিচের দিকে round করে (−7 // 2 = −4), কিন্তু C++-এর / শূন্যের দিকে (−7 / 2 = −3). দুই ভাষায় একই ফল পেতে এখানে int(a / b) লেখা হয়েছে (বড় সংখ্যায় float-এর নির্ভুলতার সীমা মাথায় রাখবেন)।

আরও গভীরে: postfix হিসাবের এই কৌশলটিই বহু virtual machine-এর হৃদয়। JVM, CPython-এর bytecode আর WebAssembly — তিনটিই stack machine: a + b * c compile হয়ে প্রায় হুবহু “load a, load b, load c, multiply, add” হয়, যা postfix ছাড়া কিছু নয়। Python-এ import dis; dis.dis("a + b * c") চালিয়ে নিজেই দেখুন। ১৯৭২ সালের HP-35 থেকে শুরু করে HP-র বহু বৈজ্ঞানিক ক্যালকুলেটরে RPN-এ সংখ্যা ঢোকানো হতো — সেখানে কোনো “=” বা বন্ধনী বোতাম লাগত না।

৭ · আরও প্রয়োগ — undo/redo, back বোতাম আর DFS

Undo আর redo — দুটি stack

Word-এ লিখছেন, ভুল করে একটি অনুচ্ছেদ মুছে ফেললেন, Ctrl+Z চাপলেন। সবচেয়ে শেষের কাজটিই ফিরে আসে — LIFO. Undo/redo-এর নকশা দুটি stack দিয়ে:

  • নতুন কাজ করলে সেটি undoStack-এ push, আর redoStack খালি করে দিন (নতুন পথ শুরু হলো, পুরোনো “ভবিষ্যৎ” আর বৈধ নয়)।
  • Undo: undoStack থেকে pop করে কাজটি উল্টে দিন, তারপর সেটি redoStack-এ push.
  • Redo: redoStack থেকে pop করে আবার করুন, তারপর undoStack-এ push.

প্রতিটি কাজ O(1) (কাজটি উল্টানোর নিজের খরচ বাদে)। বাস্তব editor-গুলো সাধারণত undoStack-এর সর্বোচ্চ আকার বেঁধে দেয় — যেমন শেষ কয়েকশো কাজ — যাতে মেমরি অসীম না বাড়ে।

ব্রাউজারের back আর forward

একই নকশা। আপনি bKash-এর ওয়েবসাইট থেকে “অফার” পেজে, সেখান থেকে একটি অফারের বিস্তারিত পেজে গেলেন। প্রতিটি নতুন পেজে যাওয়ার সময় আগের পেজ back stack-এ push হয় আর forward stack খালি হয়। Back চাপলে বর্তমান পেজ forward-এ push, back থেকে pop করা পেজ খোলে। Forward ঠিক উল্টো। এজন্যই back চেপে পেছনে গিয়ে নতুন কোনো লিংকে ক্লিক করলে forward বোতাম ধূসর হয়ে যায়।

Explicit stack দিয়ে DFS — পর্ব ৪১-এর আভাস

একটি গোলকধাঁধা বা গ্রাফে DFS (depth-first search) একটি পথ ধরে যতদূর যাওয়া যায় যায়, আটকে গেলে সবচেয়ে সাম্প্রতিক মোড়ে ফিরে অন্য পথ ধরে। “সবচেয়ে সাম্প্রতিক মোড়” — আবার stack. রিকার্শন দিয়ে লিখলে call stack এই কাজ করে; কিন্তু পর্ব ০৭-এ দেখেছেন, ১০৫ গভীরতায় সেটি stack overflow ঘটাতে পারে। তখন নিজের stack রাখুন:

// iterative DFS: our own stack instead of the call stack
vector<bool> seen(n, false);
vector<int> st{start};
while (!st.empty()) {
    int u = st.back(); st.pop_back();
    if (seen[u]) continue;
    seen[u] = true;                       // visit u
    for (int v : adj[u])
        if (!seen[v]) st.push_back(v);    // the last pushed neighbour is explored first
}

প্রতিটি vertex একবার visit হয়, প্রতিটি edge থেকে সর্বোচ্চ একবার push — তাই সময় O(V + E), যেখানে V = vertex আর E = edge-এর সংখ্যা। (directed গ্রাফে push সর্বোচ্চ E বার, undirected-এ 2E বার — দুটোই O(E).) এখানে stack বদলে queue দিলে একই কাঠামো হয়ে যায় BFS (পর্ব ৪০) — ডেটা স্ট্রাকচার বদলালেই অ্যালগরিদমের চরিত্র বদলায়। বিস্তারিত — visit-এর ক্রম, রিকার্সিভ DFS-এর সাথে সূক্ষ্ম পার্থক্য, আর প্রয়োগ — পর্ব ৪১-এ।

৮ · Monotonic stack — ডানে প্রথম বড় সংখ্যা

এবার একটি সমস্যা, যা ইন্টারভিউ আর কনটেস্টে অসংখ্যবার আসে। একটি অ্যারে দেওয়া আছে; প্রতিটি উপাদানের জন্য বের করুন, তার ডান দিকে প্রথম কোন উপাদানটি তার চেয়ে বড় — না থাকলে −1. একে বলে next greater element (NGE). [2, 7, 3, 5, 4, 6, 8]-এর উত্তর [7, 8, 5, 6, 6, 8, -1].

সরল সমাধান: প্রতিটি i-এর জন্য ডানে হেঁটে প্রথম বড়টি খুঁজুন — worst case O(n²) (যেমন কমতে থাকা অ্যারেতে)। n = 105 হলে প্রায় ৫০০ কোটি তুলনা, সময়সীমা পার।

চালাক সমাধান: বাঁ থেকে ডানে যান, আর একটি stack-এ রাখুন সেই index-গুলো যাদের উত্তর এখনো পাওয়া যায়নি। নতুন উপাদান x এলে, stack-এর ওপরে যতক্ষণ x-এর চেয়ে ছোট মান আছে, ততক্ষণ সেগুলো pop করুন — তাদের সবার উত্তর x. তারপর x-এর index push করুন। ফলে stack-এর মানগুলো নিচ থেকে ওপরে সবসময় কমতে থাকে (বা সমান থাকে) — এজন্যই নাম monotonic stack. একটি উপমা: স্কুলের মাঠে ছাত্রেরা একে একে লাইনে এসে দাঁড়াচ্ছে, আর প্রত্যেকে জানতে চায় তার পরে আসা প্রথম কোন জন তার চেয়ে লম্বা। একজন লম্বা ছাত্র এলে, অপেক্ষায় থাকা তার চেয়ে খাটো সবাই একসাথে উত্তর পেয়ে যায় — আর অপেক্ষার তালিকা থেকে বেরিয়ে যায়।

প্রতিটি i-এর পর (stack-এ index থাকে; বোঝার সুবিধায় এখানে মান দেখানো হলো)
i, a[i]
এই ধাপে pop (উত্তর)
stack (নিচ → ওপর)
ans[0..6]
মোট pop
  1. i = 0, a = 2. stack খালি, তুলনার কেউ নেই। 2 push — এর উত্তর এখনো অজানা।
  2. i = 1, a = 7. top 2 < 7 — অর্থাৎ 2-এর ডানে প্রথম বড় সংখ্যা 7. pop, ans[0] = 7. stack খালি, তাই থামলাম; 7 push.
  3. i = 2, a = 3. top 7 > 3, কিছু pop হয় না। 3 push. stack: 7 3 — ওপরের দিকে কমছে।
  4. i = 3, a = 5. top 3 < 5 — pop, ans[2] = 5. এবার top 7 > 5, থামলাম। 5 push. stack: 7 5.
  5. i = 4, a = 4. top 5 > 4, শুধু push. stack: 7 5 4. তিনজনই উত্তরের অপেক্ষায়।
  6. i = 5, a = 6. একটি উপাদানে দুটি pop: 4 < 6 → ans[4] = 6; 5 < 6 → ans[3] = 6. তারপর top 7 > 6, থামলাম। 6 push. stack: 7 6. মোট pop এখন ৪.
  7. i = 6, a = 8. 6 < 8 → ans[5] = 8; 7 < 8 → ans[1] = 8. stack খালি, 8 push. মোট pop ৬.
  8. শেষ. stack-এ বাকি 8-এর ডানে কেউ নেই — ans[6] = −1 (অ্যারেটি শুরুতেই −1 দিয়ে ভরলে এই ধাপে আলাদা কিছু করতে হয় না)। ফল: 7 8 5 6 6 8 -1. push ৭টি, pop ৬টি — মোট কাজ 2n-এর কম।

শুদ্ধতা। Invariant: stack-এ ঠিক সেই index-গুলো আছে যাদের ডানে এখনো বড় কেউ আসেনি, আর তাদের মান নিচ থেকে ওপরে কমে বা সমান থাকে। x যখন index j-কে pop করে, তখন x-ই j-এর প্রথম বড় — কারণ j আর i-এর মাঝের যেকোনো উপাদান a[j]-এর চেয়ে বড় হলে সে আগেই j-কে pop করে ফেলত। আর stack-এ x-এর চেয়ে বড় বা সমান মান পেলেই থামা নিরাপদ: monotonic ক্রমের কারণে তার নিচের সবাই আরও বড় বা সমান, তাদের কারও উত্তর x নয়।

কেন O(n) — amortized যুক্তি। কোডে for-এর ভেতরে while — দেখে O(n²) মনে হয়। কিন্তু হিসাবটা অন্যভাবে করুন: প্রতিটি index ঠিক একবার push হয়, আর pop হলে আর কখনো ফেরে না — তাই সর্বোচ্চ একবার pop. পুরো চলায় while-এর মোট ঘূর্ণন = মোট pop ≤ n. ফলে মোট কাজ ≤ n (push) + n (pop) + n (for-এর তুলনা যা থামায়) = O(n). একটি নির্দিষ্ট ধাপে অনেক pop হতে পারে (ধাপ ৬, ৭), কিন্তু সেই খরচ আগের push-গুলো আগেই “জমা” রেখেছিল। এটিই amortized analysis-এর aggregate পদ্ধতি; পর্ব ৬০-এ এটিকে potential function (Φ = stack-এর আকার) দিয়ে নিখুঁতভাবে প্রমাণ করা হবে। জায়গা O(n).

#include <iostream>
#include <vector>
#include <initializer_list>
using namespace std;

vector<int> nextGreater(const vector<int>& a, int& pops) {
    vector<int> ans(a.size(), -1);
    vector<int> st;                       // indices still waiting; values decrease upward
    for (int i = 0; i < (int)a.size(); ++i) {
        while (!st.empty() && a[st.back()] < a[i]) {
            ans[st.back()] = a[i];        // a[i] is the first greater value to the right
            st.pop_back();
            ++pops;
        }
        st.push_back(i);
    }
    return ans;
}

int main() {
    for (const vector<int>& a : {vector{2, 7, 3, 5, 4, 6, 8}, vector{5, 4, 3, 2, 1},
                                 vector{1, 2, 3, 4, 5}}) {
        int pops = 0;
        vector<int> ans = nextGreater(a, pops);
        cout << "a   ="; for (int x : a) cout << ' ' << x;
        cout << "\nnge ="; for (int x : ans) cout << ' ' << x;
        cout << "\npushes = " << a.size() << ", pops = " << pops << '\n';
    }
}

আউটপুট:

a   = 2 7 3 5 4 6 8
nge = 7 8 5 6 6 8 -1
pushes = 7, pops = 6
a   = 5 4 3 2 1
nge = -1 -1 -1 -1 -1
pushes = 5, pops = 0
a   = 1 2 3 4 5
nge = 2 3 4 5 -1
pushes = 5, pops = 4

প্রথম লাইন ওপরের ডায়াগ্রামের সাথে হুবহু মেলে। কমতে থাকা অ্যারেতে (5 4 3 2 1) কেউ pop হয় না — সবাই শেষ পর্যন্ত stack-এ অপেক্ষা করে, সবার উত্তর −1; অথচ এটিই সরল O(n²) সমাধানের সবচেয়ে খারাপ ইনপুট। বাড়তে থাকা অ্যারেতে প্রতিটি নতুন উপাদান ঠিক আগেরটিকে pop করে। তিন ক্ষেত্রেই pops < pushes = n — amortized যুক্তির সংখ্যাগত প্রমাণ। stack-এ মানের বদলে index রাখা হয়েছে, কারণ উত্তর বসাতে হবে ans[index]-এ।

def next_greater(a):
    ans = [-1] * len(a)
    st = []                    # indices still waiting; values decrease upward
    pops = 0
    for i, x in enumerate(a):
        while st and a[st[-1]] < x:
            ans[st.pop()] = x  # x is the first greater value to the right
            pops += 1
        st.append(i)
    return ans, pops

for a in [[2, 7, 3, 5, 4, 6, 8], [5, 4, 3, 2, 1], [1, 2, 3, 4, 5]]:
    ans, pops = next_greater(a)
    print("a   =", *a)
    print("nge =", *ans)
    print(f"pushes = {len(a)}, pops = {pops}")

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

a   = 2 7 3 5 4 6 8
nge = 7 8 5 6 6 8 -1
pushes = 7, pops = 6
a   = 5 4 3 2 1
nge = -1 -1 -1 -1 -1
pushes = 5, pops = 0
a   = 1 2 3 4 5
nge = 2 3 4 5 -1
pushes = 5, pops = 4

ans[st.pop()] = x এক লাইনে দুটি কাজ করে: index বের করে আনে, আর সেই ঘরে উত্তর বসায়। print("a =", *a)-এ *a list-এর উপাদানগুলো আলাদা আলাদা argument হিসেবে দেয়, তাই মাঝে একটি করে space বসে। তুলনাটি < (কঠোর) — তাই সমান মান একে অপরকে pop করে না; “next greater or equal” চাইলে লিখতে হবে <=. ডুপ্লিকেট-সহ ইনপুটে এই এক অক্ষরের পার্থক্যই সঠিক আর ভুল উত্তরের পার্থক্য।

একই ছাঁচ: stock span

Stock span সমস্যা: প্রতিদিনের শেয়ার-দাম দেওয়া আছে। প্রতিটি দিনের “span” মানে আজসহ পরপর কতদিন (পেছন দিকে) দাম আজকের সমান বা কম ছিল। দাম [100, 80, 60, 70, 60, 75, 85] হলে span [1, 1, 1, 2, 1, 4, 6]. এটি আসলে “বাঁ দিকে প্রথম বড়” খোঁজা: বাঁয়ে সবচেয়ে কাছের যে দিনের দাম আজকের চেয়ে বেশি, তার index p হলে span = i − p (না থাকলে i + 1).

Pseudocode — stock span (0-based)

st = empty stack of indices        // prices on the stack strictly decrease upward
for i = 0 .. n-1:
    while st not empty and price[top(st)] <= price[i]:
        pop(st)                    // those days can never block a later day
    span[i] = (st empty) ? i + 1 : i - top(st)
    push(st, i)

হাতে দেখুন দিন ৫ (দাম 75): stack-এ তখন দিন 0, 1, 3, 4 (দাম 100, 80, 70, 60). 60 আর 70 pop হয়, top-এ থামে দিন 1 (80 > 75) — span = 5 − 1 = 4. pop হওয়া দিনগুলো আর কখনো লাগবে না: পরের যেকোনো দিনের জন্য 75 তাদের চেয়ে বড় বা সমান, আর 75 তাদের চেয়ে ডানে। একই amortized যুক্তিতে সময় O(n).

৯ · Histogram-এর সবচেয়ে বড় আয়তক্ষেত্র আর min-stack

Largest rectangle in a histogram — O(n)

পাশাপাশি কয়েকটি ১ চওড়া স্তম্ভ, উচ্চতা [2, 1, 5, 6, 2, 3]. এদের ভেতরে আঁকা যায় এমন সবচেয়ে বড় আয়তক্ষেত্র কত? (ঢাকার রাস্তার পাশে বিভিন্ন উচ্চতার দালানের সারি ভাবুন — সবচেয়ে বড় আয়তাকার বিলবোর্ড, যা দালানগুলোর গায়ে লাগিয়ে রাখা যায়।) উত্তর 10: উচ্চতা 5 আর 6-এর দুটি স্তম্ভ জুড়ে, উচ্চতা 5, চওড়া 2. এটি largest rectangle in a histogram সমস্যা।

মূল পর্যবেক্ষণ: সবচেয়ে বড় আয়তক্ষেত্রের উচ্চতা অবশ্যই কোনো একটি স্তম্ভ j-এর উচ্চতা h[j] (নইলে আরও উঁচু করা যেত)। স্তম্ভ j-কে “সবচেয়ে নিচু” ধরে আয়তক্ষেত্রটি দুই দিকে ছড়ায় যতক্ষণ না h[j]-এর চেয়ে নিচু স্তম্ভে ধাক্কা লাগে। তাই প্রতিটি j-এর জন্য দরকার বাঁ দিকে প্রথম ছোট (L) আর ডান দিকে প্রথম ছোট (R) — চওড়া = R − L − 1. এটি NGE-রই আয়না-রূপ, আর একটি monotonic stack একবারের যাত্রায় দুটোই দেয়: stack-এ উচ্চতা নিচ থেকে ওপরে বাড়ে; নতুন স্তম্ভ i ছোট হলে top j pop হয় — তখন ডানের প্রথম ছোট R = i, আর pop-এর পর নতুন top-ই বাঁয়ের প্রথম ছোট L.

Pseudocode — largest rectangle (0-based; h[n] = 0 is a sentinel that empties the stack)

best = 0; st = empty stack of indices     // heights on the stack increase upward
for i = 0 .. n:
    cur = (i == n) ? 0 : h[i]
    while st not empty and h[top(st)] > cur:
        j = pop(st)                        // first smaller to the right of j is i
        L = (st empty) ? -1 : top(st)      // first smaller to the left of j
        best = max(best, h[j] * (i - L - 1))
    push(st, i)
return best
i (h[i])pop হলো (উচ্চতা × চওড়া)stack-এর index (নিচ → ওপর)best
0 (2)—00
1 (1)j=0: 2 × 1 = 212
2 (5)—1 22
3 (6)—1 2 32
4 (2)j=3: 6 × 1 = 6; j=2: 5 × 2 = 101 410
5 (3)—1 4 510
6 (sentinel 0)j=5: 3 × 1 = 3; j=4: 2 × 4 = 8; j=1: 1 × 6 = 6610

কেন O(n): সরল পদ্ধতিতে প্রতিটি j থেকে দুই দিকে হেঁটে O(n²). এখানে আবার সেই হিসাব — প্রতিটি index (sentinel-সহ n + 1 টি) ঠিক একবার push, সর্বোচ্চ একবার pop, আর প্রতিটি pop-এ O(1) কাজ (একটি গুণ, একটি তুলনা)। while লুপের মোট ঘূর্ণন ≤ n, তাই সারা অ্যালগরিদম Θ(n), জায়গা O(n). শুদ্ধতার যুক্তি: প্রতিটি j pop-এর মুহূর্তে তার আসল L ও R জানা থাকে, তাই প্রতিটি “j সবচেয়ে নিচু” আয়তক্ষেত্র ঠিক একবার পরীক্ষা হয় — আর সর্বোচ্চটি এদেরই একটি। সমান উচ্চতা থাকলে (যেমন [3, 3]) শেষের 3-এর জন্য হিসাব কম আসে (চওড়া 1), কিন্তু প্রথম 3 pop হওয়ার সময় পুরো চওড়া (2) পায় — তাই > লিখলেও সর্বোচ্চ ঠিক থাকে।

Min-stack — সর্বনিম্ন মানও O(1)-এ

এমন একটি stack চাই যা push, pop, top-এর সাথে getMin()-ও O(1)-এ দেয়। একটি min ভ্যারিয়েবল রাখলে pop-এর সময় বিপদ: সর্বনিম্নটি pop হয়ে গেলে নতুন সর্বনিম্ন কে, তা জানতে পুরো stack ঘাঁটতে হবে — O(n). কৌশল: প্রতিটি ঘরে মানের সাথে লিখে রাখুন “এই ঘর পর্যন্ত (নিচ থেকে) সর্বনিম্ন”। Min-stack-এর প্রতিটি ঘর তখন নিজের নিচের অংশের একটি স্থির ছবি; pop করলে নিচের ঘরের লেখা min আপনাআপনি সঠিক — কারণ সেটি লেখার সময় ওপরের কেউ ছিলই না।

struct MinStack {
    vector<pair<int,int>> st;                       // (value, min of this and everything below)
    void push(int x) { st.push_back({x, st.empty() ? x : min(x, st.back().second)}); }
    void pop()       { st.pop_back(); }
    int  top()    const { return st.back().first; }
    int  getMin() const { return st.back().second; }
};
// push 5, 3, 7, 3  -> mins 5, 3, 3, 3;  pop, pop -> getMin() = 3;  pop -> getMin() = 5

সব কাজ O(1), জায়গা 2n (প্রতি উপাদানে একটি বাড়তি সংখ্যা)। জায়গা বাঁচাতে দ্বিতীয় একটি stack-এ শুধু নতুন সর্বনিম্নগুলো (সমানসহ) রাখা যায়। একই কৌশলে max-stack, বা দুটি stack জুড়ে একটি queue বানালে “sliding window-এর সর্বনিম্ন” পাওয়া যায় (পর্ব ১৪ ও ১৬).

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

  • Stack-এর push আর pop-এর time complexity? — দুটোই O(1); dynamic array-তে push amortized O(1), linked list-এ worst case O(1).
  • (a + b) * c - d / e-এর postfix? — a b + c * d e / -.
  • Postfix হিসাবে operator পেলে প্রথম pop কোন operand? — ডান operand; তাই হিসাব হয় (দ্বিতীয় pop) op (প্রথম pop).
  • Monotonic stack-এ nested লুপ থাকা সত্ত্বেও O(n) কেন? — প্রতিটি index একবার push আর সর্বোচ্চ একবার pop, তাই while-এর মোট ঘূর্ণন ≤ n.
  • দুটি stack দিয়ে কী বানানো যায়? — undo/redo, ব্রাউজারের back/forward, আর একটি queue (প্রতিটি কাজ amortized O(1), পর্ব ১৪).

১০ · AI-এর যুগে stack — ছোট কোড, বড় ফাঁদ

Stack-এর কোড ছোট, তাই AI সেটি নির্ভুলভাবে লিখবে — এমন ধারণা স্বাভাবিক। বাস্তবে ছোট কোডেই ভুল লুকোতে সুবিধা: একটি < বনাম <=, দুটি pop-এর ক্রম, বা একটি খালি-যাচাই। ছোট উদাহরণে উত্তর মিলে যায়, আসল ইনপুটে নয়। আর monotonic stack-এর complexity নিয়ে AI কখনো “nested loop, তাই O(n²)” বলে, কখনো আবার প্রমাণ ছাড়াই O(n) দাবি করে — আপনাকে নিজে যুক্তিটি দিতে পারতে হবে।

Stack-এর কোডে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
Postfix-এ operand-এর ক্রম উল্টো: b - a8 2 - চালান — উত্তর 6 আসতে হবে
খালি stack-এ top()/pop() — বন্ধনী-সমস্যায় শুরুতেই )")(", "]", খালি string দিয়ে চালান
শেষে stack খালি কি না যাচাই ভুলে যাওয়া"((" দিন — NOT balanced আসতে হবে
Shunting-yard-এ ^-কে বাঁ-associative ধরা2 ^ 3 ^ 2 → 2 3 2 ^ ^ = 512 কি না দেখুন
Monotonic stack-এ < আর <= গুলিয়ে ফেলাডুপ্লিকেট-সহ ইনপুট ([2, 2, 1, 2]) দিয়ে brute force-এর সাথে মেলান
Python-এ pop(0) বা insert(0, x) দিয়ে stack105 উপাদানে সময় মাপুন; শেষ প্রান্ত ব্যবহার করতে বলুন
“while লুপ ভেতরে, তাই O(n²)” বা প্রমাণহীন “O(n)”pop গোনার counter বসান; মোট pop ≤ n কি না দেখুন

AI-কে সমাধান লিখতে না বলে বরং আপনার trace আর যুক্তি পরীক্ষা করতে বলুন। নিচের prompt-টি সেভাবেই সাজানো:

আমি DSA শিখছি, এখন stack আর monotonic stack. নিচে আমার লেখা কোড আর আমার হাতে করা trace দিলাম: [এখানে কোড আর trace পেস্ট করুন] পুরো সমাধান লিখে দিও না। শুধু এগুলো করো: ১. [2, 2, 1, 2] আর [5, 4, 3, 2, 1] ইনপুটে প্রতিটি ধাপে stack-এর অবস্থা দেখাও, আমার trace-এর সাথে কোথায় অমিল বলো। ২. আমার তুলনাটি < না <= হওয়া উচিত — ডুপ্লিকেট দিয়ে উদাহরণসহ ব্যাখ্যা করো। ৩. খালি stack-এ top/pop হতে পারে এমন কোনো ইনপুট আছে কি? ৪. time complexity কত — প্রতিটি index কতবার push আর pop হয়, সেই যুক্তিতে বলো।

Stack-এর কোড যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Stack-এর ধারণা কে আবিষ্কার করেছিলেন — আর কেন একাধিক মানুষ প্রায় একই সময়ে একই জিনিস ভেবেছিলেন?

Stack-এর কোনো একক আবিষ্কারক নেই; কম্পিউটারের প্রথম যুগে কয়েকজন আলাদা আলাদা পথে একই ধারণায় পৌঁছান। Alan Turing ১৯৪৫–৪৬ সালে ব্রিটেনের ACE কম্পিউটারের নকশার রিপোর্টে subroutine থেকে ফেরার ঠিকানা রাখার জন্য একটি ব্যবস্থার কথা লেখেন, যার জন্য তিনি “bury” আর “unbury” শব্দ ব্যবহার করেন — আজকের push আর pop-এর পূর্বসূরি। তাঁর সমস্যাটি ছিল ঠিক পর্ব ০৭-এর call stack-এর সমস্যা: একটি subroutine আরেকটিকে ডাকলে ফেরার ঠিকানাগুলো উল্টো ক্রমে লাগে।

১৯৫০-এর দশকে জার্মানির মিউনিখে Friedrich L. Bauer আর Klaus Samelson গাণিতিক সূত্র যান্ত্রিকভাবে অনুবাদ করার কাজে একটি “Kellerprinzip” (জার্মান Keller = ভাঁড়ার ঘর বা সেলার — যেখানে শেষে রাখা জিনিস আগে বের করতে হয়) ব্যবহার করেন। এর জন্য তাঁরা ১৯৫৭ সালে জার্মানিতে patent-এর আবেদন করেন, আর ১৯৬০ সালে Communications of the ACM-এ “Sequential Formula Translation” প্রবন্ধে পদ্ধতিটি প্রকাশ করেন। Bauer পরে stack-এর ধারণায় অবদানের জন্য IEEE Computer Society-র Computer Pioneer সম্মাননা পান। প্রায় একই সময়ে অস্ট্রেলিয়ায় দার্শনিক ও কম্পিউটার বিজ্ঞানী Charles Hamblin স্বাধীনভাবে Reverse Polish Notation আর একটি stack-ভিত্তিক হিসাবপদ্ধতির প্রস্তাব দেন।

কেন একসাথে এত মানুষ? কারণ সমস্যাগুলো একই সময়ে সামনে এসেছিল। প্রথম উচ্চ-স্তরের ভাষা আর compiler তৈরি হচ্ছিল, আর প্রত্যেককেই দুটি প্রশ্নের মুখোমুখি হতে হয়েছিল: বন্ধনী আর precedence-সহ সূত্রকে মেশিনের নির্দেশে কীভাবে বদলানো যায়, আর ফাংশনের ভেতরে ফাংশন ডাকলে ফেরার পথ কীভাবে মনে রাখা যায়। দুটি প্রশ্নেরই কাঠামো এক — “সবচেয়ে সাম্প্রতিক অসমাপ্ত কাজ আগে শেষ” — তাই উত্তরও এক। বিজ্ঞানের ইতিহাসে এমন “একসাথে আবিষ্কার” প্রায়ই ঘটে, যখন সমস্যা পরিপক্ব হয়।

আজ stack এতটাই মৌলিক যে প্রায় প্রতিটি CPU-তে stack pointer নামের আলাদা register, আর push, pop, call, ret নির্দেশ হার্ডওয়্যারেই তৈরি। ১৯৬১ সালের Burroughs B5000-এর মতো কিছু মেশিন পুরোপুরি stack-কে ঘিরেই নকশা করা হয়েছিল।

মূল উপলব্ধি: Stack কোনো চতুর কৌশল নয় যা কেউ হঠাৎ ভেবে বের করেছে — এটি “নেস্টেড” কাঠামোর (বন্ধনীর ভেতরে বন্ধনী, ফাংশনের ভেতরে ফাংশন) স্বাভাবিক গাণিতিক উত্তর। যেখানে nesting, সেখানেই stack.

প্র ০২Compiler কি আজও Shunting-yard ব্যবহার করে? Expression পার্সিংয়ের অন্য পথগুলো কী?

Dijkstra-র Shunting-yard ছিল ALGOL 60 যুগের একটি চমৎকার সমাধান: একবার বাঁ থেকে ডানে পড়া, একটি stack, O(n) সময়, আর কোনো রিকার্শন নয় — সেই সময়ের মাত্র কয়েক হাজার word মেমরির কম্পিউটারের জন্য আদর্শ। আজও ক্যালকুলেটর অ্যাপ, spreadsheet-এর সূত্র, ছোট scripting ভাষা আর কনটেস্টের “expression evaluate করো” সমস্যায় এটি বা এর রূপান্তর প্রচুর ব্যবহৃত হয়।

তবে বড় compiler-গুলো (যেমন GCC বা Clang-এর C++ parser) সাধারণত recursive descent পার্সিং ব্যবহার করে — প্রতিটি ব্যাকরণ-নিয়মের জন্য একটি ফাংশন (expr(), term(), factor()), যারা একে অপরকে ডাকে — পর্ব ০৭-এ দেখা mutual recursion-এর মতো। এখানে stack উধাও হয়নি — সেটি লুকিয়ে আছে call stack-এ। Recursive descent-এর সুবিধা হলো ভালো error বার্তা দেওয়া আর জটিল ব্যাকরণ সামলানো সহজ। Precedence-এর অনেক স্তর থাকলে (C++-এ ১৫টির বেশি) প্রতিটির জন্য আলাদা ফাংশন লেখা ক্লান্তিকর, তাই অনেক parser expression-এর অংশে precedence climbing বা Vaughan Pratt-এর ১৯৭৩ সালের Pratt parsing ব্যবহার করে — যা মূলত Shunting-yard-এর রিকার্সিভ রূপ। অন্য দিকে yacc/bison-এর মতো parser generator যে LR parser তৈরি করে, সেটি একটি explicit stack-এর ওপর shift আর reduce করে — Shunting-yard-এর একটি সাধারণীকৃত রূপ বলা যায়।

লক্ষ করুন, প্রতিটি পদ্ধতিতেই কোথাও না কোথাও একটি stack আছে — explicit অথবা call stack. এর একটি গভীর তাত্ত্বিক কারণ আছে: বন্ধনীর মতো nested গঠন চিনতে finite automaton (নির্দিষ্ট পরিমাণ মেমরির মেশিন) যথেষ্ট নয়; লাগে অন্তত একটি stack. Automata তত্ত্বে একে বলে pushdown automaton, আর এরা ঠিক “context-free” ভাষাগুলো চিনতে পারে — বেশিরভাগ প্রোগ্রামিং ভাষার ব্যাকরণ মূলত এই শ্রেণির।

আরেকটি সুন্দর মিল: Shunting-yard যে postfix তৈরি করে, সেটিই JVM বা CPython-এর bytecode-এর মতো stack machine-এর নির্দেশ। অর্থাৎ ১৯৬১ সালের এই অ্যালগরিদম কার্যত একটি ছোট compiler — infix থেকে stack machine-এর কোড।

মূল উপলব্ধি: পার্সিং পদ্ধতি বদলেছে — explicit stack থেকে রিকার্শন, আবার রিকার্শন থেকে Pratt — কিন্তু নিচের নীতিটি বদলায়নি। Nested গঠন বোঝার জন্য stack অপরিহার্য; প্রশ্ন শুধু সেটি আপনি নিজে রাখবেন, নাকি call stack-কে দেবেন।

প্র ০৩C++-এর std::stack-এর pop() কেন মান ফেরত দেয় না, যেখানে Python-এর pop() দেয়?

নতুনরা প্রায়ই অবাক হন: int x = st.pop(); C++-এ compile হয় না। মান চাইলে আগে st.top(), তারপর আলাদা করে st.pop(). এটি অলসতা নয় — এর পেছনে আছে exception safety-এর একটি বিখ্যাত বিতর্ক।

১৯৯৪ সালে Tom Cargill “C++ Report” পত্রিকায় “Exception Handling: A False Sense of Security” নামে একটি প্রবন্ধে একটি generic Stack template বিশ্লেষণ করেন এবং দেখান যে exception থাকলে এমন একটি সরল class-কেও সঠিক রাখা কতটা কঠিন। সমস্যার কেন্দ্র ছিল ঠিক “মান ফেরত দেওয়া pop”. ধরুন T pop() প্রথমে ওপরের উপাদানটি stack থেকে সরিয়ে দিল, তারপর সেটি কপি করে ফেরত দিতে গেল। যদি কপি করার মুহূর্তে exception হয় (যেমন বড় একটি string কপি করতে গিয়ে মেমরি ফুরোল), তাহলে উপাদানটি stack থেকেও গেছে, কলকারীর হাতেও পৌঁছায়নি — চিরতরে হারিয়ে গেছে। আর আগে কপি করে পরে সরালেও ফেরত দেওয়ার সময় আরেকটি কপি হতে পারে, যা আবার ব্যর্থ হতে পারে।

সমাধান: কাজ দুই ভাগ করা। top() একটি reference দেয় — কোনো কপি নেই, কিছু বদলায়ও না। কলকারী চাইলে নিজে কপি করেন; সেটি ব্যর্থ হলে stack অক্ষত থাকে। তারপর pop() শুধু সরায়, আর destructor-এর মতো কাজ সাধারণত ব্যর্থ হয় না। Herb Sutter-সহ অনেকে পরে এই বিশ্লেষণ বিস্তারিত করেন, আর C++ standard library-র নকশায় এই নীতি গৃহীত হয় — vector::pop_back, queue::pop-ও তাই void.

Python-এ সমস্যাটি নেই, কারণ সেখানে list-এ আসলে অবজেক্টের reference থাকে — pop মানে শুধু একটি reference হাতবদল, কোনো গভীর কপি নয়, যা ব্যর্থ হতে পারে। তাই x = st.pop() নিরাপদ আর সুবিধাজনক। আধুনিক C++-এ move semantics আসার পর বিতর্ক কিছুটা নরম হয়েছে, কিন্তু standard-এর interface বদলায়নি, কারণ সব টাইপের move ব্যর্থতাহীন নয়।

মূল উপলব্ধি: একটি ডেটা স্ট্রাকচারের interface শুধু “কী করে” নয়, “ব্যর্থ হলে কী অবস্থায় থাকে” — সেটিও নকশার অংশ। দুই ভাষার আলাদা উত্তরের পেছনে আছে তাদের মেমরি মডেলের পার্থক্য।

প্র ০৪Monotonic stack কি শুধু ইন্টারভিউয়ের কৌশল, নাকি এর পেছনে গভীর কোনো কাঠামো আছে?

“Next greater element” প্রথমে একটি ধাঁধার মতো লাগে, কিন্তু এটি একটি বড় পরিবারের সদস্য, যার আনুষ্ঠানিক নাম all nearest smaller (বা greater) values — প্রতিটি উপাদানের জন্য বাঁয়ে ও ডানে সবচেয়ে কাছের ছোট উপাদান খোঁজা। ১৯৯০-এর দশকের শুরুতে Berkman, Schieber আর Vishkin এই সমস্যার দক্ষ parallel অ্যালগরিদম নিয়ে কাজ করেন এবং দেখান যে বহু সমস্যা — বন্ধনী মেলানো থেকে শুরু করে কিছু tree-গঠন পর্যন্ত — এর ওপর দাঁড় করানো যায়। ক্রমিক (একটি প্রসেসরে) সমাধান ঠিক এই পর্বের stack.

একটি সুন্দর উদাহরণ Cartesian tree (Jean Vuillemin, ১৯৮০): অ্যারের সর্বনিম্ন উপাদান মূল, তার বাঁয়ের অংশ থেকে বাঁ subtree, ডানের অংশ থেকে ডান subtree — রিকার্সিভভাবে। সরলভাবে বানালে O(n²) পর্যন্ত লাগতে পারে, কিন্তু monotonic stack দিয়ে বাঁ থেকে ডানে একবার গিয়ে O(n)-এ বানানো যায়: stack-এ থাকে tree-র “ডান কাঁধ” (মূল থেকে ডানদিকের পথ)। এই tree-ই range minimum query-র কিছু O(1) কৌশলের ভিত্তি (পর্ব ৩৭) এবং treap-এর গঠনের সাথে সম্পর্কিত (পর্ব ৩৮)।

Histogram-এর সমস্যাটি আবার একটি বড় সমস্যার ভেতরের অংশ: 0/1-এর একটি grid-এ সবচেয়ে বড় পুরোপুরি-১ আয়তক্ষেত্র খোঁজা (maximal rectangle). প্রতিটি সারিকে “ভিত্তি” ধরে ওপরের দিকে টানা ১-এর উচ্চতা গুনলে প্রতিটি সারি একটি histogram হয়ে যায়; R সারি আর C কলামে মোট সময় O(R·C). ছবি-প্রক্রিয়াকরণে খালি জায়গা খোঁজা বা চিপ-নকশায় ফাঁকা এলাকা বের করার মতো কাজে এ ধরনের হিসাব লাগে।

আর বিশ্লেষণের দিক থেকে monotonic stack হলো amortized analysis-এর সবচেয়ে পরিষ্কার শিক্ষণ-উদাহরণ। Potential function Φ = stack-এর আকার ধরলে প্রতিটি push-এর amortized খরচ 2, আর প্রতিটি pop-এর 0 — তাই n টি কাজে মোট O(n). Robert Tarjan ১৯৮৫ সালে amortized analysis-কে একটি আনুষ্ঠানিক পদ্ধতি হিসেবে গুছিয়ে উপস্থাপন করেন; পর্ব ৬০-এ সেটি বিস্তারিত।

মূল উপলব্ধি: Monotonic stack আসলে একটি নীতি: “যে উপাদান আর কখনো উত্তর হতে পারবে না, তাকে এখনই ফেলে দাও।” এই অপ্রয়োজনীয়কে বাদ দেওয়ার চিন্তাই monotonic queue, convex hull trick-এর মতো আরও উন্নত কৌশলেরও প্রাণ।

প্র ০৫Call stack কীভাবে হ্যাকারদের সবচেয়ে প্রিয় আক্রমণের লক্ষ্য হয়ে উঠল?

পর্ব ০৭-এ দেখেছেন, প্রতিটি stack frame-এ local ভ্যারিয়েবলের পাশেই থাকে return address — ফাংশন শেষে CPU কোথায় ফিরবে। C-তে একটি local অ্যারে, যেমন char buf[64], ঠিক সেই frame-এ থাকে। যদি কোড সীমা না দেখে এতে লেখে — যেমন পুরোনো gets() ফাংশন, যা কতটুকু ইনপুট আসবে তা জানেই না — তাহলে লম্বা ইনপুট অ্যারে ছাপিয়ে return address-এর ওপর লিখে ফেলে। আক্রমণকারী সাবধানে ইনপুট সাজালে ফাংশন “ফেরার” সময় CPU তাঁর পছন্দের ঠিকানায় চলে যায়। এর নাম stack buffer overflow বা stack smashing.

১৯৮৮ সালের Morris worm, ইন্টারনেটের প্রথম দিকের সবচেয়ে আলোচিত worm-গুলোর একটি, যে কয়েকটি উপায়ে ছড়াত তার একটি ছিল Unix-এর fingerd সার্ভিসের ঠিক এমন একটি gets()-ভিত্তিক দুর্বলতা। ১৯৯৬ সালে Phrack পত্রিকায় “Aleph One” ছদ্মনামে প্রকাশিত “Smashing the Stack for Fun and Profit” প্রবন্ধটি কৌশলটিকে ধাপে ধাপে ব্যাখ্যা করে, আর এরপর বহু বছর এটি সবচেয়ে প্রচলিত আক্রমণের একটি হয়ে থাকে। শেষ পর্যন্ত gets() ফাংশনটিই C11 standard থেকে সরিয়ে দেওয়া হয়।

প্রতিরক্ষা এসেছে কয়েক স্তরে, আর প্রায় সবই stack-এর গঠন বোঝার ওপর দাঁড়িয়ে। Stack canary: compiler return address-এর ঠিক আগে একটি গোপন সংখ্যা রাখে, ফেরার আগে দেখে সেটি বদলেছে কি না (GCC-র -fstack-protector). Non-executable stack: stack-এর মেমরিতে কোড চালানো নিষেধ। ASLR: প্রতিবার প্রোগ্রাম চালালে stack-এর ঠিকানা এলোমেলো করা, যাতে আক্রমণকারী আগে থেকে ঠিকানা জানতে না পারেন। আধুনিক কিছু CPU-তে আছে shadow stack — হার্ডওয়্যারের রাখা return address-এর আলাদা একটি কপি, যা মূল stack-এর সাথে মেলানো হয়; না মিললে প্রোগ্রাম থামে। অর্থাৎ নিরাপত্তার জন্য একটি দ্বিতীয় stack!

এখান থেকে প্রোগ্রামারের জন্য শিক্ষা: অ্যারের সীমা যাচাই না করা শুধু “ভুল উত্তর” নয়, নিরাপত্তার ছিদ্র। C++-এ std::vector, std::string আর সীমা-যাচাই করা ফাংশন ব্যবহারের পরামর্শ এই ইতিহাস থেকেই।

মূল উপলব্ধি: Stack-এর LIFO গঠন যতটা কাজের, তার সরলতা ঠিক ততটাই পূর্বানুমেয় — আর পূর্বানুমেয় জিনিসই আক্রমণের লক্ষ্য। ডেটা স্ট্রাকচার কীভাবে মেমরিতে বসে, তা জানা নিরাপদ কোড লেখার পূর্বশর্ত।

অনুশীলন

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

  1. হাতে trace করুন: একটি খালি stack-এ পরপর করা হলো: push 1, push 2, pop, push 3, push 4, pop, pop, push 5. প্রতিটি pop কোন মান ফেরত দেয়? শেষে stack-এ (নিচ থেকে ওপরে) কী থাকে?

    অবস্থাগুলো (নিচ → ওপর): [1] → [1 2] → pop দেয় 2, [1] → [1 3] → [1 3 4] → pop দেয় 4, [1 3] → pop দেয় 3, [1] → [1 5].

    pop-এর ক্রম: 2, 4, 3. শেষে stack: 1 5 (ওপরে 5). লক্ষ করুন, 1 সবার আগে ঢুকেছে, তাই সবার নিচে থেকে এখনো বেরোতে পারেনি — LIFO-র সরাসরি ফল।

  2. Shunting-yard হাতে চালান: (a + b) * c - d ^ e ^ f-কে postfix-এ রূপান্তর করুন। ^ সবচেয়ে বেশি precedence-এর আর ডান-associative.

    ( push; a → out; + push (ওপরে দেয়াল); b → out; ) এলে + out, ( ফেলে দিন। out: a b +. * push; c → out: a b + c. − এলে top-এ * (বেশি precedence) pop: a b + c *; − push. d → out. ^ এলে top − কম precedence, push. e → out. দ্বিতীয় ^ এলে top-এ ^ — precedence সমান কিন্তু ^ ডান-associative, তাই pop নয়, push. f → out. শেষে stack - ^ ^ ওপর থেকে খালি করুন।

    উত্তর: a b + c * d e f ^ ^ -. অর্থাৎ ((a + b) × c) − d(ef). যদি ^-কে ভুল করে বাঁ-associative ধরতেন, পেতেন … d e ^ f ^ - — যা (de)f, ভিন্ন মান।

  3. Stock span বের করুন: দাম [100, 80, 60, 70, 60, 75, 85]-এর প্রতিটি দিনের span হাতে বের করুন, আর প্রতিটি দিনের শেষে stack-এ কোন দিনগুলো (index) আছে লিখুন। মোট কয়টি pop হলো?

    দিন 0 (100): stack খালি → span 1; stack [0]. দিন 1 (80): top 100 > 80 → span 1 − 0 = 1; [0 1]. দিন 2 (60): span 1; [0 1 2]. দিন 3 (70): 60 pop; top 80 → span 3 − 1 = 2; [0 1 3]. দিন 4 (60): span 1; [0 1 3 4]. দিন 5 (75): 60, 70 pop; top 80 → span 5 − 1 = 4; [0 1 5]. দিন 6 (85): 75, 80 pop; top 100 → span 6 − 0 = 6; [0 6].

    Span: 1 1 1 2 1 4 6. মোট pop ৫টি, push ৭টি — pop কখনো push-এর বেশি হতে পারে না, এটিই O(n)-এর যুক্তি।

  4. কোড লিখুন: একটি MaxStack লিখুন যা push(x), pop(), top() আর getMax() — চারটিই O(1)-এ করে। তারপর হাতে দেখান: push 3, push 8, push 5, push 8, pop, pop — প্রতিটি কাজের পর getMax() কত?

    মূল চিন্তা min-stack-এর মতোই: প্রতিটি ঘরে মানের সাথে “এই ঘর পর্যন্ত সর্বোচ্চ” রাখুন।

    struct MaxStack {
        vector<pair<int,int>> st;               // (value, max of this and everything below)
        void push(int x) { st.push_back({x, st.empty() ? x : max(x, st.back().second)}); }
        void pop()       { st.pop_back(); }       // caller checks empty()
        int  top()    const { return st.back().first; }
        int  getMax() const { return st.back().second; }
    };

    Trace: push 3 → max 3; push 8 → 8; push 5 → 8; push 8 → 8; pop → 8 (নিচের ঘরে লেখা 8, যা দ্বিতীয় push-এর 8 থেকে এসেছে); pop → 8 (এখন top 8 নিজেই). এখানে আরেকবার pop করলে getMax() হতো 3. Python-এ একই কাঠামো: st.append((x, x if not st else max(x, st[-1][1]))). প্রতিটি কাজ O(1), জায়গা O(n).

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

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

ক্রমPostfix expression-এর মান বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. একটি খালি stack নিয়ে বাঁ থেকে ডানে পরের token পড়া
  2. token সংখ্যা হলে সেটি stack-এ push করা
  3. token operator হলে প্রথমে ডান operand, তারপর বাঁ operand pop করা
  4. বাঁ op ডান হিসাব করে ফল আবার push করা
  5. সব token শেষে stack-এ থাকা একমাত্র মানটিকে উত্তর হিসেবে নেওয়া

একটি খালি stack-এ push 1, push 2, push 3, pop, push 4, pop, pop করা হলো। pop-গুলো কোন ক্রমে মান ফেরত দেয়?

  • 1, 2, 3
  • 3, 2, 1
  • 3, 4, 2
  • 4, 3, 2

Postfix expression 6 2 3 + *-এর মান কত?

  • 15
  • 30
  • 11
  • 36

বাঁ-associative - ধরে infix expression a - b - c-এর সঠিক postfix কোনটি?

  • a b - c -
  • a b c - -
  • - - a b c
  • a - b c -

n উপাদানের অ্যারেতে monotonic stack দিয়ে সব next greater element বের করা হয়। for লুপের ভেতরে একটি while লুপ আছে। মোট time complexity কত?

  • O(n²), কারণ লুপের ভেতরে লুপ
  • O(n log n)
  • O(log n)
  • O(n), কারণ প্রতিটি index সর্বোচ্চ একবার push ও একবার pop হয়

Stack দিয়ে বন্ধনী মেলানোর অ্যালগরিদমে ইনপুট (() পড়া শেষ হলে কী অবস্থা হয়, আর ফল কী?

  • stack খালি, ফল balanced
  • stack-এ একটি ( রয়ে যায়, ফল NOT balanced
  • খালি stack-এ pop-এর চেষ্টা হয়, ফল NOT balanced
  • stack-এ দুটি ( থাকে, ফল balanced

স্তম্ভের উচ্চতা [2, 1, 5, 6, 2, 3] (প্রতিটি ১ চওড়া) হলে histogram-এর ভেতরে সবচেয়ে বড় আয়তক্ষেত্রের ক্ষেত্রফল কত?

  • 6
  • 8
  • 10
  • 12

শব্দকোষ

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

মনে রাখুন

  1. Stack = LIFO; push, pop, top, empty — চারটিই O(1). খালি stack-এ top/pop কখনো নয়।
  2. বাস্তবে stack মানে vector/list-এর শেষ প্রান্ত; C++-এ pop() কিছু ফেরত দেয় না, Python-এ pop(0) নয়।
  3. “সবচেয়ে সাম্প্রতিক অসমাপ্ত কাজ আগে” = stack: call stack, বন্ধনী, undo/redo, back বোতাম, DFS.
  4. Shunting-yard: precedence আর associativity মেনে infix → postfix; postfix হিসাবে প্রথম pop = ডান operand.
  5. Monotonic stack: প্রতিটি index একবার push, সর্বোচ্চ একবার pop ⇒ মোট O(n) — NGE, stock span, histogram.

পরের পর্বে: Stack-এর যমজ ভাই — queue, যেখানে আগে এলে আগে যায় (FIFO), আর deque, যেখানে দুই প্রান্তেই কাজ চলে। ট্রেনের টিকিটের লাইন থেকে শুরু করে circular buffer আর দুটি stack দিয়ে queue বানানোর কৌশল।

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

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