সরাসরি পর্বে যান
পর্ব ৩৬ · ৭২-এর মধ্যে · মডিউল ৫

Fenwick tree (BIT)

Fenwick Tree (Binary Indexed Tree) — Lowbit, Prefix Query, Point Update, Range Updates, 2D BIT, k-th Element & Inversion Counting

Taught in Bangla, this lesson explains the Fenwick tree: each cell covering a block of length lowbit, prefix queries by removing the lowest bit, point updates by adding it, O(n) construction and range sums, range updates with a difference array and the two-BIT trick, finding the k-th element by binary lifting, counting inversions with coordinate compression, and BIT versus segment tree, with C++ and Python code.

এই পর্বে যা শিখবেন

  • lowbit আর 2's complement-এর সম্পর্ক বোঝা, আর কোন ঘর অ্যারের কোন অংশ রাখে তা binary দেখে বলতে পারা
  • prefix query আর point update binary রূপসহ হাতে trace করা, আর দুটির সঠিকতা ও O(log n) সময়ের যুক্তি লেখা
  • O(n) build, রেঞ্জের যোগফল, range update + point query আর দুই BIT-এর range update + range query সূত্রটি নিজে প্রমাণ করা
  • BIT-এর ওপর binary lifting দিয়ে k-তম উপাদান খোঁজা, আর coordinate compression-সহ inversion গোনা, C++ ও Python-এ
  • কখন Fenwick tree, কখন segment tree — যুক্তিসহ বেছে নেওয়া

১ · সমস্যা — বদলাতে থাকা খাতায় “এ পর্যন্ত মোট কত?”

ধরুন একজন bKash এজেন্ট দিনের প্রতিটি ঘণ্টার লেনদেন একটি খাতায় লেখেন: ঘর ১-এ প্রথম ঘণ্টার মোট টাকা, ঘর ২-এ দ্বিতীয় ঘণ্টার, এভাবে। সারাদিন দুই ধরনের ঘটনা আসে। কখনো খবর আসে “৩ নম্বর ঘণ্টার একটি লেনদেন ভুল লেখা হয়েছিল, ৫০০ টাকা বাড়াও” — একটি ঘরের মান বদলাল। আবার কখনো মালিক জানতে চান “শুরু থেকে ৭ নম্বর ঘণ্টা পর্যন্ত মোট কত টাকা?” — একটি prefix sum. প্রশ্ন আর বদল মিশে আসছে, আর দুটিই লাখে লাখে।

পর্ব ১৭-এ আমরা দুটি সরল উপায় দেখেছি, আর দুটিরই একটি দুর্বল দিক আছে।

উপায়একটি ঘর বদলানো (point update)prefix(i) জানাq টি মিশ্র কাজ, n টি ঘর
শুধু অ্যারে aO(1)O(n) — i টি ঘর যোগO(qn)
prefix sum অ্যারে PO(n) — পরের সব P বদলায়O(1)O(qn)
Fenwick treeO(log n)O(log n)O(n + q log n)

n = q = 106 হলে প্রথম দুই উপায়ে প্রায় 1012 ধাপ — অচল। Fenwick tree-তে প্রায় 2 × 107 ধাপ — এক সেকেন্ডের অনেক কম। পর্ব ৩৪-এর segment tree-ও একই কাজ O(log n)-এ করে। তবে Fenwick tree, যার আরেক নাম Binary Indexed Tree (BIT), অনেক ছোট: কোড প্রায় দশ লাইন, আর জায়গা ঠিক n + 1 টি সংখ্যা। এর পুরো রহস্য লুকিয়ে আছে সংখ্যার binary রূপে।

উপমা — টাকার নোট দিয়ে যেকোনো অঙ্ক। ধরুন আপনার কাছে শুধু ১, ২, ৪, ৮, ১৬ … টাকার নোট আছে, প্রতিটি একটি করে। যেকোনো অঙ্ক — যেমন ১৩ টাকা — ঠিক একভাবে দেওয়া যায়: ৮ + ৪ + ১. এটিই ১৩-এর binary রূপ, 1101. Fenwick tree ঠিক এভাবে কাজ করে: “প্রথম ১৩টি ঘরের যোগফল” সে ১৩টি আলাদা ঘর থেকে নয়, মাত্র তিনটি আগে-জমানো টুকরো থেকে জোড়া দেয় — একটিতে ৮টি ঘরের যোগফল, একটিতে ৪টির, একটিতে ১টির। আর কোনো অঙ্কের binary রূপে 1-এর সংখ্যা কখনো log₂ n + 1-এর বেশি নয়।

২ · মূল ধারণা — প্রতিটি ঘরে একটি ব্লক, দৈর্ঘ্য lowbit

অ্যারে a-কে 1-based ধরুন: a[1], a[2], …, a[n]. Fenwick tree হলো আরেকটি অ্যারে t[1..n]. প্রতিটি i-এর জন্য সংজ্ঞা একটি:

সংজ্ঞা

t[i] = a[i − lowbit(i) + 1] + … + a[i], যেখানে lowbit(i) হলো i-এর binary রূপের সবচেয়ে ডানের (সবচেয়ে নিচের) 1-bit-এর মান। অর্থাৎ t[i] রাখে i-তে শেষ হওয়া, lowbit(i) দৈর্ঘ্যের একটি ব্লকের যোগফল — অর্ধ-খোলা ব্যবধানে লিখলে (i − lowbit(i), i].

যেমন 6 = 110, সবচেয়ে নিচের 1-bit-এর মান 2, তাই t[6] = a[5] + a[6]. আবার 8 = 1000, lowbit 8, তাই t[8] = a[1] + … + a[8]. বিজোড় সংখ্যার lowbit সবসময় 1, তাই t[1], t[3], t[5], t[7] শুধু নিজের ঘর রাখে। নিচের ছবিতে n = 8-এর সব ব্লক:

pos     1  2  3  4  5  6  7  8     i (binary)  lowbit  block
t[1]    ███                        0001        1       a[1]
t[2]    ██████                     0010        2       a[1..2]
t[3]          ███                  0011        1       a[3]
t[4]    ████████████               0100        4       a[1..4]
t[5]                ███            0101        1       a[5]
t[6]                ██████         0110        2       a[5..6]
t[7]                      ███      0111        1       a[7]
t[8]    ████████████████████████   1000        8       a[1..8]
প্রতিটি ব্লক i-তে শেষ হয় আর দৈর্ঘ্যে lowbit(i). যেকোনো দুটি ব্লক হয় পুরোপুরি আলাদা, নয়তো একটি অন্যটির ভেতরে — কখনো আংশিক মেলে না।

এই পর্বের উদাহরণ-অ্যারে a = [5, 2, 9, 1, 6, 3, 4, 8] (ঘর 1 থেকে 8). সংজ্ঞা থেকে t = [5, 7, 9, 17, 6, 9, 4, 38]: যেমন t[4] = 5 + 2 + 9 + 1 = 17, t[6] = 6 + 3 = 9, আর t[8] = সবার যোগফল 38.

lowbit বের করা — i & −i আর 2's complement

সবচেয়ে নিচের 1-bit এক লাইনে পাওয়া যায়: i & -i. এখানে & হলো bitwise AND, আর −i কম্পিউটার রাখে 2's complement রূপে: সব bit উল্টে দিয়ে 1 যোগ (বিস্তারিত: কম্পিউটারের মৌলিক বিষয় · বাইনারি গণিত আর complement). কেন এটি কাজ করে, i = 12 নিয়ে দেখুন:

8-bit binary রূপসবুজ = এই ধাপের নতুন সারি

            
প্রতিটি ধাপে
কাজ
ফল
  1. i = 12. binary-তে 0000 1100 — দুটি 1-bit, মান ৮ আর ৪. সবচেয়ে নিচের 1-bit-টি ৪-এর ঘরে, আর তার নিচে দুটি 0.
  2. সব bit উল্টানো. ~12 = 1111 0011, যা 2's complement-এ −13. সবচেয়ে নিচের 1-bit (৪-এর ঘর) এখন 0, আর তার নিচের দুটি 0 এখন 1.
  3. 1 যোগ. নিচের 1-গুলোতে carry চলতে চলতে ৪-এর ঘরে থামে: 1111 0100 = −12. ফলে ৪-এর ঘরে আবার 1, তার নিচে আবার 0 — কিন্তু তার ওপরের সব bit i-এর উল্টো রয়ে গেল।
  4. i & −i. ওপরের bit-গুলো দুই সংখ্যায় উল্টো, তাই AND-এ 0; নিচের bit-গুলো দুটিতেই 0; শুধু ৪-এর ঘর দুটিতেই 1. ফল 0000 0100 = 4 = lowbit(12).
  5. ব্লক. 12 − 4 = 8, তাই t[12] রাখে (8, 12] = a[9] থেকে a[12], চারটি ঘর। একই যুক্তি যেকোনো i ≥ 1-এ খাটে; i = 0-তে 0 & 0 = 0, তাই index 0 কখনো ব্যবহার করা চলে না।

Python-এ int-এর আকারের কোনো সীমা নেই, তবু i & -i একই ফল দেয় — Python ঋণাত্মক সংখ্যার bitwise কাজ এমনভাবে করে যেন বাঁ দিকে অসীম 1 আছে, অর্থাৎ 2's complement-এর মতোই। C++-এ int বা long long-এ এটি নিরাপদ। unsigned ধরনেও -i একই bit দেয় (কিছু compiler সতর্কবার্তা দেখায়); চাইলে i & (~i + 1) লিখুন, বা C++20-এর <bit> header-এর std::countr_zero দিয়ে (1 << countr_zero(i)) পাওয়া যায়।

কেন 1-based?

পুরো কাঠামো দাঁড়িয়ে আছে “i-এর সবচেয়ে নিচের 1-bit”-এর ওপর। i = 0-এর কোনো 1-bit নেই — lowbit(0) = 0, তাই 0 থেকে নামলে বা উঠলে i বদলায় না, লুপ চিরকাল চলে। এজন্য Fenwick tree-তে index 1 থেকে n; অ্যারেটি n + 1 আকারের, আর t[0] অব্যবহৃত। 0-based ইনপুট এলে সবচেয়ে নিরাপদ নিয়ম: BIT-এর সীমানায় ঢোকার সময় +1 করুন (add(i + 1, v)), ভেতরে সবকিছু 1-based রাখুন। (0-based-এর একটি আলাদা রূপও আছে, ভাবনার প্রশ্ন ০৩ দেখুন।)

