073 — Subarray Sum Equals K¶
এটাই এই level-এর তারকা problem — interview-তে অসম্ভব জনপ্রিয়, আর prefix sum-এর সাথে hash map-এর বিয়ে এখানেই। চাবিটা সুন্দর: subarray ধরে না খুঁজে, প্রতিটা প্রান্তে দাঁড়িয়ে জিজ্ঞেস করো "এই sum-এর জন্য দরকারি prefix কি আগে দেখেছি?" — হুবহু Two Sum-এর চাল। ধীরে পড়ো,
seen[0] = 1লাইনটা কেন লাগে সেটা বুঝে নাও — ওটাই কুখ্যাত bug।
Source¶
- Original source: Classic technique (prefix sum + hash map)
- Platform: LeetCode — https://leetcode.com/problems/subarray-sum-equals-k/
- Topic: Math-based programming fundamentals → Level 5: Prefix, Difference, Contribution
- Difficulty: Medium
- Pattern: prefix sum + hash map (complement lookup)
- Basic idea inherited from: 067 — Prefix Sum
1. Problem in simple words¶
একটা সংখ্যার array a (negative-ও থাকতে পারে) আর একটা সংখ্যা K দেওয়া। তোমাকে বলতে হবে — কতগুলো contiguous subarray (পরপর element-এর টুকরো) আছে যাদের sum ঠিক K?
মানে শুধু একটা খুঁজে পেলে হবে না, মোট কতগুলো আছে গুনতে হবে। যেমন a = [1, 1, 1], K = 2 হলে উত্তর 2 — [1,1] (index 0-1) আর [1,1] (index 1-2), দুটো আলাদা টুকরো।
2. What is the math idea?¶
মূল idea হলো prefix sum + complement খোঁজা hash map দিয়ে।
P[r] = প্রথম r+1 টা element-এর মোট ধরো। তাহলে subarray (l..r)-এর sum = P[r] - P[l-1]। আমরা চাই এটা K:
মানে r-এ দাঁড়িয়ে প্রশ্ন: "P[r] - K মানের prefix আগে কতবার এসেছে?" যতবার এসেছে, ততগুলো নতুন valid subarray। আগের সব prefix কতবার করে এসেছে সেটা একটা hash map-এ রাখলেই এক pass-এ গোনা যায়। এটা Two Sum-এর হুবহু চাল — "complement আগে দেখেছি কি?"
3. Which basic idea does this inherit from?¶
মূলত 067 (Prefix Sum) থেকে — running prefix P চালিয়ে যাওয়া। কিন্তু এখানে নতুন বন্ধু হলো hash map (Python dict), যার গভীর ঘর ../../05-hashing/।
067-এ prefix বানিয়ে রাখতাম পরে query-র জন্য। এখানে prefix বানাতে বানাতেই, প্রতিটা মান কতবার এসেছে hash map-এ জমাচ্ছি, আর সাথে সাথে complement খুঁজছি। এই "prefix + complement lookup" combo টা Two Sum (hashing folder-এর প্রথম problem)-এরই বড় ভাই — সেখানে number খুঁজতাম, এখানে prefix sum খুঁজছি।
4. Real-life analogy¶
ভাবো তুমি একটা রাস্তায় হাঁটছ আর প্রতি ল্যাম্পপোস্টে লেখা "শুরু থেকে এখান পর্যন্ত মোট কত মিটার হাঁটলে" (এটাই prefix sum)।
তুমি জানতে চাও — কোন কোন অংশে তুমি ঠিক K মিটার হেঁটেছ। প্রতিটা ল্যাম্পপোস্টে দাঁড়িয়ে তুমি ভাবো: "এখন মোট হাঁটলাম P মিটার। তাহলে যদি আগে কোথাও ঠিক P - K মিটারে থেকে থাকি, সেখান থেকে এ পর্যন্ত অংশটা ঠিক K মিটার!"
তাই তুমি একটা নোটবুকে (hash map) লিখে রাখো প্রতিটা "মোট মিটার" মান কতবার পেরিয়েছ। প্রতি ল্যাম্পপোস্টে শুধু নোটবুক দেখো — "P - K মান কতবার লেখা আছে?" — ততগুলো বৈধ অংশ। গোটা পথ আবার মাপার দরকার নেই।
5. Visual explanation¶
a = [1, 2, 3], K = 3। running prefix P আর hash map seen (শুরুতে {0: 1}):
শুরু: seen = {0: 1}, P = 0, count = 0
(seen[0] = 1 মানে: "খালি prefix একবার দেখেছি" — index 0 থেকে শুরু subarray-র জন্য)
x = 1: P = 0 + 1 = 1
খুঁজি P - K = 1 - 3 = -2 → seen-এ নেই (0 বার)
seen[1] += 1 → seen = {0:1, 1:1}
x = 2: P = 1 + 2 = 3
খুঁজি P - K = 3 - 3 = 0 → seen-এ আছে 1 বার! count = 1 ([1,2])
seen[3] += 1 → seen = {0:1, 1:1, 3:1}
x = 3: P = 3 + 3 = 6
খুঁজি P - K = 6 - 3 = 3 → seen-এ আছে 1 বার! count = 2 ([3])
seen[6] += 1 → seen = {0:1, 1:1, 3:1, 6:1}
উত্তর: count = 2 → [1,2] আর [3]
মূল কথা: প্রতি step-এ আগে খোঁজো (complement P-K কতবার), তারপর নিজেরটা লেখো। ক্রমটা উল্টালে নিজের prefix নিজেকে গোনার ভুল হতে পারে। আর seen[0] = 1 ছাড়া [1,2] (একদম শুরু থেকে) ধরা পড়ত না।
6. Example input and output¶
a K -> count কোন subarray গুলো
-------------------------------------------------------------------
[1, 2, 3] 3 -> 2 [1,2], [3]
[1, 1, 1] 2 -> 2 [1,1] (0-1), [1,1] (1-2)
[3, 4, 7, 2, -3, 1, 4, 2] 7 -> 4 (overlap সহ একাধিক)
[-1, -1, 1] 0 -> 1 [-1,1] (1-2)
[1] 0 -> 0 কোনোটাই নয়
[0, 0, 0] 0 -> 6 সব টুকরো (0 দিয়ে বহু combination)
Edge case খেয়াল করো: negative সংখ্যা থাকতে পারে ([-1,-1,1]) — তাই sliding window চলবে না, hash map লাগবেই। আর [0,0,0], K=0 → 6, কারণ 0 অনেকভাবে জোড়া বাঁধে: hash map এই গোনা স্বাভাবিকভাবেই সামলায়।
7. Brute force thinking¶
সব সম্ভাব্য (l, r) জোড়ার subarray-র sum বের করে K-এর সাথে মেলাও:
def subarray_sum_k_brute(a, K):
n = len(a)
count = 0
for l in range(n):
s = 0
for r in range(l, n): # l থেকে শুরু করে r বাড়াই
s += a[r] # running sum (প্রতিবার গোড়া থেকে না)
if s == K:
count += 1
return count
সরল, ঠিক উত্তর দেয় — প্রতিটা শুরু l-এর জন্য r বাড়িয়ে sum জমায়, K মিললে count বাড়ায়।
8. Why brute force may be slow¶
দুটো nested loop — প্রতিটা শুরু l-এর জন্য সব শেষ r। মোট subarray সংখ্যা প্রায় n²/2, তাই O(n²)।
n = 20000 হলে:
brute force: ~2 × 10^8 operation (O(n²)) — বড় input-এ TLE
hash way : ~20000 operation (এক pass, O(n))
অপচয়টা কোথায়? প্রতিটা l-এর জন্য আবার ডান দিকে হাঁটছি — অথচ prefix sum দিয়ে "কতবার আগে দরকারি মান এসেছে" সরাসরি জানা যায়, পুরো বাঁ দিক আবার না ঘুরে।
9. Better thinking¶
মূল insight: subarray (l..r)-এর sum = P[r] - P[l-1], তাই sum = K মানে P[l-1] = P[r] - K। প্রতিটা r-এ শুধু জানতে হবে মান P[r] - K আগে কতবার এসেছে — সেটা hash map থেকে O(1):
def subarray_sum_k(a, K):
from collections import defaultdict
seen = defaultdict(int)
seen[0] = 1 # খালি prefix — index 0 থেকে শুরু subarray-র জন্য
P = 0
count = 0
for x in a:
P += x
count += seen[P - K] # আগে কতবার P-K দেখেছি = তত নতুন subarray
seen[P] += 1 # নিজের prefix এখন জমা
return count
এক pass, প্রতি step O(1) — মোট O(n)। O(n²) থেকে নেমে এল।
10. Thinking tweak¶
আসল "আহা!" মুহূর্ত: subarray গুনতে subarray ঘুরো না — প্রান্তে দাঁড়িয়ে জিজ্ঞেস করো "দরকারি prefix কি আগে দেখেছি?" sum = P[r] - P[l-1] = K কে উল্টে P[l-1] = P[r] - K লেখাটাই পুরো চাল — এটা Two Sum-এর "complement খোঁজা"-রই রূপ।
দ্বিতীয়, কম-আলোচিত কিন্তু সমান গুরুত্বপূর্ণ tweak: seen[0] = 1 দিয়ে শুরু করো। এটা বলে "index 0-এর আগে একটা খালি prefix (মান 0) আছে" — যাতে একদম শুরু থেকে শুরু হওয়া subarray-ও গোনা যায়। এই এক লাইন ভুললেই কিছু উত্তর হারায়।
11. Step-by-step dry run¶
a = [1, 1, 1], K = 2। শুরুতে seen = {0: 1}, P = 0, count = 0:
| x | P += x | খুঁজি P - K | seen-এ কতবার | count | seen আপডেট |
|---|---|---|---|---|---|
| 1 | 1 | 1 - 2 = -1 | 0 | 0 | {0:1, 1:1} |
| 1 | 2 | 2 - 2 = 0 | 1 | 1 | {0:1, 1:1, 2:1} |
| 1 | 3 | 3 - 2 = 1 | 1 | 2 | {0:1, 1:1, 2:1, 3:1} |
শেষ অবস্থা: count = 2। দুটো subarray: প্রথম দুই 1 (P=2-এ P-K=0 পেলাম, খালি prefix থেকে) আর শেষ দুই 1 (P=3-এ P-K=1 পেলাম, প্রথম 1-এর পর থেকে) ✓।
12. Python solution¶
from collections import defaultdict
def subarray_sum_k(a, K):
"""sum ঠিক K এমন contiguous subarray-র সংখ্যা; prefix + hash map, O(n)।"""
seen = defaultdict(int)
seen[0] = 1 # খালি prefix (index 0 থেকে শুরুর জন্য) — কুখ্যাত bug-এর রক্ষাকবচ
P = 0
count = 0
for x in a:
P += x
count += seen[P - K] # আগে P-K কতবার = তত নতুন subarray
seen[P] += 1
return count
def subarray_sum_k_brute(a, K):
"""ধীর O(n^2) version — মিলিয়ে দেখার জন্য।"""
n = len(a)
count = 0
for l in range(n):
s = 0
for r in range(l, n):
s += a[r]
if s == K:
count += 1
return count
# --- ছোট test গুলো (সব pass করা উচিত) ---
assert subarray_sum_k([1, 2, 3], 3) == 2
assert subarray_sum_k([1, 1, 1], 2) == 2
assert subarray_sum_k([3, 4, 7, 2, -3, 1, 4, 2], 7) == 4 # overlap সহ
assert subarray_sum_k([-1, -1, 1], 0) == 1 # negative
assert subarray_sum_k([1], 0) == 0
assert subarray_sum_k([0, 0, 0], 0) == 6 # 0-এর বহু জোড়া
# fast আর brute একই উত্তর কিনা (random brute-force cross-check):
import random
random.seed(17)
for _ in range(500):
arr = [random.randint(-3, 3) for _ in range(random.randint(0, 10))]
K = random.randint(-5, 5)
assert subarray_sum_k(arr, K) == subarray_sum_k_brute(arr, K)
print(subarray_sum_k([1, 2, 3], 3)) # 2
print(subarray_sum_k([1, 1, 1], 2)) # 2
print("সব test pass!")
13. Line-by-line explanation¶
hash map বানাচ্ছি (key = prefix sum মান, value = কতবার এসেছে)। seen[0] = 1 হলো base entry — "index 0-এর আগে খালি prefix একবার আছে", যাতে শুরু থেকে শুরু হওয়া subarray গোনা যায়।
running prefix P বাড়ালাম। তারপর জিজ্ঞেস করলাম — মান P - K আগে কতবার দেখেছি? ততগুলো নতুন valid subarray এই x-এ শেষ হচ্ছে। (defaultdict হওয়ায় না থাকলে 0 দেয়, KeyError নয়।)
এবার নিজের prefix P map-এ জমালাম — পরের element-দের জন্য। লক্ষ করো ক্রম: আগে খোঁজো, পরে লেখো — নইলে নিজের prefix নিজেকে গুনে ভুল হতে পারে (K=0 হলে)।
বাকি assert গুলো নির্দিষ্ট উদাহরণ + random array-তে fast আর brute মিলিয়ে দেখছে (brute-force cross-check)। সব মিললে সব test pass!।
14. Output walkthrough¶
a = [1, 2, 3], K = 3:
- x=1: P=1, খুঁজি -2 (নেই), count=0, seen={0:1,1:1}
- x=2: P=3, খুঁজি 0 (আছে 1), count=1, seen={0:1,1:1,3:1}
- x=3: P=6, খুঁজি 3 (আছে 1), count=2, seen={...,6:1}
- return 2 → প্রথম
printছাপে2 subarray_sum_k([1,1,1], 2)→ 2 → দ্বিতীয়printছাপে2- সব
assertpass →সব test pass!
ছাপা output:
15. Time complexity¶
O(n) — array-র উপর এক pass, প্রতি element-এ একটা hash lookup আর একটা insert, দুটোই গড়ে O(1)। brute force-এর O(n²) থেকে বিশাল উন্নতি।
16. Space complexity¶
O(n) — খারাপ ক্ষেত্রে সব prefix sum আলাদা, তাই hash map-এ n+1 পর্যন্ত entry। input ছাড়া এটুকুই বাড়তি। (সময় বাঁচাতে জায়গা খরচ — সাধারণ trade-off।)
17. Common mistakes¶
seen[0] = 1ভুলে যাওয়া — index 0 থেকে শুরু হওয়া subarray গোনা হয় না; এটা এই problem-এর #1 bug।- খোঁজা আর লেখার ক্রম উল্টানো — আগে
count += seen[P-K], পরেseen[P] += 1। উল্টালে K=0-এ নিজের prefix নিজেকে গুনে বেশি আসে। - sliding window দিয়ে করার চেষ্টা — negative সংখ্যা থাকলে window expand/shrink-এর monotonic ধর্ম ভেঙে যায়; hash map লাগবেই।
- শুধু একটা subarray আছে কিনা দেখা — প্রশ্ন "কতগুলো", তাই
countজমাও, return-এTrue/Falseনয়। seen[P] = 1(= বসানো, += নয়) — একই prefix sum বারবার এলে count ভুল হয়; সবসময়+= 1দিয়ে frequency জমাও।
18. Harder problems that inherit this idea¶
- LeetCode — Subarray Sum Equals K (https://leetcode.com/problems/subarray-sum-equals-k/) — ঠিক এই problem।
- LeetCode — Continuous Subarray Sum (https://leetcode.com/problems/continuous-subarray-sum/) — prefix + hash map, কিন্তু mod-এর key দিয়ে (074-এর দিকে সেতু)।
- 074 (Count Subarrays Divisible by K) — এই repo-তে পরের ধাপ: একই hash চাল,
P-K-র জায়গায়P % K। - গভীর hashing → ../../05-hashing/problems/013-subarray-sum-equals-k.md (একই problem hashing-এর চোখে)।
19. Pattern learned¶
Prefix sum + hash map (complement lookup) — sum = K কে P[l-1] = P[r] - K বানিয়ে hash map-এ frequency খোঁজা; seen[0] = 1 দিয়ে শুরু। বড় শিক্ষা: subarray ঘুরে না খুঁজে প্রান্তে দাঁড়িয়ে "দরকারি prefix আগে দেখেছি?" জিজ্ঞেস করো — এটা Two Sum-এর complement চালের subarray রূপ।
20. Final summary¶
ভবিষ্যতের আমাকে এক লাইনে: "sum = K গোনা = prefix + hash map; প্রতি step count += seen[P-K]; seen[P] += 1, শুরুতে seen[0] = 1। negative থাকলে sliding window নয়, hash map। আগে খোঁজো, পরে লেখো।"
পরের ধাপ → 074 — Count Subarrays Divisible by K (একই চাল, P-K-র জায়গায় P % K)।
ফিরে দেখা: শিকড় → 067 — Prefix Sum · গভীর hashing → ../../05-hashing/ · এই level-এর map → ../README.md · concept ঝালাই → ../concept-notes.md · template → ../../../templates/math-problem-note-template.md।
21. JavaScript solution (with test cases)¶
JS-এ frequency রাখতে Map ব্যবহার করি, আর সেই কুখ্যাত seen[0] = 1 হয় seen.set(0, 1)।
// throwing assert — fail করলে থেমে error দেবে
const assert = (cond, msg = "") => { if (!cond) throw new Error("FAIL " + msg); };
function subarraySumK(a, K) {
const seen = new Map();
seen.set(0, 1); // খালি prefix — index 0 থেকে শুরুর জন্য (রক্ষাকবচ)
let P = 0, count = 0;
for (const x of a) {
P += x;
count += seen.get(P - K) || 0; // আগে P-K কতবার = তত নতুন subarray
seen.set(P, (seen.get(P) || 0) + 1); // নিজের prefix জমা
}
return count;
}
// brute O(n^2) — cross-check
function subarraySumKBrute(a, K) {
let count = 0;
for (let l = 0; l < a.length; l++) {
let s = 0;
for (let r = l; r < a.length; r++) { s += a[r]; if (s === K) count++; }
}
return count;
}
// ---- test cases (file-এর example + edge case) ----
assert(subarraySumK([1, 2, 3], 3) === 2, "[1,2],[3]");
assert(subarraySumK([1, 1, 1], 2) === 2, "two windows");
assert(subarraySumK([3, 4, 7, 2, -3, 1, 4, 2], 7) === 4, "overlap");
assert(subarraySumK([-1, -1, 1], 0) === 1, "negative");
assert(subarraySumK([1], 0) === 0, "none");
assert(subarraySumK([0, 0, 0], 0) === 6, "0-এর বহু জোড়া");
// fast == brute, random ছোট array জুড়ে
for (let t = 0; t < 500; t++) {
const n = Math.floor(Math.random() * 9);
const arr = [];
for (let i = 0; i < n; i++) arr.push(Math.floor(Math.random() * 7) - 3); // -3..3
const K = Math.floor(Math.random() * 11) - 5; // -5..5
assert(subarraySumK(arr, K) === subarraySumKBrute(arr, K), "cross-check");
}
console.log("সব JS test pass!");
JS টীকা: plain object
{}-এর key string হয়ে যায় (negative/large prefix-এ ভুল), তাই numeric key রাখতেMapব্যবহার করো।seen.get(k)না থাকলেundefinedদেয় —|| 0দিয়ে সামলাও (Pythondefaultdict(int)-এর বদলি)। আর ক্রম: আগেget(P-K), পরেset(P)— উল্টালে K=0-এ নিজের prefix নিজেকে গুনবে।
22. TypeScript solution (with test cases)¶
function subarraySumK(a: number[], K: number): number {
const seen = new Map<number, number>();
seen.set(0, 1);
let P = 0, count = 0;
for (const x of a) {
P += x;
count += seen.get(P - K) ?? 0;
seen.set(P, (seen.get(P) ?? 0) + 1);
}
return count;
}
function expect(actual: number, expected: number, msg = ""): void {
if (actual !== expected) throw new Error(`FAIL ${msg}: ${actual}`);
}
const cases: ReadonlyArray<readonly [number[], number, number]> = [
[[1, 2, 3], 3, 2], [[1, 1, 1], 2, 2], [[-1, -1, 1], 0, 1],
[[1], 0, 0], [[0, 0, 0], 0, 6],
];
for (const [arr, K, want] of cases) expect(subarraySumK(arr, K), want, `[${arr}],K=${K}`);
console.log("সব TS test pass!");
TS টীকা:
Map<number, number>লিখলে key/value দুটোই number-এ বাঁধা — ভুলে string key বসানো compile-time-এ ধরা পড়ে; এটাই plain-object string-key bug-টা ভাষাগতভাবে আটকায়।?? 0(nullish) দিয়ে missing key নিরাপদে 0।
23. কোথায় কাজে লাগে (real-world use)¶
- Range/target detection in streams: কোনো metric-এর running sum ঠিক target ছুঁলো কিনা — billing window, quota, anomaly detection।
- Balanced/zero-sum segment: net change 0 এমন অংশ খোঁজা (লাভ-ক্ষতি সমান, charge সমান) —
K = 0কেস। - Continuous subarray with property (mod K): একই hash চাল
P % Kদিয়ে — divisibility, hashing-এ। - Two Sum-গোত্রের complement খোঁজা: "দরকারি জিনিস আগে দেখেছি?" — caching/dedup/lookup-এর সাধারণ চাল। মূল pattern — "subarray ঘুরো না; prefix + hash map-এ 'দরকারি prefix আগে দেখেছি?' (O(n²) → O(n))" — complement lookup, sliding-window-বিকল্প জুড়ে বড় ছবিতে ফেরে।
মনে রাখার নিয়ম (legal): সব নিজের ভাষায় লেখো; problem statement copy কোরো না; official link শুধু attribution-এর জন্য রাখো; "asked by Google/Amazon" এমন দাবি কোরো না — বরং "Google-style pattern" / "common interview pattern" লেখো।