005 — Range Sum Query — Mutable¶
Source¶
- Original source: Classic point-update + range-sum exercise
- Platform: LeetCode — https://leetcode.com/problems/range-sum-query-mutable/
- Topic: segment tree and fenwick tree
- Difficulty: Medium
- Pattern: segment tree / Fenwick core (point update + range sum)
- Basic idea inherited from: arrays, recursion
1. Problem in simple words¶
আবার একটা array আর sumRange(left, right) query — কিন্তু এবার একটা নতুন operation যোগ হয়েছে: update(index, val), যা index-এর element-কে val বানিয়ে দেয়। update আর query যেকোনো order-এ, বহুবার আসতে পারে। এই একটা শব্দ — Mutable — পুরো খেলা বদলে দেয়।
NumArray([1, 3, 5])
sumRange(0, 2) -> 1 + 3 + 5 = 9
update(1, 2) -> array এখন [1, 2, 5]
sumRange(0, 2) -> 1 + 2 + 5 = 8
2. Which basic idea does this inherit from?¶
এটা arrays আর recursion-এর উপর দাঁড়িয়ে। কিন্তু আসলে এটা problem #1/#2-এর immutable prefix-sum baseline-এর সরাসরি উত্তরসূরি — যেখানে baseline ভেঙে পড়ে, ঠিক সেখান থেকেই এই problem শুরু।
3. What is the hidden math or logic?¶
লুকানো logic: prefix sums update-এর সাথে যায় না। একটা update prefix array-র পরের প্রতিটা entry stale করে দেয় (../../01-math-based-programming-fundamentals/05-prefix-difference-contribution/-এ দেখা গেছে) — তাই update O(n)। সমাধান: pre-computation-কে এত ছোট টুকরোয় ভাঙো যে একটা update মাত্র O(log n)-টা টুকরো invalid করে। এটাই segment tree / Fenwick-এর central tweak: element নয়, segment-এর answer জমাও।
4. Which data structure is needed?¶
দুটো পথই খাটে: segment tree (range sum + point update) অথবা Fenwick tree (sum-এর জন্য আরও ছোট code)। যেহেতু এটা plain sum, Fenwick-ই সংক্ষিপ্ততম; কিন্তু segment tree generalize করে (min/max-এও চলে)। নিচে দুটোই দিলাম।
5. Why this data structure fits¶
Update আর query — দুটোকেই O(log n)-এ দরকার। prefix sums query-তে দ্রুত কিন্তু update-এ O(n); plain array update-এ দ্রুত কিন্তু query-তে O(n)। Tree দুই দিকেই log n দেয়, কারণ একটা leaf মাত্র O(log n)-টা segment-এর ভেতরে থাকে আর প্রতিটা query O(log n)-টা segment জুড়ে তৈরি হয়।
6. Real-life analogy¶
একটা mall-এর প্রতিটা তলায় বিক্রির running total রাখা আছে, আর প্রতিটা wing-এ wing-total, পুরো mall-এ grand total। একটা দোকানের বিক্রি বদলালে তুমি শুধু ওই দোকান → তার wing → তার তলা → grand total — এই একটা শৃঙ্খলে খবর দাও, পুরো mall নতুন করে গুনতে হয় না। আবার "এই কয়েকটা wing-এর মোট কত" জিজ্ঞেস করলে কয়েকটা wing-total জোড়া দিলেই হয়।
7. Visual explanation¶
Segment tree-তে update(1, 2) ঠিক একটা root-to-leaf path repair করে (array [1, 3, 5]):
before: after update(1, 2):
[0..2]=9 [0..2]=8 <- repaired
/ \ / \
[0..1]=4 [2..2]=5 [0..1]=3 [2..2]=5
/ \ / \
[0..0]=1 [1..1]=3 [0..0]=1 [1..1]=2 <- changed leaf
path touched: [1..1] -> [0..1] -> [0..2] (3 nodes, O(log n))
সব leaf rebuild করতে হয়নি — শুধু এক শৃঙ্খল।
8. Example input and output¶
NumArray([1, 3, 5])
sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8
Edge case 1 (একই value-তে update): update(0, 1) পরে sumRange(0,0) -> 1
Edge case 2 (শেষ element update): update(2, 10) পরে sumRange(0,2) -> 1+2+10 = 13
9. Brute force thinking¶
প্রথম সরল চিন্তা: plain array রাখো। update মানে সরাসরি a[index] = val (O(1)); sumRange মানে loop চালিয়ে যোগ (O(n))।
10. Why brute force is slow¶
plain array-তে sumRange প্রতিবার O(n)। উল্টোটা — prefix-sum array রাখলে sumRange O(1) কিন্তু update O(n) (পরের সব prefix stale)। দুটো operation মিশে q-বার এলে দুই ক্ষেত্রেই O(n·q)। LeetCode-এ এটা টাইম-আউট করায়। কোনো plain approach দুটোকে একসাথে দ্রুত করতে পারে না।
11. Key observation¶
মূল observation: একটা point update পুরো prefix structure ভাঙার দরকার নেই — শুধু কয়েকটা segment। একটা element ঠিক O(log n)-টা segment-এর ভেতরে থাকে (tree-র প্রতি level-এ একটা)। তাই ওই কটা segment-ই repair করো, বাকি সব অক্ষত থাকে।
12. Optimized intuition¶
Segment tree-তে: leaf set করো, ফেরার পথে প্রতিটা parent re-combine করো — O(log n)। query-তে তিন-ভাগের decision নিয়ে নামো — O(log n)। Fenwick-এ: update মানে delta = val - current[index], তারপর lowbit hop দিয়ে covering block-গুলোয় delta যোগ; sumRange = prefix(r) - prefix(l-1)।
13. Thinking tweak¶
মোচড়: "update এলে সব আবার হিসাব করতে হবে" — এই ভয় থেকে সরে এসে ভাবো "answer-গুলো এমনভাবে টুকরো করে রাখব যেন একটা update মাত্র এক শৃঙ্খল টুকরোকে ছোঁয়।" Immutable baseline (#1, #2) থেকে এই একটাই বড় লাফ — আর এটাই পুরো folder-এর core।
14. Step-by-step dry run¶
[1, 3, 5], Fenwick পথে, update(1, 2) তারপর sumRange(0, 2):
- শুরুতে Fenwick build: element (1-indexed) 1,2,3 ধরে 1,3,5।
update(1, 2)→ 0-indexed 1 = 1-indexed 2। delta =2 - 3 = -1।add(2, -1): index 2 → 4 ছোঁয়, ওদের value -1 হয়।sumRange(0, 2)→prefix(3) - prefix(0)= (1 + 2 + 5) - 0 = 8।- ✓ ঠিক
[1, 2, 5]-এর মোট।
15. Python solution¶
class Fenwick:
"""update-সহ prefix sums, 1-indexed। sum-এর জন্য সংক্ষিপ্ততম structure।"""
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def add(self, i, delta): # element i += delta (1-indexed)
while i <= self.n:
self.tree[i] += delta
i += i & (-i)
def prefix_sum(self, i): # sum of 1..i
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i)
return s
class NumArray:
"""Mutable range sum — Fenwick দিয়ে। 0-indexed API, ভেতরে 1-indexed BIT।
point update + range sum দুটোই O(log n)। (sum বলে BIT যথেষ্ট;
min/max হলে segment tree লাগত।)"""
def __init__(self, nums):
self.nums = list(nums) # set semantics-এর জন্য current value
self.fw = Fenwick(len(nums))
for i, v in enumerate(nums):
self.fw.add(i + 1, v) # build via adds
def update(self, index, val):
delta = val - self.nums[index] # set = (new - old) যোগ করা
self.nums[index] = val
self.fw.add(index + 1, delta)
def sumRange(self, left, right): # 0-indexed inclusive
return self.fw.prefix_sum(right + 1) - self.fw.prefix_sum(left)
def brute(arr, l, r):
return sum(arr[l:r + 1])
# ---- tests ----
na = NumArray([1, 3, 5])
assert na.sumRange(0, 2) == 9
na.update(1, 2)
assert na.sumRange(0, 2) == 8 # [1, 2, 5]
na.update(2, 10)
assert na.sumRange(0, 2) == 13 # [1, 2, 10]
# update আর query মিশিয়ে, একটা mirror array-র সাথে exhaustively মেলানো
import random
random.seed(7)
arr = [random.randint(-5, 5) for _ in range(12)]
na = NumArray(arr)
for _ in range(300):
if random.random() < 0.5: # random update
i = random.randrange(len(arr))
v = random.randint(-5, 5)
arr[i] = v
na.update(i, v)
else: # random range query, brute-checked
l = random.randrange(len(arr))
r = random.randrange(l, len(arr))
assert na.sumRange(l, r) == brute(arr, l, r)
print("সব test pass!")
16. Line-by-line code explanation¶
self.nums = list(nums)— set semantics-এর জন্য current value মনে রাখি, যাতে delta বের করা যায়।update:delta = val - old; BIT শুধু "যোগ" বোঝে, তাই set করতে delta যোগ করি।sumRange:prefix(right+1) - prefix(left)— 0-indexed inclusive থেকে 1-indexed prefix-এ রূপান্তর।brute+ random fuzz — update/query মিশিয়ে একটা সরল mirror array-র সাথে মিলিয়ে দেখা।
17. Output walkthrough¶
NumArray([1,3,5]): sumRange(0,2)=9। update(1,2) delta -1 ঠেলে; sumRange(0,2)=8। update(2,10) পরে 13। তারপর 300 random op-এর fuzz: প্রতিটা query mirror array-র brute sum-এর সাথে মেলে। সব assert pass। শেষে print: সব test pass!।
18. Time complexity¶
update O(log n), sumRange O(log n) (দুটো prefix call)। Build O(n log n) (n বার add)।
19. Space complexity¶
O(n) — Fenwick array + current-value array।
20. Common mistakes¶
update-এ delta-র বদলে সরাসরিvaladd করা — তখন পুরোনো value যোগ থেকে যায়, ভুল।- 0-indexed/1-indexed গুলিয়ে ফেলা —
sumRange-এright+1আরprefix_sum(left)দরকার। - Mutable জেনেও plain prefix-sum array ব্যবহার করা — update-এ O(n), টাইম-আউট।
21. Which basic problem this inherits from¶
ভিত্তি: arrays + recursion, আর সরাসরি #1/#2-এর immutable baseline (../../01-math-based-programming-fundamentals/05-prefix-difference-contribution/)। তফাত একটাই: update যোগ হওয়ায় baseline ভেঙে পড়েছে। গভীর: ../segment-tree.md, ../fenwick-tree.md, ../implementation.py।
22. Similar harder problems¶
- Dynamic Range Sum Queries (একই, contest version) — https://cses.fi/problemset/task/1648 (এই tracker-এর #6)
- Dynamic Range Minimum Queries (এখন Fenwick চলবে না, segment tree লাগবে) — CSES "Dynamic Range Minimum Queries" (#7)
- Count of Smaller Numbers After Self (BIT over values) — https://leetcode.com/problems/count-of-smaller-numbers-after-self/ (#13)
23. Pattern learned¶
Segment tree / Fenwick core: point update + range sum, দুটোই O(log n)। এই combination-ই পুরো folder-এর মেরুদণ্ড — answer-গুলো segment-এ ভেঙে রাখায় update আর query কোনোটাই আর O(n) থাকে না।
24. Final summary¶
ভবিষ্যতের আমাকে: "update আছে?" — হ্যাঁ হলে prefix sums বাদ। sum + point update হলে Fenwick (সংক্ষিপ্ততম), fancy কিছু হলে segment tree। মূল মন্ত্র: একটা update যেন মাত্র O(log n)-টা segment ছোঁয়। এই problem-টাই সেই দরজা যেখানে immutable থেকে mutable-এ লাফ দিলাম।
মনে রাখার নিয়ম (legal): সব নিজের ভাষায় লেখা; problem statement copy করা হয়নি; official link শুধু attribution-এর জন্য; "asked by Google/Amazon" দাবি নেই — এটা একটা common interview pattern।