৩ · Prefix query — lowbit বাদ দিয়ে নিচে নামা

prefix(i) = a[1] + … + a[i] কীভাবে পাবেন? t[i] দেয় শেষের lowbit(i) টি ঘর, অর্থাৎ (i − lowbit(i), i]. বাকি থাকে [1, i − lowbit(i)] — আবার একটি prefix, এবার ছোট। তাই i থেকে lowbit বাদ দিয়ে একই কাজ চালিয়ে যান, যতক্ষণ i শূন্য না হয়।

Pseudocode — Fenwick tree (1-based, t[1..n])

prefix(i):   s = 0
             while i > 0:
                 s = s + t[i]
                 i = i - lowbit(i)          // drop the lowest 1-bit
             return s

add(i, v):   while i <= n:
                 t[i] = t[i] + v
                 i = i + lowbit(i)          // jump to the next block that contains i

a = [5, 2, 9, 1, 6, 3, 4, 8] আর t = [5, 7, 9, 17, 6, 9, 4, 38] নিয়ে prefix(7) দেখুন। নিচের “used” সারিতে লেখা আছে প্রতিটি ঘর কোন t-এর ব্লক থেকে যোগ হলো:

a, t আর কোন ব্লক কোন ঘর ঢাকলসবুজ = এই ধাপে যোগ হলো

            
প্রতিটি ধাপের পর
যোগ হলো
পরের i
s
  1. শুরু. i = 7 = 0111, s = 0. 7-এর binary-তে তিনটি 1-bit, তাই আমরা ঠিক তিনটি ব্লক যোগ করব বলে আশা করি।
  2. t[7]. lowbit(7) = 1, তাই t[7] শুধু a[7] = 4. s = 4. সবচেয়ে নিচের 1-bit মুছে i = 6 = 0110.
  3. t[6]. lowbit(6) = 2, t[6] = a[5] + a[6] = 9. s = 13. আবার নিচের 1-bit মুছে i = 4 = 0100. লক্ষ করুন, নতুন ব্লকটি ঠিক আগের ব্লকের বাঁ পাশে এসে লেগেছে।
  4. t[4]. lowbit(4) = 4, t[4] = a[1..4] = 17. s = 30. i = 0 = 0000 — আর কোনো 1-bit নেই।
  5. ফল 30. সরাসরি যোগ: 5 + 2 + 9 + 1 + 6 + 3 + 4 = 30 — মিলে গেছে। তিনটি ব্লক (7), (5..6), (1..4) পাশাপাশি বসে 1..7-কে ফাঁক বা দ্বিগুণ ছাড়াই ঢেকেছে; ধাপের সংখ্যা = 7-এর 1-bit-এর সংখ্যা = 3.

কেন ঠিক, আর কেন O(log n)

সঠিকতা. লুপের প্রতিটি পুনরাবৃত্তির শুরুতে এই invariant সত্য: s = a[i + 1] + … + a[i₀], যেখানে i₀ হলো শুরুর i. শুরুতে i = i₀, যোগফলটি ফাঁকা, s = 0 — সত্য। এক ধাপে s-এ t[i] যোগ হয়, যা ঠিক a[i − lowbit(i) + 1 … i], আর i হয় i − lowbit(i) — তাই invariant আবার সত্য। লুপ শেষে i = 0, অর্থাৎ s = a[1] + … + a[i₀]. ব্লকগুলো কখনো একে অন্যের ওপর পড়ে না, কারণ প্রতিটি নতুন ব্লক ঠিক আগেরটির বাঁ পাশে শেষ হয়।

সময়. i − lowbit(i) মানে i-এর সবচেয়ে নিচের 1-bit মুছে ফেলা। প্রতি ধাপে একটি 1-bit কমে, তাই ধাপের সংখ্যা = i-এর binary রূপে 1-এর সংখ্যা, যাকে বলে popcount. i ≤ n হলে i-এ সর্বোচ্চ ⌊log₂ n⌋ + 1 টি bit, তাই prefix সর্বোচ্চ ⌊log₂ n⌋ + 1 ধাপ: O(log n) সময়, আর বাড়তি জায়গা O(1). সবচেয়ে খারাপ i হলো 2k − 1 ধাঁচের (যেমন 7 = 111, 15 = 1111), সবচেয়ে ভালো 2-এর ঘাত (একটিই ধাপ)।

৪ · Point update — lowbit যোগ করে ওপরে ওঠা

এবার উল্টো প্রশ্ন: a[p]-এ v যোগ হলে কোন কোন t বদলাবে? ঠিক সেগুলো, যাদের ব্লকে p আছে। সংখ্যায় তারা কম — আর তাদের খুঁজে পাওয়ার নিয়মটিও lowbit: p থেকে শুরু করে বারবার i += lowbit(i), যতক্ষণ i ≤ n. প্রথমে দেখুন, তারপর প্রমাণ।

a আর t (n = 8)সবুজ = এই ধাপে বদলানো ঘর

            
প্রতিটি ধাপের পর
এই ধাপে
পরের i
পরের i-এ নিচের 1-bit-এর অবস্থান
  1. শুরু. a[3]-এ 5 যোগ করতে হবে। ছবির ব্লক-তালিকা দেখলে 3 আছে তিনটি ব্লকে: t[3] = a[3], t[4] = a[1..4], t[8] = a[1..8]. আমাদের লুপ ঠিক এই তিনটিকেই খুঁজে পাবে।
  2. i = 3. t[3] = 9 + 5 = 14. lowbit(3) = 1, তাই i = 3 + 1 = 4. binary-তে 0011 + 1 = 0100: carry দুই ঘর চলে গিয়ে সবচেয়ে নিচের 1-bit অবস্থান ০ থেকে ২-এ উঠল।
  3. i = 4. t[4] = 17 + 5 = 22. lowbit(4) = 4, i = 8 = 1000 — 1-bit আরও ওপরে, অবস্থান ৩.
  4. i = 8. t[8] = 38 + 5 = 43. i = 16 > n = 8, লুপ থামে। মাত্র তিনটি ঘর বদলাল; t[5], t[6], t[7]-এর ব্লকে 3 নেই, তাই তারা অক্ষত।
  5. যাচাই. আবার prefix(7) = 4 + 9 + 22 = 35 = আগের 30 + 5. নতুন t দিয়ে প্রতিটি ব্লকের সংজ্ঞা এখনো সত্য — এটিই আপডেটের লক্ষ্য।

প্রমাণ — ওপরে ওঠার পথ ঠিক সেই ব্লকগুলো ছোঁয়, যাদের মধ্যে p আছে

ব্লক j-কে লিখি B(j) = (j − lowbit(j), j]. দুটি ছোট পর্যবেক্ষণ লাগবে। ধরুন L = lowbit(j).

  • (ক) পরের ব্লক আগেরটিকে ধারণ করে। j + L-এ carry চলে, তাই lowbit(j + L) ≥ 2L. ফলে B(j + L)-এর শুরু ≤ (j + L) − 2L = j − L, আর শেষ j + L — অর্থাৎ B(j) ⊂ B(j + L).
  • (খ) মাঝের কেউ ধারণ করে না। j < j′ < j + L হলে j′ = j + r, যেখানে 0 < r < L. j-এর নিচের সব bit 0, তাই lowbit(j′) = lowbit(r), আর B(j′) শুরু হয় j + r − lowbit(r) ≥ j থেকে। অর্থাৎ B(j′)-এ j বা তার আগের কোনো ঘর নেই।

এখন p দিয়ে শুরু করুন। B(p)-এ p আছে, আর p-এর চেয়ে ছোট কোনো j-এর ব্লকে p থাকতে পারে না (ব্লক j-তে শেষ হয়)। (খ) বলে p আর p + lowbit(p)-এর মাঝের কোনো ব্লকে p নেই, (ক) বলে পরেরটিতে আছে। Induction চালালে: ওপরে ওঠার পথ p ধারণকারী সব ব্লক ছোঁয়, আর অন্য কাউকে ছোঁয় না। তাই একটি add-এর পর প্রতিটি t[j] তার সংজ্ঞা মেনে চলে।

সময়. (ক)-এর যুক্তি থেকে প্রতি ধাপে lowbit অন্তত দ্বিগুণ হয় — সবচেয়ে নিচের 1-bit-এর অবস্থান অন্তত এক ঘর ওপরে যায়। অবস্থান ০ থেকে শুরু করে ⌊log₂ n⌋ ছাড়ালে i > n, তাই add সর্বোচ্চ ⌊log₂ n⌋ + 1 ধাপ: O(log n) সময়, O(1) বাড়তি জায়গা। মজার একটি প্রতিসাম্য: query 1-bit মোছে, update 1-bit-কে ওপরে ঠেলে।

দুটি লুকানো tree. নাম “tree”, কিন্তু কোনো pointer নেই। “i − lowbit(i)”-কে parent ধরলে একটি tree পাওয়া যায় (query-র পথ, root 0), আর “i + lowbit(i)”-কে parent ধরলে আরেকটি (update-এর পথ)। দুটিই অ্যারের index-এর পাটিগণিতে লুকানো — ঠিক পর্ব ২৩-এর heap-এর মতো, যেখানে parent হলো i/2.

৫ · O(n) build আর রেঞ্জের যোগফল

শুরুর অ্যারে থেকে t বানানোর সরল উপায়: t সব শূন্য, তারপর প্রতিটি i-এ add(i, a[i]) — n টি add, মোট O(n log n). কিন্তু O(n)-এ সম্ভব। লক্ষ করুন, update-এর tree-তে i-এর parent হলো i + lowbit(i), আর t[parent]-এর ব্লক তার সন্তানদের ব্লক আর নিজের ঘর মিলে তৈরি। তাই প্রথমে t = a কপি করুন, তারপর i = 1 থেকে n পর্যন্ত একবার হেঁটে প্রতিটি t[i]-কে শুধু তার parent-এ যোগ করুন:

