016 — Salary Queries¶
Source¶
- Original source: CSES Problem Set — "Salary Queries"
- Platform: CSES — https://cses.fi/problemset/
- Topic: segment tree and fenwick tree
- Difficulty: Hard
- Pattern: BIT over compressed values (value-দের উপর frequency table)
- Basic idea inherited from: coordinate compression, BIT
1. Problem in simple words¶
একটা company-তে n-জন কর্মী, প্রত্যেকের একটা করে salary আছে। তারপর q-টা ঘটনা একে একে আসে, দুই ধরনের —
! k x— কর্মীk-র salary বদলেxকরো।? a b— এই মুহূর্তে কতজন কর্মীর salary[a, b]range-এ আছে (দুই প্রান্ত সহ)?
প্রতিটা ? query-র উত্তর দাও।
salaries: [2200, 1500, 3000] (কর্মী 1, 2, 3)
? 2000 3500 -> 2 (2200 আর 3000)
! 1 4500 (কর্মী 1-র salary এখন 4500)
? 2000 3500 -> 1 (শুধু 3000, 2200 এখন 4500 হয়ে range-এর বাইরে)
? 4000 5000 -> 1 (4500)
2. Which basic idea does this inherit from?¶
এটা দাঁড়িয়ে আছে coordinate compression আর BIT (Fenwick tree)-এর উপর।
- BIT থেকে: একটা Fenwick tree-কে value-দের উপর frequency table হিসেবে দেখা —
add(v, +1)মানে "একটা salary v দেখলাম",prefix_count(v)মানে "কতগুলো salary<= v" (../fenwick-tree.mdsection 9-এর core idea)। - Coordinate compression থেকে: salary 10^9 পর্যন্ত হতে পারে — অত বড় array বানানো অসম্ভব। তাই শুধু যে value-গুলো আসলে দেখা যায় (initial salaries + সব update-এর
x) সেগুলোকে সংগ্রহ করে sort করে rank দাও।
দুটো মিললে: range count + update দুটোই O(log n)।
3. What is the hidden math or logic?¶
লুকানো logic দুই ধাপের: প্রথমে "কতগুলো ≤ v" → range count, কারণ count[a, b] = prefix_count(b) - prefix_count(a - 1)। এই subtraction চলে কারণ count invertible (../fenwick-tree.md section 6)।
দ্বিতীয়ত value-কে position-এ বদলানো: BIT-এর index মানে এখানে একটা value-র rank, কর্মীর position নয়। তাই 10^9-এর range-কে compress করে ছোট index-space-এ আনা হয়, যাতে BIT array ছোট থাকে।
4. Which data structure is needed?¶
একটা Fenwick tree (BIT), যেখানে index = compressed salary rank, আর tree[r] ওই rank-এ কতজন আছে তার count জমায়। সঙ্গে দরকার:
- compressed value-দের একটা sorted list (rank ↔ value mapping),
- প্রতিটা কর্মীর বর্তমান salary রাখার একটা পাশের array (update-এ আগের value সরাতে)।
5. Why this data structure fits¶
Update আছে, তাই plain prefix sums বাদ (একটা update পুরো prefix array ভাঙে — ../segment-tree.md section 1)। আর এটা count-of-values query, range-min নয় — তাই Fenwick-ই যথেষ্ট, পুরো segment tree লাগে না (../fenwick-tree.md section 8-এর rule of thumb: point update + count = Fenwick)।
Salary বিশাল হলেও আলাদা distinct value-র সংখ্যা সীমিত (n + q-এর মতো), তাই coordinate compression BIT-টাকে ছোট আর দ্রুত রাখে।
6. Real-life analogy¶
ভাবো একটা বিশাল হলঘরে salary-অনুযায়ী সাজানো খোপ — সবচেয়ে কম থেকে সবচেয়ে বেশি। প্রতিটা কর্মী তার salary-র খোপে একটা করে টোকেন রাখে।
"কতজনের salary [a, b]-তে" জানতে তুমি প্রতিটা খোপ গুনতে যাও না — দুটো ছোট হিসেব করো: "b পর্যন্ত মোট কত টোকেন" বিয়োগ "a-র ঠিক আগে পর্যন্ত মোট কত টোকেন"। কারো salary বদলালে শুধু একটা টোকেন পুরনো খোপ থেকে তুলে নতুন খোপে রাখো — পুরো হলঘর আবার গুনতে হয় না।
7. Visual explanation¶
salaries [2200, 1500, 3000], update-এ আসবে 4500। distinct value sort করে rank:
distinct sorted values: 1500 2200 3000 4500
rank (1-indexed) : 1 2 3 4
শুরুর frequency BIT (rank-এ count):
rank: 1 2 3 4
cnt : 1 1 1 0 (1500,2200,3000 প্রত্যেকে একবার)
prefix_count(rank) = ওই rank পর্যন্ত মোট কর্মী
prefix_count(3) = 1+1+1 = 3 ("salary <= 3000": 3 জন)
prefix_count(1) = 1 ("salary <= 1500": 1 জন)
? 2000 3500: a=2000 -> rank ঠিক a-র নিচে = 1 (শুধু 1500)
b=3500 -> rank <= b = 3
count = prefix_count(3) - prefix_count(1) = 3 - 1 = 2
8. Example input and output¶
salaries = [2200, 1500, 3000]
? 2000 3500 -> 2
! 1 4500 (কর্মী 1: 2200 -> 4500)
? 2000 3500 -> 1
? 4000 5000 -> 1
? 1000 5000 -> 3
9. Brute force thinking¶
প্রথম সরল চিন্তা: salary-গুলো একটা list-এ রাখো। প্রতিটা ? query-তে পুরো list ঘেঁটে a <= s <= b গোনো; প্রতিটা ! update-এ ওই কর্মীর salary সরাসরি বদলে দাও।
10. Why brute force is slow¶
প্রতিটা ? query পুরো n-জন কর্মী scan করে — O(n)। q-টা query থাকলে মোট O(n·q), আর n, q দুটোই 2·10^5 হলে প্রায় 4·10^10 — time limit exceeded। count আর update দুটোকেই O(log n)-এ নামাতে BIT লাগবে।
11. Key observation¶
মূল observation: range count = দুটো prefix count-এর বিয়োগ, আর prefix count মানে "≤ v কতজন" — যেটা Fenwick frequency tree ঠিক যা দেয়। তাই salary-কে rank-এ বদলে BIT-এ count জমালে, প্রতিটা query/update O(log n)। index এখানে value-র rank, কর্মীর নয় — এই shift-ই পুরো trick।
12. Optimized intuition¶
সব value (initial + future updates) আগে থেকে সংগ্রহ করো, sort করে rank দাও (offline compression)। প্রতিটা initial salary-র rank-এ add(+1)। তারপর events চালাও:
? a b→prefix_count(rank_of_value_le(b)) - prefix_count(rank_of_value_le(a-1))।! k x→ পুরনো salary-র rank-এadd(-1), নতুনx-এর rank-এadd(+1), পাশের array আপডেট।
দুটো প্রান্তের rank খুঁজতে compressed list-এ binary search।
13. Thinking tweak¶
মোচড়: "salary-গুলো position অনুযায়ী রাখব" ভাবার বদলে ভাবো "salary-গুলো value অনুযায়ী গুনে রাখব — BIT-এর index মানে একটা value-র rank, কর্মী নয়।" তাহলে range count স্রেফ দুটো prefix-এর বিয়োগ, আর update স্রেফ এক টোকেন এ-খোপ থেকে ও-খোপে সরানো।
14. Step-by-step dry run¶
salaries [2200, 1500, 3000], query ? 2000 3500:
- compressed values
[1500, 2200, 3000, 4500], rank 1..4। BIT count =[1,1,1,0]। b = 3500:<= 3500সবচেয়ে বড় value 3000 → rank 3।prefix_count(3) = 3।a = 2000:a - 1 = 1999,<= 1999সবচেয়ে বড় value 1500 → rank 1।prefix_count(1) = 1।- উত্তর =
3 - 1 = 2। ✓ (2200 আর 3000)
15. Python solution¶
import bisect
class Fenwick:
"""value-দের উপর FREQUENCY table। index = compressed salary rank (1-indexed)।
add(r, delta): rank r-এ count বদলাও; prefix_count(r): rank <= r মোট কতজন।
`../fenwick-tree.md` section 9-এর 'BIT as a frequency table' এখানেই কাজে লাগে।"""
def __init__(self, size):
self.n = size
self.tree = [0] * (size + 1) # 1-indexed
def add(self, i, delta):
while i <= self.n:
self.tree[i] += delta
i += i & (-i) # পরের block-এ লাফ
def prefix_count(self, i): # rank 1..i মোট count
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i) # last set bit ঝাড়ো
return s
def solve(salaries, events):
"""events: ('set', k, x) বা ('query', a, b)। 'query'-গুলোর উত্তরের list ফেরে।
Offline coordinate compression: সব value আগে জড়ো করে rank দিই।"""
# 1) সব আসন্ন value সংগ্রহ (initial + future set values)
vals = set(salaries)
for ev in events:
if ev[0] == 'set':
vals.add(ev[2])
comp = sorted(vals) # rank = bisect position + 1
def rank_le(v):
# value <= v এমন সবচেয়ে বড় rank (1-indexed); কেউ না হলে 0
return bisect.bisect_right(comp, v)
bit = Fenwick(len(comp))
cur = list(salaries) # প্রতিটা কর্মীর বর্তমান salary
for s in cur:
bit.add(rank_le(s), +1) # rank_le(s) == s-এর নিজের rank
out = []
for ev in events:
if ev[0] == 'set':
_, k, x = ev
bit.add(rank_le(cur[k]), -1) # পুরনো salary সরাও
cur[k] = x
bit.add(rank_le(x), +1) # নতুন salary বসাও
else: # 'query'
_, a, b = ev
out.append(bit.prefix_count(rank_le(b)) -
bit.prefix_count(rank_le(a - 1)))
return out
def brute(salaries, events):
# obviously-correct reference: প্রতি query-তে linear scan
cur = list(salaries)
out = []
for ev in events:
if ev[0] == 'set':
cur[ev[1]] = ev[2]
else:
_, a, b = ev
out.append(sum(1 for s in cur if a <= s <= b))
return out
# ---- tests ----
salaries = [2200, 1500, 3000] # কর্মী index 0, 1, 2
events = [
('query', 2000, 3500), # -> 2 (2200, 3000)
('set', 0, 4500), # কর্মী 0: 2200 -> 4500
('query', 2000, 3500), # -> 1 (শুধু 3000)
('query', 4000, 5000), # -> 1 (4500)
('query', 1000, 5000), # -> 3 (4500, 1500, 3000)
]
assert solve(salaries, events) == [2, 1, 1, 3]
assert solve(salaries, events) == brute(salaries, events)
# range-এর বাইরে / প্রান্ত মেলানো
assert solve([5, 5, 5], [('query', 5, 5)]) == [3] # সবাই ঠিক প্রান্তে
assert solve([1, 10], [('query', 2, 9)]) == [0] # কেউ ভেতরে নেই
assert solve([1, 10], [('query', 1, 10)]) == [2] # দুই প্রান্ত inclusive
# random fuzz: compressed BIT vs brute
import random
random.seed(16)
for _ in range(300):
n = random.randint(1, 10)
sal = [random.randint(1, 50) for _ in range(n)]
evs = []
for _ in range(random.randint(1, 15)):
if random.random() < 0.4:
evs.append(('set', random.randrange(n), random.randint(1, 50)))
else:
a = random.randint(1, 50)
b = random.randint(a, 50)
evs.append(('query', a, b))
assert solve(sal, evs) == brute(sal, evs)
print("সব test pass!")
16. Line-by-line code explanation¶
Fenwick.add / prefix_count— standard BIT:addlowbit যোগ করে n-এর দিকে হাঁটে,prefix_countlowbit বিয়োগ করে 0-র দিকে।vals/comp— সব initial আর future value জড়ো করে sort: offline coordinate compression, rank ঠিক হয় এখানে।rank_le(v)—bisect_rightদিয়ে "≤ v কতগুলো compressed value" =v-এর (বা তার নিচের) rank।solve— initial salaries BIT-এ ভরে, প্রতিটাset-এ পুরনো rank-1নতুন rank+1, প্রতিটাquery-তে দুটো prefix-এর বিয়োগ।brute— প্রতি query-তে linear count reference, BIT-এর উত্তর যাচাই করতে।
17. Output walkthrough¶
[2200, 1500, 3000]-এ প্রথম query [2000,3500] দেয় 2 (2200, 3000)। কর্মী 0-এর salary 2200 থেকে 4500 হলে BIT-এ 2200-র টোকেন সরে 4500-এ যায়; তখন [2000,3500] দেয় 1 (শুধু 3000, কারণ 2200 এখন range-এর বাইরে), [4000,5000] দেয় 1 (4500), [1000,5000] দেয় 3 (সবাই)। ফল [2,1,1,3], brute-এর সাথে মেলে। প্রান্ত-মেলানো আর 300 random fuzz case-ও pass। শেষে print: সব test pass!।
18. Time complexity¶
Compression: distinct value sort, O((n+q) log(n+q))। প্রতিটা event-এ কয়েকটা add/prefix_count + একটা binary search — প্রতিটা O(log(n+q))। মোট O((n+q) log(n+q))।
19. Space complexity¶
O(n + q) — BIT array distinct-value-সংখ্যার সমান, compressed value list, আর প্রতি কর্মীর বর্তমান salary।
20. Common mistakes¶
prefix_count(a)ব্যবহার করা যেখানেprefix_count(a - 1)দরকার — তাহলে salary ঠিকa-তে থাকা কর্মীরা বাদ পড়ে (../fenwick-tree.mdsection 6-এর off-by-one)।- Future update-এর value-গুলো compression-এ অন্তর্ভুক্ত না করা — পরে rank খুঁজতে গিয়ে সেই value list-এ নেই।
- Update-এ পুরনো salary
-1করতে ভুলে যাওয়া — তাহলে একই কর্মী দুবার গোনা হয়। - BIT-এর index-কে কর্মীর position ভাবা — এখানে index মানে value-র rank।
21. Which basic problem this inherits from¶
ভিত্তি: BIT-as-frequency-table (../fenwick-tree.md section 9) + coordinate compression (10^9 value-কে rank-এ নামানো, এই tracker-এর #13/#14-এও একই reflex)। base BIT-এর runnable code ../implementation.py-তে।
22. Similar harder problems¶
- Count of Smaller Numbers After Self (BIT + compression) — https://leetcode.com/problems/count-of-smaller-numbers-after-self/ (এই tracker-এর #13)
- Reverse Pairs (compression + BIT/merge counting) — https://leetcode.com/problems/reverse-pairs/ (#14)
- Range Sum Query — Mutable (point update + range sum) — https://leetcode.com/problems/range-sum-query-mutable/ (#5)
23. Pattern learned¶
BIT over compressed values: salary-র মতো বিশাল-range value-কে rank-এ compress করে Fenwick-এ frequency রাখা, তারপর range count = দুটো prefix count-এর বিয়োগ, আর update = এক টোকেন এ-rank থেকে ও-rank-এ সরানো। index মানে value, position নয় — এই চিন্তাটাই counting problem-গুলোর চাবি।
24. Final summary¶
ভবিষ্যতের আমাকে: "কতগুলো value একটা range-এ, value বদলাতে বদলাতে" দেখলেই BIT-over-values মনে করবে। Value বিশাল হলে আগে compress করো; count জমাও rank-এ; range count = prefix_count(b) - prefix_count(a-1); update মানে পুরনো rank -1, নতুন rank +1। সব O(log n) — prefix-sum subtraction trick-এর সবচেয়ে শক্তিশালী রূপ।
মনে রাখার নিয়ম (legal): সব নিজের ভাষায় লেখা; problem statement copy করা হয়নি; official link শুধু attribution-এর জন্য; "asked by Google/Amazon" দাবি নেই — এটা একটা common interview pattern।