Heap Patterns¶
ছয়টা reusable thinking move। প্রত্যেকটার trigger words শিখে নাও — একটা problem কোন pattern-এর পোশাক পরে আছে সেটা চিনে ফেলাই অর্ধেক যুদ্ধ জেতা।
Pattern 1 — Size-k heap দিয়ে Top-K¶
Trigger words: "k largest", "k smallest", "k most frequent", "k closest", "kth largest in a stream"।
Core idea: k-টা largest item খুঁজতে হলে একটা size k-এর min-heap রাখো। প্রতিটা newcomer লড়াই করে বর্তমান সবচেয়ে ছোট member-এর সাথে (heap top)। তাকে হারালে তুমি তার জায়গা নাও; হারলে তুমি rejected।
Thinking tweak: min-heap টা top-K club-এর দরজা পাহারা দেয়। সবচেয়ে দুর্বল member দরজায় দাঁড়িয়ে। Newcomer-কে সব k-জন member-এর সাথে compare করতে হয় না — শুধু দুর্বলতমের সাথে। Beginners এই counterintuitive অংশটাই miss করে: largest items খুঁজতে একটা min-heap লাগে, কারণ একটাই প্রশ্ন matter করে — "তুমি কি আমাদের worst-এর চেয়ে ভালো?"
Tiny example: [5, 1, 9, 3, 7, 6]-এর 3-টা largest খুঁজে বের করো।
import heapq
def top_k(nums, k):
club = nums[:k] # first k get in free
heapq.heapify(club) # weakest member at club[0]
for x in nums[k:]:
if x > club[0]: # beats the weakest?
heapq.heapreplace(club, x) # pop weakest, push x (one repair)
return club
print(top_k([5, 1, 9, 3, 7, 6], 3)) # contains 9, 7, 6 in some heap order
Dry run: club শুরু হয় [1, 5, 9] (weakest = 1)। 3 > 1 → replace, club-এ {3, 5, 9}। 7 > 3 → replace, club-এ {5, 7, 9}। 6 > 5 → replace, club-এ {6, 7, 9}। Cost: O(n log k) time, O(k) memory — এমনকি একটা endless stream-এও কাজ করে।
Inherits from: plain heap push/pop; "k most frequent" variant-টা প্রথমে dict দিয়ে count করলে ../05-hashing/-এর frequency counting।
Representative problems: Kth Largest Element in an Array, Top K Frequent Elements, K Closest Points to Origin।
Pattern 2 — K-way merge¶
Trigger words: "merge k sorted lists", "k sorted arrays/streams", "smallest in a sorted matrix", "smallest range across lists"।
Core idea: k-টা sorted list, প্রতিটা list-এ একটা করে cursor। Overall merge-এর পরের item অবশ্যই হতে হবে কোনো না কোনো cursor যেদিকে দেখাচ্ছে তার মধ্যে smallest টা। k-টা cursor-value-ই একটা heap-এ রাখো; smallest-টা pop করো, সেই cursor-টা advance করো, তার পরের value push করো।
Thinking tweak: ছবিটা ভাবো — airport security-তে k-টা লাইন আর একজনই officer। Officer সবসময় সবচেয়ে ছোট number-ওয়ালা front-most লোকটাকে ডাকেন। Heap টা হলো officer-এর চোখে শুধু সামনের k-জন — কখনোই পুরো ভিড় না।
Tiny example: merge করো [1, 4, 7], [2, 5], [3, 6]।
import heapq
def merge_k(lists):
h = [(lst[0], i, 0) for i, lst in enumerate(lists) if lst]
heapq.heapify(h) # (value, which list, index in it)
out = []
while h:
val, li, idx = heapq.heappop(h)
out.append(val)
if idx + 1 < len(lists[li]):
heapq.heappush(h, (lists[li][idx + 1], li, idx + 1))
return out
print(merge_k([[1, 4, 7], [2, 5], [3, 6]])) # [1, 2, 3, 4, 5, 6, 7]
Heap-এ কখনো k-টার বেশি entry থাকে না: N items-এর জন্য মোট O(N log k), যেখানে "সব ঢেলে দিয়ে sort" হতো O(N log N)। Tuple-এর মাঝের field i একই সাথে tie-breaker-এর কাজ করে, যাতে সমান values কখনো lists compare না করে।
Inherits from: merge sort-এর two-pointer merge step (../02-arrays-and-strings/) — এটা আসলে "k pointers", যেখানে কোন pointer নড়বে সেটা ঠিক করে একটা heap।
Representative problems: Merge k Sorted Lists, Kth Smallest Element in a Sorted Matrix, Smallest Range Covering Elements from K Lists।
Pattern 3 — Running median-এর জন্য two heaps¶
Trigger words: "median of a stream", "median of a window", "balance two halves"।
Core idea: numbers-গুলোকে একটা lower half আর একটা upper half-এ ভাগ করো। Lower half টা একটা max-heap-এ রাখো (এর top = ছোট numbers-দের মধ্যে সবচেয়ে বড়) আর upper half টা একটা min-heap-এ (এর top = বড় numbers-দের মধ্যে সবচেয়ে ছোট)। Median থাকে এই দুটো top-এই।
Thinking tweak: একটা seesaw, যার pivot-এ median। প্রতিটা heap-কে শুধু তার ভেতরের কিনারাটা দেখাতে হয় — মাঝখান ছুঁয়ে থাকা দুটো value। Sizes balanced রাখো (পার্থক্য বড়জোর 1), আর median সবসময় এক peek দূরে।
Tiny example sketch:
Stream so far: 5, 2, 8, 1
lower (max-heap) | upper (min-heap)
[2, 1] | [5, 8]
top = 2 ----------+---------- top = 5
median = (2 + 5) / 2 = 3.5
Insert rule: newcomer-কে lower-এর top-এর সাথে compare করে তার side ঠিক করো, তারপর sizes 2-এর ব্যবধানে চলে গেলে একটা top অন্য পাশে সরিয়ে rebalance করো। দুটো step-ই O(log n); median query O(1)। Full code আছে implementation.py-তে (MedianFinder)।
Inherits from: concept.md-র max-heap-via-negation trick; আর sliding windows-এর (../02-arrays-and-strings/) invariant-ধাঁচের thinking — প্রতিটা step-এর পরে একটা property maintain করা।
Representative problems: Find Median from Data Stream, Sliding Window Median (lazy deletion যোগ হয়), IPO (two heaps, কিন্তু অন্য split: affordable vs not-yet-affordable)।
Pattern 4 — Earliest end দিয়ে scheduling¶
Trigger words: "minimum number of rooms / machines / platforms", "intervals", "can all meetings fit", "CPU picks next job"।
Core idea: jobs-গুলোকে start time দিয়ে sort করো। এখন চলমান jobs-দের end times-এর একটা min-heap রাখো। নতুন job শুরু হলে, প্রথমে তার start-এর <= সব end time pop করে দাও (ওই rooms আবার free)। সবকিছু process করার পরে heap-এর size-ই হলো একসাথে চলা jobs-এর peak সংখ্যা।
Thinking tweak: heap টা একটাই প্রশ্নের উত্তর বারবার দেয়: "এখনকার কোন job সবার আগে শেষ হবে?" Newcomer-এর জন্য সম্ভাব্য free room ওই একটাই হতে পারে। সব room scan করতে হয় না — earliest end time টাই heap top।
Tiny example: meetings (0, 30), (5, 10), (15, 20)।
sort by start: (0,30) (5,10) (15,20)
(0,30): no room ends by 0 -> heap of ends: [30] rooms = 1
(5,10): earliest end 30 > 5 -> need new room: [10, 30] rooms = 2
(15,20): earliest end 10 <= 15 -> reuse it: [20, 30] rooms = 2
answer: 2 rooms
Inherits from: interval sorting; greedy proofs ("earliest end first" হলো classic activity selection-এর সেই একই exchange argument)।
Representative problems: Meeting Rooms II, Single-Threaded CPU, Car Pooling (heap বা difference-array solution)।
Pattern 5 — Lazy deletion¶
Trigger words: "remove an arbitrary element from the heap", "sliding window with a heap", "values expire / become stale"।
Core idea: heap মাঝখান থেকে সস্তায় delete করতে পারে না। তাহলে কোরো না। Deletion টা অন্য কোথাও লিখে রাখো (একটা counter dict, বা window bounds-এর সাথে তুলনা), stale entry টা heap-এই থাকতে দাও, আর stale top-গুলো শুধু তখনই purge করো যখন peek/pop-এর সময় সেগুলো উপরে ভেসে ওঠে।
Thinking tweak: এটা অনেকটা party-র ভেতর guest-কে তাড়া করার বদলে guest list থেকে নামটা কেটে দেওয়ার মতো। Bouncer (তোমার pop loop) list-টা তখনই check করে যখন কেউ দরজায় পৌঁছায়।
Tiny example: (-value, index)-এর max-heap দিয়ে sliding-window maximum:
import heapq
def window_max(nums, k):
h, out = [], []
for i, x in enumerate(nums):
heapq.heappush(h, (-x, i))
if i >= k - 1:
while h[0][1] <= i - k: # top is outside the window?
heapq.heappop(h) # purge stale entries lazily
out.append(-h[0][0])
return out
print(window_max([1, 3, -1, -3, 5, 3], 3)) # [3, 3, 5, 5]
প্রতিটা element একবার push হয় আর বড়জোর একবার pop হয়, তাই পুরো run O(n log n) — যদিও কোনো কোনো window-তে একসাথে কয়েকটা stale top purge হয়। (../04-stack-and-queue/-র monotonic deque এটা O(n)-এ করে — heap version টা হলো মনে রাখতে সহজ, general tool।)
Inherits from: sliding window-এর size-k discipline; আর heap-এর "শুধু top-টাই দেখা যায়" limitation — যেটাকে এখানে feature বানানো হলো।
Representative problems: Sliding Window Maximum (heap variant), Sliding Window Median, আর অনেক Codeforces problem যেখানে "multiset with delete" লাগে — দেখো Codeforces problemset।
Pattern 6 — Heap + greedy combos¶
Trigger words: "repeatedly combine/smash the two smallest/largest", "always pick the most frequent next", "spend resources on the biggest costs"।
Core idea: একটা greedy algorithm একই locally-best choice টা বারবার করে — আর heap হলো সেই machine যেটা "এখনকার best"-টা O(log n)-এ পরিবেশন করে। Greedy ঠিক করে কী নেবে; heap সেটা নেওয়াকে fast বানায়।
Thinking tweak: দুটো আলাদা প্রশ্ন করো। (1) Greedy question: আমি সবসময় পরে কী নেব, আর সেটা কেন safe? (2) Data-structure question: ওই pick-টা সস্তায় কোন structure দেয়? যখন (2)-এর উত্তর হয় "একটা changing set-এর min বা max", তখন heap-ই তোমার engine।
Tiny example: Last-Stone-Weight ধাঁচের smashing — দুটো heaviest পাথর বারবার smash করো; সমান হলে দুটোই vanish, অসমান হলে পার্থক্যটা থেকে যায়।
import heapq
def last_stone(stones):
h = [-s for s in stones] # max-heap via negation
heapq.heapify(h)
while len(h) > 1:
a = -heapq.heappop(h) # heaviest
b = -heapq.heappop(h) # second heaviest
if a != b:
heapq.heappush(h, -(a - b))
return -h[0] if h else 0
print(last_stone([2, 7, 4, 1, 8, 1])) # 1
এই একই skeleton — এক-দুটো extreme pop করো, compute করো, হয়তো কিছু push করে ফেরত দাও — চালায় Huffman coding ("দুটো least frequent merge করো") আর rope-joining cost problems।
Inherits from: plain heap operations; greedy reasoning। এই pattern-টাই graphs-এর bridge: ../09-graphs/shortest-path.md-এর Dijkstra হলো ঠিক heap + greedy, যেখানে "এখনকার best" মানে cheapest known path।
Representative problems: Last Stone Weight, Reorganize String, Furthest Building You Can Reach, Network Delay Time (graph version-টা)।
Pattern picker — এক table-এ¶
| তুমি শুনছ... | হাত বাড়াও | Heap-এ থাকে |
|---|---|---|
| "k largest / most frequent / closest" | Top-K | size k-এর বর্তমান club (largest-এর জন্য min-heap) |
| "merge k sorted ..." | K-way merge | প্রতি list-এ একটা cursor value |
| "running / window median" | Two heaps | lower half (max-heap) + upper half (min-heap) |
| "minimum rooms / machines" | Earliest end | active jobs-দের end times |
| "delete from a heap / window with heap" | Lazy deletion | live + stale entries; top-এ এলে purge |
| "repeatedly take the extreme and recombine" | Heap + greedy | পুরো evolving multiset টা |
কোনো problem এগুলোর কোনোটার সাথে না মিললেও যদি ফিসফিস করে বলে "best item next" — তাহলে সম্ভবত তোমার heap-ই লাগবে। এই ছয়টা হলো common costume, পুরো wardrobe না।