t অ্যারে (a = 5 2 9 1 6 3 4 8)সবুজ = এই ধাপে বদলানো

            
প্রতিটি ধাপে
i → i + lowbit(i)
কাজ
  1. t = a. প্রতিটি ঘরে আপাতত শুধু নিজের মান। বিজোড় i-এর (ব্লক দৈর্ঘ্য ১) t এখনই ঠিক।
  2. i = 1. parent 1 + 1 = 2: t[2] = 2 + 5 = 7 = a[1] + a[2]. t[2] এখন সম্পূর্ণ।
  3. i = 2. t[2] সম্পূর্ণ, তাই তাকে parent 4-এ ঠেলে দিন: t[4] = 1 + 7 = 8.
  4. i = 3. parent 4: t[4] = 8 + 9 = 17 = a[1..4]. t[4]-এর দুই সন্তান (2 আর 3) দুজনেই যোগ হয়েছে।
  5. i = 4. parent 8: t[8] = 8 + 17 = 25.
  6. i = 5. parent 6: t[6] = 3 + 6 = 9 = a[5..6].
  7. i = 6. parent 8: t[8] = 25 + 9 = 34.
  8. i = 7, 8. t[8] = 34 + 4 = 38; i = 8-এর parent 16 > n, বাদ। ফল 5 7 9 17 6 9 4 38 — সংজ্ঞা থেকে যা পেয়েছিলাম, হুবহু। মোট n টি ধাপ, প্রতিটি O(1): Θ(n).

কেন কাজ করে: i-এর সব সন্তান i-এর চেয়ে ছোট, তাই i-তে পৌঁছানোর আগেই তারা নিজেদের যোগফল t[i]-এ ঠেলে দিয়েছে — i-তে পৌঁছালে t[i] সম্পূর্ণ। আরেকটি O(n) উপায়: আগে prefix sum P বানিয়ে t[i] = P[i] − P[i − lowbit(i)].

যেকোনো রেঞ্জের যোগফল

a[l] + … + a[r] = prefix(r) − prefix(l − 1) — পর্ব ১৭-এর সেই পুরোনো কৌশল, শুধু এবার প্রতিটি prefix O(log n). যেমন sum(3..6) = prefix(6) − prefix(2) = (t[6] + t[4]) − t[2] = (9 + 17) − 7 = 19 = 9 + 1 + 6 + 3. সাবধান: l = 1 হলে prefix(0) = 0, লুপ একবারও চলে না — ঠিক যা চাই। এই বিয়োগটিই Fenwick tree-র শক্তি আর সীমা দুটোই, অংশ ৯-এ ফিরব।

পুরো কোড — BIT, আর brute force-এর সাথে মিলিয়ে দেখা

প্রথমে উপরের উদাহরণটি চালানো হয়, তারপর একটি stress test: ৫০ ঘরের একটি সাধারণ অ্যারে আর একটি BIT-এ একই ২০০০টি এলোমেলো আপডেট, প্রতিটির পর একটি এলোমেলো রেঞ্জে দুজনের উত্তর মেলানো (brute force সরাসরি যোগ করে)। এলোমেলো সংখ্যা আসে নিজের লেখা একটি ছোট সূত্র থেকে, যাতে C++ আর Python ঠিক একই ক্রম পায়।

#include <iostream>
#include <vector>
using namespace std;
struct BIT {                                        // 1-based: t[0] is unused
    int n; vector<long long> t;
    BIT(const vector<long long>& a) : n(int(a.size())), t(a.size() + 1, 0) {   // O(n) build
        for (int i = 1; i <= n; ++i) {
            t[i] += a[i - 1];
            if (int j = i + (i & -i); j <= n) t[j] += t[i];     // push into the parent
        }
    }
    void add(int i, long long v) { for (; i <= n; i += i & -i) t[i] += v; }   // walk UP
    long long prefix(int i) const { long long s = 0; for (; i > 0; i -= i & -i) s += t[i]; return s; }  // walk DOWN
    long long range_sum(int l, int r) const { return prefix(r) - prefix(l - 1); }
};
unsigned long long seed = 12345;
int rnd(int m) { seed = (seed * 1103515245 + 12345) % 2147483648ULL; return int(seed % m); }
void show(const BIT& b) { for (int i = 1; i <= b.n; ++i) cout << ' ' << b.t[i]; }
int main() {
    BIT b({5, 2, 9, 1, 6, 3, 4, 8});
    cout << "tree :"; show(b);
    cout << "\nprefix(7) = " << b.prefix(7) << " | sum(3..6) = " << b.range_sum(3, 6) << '\n';
    b.add(3, 5);
    cout << "after add(3, +5) tree :"; show(b); cout << " | prefix(7) = " << b.prefix(7) << '\n';
    const int n = 50; int bad = 0;                              // stress test against a plain array
    vector<long long> arr(n + 1, 0); BIT f(vector<long long>(n, 0));
    for (int it = 0; it < 2000; ++it) {
        int i = rnd(n) + 1, v = rnd(21) - 10;
        arr[i] += v; f.add(i, v);
        int l = rnd(n) + 1, r = l + rnd(n - l + 1);
        long long s = 0; for (int k = l; k <= r; ++k) s += arr[k];
        bad += f.range_sum(l, r) != s;
    }
    cout << "stress: 2000 random add+query pairs on n = 50, mismatches = " << bad << '\n';
}

আউটপুট:

tree : 5 7 9 17 6 9 4 38
prefix(7) = 30 | sum(3..6) = 19
after add(3, +5) tree : 5 7 14 22 6 9 4 43 | prefix(7) = 35
stress: 2000 random add+query pairs on n = 50, mismatches = 0

প্রথম তিন লাইন ঠিক তিনটি ডায়াগ্রামের সংখ্যা: build-এর পর t = 5 7 9 17 6 9 4 38, prefix(7) = 30, sum(3..6) = 19, আর add(3, +5) শুধু t[3], t[4], t[8] বদলেছে। শেষ লাইনটি সবচেয়ে গুরুত্বপূর্ণ: ২০০০টি এলোমেলো আপডেট আর রেঞ্জ (l = 1 আর r = n-এর মতো সীমানার ক্ষেত্রসহ) — একটিতেও অমিল নেই। যোগফল রাখা হয়েছে long long-এ, কারণ 105 টি 109-এর যোগ 32-bit int-এ ধরে না (integer overflow). int(seed % m) লেখা হয়েছে, যাতে unsigned থেকে int-এ রূপান্তর স্পষ্ট থাকে। সময়: build O(n), প্রতিটি add আর prefix O(log n); জায়গা Θ(n).

class BIT:                                # 1-based: t[0] is unused
    def __init__(self, a):                # O(n) build: each i pushes into one parent
        self.n, self.t = len(a), [0] + list(a)
        for i in range(1, self.n + 1):
            j = i + (i & -i)
            if j <= self.n:
                self.t[j] += self.t[i]
    def add(self, i, v):                  # point update: walk UP, adding lowbit
        while i <= self.n:
            self.t[i] += v; i += i & -i
    def prefix(self, i):                  # a[1] + ... + a[i]: walk DOWN, removing lowbit
        s = 0
        while i > 0:
            s += self.t[i]; i -= i & -i
        return s
    def range_sum(self, l, r): return self.prefix(r) - self.prefix(l - 1)

b = BIT([5, 2, 9, 1, 6, 3, 4, 8])
print("tree :", *b.t[1:])
print("prefix(7) =", b.prefix(7), "| sum(3..6) =", b.range_sum(3, 6))
b.add(3, 5)
print("after add(3, +5) tree :", *b.t[1:], "| prefix(7) =", b.prefix(7))
seed, n, bad = 12345, 50, 0               # stress test against a plain list
def rnd(m):
    global seed
    seed = (seed * 1103515245 + 12345) % 2147483648
    return seed % m
arr, f = [0] * (n + 1), BIT([0] * n)
for _ in range(2000):
    i, v = rnd(n) + 1, rnd(21) - 10
    arr[i] += v; f.add(i, v)
    l = rnd(n) + 1; r = l + rnd(n - l + 1)
    bad += f.range_sum(l, r) != sum(arr[l:r + 1])
print("stress: 2000 random add+query pairs on n = 50, mismatches =", bad)

আউটপুট C++-এর সাথে হুবহু এক:

tree : 5 7 9 17 6 9 4 38
prefix(7) = 30 | sum(3..6) = 19
after add(3, +5) tree : 5 7 14 22 6 9 4 43 | prefix(7) = 35
stress: 2000 random add+query pairs on n = 50, mismatches = 0

Python-এ int-এর আকারের সীমা নেই, তাই overflow-এর ভয় নেই, আর i & -i ঋণাত্মক সংখ্যায়ও 2's complement-এর মতো আচরণ করে। bad += ... != ... লাইনে True/False যথাক্রমে 1/0 হিসেবে যোগ হয়। একটি সতর্কতা: Python-এ প্রতিটি লুপ-ধাপ C++-এর চেয়ে অনেক ধীর, তাই 106 টি কাজের কনটেস্ট-সমস্যায় BIT-এর পদ্ধতি এক হলেও সময়ের সীমা টপকাতে দ্রুত ইনপুট আর লোকাল ভেরিয়েবল কাজে লাগে।

৬ · রেঞ্জ আপডেট — difference array-এর ওপর BIT, আর দুই BIT-এর কৌশল

Range update + point query

এবার উল্টো চাহিদা: “a[l..r]-এর প্রতিটি ঘরে v যোগ করো” (range update), আর প্রশ্ন “এখন a[i] কত?”. পর্ব ১৭-এর difference array মনে করুন: d[i] = a[i] − a[i − 1], তাই a[i] = d[1] + … + d[i] — একটি prefix sum! আর a[l..r]-এ v যোগ মানে শুধু d[l] += v আর d[r + 1] −= v. তাই BIT রাখুন d-এর ওপর:

Pseudocode — range update, point query (1-based, BIT over d)

rangeAdd(l, r, v):  add(l, v);  add(r + 1, -v)      // skip the second if r + 1 > n
pointQuery(i):      return prefix(i)                 // a[i] = d[1] + ... + d[i]

দুটিই O(log n). উদাহরণ: n = 6, সব শূন্য; rangeAdd(2, 4, +3) মানে add(2, 3) আর add(5, −3). এখন pointQuery(3) = d[1] + d[2] + d[3] = 0 + 3 + 0 = 3, আর pointQuery(5) = 3 − 3 = 0 — ঠিক।

Range update + range query — দুই BIT, সূত্রটি নিজে বের করুন

যদি রেঞ্জে যোগও করতে হয় আর রেঞ্জের যোগফলও জানতে হয়? চাই S(x) = a[1] + … + a[x]. a[i]-এর জায়গায় d বসান:

প্রমাণ

