Concept Notes — Prefix, Difference, Contribution (ভেতর থেকে বোঝা)¶
সহজ করে শুরু: একই হিসাব বারবার করা এড়ানো¶
prefix[i]-তে প্রথম iটি উপাদানের যোগ রাখো; prefix[0]=0। তখন [l,r) অংশের যোগ prefix[r]-prefix[l]। Difference array বিপরীত দিকে কাজ করে: অনেক range update জমা রেখে শেষে একবার prefix নিয়ে ফল বানায়।
প্রোগ্রামে কোথায় ব্যবহার করবে: দিনভিত্তিক বিক্রির নির্দিষ্ট সময়ের যোগ, image-এর আয়তক্ষেত্রের মোট মান, অনেক range update এবং সব subarray-তে একটি উপাদানের মোট অবদান বের করতে লাগে।
কৌশল বাছার আগে শর্ত: এখানে range লেখার সময় [l,r) মানে l আছে, r নেই। সাধারণ prefix sum মূল array বদলে গেলে নিজে আপডেট হয় না; বারবার update ও query মিশলে Fenwick/segment tree দেখবে।
পড়ার ক্রম: এই পরিচয় → নিচের বিস্তারিত ধারণা → চিত্রে হাতে-কলমে হিসাব → সমস্যার তালিকা। প্রতিটি উদাহরণে আগে input, পরে বদলানো state, শেষে output মেলাও।
এই level-এর তিনটা কৌশলের পেছনে একটাই প্রশ্ন: "একই হিসাব বারবার করছি না তো?" যেখানেই উত্তর হ্যাঁ, সেখানেই precompute করার সুযোগ। চলো একটা একটা করে ছবি বানাই।
1. Prefix sum = দোকানদারের running total¶
ধরো array a = [2, 5, 1, 3, 4]। কেউ জিজ্ঞেস করল "index 1 থেকে 3 পর্যন্ত sum কত?" Loop চালালে O(n)। এমন qটি প্রশ্নে একইভাবে হিসাব করলে মোট O(n × q) সময় লাগবে।
সমাধান: আগে থেকেই শুরু থেকে প্রতিটা জায়গা পর্যন্ত মোট লিখে রাখো। এটাই prefix sum:
a = [2, 5, 1, 3, 4]
P = [0] * (len(a) + 1) # P[0] = 0 — sentinel, এটাই জাদুর চাবি
for i in range(len(a)):
P[i + 1] = P[i] + a[i]
# P = [0, 2, 7, 8, 11, 15]
Mini dry run:
| i | আগের মোট P[i] | নতুন a[i] | নতুন মোট P[i+1] |
|---|---|---|---|
| 0 | 0 | 2 | 2 |
| 1 | 2 | 5 | 7 |
| 2 | 7 | 1 | 8 |
| 3 | 8 | 3 | 11 |
| 4 | 11 | 4 | 15 |
P[i] মানে: "প্রথম i টা element-এর মোট"। এবার রেঞ্জের sum বের করা হলো ফিতা কাটার মতো — পুরো ফিতা থেকে বাঁ দিকের অপ্রয়োজনীয় টুকরো কেটে ফেলো:
Array: index 1 থেকে 3 পর্যন্ত চাই
02চিহ্নিত ঘর: 15↑ Lচিহ্নিত ঘর: 21চিহ্নিত ঘর: 33↑ R44নির্বাচিত মান 5 + 1 + 3 = 9।
Prefix: সীমা দুটির মোট
00চিহ্নিত ঘর: 12↑ L2738চিহ্নিত ঘর: 411↑ R+1515P[4] − P[1] = 11 − 2 = 9। ডানের সীমার জন্য R+1 নিই।
সাধারণ নিয়ম (0-indexed, P-র দৈর্ঘ্য n+1): sum(l..r) = P[r + 1] - P[l]। বানাতে O(n), প্রতি query O(1)। দোকানদার দিনশেষে খাতা একবারই যোগ করে — তারপর যে-ই জিজ্ঞেস করুক, উত্তর তৈরি।
2. Difference array = prefix-এর আয়নার উল্টো পিঠ¶
Prefix-এ ছিল: array দেওয়া, range-এর প্রশ্ন অনেক। এবার উল্টো: range-এ update অনেক ("l থেকে r পর্যন্ত সবার সাথে x যোগ করো"), শেষে পুরো array চাই।
প্রতিবার loop চালালে আবার O(n × q)। চালাকি: পুরো রেঞ্জে লেখার বদলে শুধু দুই প্রান্তে দাগ দাও — "এখান থেকে +x শুরু" আর "এখানে এসে +x শেষ":
n = 5
diff = [0] * (n + 1) # এক ঘর বেশি — r+1 এর জায়গা
def range_add(l, r, x):
diff[l] += x # এখান থেকে x-এর প্রভাব শুরু
diff[r + 1] -= x # এখানে এসে প্রভাব শেষ
range_add(1, 3, 10)
range_add(2, 4, 5)
a = [0] * n
running = 0
for i in range(n):
running += diff[i] # দাগগুলোর উপর দিয়ে হাঁটা = prefix sum!
a[i] = running
print(a) # [0, 10, 15, 15, 5]
Mini dry run:
১. Index 1 থেকে 3-এ +10
00চিহ্নিত ঘর: 1102030চিহ্নিত ঘর: 4-1050Index 1-এ চালু, index 4-এ বন্ধ। Index 5 হলো অতিরিক্ত sentinel।
২. Index 2 থেকে 4-এ +5
00110চিহ্নিত ঘর: 25304-10চিহ্নিত ঘর: 5-5আগের দাগ রেখে নতুন শুরু ও শেষের দাগ যোগ করি।
৩. বাঁ থেকে চলমান যোগ = চূড়ান্ত array
00110চিহ্নিত ঘর: 215চিহ্নিত ঘর: 31545দুটি update একসঙ্গে কাজ করেছে index 2 ও 3-এ: 10 + 5 = 15।
ছবিটা ভাবো: রাস্তায় দুটো সাইনবোর্ড — "টোল শুরু" আর "টোল শেষ"। মাঝের প্রতিটা গাড়ি টোল দেয়, কিন্তু সাইনবোর্ড মাত্র দুটো। q টা update = O(q), শেষে এক pass prefix = O(n)। Difference নেয় update, prefix বানায় উত্তর — দুটো অপারেশন একে অপরের উল্টো, ঠিক যেমন derivative আর integral।
3. Prefix XOR — template এক, operator বদল¶
Level 4 মনে আছে? XOR self-inverse: a ^ a = 0। তাহলে যোগের জায়গায় XOR বসালেও prefix কৌশল চলে — কারণ "বাঁ দিকের টুকরো বাদ দেওয়া"-র কাজটা বিয়োগের বদলে আবার XOR-ই করে:
a = [3, 5, 2, 7]
PX = [0] * (len(a) + 1)
for i in range(len(a)):
PX[i + 1] = PX[i] ^ a[i]
# PX = [0, 3, 6, 4, 3]
# xor(l..r) = PX[r+1] ^ PX[l]
print(PX[3] ^ PX[1]) # xor(1..2) = 5 ^ 2 = 7 ✓
কেন কাজ করে: PX[3] = 3 ^ 5 ^ 2, আর PX[1] = 3। দুটো XOR করলে 3 দুবার এসে কেটে যায় — পড়ে থাকে 5 ^ 2। বিয়োগ যা subtraction-এ করে, XOR তা cancel-এ করে। এই দেখার চোখটা মূল্যবান: prefix কৌশল চলে যেকোনো operation-এ যার "undo" আছে (যোগ ↔ বিয়োগ, XOR ↔ XOR)। Min/max-এর undo নেই — তাই prefix min দিয়ে যেকোনো range-এর min হয় না।
4. 2D prefix sum — rectangle আর inclusion-exclusion¶
Matrix-এ "এই rectangle-এর sum কত?" — একই গল্প, এবার দুই দিকে। P[i][j] = উপরের-বাঁ কোণ থেকে (i-1, j-1) পর্যন্ত rectangle-এর মোট। Query-র ছবি:
সারি 1–2, কলাম 1–2; সূচক শূন্য থেকে
ফাঁকা বা বাদ দেওয়া ঘর: 1ফাঁকা বা বাদ দেওয়া ঘর: 2ফাঁকা বা বাদ দেওয়া ঘর: 3ফাঁকা বা বাদ দেওয়া ঘর: 4চিহ্নিত ঘর: 5চিহ্নিত ঘর: 6ফাঁকা বা বাদ দেওয়া ঘর: 7চিহ্নিত ঘর: 8চিহ্নিত ঘর: 9চাই 5 + 6 + 8 + 9 = 28। ওপরের row ও বাঁয়ের column বাদ দেব।
| কাজ | যে অংশের যোগ | মান |
|---|---|---|
| পুরোটা নিই | 1 থেকে 9-এর সব ঘর | +45 |
| ওপরের row বাদ | 1, 2, 3 | −6 |
| বাঁয়ের column বাদ | 1, 4, 7 | −12 |
| দুবার বাদ পড়া কোণা ফেরত | 1 | +1 |
| উত্তর | 45 − 6 − 12 + 1 | 28 |
# P-র size (rows+1) x (cols+1), সব sentinel 0
# build: P[i+1][j+1] = mat[i][j] + P[i][j+1] + P[i+1][j] - P[i][j]
# query sum of rect (r1,c1)..(r2,c2):
ans = P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1]
4-টা term মুখস্থ নয় — উপরের ছবিটা আঁকো: বড় থেকে দুই পট্টি বাদ, কোণা ফেরত। এটাই inclusion-exclusion, Level 3-এর সেই Venn-চিন্তারই geometry version।
5. Subarray Sum = K — "P[r] - K কি আগে দেখেছি?"¶
এবার তারকা problem। "কয়টা subarray-র sum ঠিক K?" — সব (l, r) try করলে O(n²)। চাবি হলো এই সমীকরণটা উল্টে পড়া:
| যা চাই | একই কথা prefix দিয়ে |
|---|---|
Half-open range [l,r)-এর যোগ K |
P[r] - P[l] = K |
| বর্তমান prefix P[r] জানা আছে | আগের prefix চাই P[l] = P[r] - K |
প্রতিটা r-এ পেছনের সব l খোঁজার দরকার নেই — শুধু জানতে হবে মান P[r] - K কতবার আগে এসেছে। গোনার জন্য hash map। চিনতে পারছ? এটা Two Sum-এর হুবহু চাল — "complement আগে দেখেছি কি?" (hash-এর গভীরে: ../../05-hashing/)।
def subarray_sum_k(a, K):
from collections import defaultdict
seen = defaultdict(int)
seen[0] = 1 # খালি prefix — index 0 থেকে শুরু subarray-র জন্য
P, count = 0, 0
for x in a:
P += x
count += seen[P - K] # আগে কতবার P-K দেখেছি = তত নতুন subarray
seen[P] += 1 # নিজেরটা এখন লিখে রাখলাম
return count
print(subarray_sum_k([1, 2, 3], 3)) # 2 → [1,2] আর [3]
Mini dry run (a = [1, 2, 3], K = 3):
শুরুতে seen = {0: 1}। টেবিলে শুধু positive frequency-র entries দেখানো হয়েছে; lookup-এর সময় defaultdict শূন্যের entry-ও তৈরি করতে পারে।
| x | P | খুঁজি P−3 | আগে পাওয়া বার | মোট count | ধাপ শেষে positive frequency |
|---|---|---|---|---|---|
| 1 | 1 | −2 | 0 | 0 | {0:1, 1:1} |
| 2 | 3 | 0 | 1 | 1 | {0:1, 1:1, 3:1} |
| 3 | 6 | 3 | 1 | 2 | {0:1, 1:1, 3:1, 6:1} |
উত্তর 2: [1,2] ও [3]।
seen[0] = 1 লাইনটা ভুললে [1, 2] (যেটা একদম শুরু থেকে) ধরা পড়ত না — এটাই এই problem-এর কুখ্যাত bug।
6. Divisible by K — remainder-এর জোড়া¶
"কয়টা subarray-র sum K দিয়ে ভাগ যায়?" — সমীকরণ এবার mod-এ:
Half-open [l,r) range-এ শর্ত (P[r] - P[l]) % K == 0। অর্থাৎ P[r] % K == P[l] % K।
মানে: একই remainder-ওয়ালা দুটো prefix = একটা valid subarray। তাহলে remainder ধরে ধরে গোনো — প্রতি r-এ "আমার remainder আগে কতবার এসেছে" যোগ করো। Code টা section 5-এর প্রায় copy — শুধু P - K খোঁজার বদলে P % K খোঁজা, আর seen[0] = 1 একই কারণে থাকে। (Level 2-এর mod-চিন্তা এখানে ফসল দিল!)
7. Contribution — প্রশ্নটা উল্টে দাও¶
"সব subarray-র sum-এর যোগফল কত?" Subarray-এর সংখ্যা O(n²), তাই একেকটির sum আলাদা করে গুনলে কাজ অনেক। দৃষ্টিভঙ্গি উল্টাও: প্রতিটা element-কে জিজ্ঞেস করো — "তুমি মোট কয়টা subarray-তে আছ?" যে যতবার আছে, মোট sum-এ সে ততবার নিজের মান জমা দেয়।
a[i] কয়টা subarray-তে? Subarray-টার শুরু হতে হবে i-এর বাঁয়ে বা i-তে — option i + 1টা (index 0..i)। শেষ হতে হবে i-তে বা ডানে — option n - iটা (index i..n-1)। স্বাধীন পছন্দ, তাই গুণ:
n = 3, i = 1
0xচিহ্নিত ঘর: 1y↑ i2zশুরু 0 বা 1; শেষ 1 বা 2। স্বাধীন দুই পছন্দে 2 × 2 = 4টি subarray।
চারটি subarray: [x,y], [x,y,z], [y], [y,z]। প্রতিটিতেই y ঠিক একবার আছে।
def sum_of_all_subarrays(a):
n = len(a)
return sum(a[i] * (i + 1) * (n - i) for i in range(n))
print(sum_of_all_subarrays([1, 2, 3])) # 20
Dry run: 1×(1×3) + 2×(2×2) + 3×(3×1) = 3 + 8 + 9 = 20। হাতে মিলাও: subarray গুলো [1],[2],[3],[1,2],[2,3],[1,2,3] → sum গুলো 1+2+3+3+5+6 = 20 ✓। O(n²) নেমে এল O(n)-এ — শুধু প্রশ্নটা ঘুরিয়ে।
এটাই contribution technique: মোটের হিসাবকে ভেঙে দাও "প্রতিটা টুকরো মোটে কত যোগ করে"-তে। Pair নিয়ে problem-এও একই চাল — প্রতি pair-এর বদলে প্রতি element ভাবো "আমি কয়টা pair-এ কী ভূমিকায়?"
8. Sum of Subarray Minimums — contribution-এর কঠিন রূপ¶
"সব subarray-র minimum-এর যোগফল?" — এবার a[i]-র contribution = (যে subarray গুলোতে সে-ই minimum, সেগুলোর সংখ্যা) × a[i]। কয়টায় সে minimum? যতদূর বাঁয়ে গেলে তার চেয়ে ছোট কেউ নেই × যতদূর ডানে। অর্থাৎ দরকার প্রতিটা i-এর জন্য nearest smaller element বাঁয়ে আর ডানে — এই "span" খোঁজার দ্রুত যন্ত্রের নাম monotonic stack, যার আসল বাড়ি ../../04-stack-and-queue/।
i = 1; দুই পাশেই সব মান বড়
03চিহ্নিত ঘর: 11↑ i2234বাঁয়ে শুরুর পছন্দ 0 বা 1; ডানে শেষের পছন্দ 1, 2 বা 3। মোট 2 × 3 = 6।
Subarray ছয়টি: [1], [3,1], [1,2], [3,1,2], [1,2,4], [3,1,2,4]। তাই 1-এর contribution 1×6=6। সমান minimum একাধিক হলে tie-breaking দিয়ে একই subarray দুবার গোনা ঠেকাতে হয়।
এখানে শুধু কাঠামোটা নাও: contribution × span। বিস্তারিত problem 077-এর note-এ।
9. Imos method আর sweep line — event-এর জগতে difference¶
Imos method আসলে difference array-রই আরেক নাম, শুধু দৃষ্টিটা event-ভিত্তিক: "একজন অতিথি সময় l-এ ঢোকে, r-এ বেরোয়" মানে timeline-এ +1 দাগ l-এ, -1 দাগ r-এ (বা r+1-এ, প্রান্ত-নিয়ম অনুযায়ী)। সব অতিথির দাগ বসিয়ে এক pass prefix — প্রতি মুহূর্তে কতজন ভেতরে, বেরিয়ে গেল।
Sweep line সেটারই sorted-event রূপ — timeline টা array না হয়ে যখন বিশাল বা continuous, তখন শুধু event গুলো sort করে বাঁ থেকে ডানে "ঝাড়ু" চালাও:
def max_overlap(intervals):
events = []
for l, r in intervals:
events.append((l, +1)) # ঢুকল
events.append((r, -1)) # বেরোল
events.sort()
best = cur = 0
for _, delta in events:
cur += delta
best = max(best, cur)
return best
print(max_overlap([(1, 4), (2, 5), (3, 6)])) # 3
Mini dry run: sorted events = (1,+1) (2,+1) (3,+1) (4,-1) (5,-1) (6,-1) → cur যায় 1, 2, 3, 2, 1, 0 → best 3 ✓। একই বিন্দুতে ঢোকা-বেরোনো থাকলে কোনটা আগে — এই tie-এর নিয়মটা problem অনুযায়ী ঠিক করতে হয় (079/080-এর note-এ বিস্তারিত)।
এক নজরে cheat sheet¶
| কাজ | মনে রাখার নিয়ম |
|---|---|
| Prefix বানানো | P[i+1] = P[i] + a[i]; শুরুতে P[0] = 0 |
Inclusive [l,r]-এর যোগ |
P[r+1] - P[l] |
Inclusive [l,r]-এ update |
diff[l] += x; diff[r+1] -= x; শেষে prefix pass |
Inclusive [l,r]-এর XOR |
PX[r+1] ^ PX[l] |
| 2D query | পুরোটা − ওপরের strip − বাঁয়ের strip + কোণা |
| Sum = K গোনা | শুরুতে seen[0]=1; প্রতি ধাপে seen[P-K] যোগ |
| K দিয়ে বিভাজ্য sum | একই remainder-র আগের prefix গোনা |
| Contribution | a[i] আছে (i+1) * (n-i)টি subarray-তে |
| Sweep line | শুরুর +1 ও শেষের −1 event সাজিয়ে চলমান count |
প্রতিটা লাইনের পেছনে একটা ছবি আছে — ফিতা, সাইনবোর্ড, rectangle, জোড়া remainder, বাঁ-ডান পছন্দ। Formula ভুলে গেলে ছবিতে ফেরো — formula নিজেই ফিরে আসবে।