S(x) = Σi=1..x Σj=1..i d[j]. প্রতিটি d[j] যোগ হয় i = j, j + 1, …, x — মোট (x − j + 1) বার। তাই

S(x) = Σj=1..x d[j] · (x + 1 − j) = (x + 1) · Σj≤x d[j] − Σj≤x j · d[j].

দুটি যোগফলই prefix sum! তাই দুটি BIT রাখুন: B1 রাখে d[j], B2 রাখে j · d[j]. rangeAdd(l, r, v)-এ d[l] += v আর d[r + 1] −= v, তাই B1.add(l, v), B1.add(r + 1, −v), B2.add(l, l · v), B2.add(r + 1, −(r + 1) · v). তারপর S(x) = (x + 1) · B1.prefix(x) − B2.prefix(x), আর রেঞ্জের যোগফল S(r) − S(l − 1).

হাতে যাচাই: n = 5, সব শূন্য, rangeAdd(2, 4, +3) → a = [0, 3, 3, 3, 0]. B1-এ ঘর 2-এ +3, ঘর 5-এ −3; B2-এ ঘর 2-এ +6, ঘর 5-এ −15. S(3) = 4 · 3 − 6 = 6 = 0 + 3 + 3. S(5) = 6 · (3 − 3) − (6 − 15) = 9 = 3 + 3 + 3. দুটি কাজই চারটি বা কম BIT-কাজ: O(log n) প্রতিটি, জায়গা 2(n + 1). এটি পর্ব ৩৫-এর lazy segment tree-র একটি হালকা বিকল্প — শুধু যোগের ক্ষেত্রে।

2D BIT — গ্রিডের আয়তক্ষেত্র

একটি n × m গ্রিডে কোনো ঘরের মান বদলায়, আর প্রশ্ন আসে “বাঁ-ওপরের কোণ থেকে (x, y) পর্যন্ত আয়তক্ষেত্রের যোগফল কত?”. সমাধান: BIT-এর ভেতরে BIT — 2D Fenwick tree. t[x][y] রাখে x-এর ব্লক × y-এর ব্লক, আর দুই লুপ একটির ভেতরে আরেকটি:

Pseudocode — 2D BIT (1-based, t[1..n][1..m])

add(x, y, v):  for (i = x; i <= n; i += lowbit(i))
                   for (j = y; j <= m; j += lowbit(j))  t[i][j] += v
prefix(x, y):  s = 0
               for (i = x; i > 0; i -= lowbit(i))
                   for (j = y; j > 0; j -= lowbit(j))   s += t[i][j]
               return s

প্রতিটি কাজ O(log n · log m), জায়গা Θ(nm). যেকোনো আয়তক্ষেত্র (x1, y1)–(x2, y2)-এর যোগফল আসে চারটি prefix থেকে inclusion–exclusion দিয়ে: P(x2, y2) − P(x1 − 1, y2) − P(x2, y1 − 1) + P(x1 − 1, y1 − 1) — পর্ব ১৭-এর 2D prefix sum-এর মতোই, কিন্তু এবার আপডেটসহ। গ্রিড খুব বড় হলে (যেমন 109 × 109) nm জায়গা অসম্ভব; তখন প্রশ্নগুলো আগে জেনে offline পদ্ধতিতে এক মাত্রার BIT আর সর্ট দিয়ে সমাধান করা হয়।

৭ · k-তম উপাদান — BIT-এর ওপর binary lifting

ধরুন একটি ভর্তি পরীক্ষার নম্বর 1 থেকে n-এর মধ্যে, আর BIT-এর ঘর v রাখে “কতজন v নম্বর পেয়েছে” — একটি frequency array. নতুন খাতা এলে add(v, +1), পুনর্নিরীক্ষায় নম্বর বদলালে add(পুরোনো, −1) আর add(নতুন, +1). এখন প্রশ্ন: “নম্বর অনুযায়ী সাজালে k-তম জন কত পেয়েছে?” — একটি order statistic, যেমন median.

সহজ উপায়: “prefix(v) ≥ k হয় এমন সবচেয়ে ছোট v” খুঁজতে prefix-এর ওপর binary search — log n টি prefix, প্রতিটি log n: O(log² n). কিন্তু BIT-এর গঠন কাজে লাগালে একবারের হাঁটাতেই O(log n) হয়। ধারণা: উত্তরের ঠিক আগের অবস্থান pos-কে bit ধরে ধরে ওপর থেকে বানান — binary lifting.

Pseudocode — k-th smallest (1-based, all counts >= 0, 1 <= k <= total)

kth(k):  pos = 0
         for step = (largest power of 2 <= n) down to 1, halving:
             if pos + step <= n and t[pos + step] < k:
                 pos = pos + step          // the answer lies further right
                 k = k - t[pos]            // skip everything in (old pos, pos]
         return pos + 1

চাবিকাঠি হলো: pos-এ এখন পর্যন্ত শুধু step-এর চেয়ে বড় 2-এর ঘাত যোগ হয়েছে, তাই lowbit(pos + step) = step — অর্থাৎ t[pos + step] ঠিক (pos, pos + step]-এর গণনা, আলাদা কোনো prefix লাগে না। একটি উদাহরণ: n = 8, গণনা c = [2, 0, 1, 3, 0, 1, 2, 1] (মোট ১০ জন), তাহলে t = [2, 2, 1, 6, 0, 1, 2, 10]. ৬ষ্ঠ জন কত পেয়েছে?

গণনা c আর BIT tসবুজ = এই ধাপে পড়া ঘর

            
প্রতিটি ধাপের পর
step
পরীক্ষা
pos / k
  1. শুরু. pos = 0, k = 6. n = 8-এর নিচে বা সমান সবচেয়ে বড় 2-এর ঘাত 8, তাই step শুরু 8 থেকে।
  2. step = 8. t[8] = 10 মানে 1..8-এ ১০ জন — ৬ষ্ঠ জন এর ভেতরেই, তাই pos নড়ে না।
  3. step = 4. t[0 + 4] = 6: 1..4-এ ঠিক ৬ জন। “৬-এর কম” নয়, তাই ৬ষ্ঠ জন ≤ 4-এ; pos নড়ে না। (এখানে < না লিখে ≤ লিখলে উত্তর ভুল হতো — 4-এর বদলে 6.)
  4. step = 2. t[0 + 2] = 2: 1..2-এ মাত্র ২ জন, ৬-এর কম — উত্তর আরও ডানে। pos = 2, আর এই ২ জনকে বাদ দিয়ে k = 4.
  5. step = 1. pos + 1 = 3; lowbit(3) = 1, তাই t[3] = 1 ঠিক মান 3-এর গণনা। ৪-এর কম, তাই pos = 3, k = 3.
  6. উত্তর 4. pos + 1 = 4. সাজানো তালিকা 1 1 3 4 4 4 6 7 7 8-এর ৬ষ্ঠটি সত্যিই 4. পড়া হলো শুধু t[8], t[4], t[2], t[3] — log₂ 8 + 1 = 4 টি ঘর: O(log n).

শর্ত দুটি মনে রাখুন: সব গণনা অ-ঋণাত্মক হতে হবে (তবেই prefix একমুখীভাবে বাড়ে আর “সবচেয়ে ছোট v” অর্থবহ), আর k ≤ মোট গণনা। মান বড় বা ছড়ানো হলে আগে coordinate compression — পরের অংশ। কনটেস্টে এই কৌশল দিয়ে “চলমান median” বা “k-তম ছোট মান, মাঝে মাঝে মোছাসহ” সমস্যা মাত্র কয়েক লাইনে হয়, balanced BST (পর্ব ২৮–২৯) ছাড়াই।

৮ · Inversion গোনা — BIT আর coordinate compression

একটি অ্যারের inversion হলো এমন জোড়া (i, j), যেখানে i < j কিন্তু a[i] > a[j]. পর্ব ২১-এ merge sort-এর merge ধাপে এগুলো গুনেছি, O(n log n)-এ। BIT দিয়ে আরেকটি সমান দ্রুত, আর অনেক সময় বেশি নমনীয় উপায় আছে: বাঁ থেকে ডানে হাঁটুন, আর প্রতিটি নতুন x-এর জন্য জিজ্ঞেস করুন “আগে দেখা মানগুলোর কয়টি x-এর চেয়ে বড়?”. আগে দেখা মানগুলোর একটি frequency BIT রাখলে উত্তর = (এ পর্যন্ত দেখা) − prefix(x), কারণ prefix(x) গোনে কয়টি ≤ x.

সমস্যা একটাই: মান 109 পর্যন্ত হলে 109 ঘরের BIT অসম্ভব। কিন্তু inversion শুধু মানগুলোর ক্রম দেখে, আকার নয়। তাই পর্ব ২৫-এর coordinate compression: মানগুলো সর্ট করে ডুপ্লিকেট বাদ দিন, আর প্রতিটি মানকে তার অবস্থান (rank) 1..k দিয়ে বদলান। ক্রম অক্ষত থাকে, BIT লাগে মাত্র k ≤ n ঘরের।

অ্যারে, rank, আর rank-এর ওপর BIT (k = 5)সবুজ = এখনকার উপাদান

            
প্রতিটি ধাপের পর
x (rank r)
seen − prefix(r)
inv
  1. Compression. আলাদা মান সাজিয়ে 10, 20, 30, 40, 50 → rank 1, 2, 3, 4, 5. অ্যারে হয় 4 1 3 1 5 2. BIT খালি, inv = 0.
  2. 40 (r = 4). আগে কেউ নেই: 0 − prefix(4) = 0. তারপর add(4, 1): শুধু t[4] বদলায় (4 + 4 = 8 > 5).
  3. 10 (r = 1). আগে ১ জন, তাদের মধ্যে ≤ 10 আছে prefix(1) = 0 জন — তাই 1 টি বড় (40). inv = 1. add(1, 1) ছোঁয় t[1], t[2], t[4].
  4. 30 (r = 3). আগে ২ জন; prefix(3) = t[3] + t[2] = 0 + 1 = 1 (মান 10). বড়: 2 − 1 = 1 (40). inv = 2.
  5. 10 (r = 1), আবার. আগে ৩ জন; prefix(1) = 1 — আগের 10, যা সমান, বড় নয়। prefix(r)-এ ≤ গোনা হয় বলেই সমান মান inversion হিসেবে গোনা পড়ে না। বড়: 3 − 1 = 2 (40, 30). inv = 4.
  6. 50 (r = 5). আগে ৪ জন, prefix(5) = 4 — সবাই ছোট। +0, inv = 4.
  7. 20 (r = 2). আগে ৫ জন; prefix(2) = t[2] = 2 (দুটি 10). বড়: 5 − 2 = 3 (40, 30, 50). inv = 7. হাতে মিলিয়ে দেখুন: (40,10), (40,30), (40,10), (40,20), (30,10), (30,20), (50,20) — সাতটি।

সময়: compression-এর সর্ট O(n log n), তারপর n টি prefix আর n টি add, প্রতিটি O(log k): মোট O(n log n), জায়গা O(n). উত্তর n(n − 1)/2 পর্যন্ত হতে পারে — n = 2 × 105-এ প্রায় 2 × 1010, তাই C++-এ long long বাধ্যতামূলক।

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
long long inversions_bit(const vector<long long>& a) {         // O(n log n)
    vector<long long> vals(a); sort(vals.begin(), vals.end());   // coordinate compression
    vals.erase(unique(vals.begin(), vals.end()), vals.end());
    int k = int(vals.size()); vector<int> t(k + 1, 0); long long inv = 0;
    for (int seen = 0; seen < int(a.size()); ++seen) {
        int r = int(lower_bound(vals.begin(), vals.end(), a[seen]) - vals.begin()) + 1;   // rank 1..k
        int s = 0;
        for (int j = r; j > 0; j -= j & -j) s += t[j];           // s = how many seen values are <= x
        inv += seen - s;                                         // seen values strictly greater than x
        for (; r <= k; r += r & -r) t[r] += 1;
    }
    return inv;
}
long long inversions_brute(const vector<long long>& a) {      // O(n^2) reference
    long long c = 0;
    for (size_t i = 0; i < a.size(); ++i) for (size_t j = i + 1; j < a.size(); ++j) c += a[i] > a[j];
    return c;
}
unsigned long long seed = 2026;
int rnd(int m) { seed = (seed * 1103515245 + 12345) % 2147483648ULL; return int(seed % m); }
int main() {
    vector<long long> a = {40, 10, 30, 10, 50, 20};
    cout << "a ="; for (auto x : a) cout << ' ' << x;
    cout << " -> BIT: " << inversions_bit(a) << " | brute: " << inversions_brute(a) << '\n';
    int bad = 0;
    for (int it = 0; it < 300; ++it) {
        vector<long long> b(rnd(40) + 1);
        for (auto& x : b) x = rnd(21) * 1000003LL;              // big values, many repeats
        bad += inversions_bit(b) != inversions_brute(b);
    }
    cout << "stress: 300 random arrays, mismatches = " << bad << '\n';
    vector<long long> rev(2000); for (int i = 0; i < 2000; ++i) rev[i] = 2000 - i;
    cout << "reversed 1..2000: " << inversions_bit(rev) << " | expected " << 2000LL * 1999 / 2 << '\n';
}

আউটপুট:

a = 40 10 30 10 50 20 -> BIT: 7 | brute: 7
stress: 300 random arrays, mismatches = 0
reversed 1..2000: 1999000 | expected 1999000

প্রথম লাইন ডায়াগ্রামের 7. দ্বিতীয় লাইনে ৩০০টি এলোমেলো অ্যারে, প্রতিটিতে ১ থেকে ৪০টি উপাদান আর মাত্র ২১টি আলাদা (বড়) মান — অর্থাৎ প্রচুর সমান মান, ঠিক যেখানে ≤ আর <-এর ভুল ধরা পড়ে; O(n²) brute force-এর সাথে একটিও অমিল নেই। তৃতীয় লাইন একটি পরিচিত চরম ক্ষেত্র: উল্টো সাজানো অ্যারেতে প্রতিটি জোড়াই inversion, n(n − 1)/2 = 1999000. Rank বের করতে lower_bound ব্যবহার করা হয়েছে — upper_bound দিলে সমান মানের rank এক বেশি হয়ে যেত।

def inversions_bit(a):                    # O(n log n)
    vals = sorted(set(a))                 # coordinate compression
    rank = {v: i + 1 for i, v in enumerate(vals)}   # ranks 1..k
    k = len(vals)
    t = [0] * (k + 1)
    inv = 0
    for seen, x in enumerate(a):          # seen = elements already inserted
        r, s = rank[x], 0
        j = r
        while j > 0:                      # s = how many seen values are <= x
            s += t[j]; j -= j & -j
        inv += seen - s                   # seen values strictly greater than x
        while r <= k:
            t[r] += 1; r += r & -r
    return inv

def inversions_brute(a):                  # O(n^2) reference
    return sum(a[i] > a[j] for i in range(len(a)) for j in range(i + 1, len(a)))

a = [40, 10, 30, 10, 50, 20]
print("a =", *a, "-> BIT:", inversions_bit(a), "| brute:", inversions_brute(a))
seed, bad = 2026, 0
def rnd(m):
    global seed
    seed = (seed * 1103515245 + 12345) % 2147483648
    return seed % m
for _ in range(300):
    b = [rnd(21) * 1000003 for _ in range(rnd(40) + 1)]   # big values, many repeats
    if inversions_bit(b) != inversions_brute(b): bad += 1
print("stress: 300 random arrays, mismatches =", bad)
print("reversed 1..2000:", inversions_bit(list(range(2000, 0, -1))), "| expected", 2000 * 1999 // 2)

আউটপুট C++-এর সাথে হুবহু এক:

a = 40 10 30 10 50 20 -> BIT: 7 | brute: 7
stress: 300 random arrays, mismatches = 0
reversed 1..2000: 1999000 | expected 1999000

Python-এ compression হয়েছে sorted(set(a)) আর একটি dictionary দিয়ে — rank পাওয়া গড়ে O(1), C++-এর lower_bound-এর O(log k)-এর বদলে; মোট সময় দুটিতেই O(n log n). List comprehension-এ range(rnd(40) + 1) আগে একবার হিসাব হয়, তারপর উপাদানগুলো — C++-এর vector b(rnd(40) + 1)-এর মতোই একই ক্রম, তাই দুই ভাষা একই ৩০০টি অ্যারে পায়। একই কাঠামো উল্টো দিক থেকে চালালে প্রতিটি উপাদানের “ডানে কয়টি ছোট” পাওয়া যায় (অনুশীলন ৪)।

৯ · BIT বনাম segment tree — আর একটু ইতিহাস

বিষয়Fenwick treeSegment tree (পর্ব ৩৪–৩৫)
জায়গাn + 1 টি ঘরসাধারণত 2n থেকে 4n টি node
কোডদুটি তিন লাইনের লুপrecursive build, query, update — কয়েক গুণ লম্বা
point update, prefix/range sumO(log n), ছোট ধ্রুবকO(log n)
রেঞ্জের min/max, ইচ্ছামতো আপডেটসাধারণভাবে নয়হ্যাঁ
যেকোনো associative কাজ (gcd, matrix গুণ…)রেঞ্জে নয়; prefix-এ শুধু কিছু শর্তে (আরও গভীরে দেখুন)হ্যাঁ
রেঞ্জ আপডেটযোগের জন্য দুই BITlazy propagation দিয়ে প্রায় সবকিছু

কেন min/max নয়? Fenwick tree রেঞ্জ [l, r]-এর উত্তর পায় prefix(r) থেকে prefix(l − 1) “বাদ দিয়ে”. যোগে বাদ দেওয়া মানে বিয়োগ; XOR-এ আবার XOR (x ^ x = 0); গুণে (শূন্য না থাকলে) ভাগ। এমন কাজকে বলা যায় উল্টানো যায় এমন (invertible). কিন্তু min(1..r) = 2 আর min(1..l − 1) = 2 জানলে min(l..r) কত — বলার উপায় নেই। তাই রেঞ্জের min/max-এর জন্য segment tree বা পর্ব ৩৭-এর sparse table. ব্যতিক্রম: শুধু prefix-এর max, আর মান শুধু বাড়ে — তখন BIT চলে (আরও গভীরে দেখুন)।

নিয়মটি সহজ: সমস্যা যদি “যোগফল বা গণনা, সাথে point update” — Fenwick tree, কারণ কম কোড মানে কম bug, আর কম জায়গা মানে cache-এ ভালো। রেঞ্জে min/max, “রেঞ্জে মান বসিয়ে দাও”, বা একাধিক তথ্য একসাথে লাগলে — segment tree.

১৯৯৪ সালে নিউজিল্যান্ডের University of Auckland-এর Peter M. Fenwick Software: Practice and Experience জার্নালে “A New Data Structure for Cumulative Frequency Tables” প্রকাশ করেন। তাঁর লক্ষ্য ছিল ডেটা সংকোচনের arithmetic coding, যেখানে প্রতিটি অক্ষর পড়ার পর তার গণনা বাড়াতে হয়, আর বারবার জানতে হয় “এই অক্ষরের আগের সব অক্ষরের মোট গণনা কত” — একটি cumulative frequency. তাঁর নামেই আজ কাঠামোটি পরিচিত। তবে প্রায় একই ধারণা এর আগেও বর্ণিত হয়েছিল: সোভিয়েত তথ্যতত্ত্ববিদ Boris Ryabko-র ১৯৮৯ সালের একটি প্রবন্ধে প্রায় একই কাঠামো ছিল বলে উল্লেখ পাওয়া যায়। পরে কনটেস্ট-প্রোগ্রামিংয়ের মাধ্যমে, বিশেষত TopCoder আর Codeforces-এর টিউটোরিয়ালে, BIT সারা পৃথিবীতে ছড়িয়ে পড়ে।

আরও গভীরে — prefix-max BIT আর LIS: min/max উল্টানো যায় না, কিন্তু যদি প্রশ্ন সবসময় prefix-এর (1..i) হয়, আর আপডেট শুধু মান বাড়ায় (t[i] = max(t[i], v)), তবে BIT ঠিকঠাক চলে, কারণ বিয়োগ লাগে না আর পুরোনো সর্বোচ্চ কখনো বাতিল হয় না। এর ক্লাসিক ব্যবহার Longest Increasing Subsequence: মানগুলো compress করে বাঁ থেকে ডানে, dp[i] = 1 + max(dp যেসব আগের মান ছোট) — prefix-max BIT দিয়ে O(n log n) (পর্ব ৫৩-এ LIS আসবে)। সাধারণ নিয়মটি বীজগণিতের ভাষায়: রেঞ্জ query-র জন্য কাজটির একটি group (উল্টানোসহ) লাগে, শুধু prefix query-র জন্য associative ও commutative একটি monoid-ই যথেষ্ট।

পরীক্ষা ও ইন্টারভিউতে:

  • Fenwick tree-তে t[i] কোন অংশের যোগফল রাখে? — a[i − lowbit(i) + 1 … i], যেখানে lowbit(i) = i & −i.
  • prefix query আর point update কেন O(log n)? — query প্রতি ধাপে একটি 1-bit মোছে; update প্রতি ধাপে সবচেয়ে নিচের 1-bit-কে অন্তত এক ঘর ওপরে তোলে; দুটিই ≤ ⌊log₂ n⌋ + 1 ধাপ।
  • BIT কেন 1-based? — lowbit(0) = 0, তাই index 0 থেকে লুপ এগোয় না, অসীম লুপ হয়।
  • BIT দিয়ে range update আর range sum দুটোই? — দুই BIT: S(x) = (x + 1)·Σd − Σ j·d[j].
  • BIT দিয়ে রেঞ্জের minimum কেন সাধারণত হয় না? — রেঞ্জ = দুই prefix-এর “বিয়োগ”, আর min-এর কোনো উল্টো কাজ নেই; segment tree বা sparse table লাগে।

১০ · AI-এর যুগে Fenwick tree — দশ লাইন, তিন জায়গায় off-by-one

“Fenwick tree লিখে দাও” বললে AI সহকারী সাধারণত ঠিকঠাক দেখতে দশ লাইন দেয়। ঝুঁকি লুকিয়ে থাকে সীমানায়: 0-based অ্যারেতে সরাসরি i & -i (i = 0-তে অসীম লুপ), i < n বনাম i <= n, prefix(l) বনাম prefix(l − 1). এগুলো off-by-one ভুল, যা ছোট উদাহরণে প্রায়ই ধরা পড়ে না। আর complexity-র প্রশ্নে AI কখনো বলে “build সবসময় O(n log n)”, বা ভুল করে রেঞ্জ minimum-এর জন্য BIT সুপারিশ করে।

BIT-এর কোডে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
0-based index সরাসরি BIT-এ, add(0, v)a[0]-এ আপডেট দিন — প্রোগ্রাম আটকে যাবে (লুপ চিরকাল)
অ্যারের আকার n, অথচ index n পর্যন্ত লেখাশেষ ঘর a[n]-এ আপডেট দিন; C++-এ -fsanitize=address দিয়ে চালান
রেঞ্জ = prefix(r) − prefix(l)l = r দিয়ে একটি ঘরের যোগফল চান — উত্তর 0 আসবে
update-এ i -= i & -i, query-তে += (দিক উল্টো)ছোট n-এ brute force-এর সাথে মেলান
inversion-এ int উত্তরউল্টো সাজানো 105 উপাদান দিন: সঠিক উত্তর 4999950000
সমান মানে inversion গুনে ফেলা (prefix(r − 1) বা upper_bound)[2, 2] দিন — উত্তর 0 হওয়ার কথা
“BIT দিয়ে range minimum, ইচ্ছামতো আপডেটসহ”প্রতি-উদাহরণ চান; segment tree-র সাথে stress test করুন

AI-কে কোড লিখে দিতে না বলে, আপনার কোড আর trace যাচাই করতে বলুন — আর প্রতিটি দাবির প্রমাণ চান:

আমি DSA শিখছি, এখন Fenwick tree (BIT). নিচে আমার লেখা BIT (1-based, add আর prefix) আর একটি ইনপুটে আমার হাতে করা trace দিলাম: [এখানে কোড আর trace পেস্ট করুন] পুরো সমাধান নতুন করে লিখে দিও না। শুধু এগুলো করো: ১. a = [5, 2, 9, 1, 6, 3, 4, 8]-এ build-এর পর t অ্যারে, তারপর prefix(7) আর add(3, +5)-এ কোন কোন index ছোঁয়া হয় — binary রূপসহ দেখাও, আর আমার trace-এর সাথে অমিল থাকলে বলো। ২. আমার কোডে কোথাও index 0, index n বা l − 1-এর সীমানায় ভুল আছে কি? প্রতিটির জন্য একটি ছোট প্রতি-উদাহরণ দাও। ৩. update-এর পথ যে ঠিক সেই t[j]-গুলো ছোঁয় যাদের ব্লকে i আছে — এর প্রমাণ ধাপে ধাপে লেখো। ৪. আমার কোডের build, add আর prefix-এর time ও space complexity কত, আর কেন? "log n" কোন যুক্তি থেকে আসে, তা bit দিয়ে ব্যাখ্যা করো।

BIT-এর কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১একটি ডেটা সংকোচনের সমস্যা থেকে কীভাবে Fenwick tree জন্মাল — arithmetic coding-এর সাথে cumulative frequency-র সম্পর্ক কী?

ফাইল ছোট করার (compression) একটি শক্তিশালী পদ্ধতি arithmetic coding. Huffman coding (পর্ব ৫১) প্রতিটি অক্ষরকে পূর্ণসংখ্যক bit দেয়; arithmetic coding পুরো বার্তাকে 0 আর 1-এর মধ্যের একটিমাত্র ভগ্নাংশে রূপ দেয়, ফলে অক্ষরপ্রতি ভগ্নাংশ bit-ও কার্যত সম্ভব। প্রতিটি অক্ষর পড়ার সময় encoder বর্তমান ব্যবধানটিকে অক্ষরগুলোর সম্ভাবনা অনুযায়ী টুকরো করে, আর পড়া অক্ষরের টুকরোটি বেছে নেয়। টুকরোর সীমানা জানতে লাগে “বর্ণমালায় এই অক্ষরের আগের সব অক্ষরের মোট গণনা” — একটি cumulative frequency, অর্থাৎ prefix sum.

বাস্তবের ভালো compressor adaptive: আগে থেকে সম্ভাবনা জানে না, পড়তে পড়তে শেখে। তাই প্রতিটি অক্ষর পড়ার পর তার গণনা এক বাড়াতে হয় (point update) আর পরের অক্ষরের জন্য আবার cumulative গণনা চাই (prefix query). বর্ণমালা বড় হলে — যেমন শব্দভিত্তিক মডেলে হাজারো “অক্ষর” — সাধারণ অ্যারেতে একটি কাজ O(n) হয়ে যায়। Decoder-এর কাজ আরও কঠিন: একটি সংখ্যা পেয়ে খুঁজতে হয় কোন অক্ষরের টুকরোয় সেটি পড়েছে — অর্থাৎ “cumulative গণনা কোথায় একটি মান ছাড়ায়”, যা ঠিক অংশ ৭-এর k-তম উপাদান খোঁজা।

১৯৯৪ সালে Peter Fenwick এই প্রয়োজন থেকেই তাঁর “cumulative frequency table” প্রস্তাব করেন, যেখানে দুটি কাজই log n-এ হয়, আর জায়গা লাগে শুধু মূল অ্যারের সমান। প্রায় একই ধারণা এর আগে Boris Ryabko-র ১৯৮৯ সালের কাজে ছিল বলে উল্লেখ পাওয়া যায়, তিনিও তথ্যতত্ত্ব আর সংকোচন নিয়ে কাজ করতেন — একই সমস্যা দুই জায়গায় প্রায় একই সমাধান জন্ম দিয়েছে। পরে কনটেস্ট-প্রোগ্রামিং সম্প্রদায় কাঠামোটিকে লুফে নেয়, কারণ এত কম কোডে এত শক্তি খুব কম কাঠামোতে আছে, আর আজ এটি অনেক কনটেস্ট-দলের তৈরি কোডের সংগ্রহে (টেমপ্লেটে) থাকে।

মূল উপলব্ধি: Fenwick tree আসলে “পরিবর্তনশীল prefix sum”-এর সমাধান, আর সেই চাহিদা এসেছিল compression থেকে। একটি নির্দিষ্ট প্রকৌশল-সমস্যার জন্য বানানো হালকা হাতিয়ার কীভাবে সাধারণ অ্যালগরিদমের মৌলিক অংশ হয়ে যায় — এটি তার আরেকটি উদাহরণ।

প্র ০২Segment tree সব পারে, তবু কনটেস্টে আর বাস্তবে Fenwick tree কেন এত জনপ্রিয় — আর কোথায় তাকে ছাড়তেই হয়?

Segment tree-র ধারণা সাধারণত Jon Bentley-র ১৯৭০-এর দশকের শেষের কাজের সাথে যুক্ত করা হয়, জ্যামিতির কিছু সমস্যার প্রসঙ্গে। এটি যেকোনো associative কাজ — যোগ, min, max, gcd, এমনকি matrix গুণ — রেঞ্জে O(log n)-এ দেয়, আর lazy propagation দিয়ে রেঞ্জ আপডেটও প্রায় সবধরনের। তাহলে Fenwick tree কেন টিকে আছে? কারণ যা সে পারে, তা সে অনেক কম খরচে পারে।

প্রথমত, কোড: BIT-এর মূল অংশ দুটি তিন লাইনের লুপ, recursion নেই। কনটেস্টের চাপে কম লাইন মানে কম bug, আর ভুল হলেও খোঁজার জায়গা কম। দ্বিতীয়ত, জায়গা: n + 1 টি সংখ্যা, যেখানে recursive segment tree প্রায়ই 4n ঘর নেয়। তৃতীয়ত, গতি: BIT-এর query একটি সরল লুপ, যা আধুনিক CPU-তে খুব দ্রুত চলে; অনুশীলনে প্রায়ই recursive segment tree-র চেয়ে কয়েক গুণ দ্রুত পাওয়া যায় (যদিও iterative segment tree এই ব্যবধান অনেকটা কমায়)। আর 2D-তে পার্থক্য আরও বড় — 2D BIT মাত্র দুটি nested লুপ, 2D segment tree লেখা রীতিমতো পরিশ্রমের।

কিন্তু সীমাটি গাণিতিক, প্রকৌশলের নয়। BIT একটি রেঞ্জের উত্তর বানায় দুটি prefix থেকে, “বাদ দিয়ে”. যোগে বিয়োগ, XOR-এ XOR, শূন্যহীন গুণে ভাগ — এরা উল্টানো যায়, বীজগণিতের ভাষায় group. min, max, gcd-এর উল্টো নেই। তাই “রেঞ্জে সর্বনিম্ন, মাঝে মাঝে আপডেটসহ” এলে segment tree; “রেঞ্জে একটি মান বসিয়ে দাও” (assignment) এলেও segment tree, কারণ সেটি যোগ দিয়ে প্রকাশ করা যায় না। Range update + range query-র দুই BIT কৌশলও শুধু যোগের জন্য। গবেষণায় BIT দিয়ে রেঞ্জ min-এর কিছু রূপ আছে (দুটি BIT রেখে, কিছু শর্তে), কিন্তু সেগুলো জটিল, আর সাধারণ কনটেস্ট-কোডে প্রায় ব্যবহৃত হয় না।

বাস্তবে প্রকৌশলীরা প্রায়ই প্রথমে জিজ্ঞেস করেন: কাজটি কি যোগফল বা গণনা? হলে BIT — ছোট, দ্রুত, ভুলের সুযোগ কম। না হলে segment tree.

মূল উপলব্ধি: “সবচেয়ে শক্তিশালী” কাঠামো সবসময় সবচেয়ে ভালো নয়। সমস্যার বীজগণিত — কাজটি উল্টানো যায় কি না — দেখে সবচেয়ে সরল যথেষ্ট কাঠামো বেছে নেওয়াই দক্ষ প্রোগ্রামারের লক্ষণ।

প্র ০৩1-based না হলে কি Fenwick tree হয় না? 0-based রূপ i | (i + 1) কীভাবে কাজ করে, আর 2's complement কেন এখানে এত স্বাভাবিক?

১-based রূপটি সবচেয়ে প্রচলিত, কারণ সেখানে সব কিছু একটি সুন্দর বাক্যে বলা যায়: “t[i]-এর ব্লকের দৈর্ঘ্য lowbit(i)”. কিন্তু 0-based একটি সমতুল্য রূপও আছে, যা কিছু লাইব্রেরি আর টিউটোরিয়ালে ব্যবহৃত হয়। সেখানে t[i] রাখে a[g(i) … i], যেখানে g(i) = i & (i + 1) — i-এর ডানদিকের সব পরপর 1-কে 0 করে দেওয়া। query-তে i = g(i) − 1 করে নামা, আর update-এ i = i | (i + 1) করে ওঠা — অর্থাৎ ডান দিকের প্রথম 0-কে 1 করা।

দুটি রূপ আসলে একই কাঠামো, শুধু index এক ঘর সরানো: 0-based-এর i মানে 1-based-এর i + 1. 1-based-এ “সবচেয়ে নিচের 1-bit” যা করে, 0-based-এ “ডানদিকের পরপর 1-গুলো” সেই কাজ করে, কারণ i + 1-এর সবচেয়ে নিচের 1-bit ঠিক i-এর ডানের পরপর 1-এর পরের ঘরে। কোনটি বেছে নেবেন, তা অভ্যাস আর আশপাশের কোডের ওপর নির্ভর করে — কিন্তু দুটি রূপ মিশিয়ে ফেলা সবচেয়ে বিপজ্জনক ভুলগুলোর একটি।

আর 2's complement? প্রায় সব আধুনিক কম্পিউটার ঋণাত্মক পূর্ণসংখ্যা এভাবেই রাখে, কারণ এতে যোগ আর বিয়োগের জন্য একই সার্কিট চলে, আর শূন্যের একটিই রূপ থাকে (1's complement-এ +0 আর −0 দুটি)। C++20 থেকে মানক নিজেই নিশ্চিত করে যে signed পূর্ণসংখ্যা 2's complement-এ রাখা হয়, তাই i & -i এখন মানক অনুযায়ীও নিরাপদ। −i = ~i + 1 — এই এক লাইনের সংজ্ঞাই lowbit-কে এক নির্দেশে বের করতে দেয়। অনেক প্রসেসরে এ ধরনের bit-কাজের জন্য আলাদা নির্দেশও আছে, যেমন x86-এর BMI1 সম্প্রসারণে সবচেয়ে নিচের 1-bit আলাদা করার নির্দেশ।

মূল উপলব্ধি: Fenwick tree হার্ডওয়্যারের সংখ্যা-উপস্থাপনের সাথে গভীরভাবে জড়িত। index কোথা থেকে শুরু হবে, তা একটি নকশার সিদ্ধান্ত — কিন্তু সিদ্ধান্ত নেওয়ার পর পুরো কোডে একটিই নিয়ম রাখতে হবে।

প্র ০৪Inversion গোনা শুধু পরীক্ষার প্রশ্ন নয় — দুটি র‍্যাঙ্কিং কতটা মেলে, সেই পরিসংখ্যানের সাথে এর সম্পর্ক কী, আর BIT কেন সেখানে কাজে লাগে?

১৯৩৮ সালে ব্রিটিশ পরিসংখ্যানবিদ Maurice Kendall একটি প্রশ্ন নিয়ে কাজ করেন: দুজন বিচারক একই প্রতিযোগীদের আলাদাভাবে র‍্যাঙ্ক করলে তাঁদের মতামত কতটা মেলে? তাঁর উত্তর Kendall-এর τ (tau) সহগ: প্রতিটি প্রতিযোগী-জোড়া দেখুন, দুই বিচারক তাদের একই ক্রমে রেখেছেন কি না। দ্বিমতের জোড়ার সংখ্যাকে বলে Kendall tau distance. প্রথম বিচারকের ক্রমে প্রতিযোগীদের সাজিয়ে দ্বিতীয় বিচারকের র‍্যাঙ্ক লিখলে যে অ্যারে পাওয়া যায়, তার inversion-সংখ্যাই ঠিক এই দূরত্ব।

সরাসরি সংজ্ঞায় সব জোড়া দেখতে হয় — O(n²). দশ লাখ আইটেমের র‍্যাঙ্কিংয়ে (যেমন একটি সার্চ ইঞ্জিনের দুটি সংস্করণের ফলাফল, বা দুটি সুপারিশ-ব্যবস্থার তালিকা) সেটি 5 × 1011 জোড়া, অচল। ১৯৬৬ সালে William Knight দেখান যে merge sort-ভিত্তিক গণনায় এটি O(n log n)-এ হয় — পর্ব ২১-এর সেই কৌশল। BIT একই সময় দেয়, আর তার একটি বাড়তি সুবিধা আছে: সে online. নতুন উপাদান ডানে যোগ হলে মাত্র O(log n)-এ জানা যায় সে কয়টি নতুন inversion আনল, পুরো অ্যারে আবার সর্ট না করে।

একই ধাঁচের অনেক প্রশ্ন BIT-এ স্বাভাবিকভাবে আসে: প্রতিটি উপাদানের ডানে কয়টি ছোট (অনুশীলন ৪), কয়টি জোড়ার যোগফল একটি সীমার মধ্যে, বা 2D-তে “কয়টি বিন্দু এই বিন্দুর বাঁ-নিচে” (একটি মাত্রায় সর্ট, অন্যটিতে BIT). পরিসংখ্যানে সমান মান (tie) নিয়ে আলাদা সূত্রও আছে — ঠিক যে কারণে আমাদের কোডে prefix(r)-এ ≤ গোনা, যাতে সমান মান দ্বিমত হিসেবে না পড়ে।

বাস্তবে এর ব্যবহার আছে সার্চের ফলাফলের তুলনায়, জীববিজ্ঞানে জিনের ক্রম তুলনায়, আর মেশিন লার্নিংয়ে র‍্যাঙ্কিং-মডেলের মূল্যায়নে।

মূল উপলব্ধি: একটি পরিসংখ্যানের প্রশ্ন (“দুটি র‍্যাঙ্কিং কতটা আলাদা”) আর একটি অ্যালগরিদমের প্রশ্ন (“কয়টি inversion”) আসলে একই। সঠিক ডেটা স্ট্রাকচার জানলে O(n²)-এর হিসাব O(n log n)-এ নামে, আর অনেক সময় online-ও হয়।

প্র ০৫কনটেস্টে কেন বলা হয় “BIT-এর ওপর binary search কোরো না, binary lifting করো” — log² n আর log n-এর পার্থক্য কি বাস্তবে গুরুত্বপূর্ণ?

k-তম উপাদান খোঁজার প্রথম যে উপায় মাথায় আসে, তা হলো v-এর ওপর binary search: মাঝের v নিয়ে prefix(v) হিসাব করুন, ≥ k হলে বাঁয়ে, নইলে ডানে। এতে log n টি ধাপ, প্রতিটিতে একটি O(log n) prefix — মোট O(log² n). n = 106-এ log₂ n ≈ 20, তাই প্রতি প্রশ্নে প্রায় ৪০০টি ঘর-পড়া; binary lifting-এ মাত্র ২০টি। দশ লাখ প্রশ্নে পার্থক্য প্রায় 4 × 108 বনাম 2 × 107 — অনেক judge-এ এটিই সময়ের সীমার ভেতরে বা বাইরে থাকার পার্থক্য।

binary lifting কেন কাজ করে, তা BIT-এর গঠন থেকে আসে। আমরা উত্তর pos-কে ওপরের bit থেকে নিচের দিকে বানাই। যখন step পরীক্ষা করি, pos-এ শুধু step-এর চেয়ে বড় 2-এর ঘাত আছে, তাই pos + step-এর সবচেয়ে নিচের 1-bit ঠিক step. ফলে t[pos + step] হুবহু (pos, pos + step]-এর যোগফল — যে তথ্য binary search আলাদা prefix হিসাব করে পেত, তা এখানে এক ঘর পড়েই পাওয়া যায়। এটি পর্ব ৪৮-এর tree-তে binary lifting-এর মতোই ধারণা: বড় লাফ আগে, যতক্ষণ লক্ষ্য না ছাড়ায়।

একই কৌশল আরও কাজে লাগে: “prefix sum প্রথম কোথায় একটি মান ছাড়ায়” (সব মান অ-ঋণাত্মক হলে), যেমন ওজন-অনুযায়ী এলোমেলো নমুনা নেওয়া — প্রতিটি আইটেমের ওজন BIT-এ, একটি এলোমেলো সংখ্যা r নিয়ে খোঁজা কোন আইটেমে cumulative ওজন r ছাড়ায়, আর ওজন বদলালে O(log n) আপডেট। ঠিক এই ধাঁচেই arithmetic coding-এর decoder অক্ষর খোঁজে।

একটি সতর্কতা: ঋণাত্মক মান থাকলে prefix একমুখী থাকে না, তখন binary search আর binary lifting দুটোই ভুল উত্তর দিতে পারে — AI-এর কোডে এই শর্তটি প্রায়ই বাদ পড়ে।

মূল উপলব্ধি: একটি ডেটা স্ট্রাকচারের ভেতরের গঠন জানলে তার ওপর “সাধারণ” অ্যালগরিদম চাপানোর বদলে গঠনটিকেই কাজে লাগানো যায় — আর log-এর একটি গুণক বাঁচানো বাস্তবে বড় পার্থক্য আনে।

অনুশীলন

খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।

  1. হাতে trace করুন: n = 16-এর একটি BIT-এ (ক) prefix(11) কোন কোন t[i] যোগ করে, আর প্রতিটি কোন অংশ ঢাকে? (খ) add(11, v) কোন কোন t[i] বদলায়? binary রূপসহ লিখুন।

    (ক) 11 = 1011 → t[11] (lowbit 1: a[11]) → 1010 = 10 → t[10] (lowbit 2: a[9..10]) → 1000 = 8 → t[8] (lowbit 8: a[1..8]) → 0. তিনটি ঘর, কারণ 11-এ তিনটি 1-bit; ব্লক (1..8), (9..10), (11) পাশাপাশি বসে 1..11 ঢাকে।

    (খ) 11 = 1011 → +1 → 1100 = 12 → +4 → 10000 = 16 → +16 = 32 > 16, থামুন। বদলায় t[11], t[12], t[16]. যাচাই: t[12]-এর ব্লক a[9..12], t[16]-এর a[1..16] — দুটিতেই 11 আছে; আর t[13], t[14], t[15]-এর ব্লক (a[13], a[13..14], a[15]) 11 ধরে না।

  2. দুই BIT হাতে চালান: n = 6, শুরুতে সব শূন্য। rangeAdd(2, 5, +4), তারপর rangeAdd(4, 6, +1). এখন S(5) = a[1] + … + a[5] কত? অংশ ৬-এর সূত্র S(x) = (x + 1)·B1.prefix(x) − B2.prefix(x) দিয়ে বের করুন, তারপর সরাসরি যোগ করে মেলান।

    rangeAdd(2, 5, +4): d[2] += 4, d[6] −= 4 → B1: ঘর 2-এ +4, ঘর 6-এ −4; B2: ঘর 2-এ 2·4 = 8, ঘর 6-এ −6·4 = −24. rangeAdd(4, 6, +1): d[4] += 1, আর r + 1 = 7 > n, বাদ → B1: ঘর 4-এ +1; B2: ঘর 4-এ 4.

    B1.prefix(5) = 4 + 1 = 5 (ঘর 6 বাদ), B2.prefix(5) = 8 + 4 = 12. S(5) = 6 · 5 − 12 = 18. সরাসরি: a = [0, 4, 4, 5, 5, 1], প্রথম পাঁচটির যোগ 0 + 4 + 4 + 5 + 5 = 18 — মিলে গেছে।

  3. k-তম খুঁজুন: মান 1..8, গণনা c = [1, 3, 0, 2, 1, 0, 0, 1]. আগে BIT t বানান, তারপর binary lifting দিয়ে ৫ম ছোট মানটি বের করুন — প্রতিটি step-এ কোন t পড়লেন আর pos ও k কী হলো লিখুন।

    t[1] = 1, t[2] = 1 + 3 = 4, t[3] = 0, t[4] = 1 + 3 + 0 + 2 = 6, t[5] = 1, t[6] = 1 + 0 = 1, t[7] = 0, t[8] = 8. অর্থাৎ t = [1, 4, 0, 6, 1, 1, 0, 8].

    pos = 0, k = 5. step 8: t[8] = 8, ৫-এর কম নয় — থাকুন। step 4: t[4] = 6, কম নয় — থাকুন। step 2: t[2] = 4 < 5 → pos = 2, k = 1. step 1: t[3] = 0 < 1 → pos = 3, k = 1. উত্তর pos + 1 = 4. যাচাই: সাজানো তালিকা 1, 2, 2, 2, 4, 4, 5, 8 — ৫ম মান 4.

  4. কোড লিখুন: একটি অ্যারে a দেওয়া আছে (মান −109 থেকে 109, ডুপ্লিকেট থাকতে পারে)। প্রতিটি i-এর জন্য বের করুন i-এর ডানে কয়টি উপাদান a[i]-এর চেয়ে কঠোরভাবে ছোট। O(n log n) চাই। a = [5, 2, 6, 1] দিয়ে হাতে মেলান।

    ডান থেকে বাঁয়ে হাঁটুন; BIT রাখে এ পর্যন্ত দেখা (ডানের) মানগুলোর গণনা। a[i]-এর rank r হলে “ডানে কঠোরভাবে ছোট” = prefix(r − 1); তারপর add(r, 1).

    // returns cnt[i] = number of j > i with a[j] < a[i]
    vector<int> smaller_to_right(const vector<long long>& a) {
        vector<long long> vals(a);
        sort(vals.begin(), vals.end());
        vals.erase(unique(vals.begin(), vals.end()), vals.end());
        int k = int(vals.size()), n = int(a.size());
        vector<int> t(k + 1, 0), cnt(n);
        for (int i = n - 1; i >= 0; --i) {
            int r = int(lower_bound(vals.begin(), vals.end(), a[i]) - vals.begin()) + 1;
            int s = 0;
            for (int j = r - 1; j > 0; j -= j & -j) s += t[j];   // strictly smaller: prefix(r - 1)
            cnt[i] = s;
            for (int j = r; j <= k; j += j & -j) t[j] += 1;
        }
        return cnt;
    }

    হাতে: rank 5→3, 2→2, 6→4, 1→1. ডান থেকে: 1 (r = 1): prefix(0) = 0. 6 (r = 4): prefix(3) = 1 (মান 1). 2 (r = 2): prefix(1) = 1. 5 (r = 3): prefix(2) = 2 (মান 2, 1). উত্তর [2, 1, 1, 0]. সময় O(n log n) (সর্ট আর n টি BIT-কাজ), জায়গা O(n). পরীক্ষা করুন সমান মানেও: [3, 3] → [0, 0].

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমFenwick tree দিয়ে একটি অ্যারের inversion গোনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. মানগুলো সর্ট করে ডুপ্লিকেট বাদ দেওয়া, আর প্রতিটি মানকে তার rank 1..k দিয়ে বদলানো
  2. k + 1 আকারের একটি শূন্য BIT তৈরি করা
  3. বাঁ থেকে ডানে প্রতিটি উপাদানের rank r-এর জন্য prefix(r) বের করা — আগে দেখা কয়টি মান ≤ এই মান
  4. উত্তরে (এ পর্যন্ত দেখা উপাদানের সংখ্যা − prefix(r)) যোগ করা
  5. add(r, 1) দিয়ে এই উপাদানটিকে BIT-এ ঢোকানো, তারপর পরের উপাদানে যাওয়া

Fenwick tree-তে lowbit(40) = 40 & −40 কত, আর তাই t[40] কোন অংশের যোগফল রাখে?

  • 2, তাই a[39..40]
  • 4, তাই a[37..40]
  • 8, তাই a[33..40]
  • 40, তাই a[1..40]

১৬ ঘরের একটি Fenwick tree-তে (1-based) prefix(13) হিসাব করতে কোন t-গুলো যোগ হয়?

  • t[13], t[14], t[16]
  • t[13], t[12], t[8]
  • t[13], t[12], t[11], …, t[1]
  • t[16] থেকে t[14], t[15] বাদ দিয়ে

n টি ঘরের Fenwick tree-তে একটি point update সর্বোচ্চ কতটি t-ঘর বদলায়, আর কেন?

  • n টি, কারণ পরের সব prefix বদলায়
  • i-এর 1-bit-এর সংখ্যার সমান
  • প্রায় √n টি
  • ⌊log₂ n⌋ + 1 টি, কারণ প্রতি ধাপে সবচেয়ে নিচের 1-bit অন্তত এক ঘর ওপরে ওঠে

কোন কাজের জন্য সাধারণ Fenwick tree (point update + range query) সরাসরি ব্যবহার করা যায় না?

  • ইচ্ছামতো point update-সহ রেঞ্জের minimum
  • ইচ্ছামতো point update-সহ রেঞ্জের XOR
  • একটি মানের সীমার মধ্যে কয়টি উপাদান, তা গোনা
  • ইচ্ছামতো point update-সহ রেঞ্জের যোগফল

দুই BIT দিয়ে range update + range query করা হচ্ছে: B1 রাখে d[j], B2 রাখে j · d[j], যেখানে d হলো difference array. তাহলে S(x) = a[1] + … + a[x] কোনটি?

  • x · B1.prefix(x) − B2.prefix(x)
  • (x + 1) · B1.prefix(x) − B2.prefix(x)
  • B1.prefix(x)
  • (x + 1) · B1.prefix(x) + B2.prefix(x)

অ্যারে [3, 1, 3, 2]-এ inversion কয়টি (i < j আর a[i] > a[j] — সমান মান গোনা হয় না)?

  • 2
  • 4
  • 3
  • 5

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. t[i] = a[i − lowbit(i) + 1 … i], lowbit(i) = i & −i (2's complement); index 1..n, কারণ lowbit(0) = 0.
  2. prefix: i -= lowbit(i), নিচে নামা (একটি 1-bit মোছে); add: i += lowbit(i), ওপরে ওঠা; দুটিই ≤ ⌊log₂ n⌋ + 1 ধাপ → O(log n).
  3. Build O(n): t = a, তারপর t[i + lowbit(i)] += t[i]; রেঞ্জ = prefix(r) − prefix(l − 1).
  4. Difference array-এর ওপর BIT → range update + point query; দুই BIT → range update + range query: S(x) = (x + 1)·Σd − Σ j·d[j]; 2D BIT O(log n · log m).
  5. Binary lifting → k-তম উপাদান O(log n); compression + BIT → inversion O(n log n); শুধু উল্টানো যায় এমন কাজে রেঞ্জ query (যোগ, XOR) — min/max-এ segment tree.

পরের পর্বে: অ্যারে যদি একেবারেই না বদলায়, তবে রেঞ্জের minimum O(1)-এ — sparse table. আর √n আকারের টুকরোয় ভাগ করার সরল কিন্তু বহুমুখী কৌশল sqrt decomposition, সাথে প্রশ্নগুলো চতুরভাবে সাজিয়ে চালানোর Mo’s algorithm.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান