012 — BIT-accelerated LIS¶
Source¶
- Original source: self-exercise bridging to DP (folder 12 LIS-এর সাথে সেতু)
- Platform: self-exercise — judge নেই, ইচ্ছে করেই DP + BIT-এর সংযোগ অনুশীলন
- Topic: segment tree and fenwick tree
- Difficulty: Medium
- Pattern: BIT accelerates DP transitions (value-indexed tree)
- Basic idea inherited from: folder 12 LIS + এই folder (BIT over values)
1. Problem in simple words¶
একটা array a দেওয়া। Longest Increasing Subsequence (LIS)-এর দৈর্ঘ্য বের করো — strictly increasing এমন সবচেয়ে লম্বা subsequence-এর length। (Folder 12 শেষ করার পরে এই note লেখো।) লক্ষ্য: O(n^2) DP-টাকে একটা value-indexed BIT দিয়ে O(n log n)-এ তোলা।
2. Which basic idea does this inherit from?¶
এটা folder 12-র LIS DP আর এই folder-এর BIT-over-values-এর সংযোগস্থল। LIS-এর classic DP তুমি folder 12-তে দেখেছ; এখানে সেই DP-র "আমার আগে আমার চেয়ে ছোট সব value-র মধ্যে best কে?" — এই transition-টা BIT দিয়ে দ্রুত করি।
3. What is the hidden math or logic?¶
লুকানো logic: LIS DP বলে dp[i] = 1 + max(dp[j] for j < i, a[j] < a[i])। ভেতরের max-টা একটা prefix-max over values — "আমার চেয়ে ছোট সব value-র মধ্যে এ পর্যন্ত দেখা সর্বোচ্চ dp কত?"। BIT এই prefix-max O(log n)-এ দিতে পারে।
4. Which data structure is needed?¶
একটা max-Fenwick tree, value-দের rank-এ indexed। সাধারণ BIT sum জমায়; এখানে আমরা একই lowbit-navigation-এ max জমাই। Value বড় হতে পারে, তাই আগে coordinate compression।
5. Why this data structure fits¶
LIS-এর bottleneck হলো প্রতি element-এ "আগের ছোট value-গুলোর best dp"। সেটা একটা value-axis-এর উপর prefix-max query — আর max-BIT ঠিক এই prefix-max (1..r) O(log n)-এ দেয়, point update-ও O(log n)। তাই n element-এ O(n log n)। (Max prefix invertible না বলে এটা শুধু prefix [1..r] max-এ কাজ করে — arbitrary range-min/max-এ segment tree লাগত, কিন্তু LIS-এ prefix-ই যথেষ্ট।)
6. Real-life analogy¶
ভাবো তুমি পাহাড়ে চড়ছ, প্রতিটা ক্যাম্পের একটা উচ্চতা আছে। প্রতিটা নতুন ক্যাম্পে দাঁড়িয়ে জানতে চাও — আমার চেয়ে নিচু সব ক্যাম্পের মধ্যে কোনটা থেকে এলে এ পর্যন্ত সবচেয়ে লম্বা চড়াই-পথ হবে? প্রতিবার সব নিচু ক্যাম্প ঘুরে দেখার বদলে, উচ্চতা-অনুযায়ী সাজানো একটা "best so far" board রাখো; নতুন উচ্চতায় শুধু তোমার-নিচের অংশটা দেখো।
7. Visual explanation¶
a = [3, 1, 4, 1, 5] compressed rank (1->1, 3->2, 4->3, 5->4):
a: 3 1 4 1 5
rank: 2 1 3 1 4
প্রক্রিয়া বাঁ -> ডান, max-BIT-এ rank-এর জায়গায় dp জমাই:
x=3 (rank2): prefix_max(1) = 0 -> dp=1; update(2, 1)
x=1 (rank1): prefix_max(0) = 0 -> dp=1; update(1, 1)
x=4 (rank3): prefix_max(2) = 1 -> dp=2; update(3, 2)
x=1 (rank1): prefix_max(0) = 0 -> dp=1; update(1, 1) (already 1)
x=5 (rank4): prefix_max(3) = 2 -> dp=3; update(4, 3)
answer = max dp = 3 ([1,4,5])
8. Example input and output¶
input: a = [3, 1, 4, 1, 5, 9, 2, 6]
output: 4 ([1,4,5,9] বা [1,4,5,6])
input: a = [7, 7, 7]
output: 1 (strictly increasing, তাই duplicate গোনা যায় না)
input: a = [1, 2, 3, 4, 5]
output: 5 (পুরোটাই increasing)
9. Brute force thinking¶
Classic O(n^2) DP: প্রতি i-র জন্য সব j < i ঘুরে, a[j] < a[i] হলে dp[i] = max(dp[i], dp[j] + 1)।
for i in range(n):
dp[i] = 1
for j in range(i):
if a[j] < a[i]:
dp[i] = max(dp[i], dp[j] + 1)
answer = max(dp)
নির্ভুল, আর এটাই আমাদের reference।
10. Why brute force is slow¶
দুই nested loop = O(n^2)। n = 100,000 হলে 10^10 — TLE। ভেতরের loop আসলে একটা prefix-max query; সেটা BIT-এ O(log n)-এ করলে পুরোটা O(n log n)।
11. Key observation¶
মূল observation: DP-র ভেতরের max(dp[j] for a[j] < a[i]) একটা prefix-max over values। i বাড়ার সাথে সাথে আমরা value-axis-এ dp জমাচ্ছি; নতুন element-এ শুধু "আমার rank-এর নিচের সব" prefix-max চাই। এটাই BIT-এর কাজ।
12. Optimized intuition¶
element-গুলো বাঁ থেকে ডানে process করো (index-order রাখা জরুরি, যাতে শুধু আগের element-রাই BIT-এ থাকে)। প্রতি x-এ: rank r বের করো, best = prefix_max(r - 1) (strictly ছোট value), dp = best + 1, তারপর point_update_max(r, dp)। উত্তর = সব dp-র max। Strict চাই বলে r - 1 (নিজের সমান value বাদ)।
13. Thinking tweak¶
মোচড়: DP transition-কে একটা "range query"-তে দেখো। তখন ধীর ভেতরের loop-টাকে একটা data structure (এখানে value-indexed max-BIT) দিয়ে replace করা যায়। এই DP + BIT/segment tree জোড়া অনেক counting/optimization DP-কে দ্রুত করে।
14. Step-by-step dry run¶
a = [3, 1, 4, 1, 5], compressed rank {1:1, 3:2, 4:3, 5:4}, max-BIT খালি। বাঁ → ডান:
- x=3 (r=2):
prefix_max(1)=0→ dp=1;update(2, 1)। - x=1 (r=1):
prefix_max(0)=0→ dp=1;update(1, 1)। - x=4 (r=3):
prefix_max(2)=1→ dp=2;update(3, 2)। - x=1 (r=1):
prefix_max(0)=0→ dp=1;update(1, 1)(1-ই থাকে)। - x=5 (r=4):
prefix_max(3)=2→ dp=3;update(4, 3)। - answer = max(1,1,2,1,3) = 3। ✓
15. Python solution¶
class FenwickMax:
"""1-indexed max-Fenwick। index মানে একটা VALUE-র rank।
update(i, val): tree[i]-কে max(., val) করো (point);
prefix_max(i): 1..i-র max। দুটোই O(log n)।
সতর্কতা: max invertible নয় -> শুধু PREFIX [1..i] max পাওয়া যায়,
arbitrary range নয় (এই problem-এ prefix-ই যথেষ্ট)।"""
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1) # dp মান >= 1, তাই 0 = "এখনো কিছু নেই"
def update(self, i, val):
while i <= self.n:
if val > self.tree[i]:
self.tree[i] = val
i += i & (-i)
def prefix_max(self, i):
best = 0
while i > 0:
if self.tree[i] > best:
best = self.tree[i]
i -= i & (-i)
return best
def lis_length_bit(a):
if not a:
return 0
# coordinate compression: value -> 1-indexed rank
order = sorted(set(a))
rank = {v: idx + 1 for idx, v in enumerate(order)}
bit = FenwickMax(len(order))
best_overall = 0
for x in a: # বাঁ -> ডান, index-order
r = rank[x]
cur = bit.prefix_max(r - 1) + 1 # strictly ছোট value-র best + 1
bit.update(r, cur)
if cur > best_overall:
best_overall = cur
return best_overall
def lis_length_brute(a):
# obviously-correct O(n^2) DP reference
n = len(a)
if n == 0:
return 0
dp = [1] * n
for i in range(n):
for j in range(i):
if a[j] < a[i] and dp[j] + 1 > dp[i]:
dp[i] = dp[j] + 1
return max(dp)
# ---- tests ----
a = [3, 1, 4, 1, 5, 9, 2, 6]
assert lis_length_bit(a) == 4
assert lis_length_brute(a) == 4
assert lis_length_bit([7, 7, 7]) == 1 # strict: duplicate গোনা যায় না
assert lis_length_bit([1, 2, 3, 4, 5]) == 5 # পুরোটাই increasing
assert lis_length_bit([5, 4, 3, 2, 1]) == 1 # পুরোটাই decreasing
assert lis_length_bit([]) == 0 # empty
assert lis_length_bit([42]) == 1 # single
# BIT version সবসময় brute DP-র সাথে একমত (duplicate, negative-সহ)
import random
for _ in range(400):
arr = [random.randint(-5, 5) for _ in range(random.randint(0, 14))]
assert lis_length_bit(arr) == lis_length_brute(arr)
print("সব test pass!")
16. Line-by-line code explanation¶
FenwickMax— সাধারণ BIT-এর lowbit navigation, কিন্তু sum-এর বদলে point-max জমায় (prefix_maxশুধু [1..i]-এ valid)।order/rank— coordinate compression, value →1..krank; বড়/negative value সামলায়।lis_length_bit— বাঁ থেকে ডানে;prefix_max(r-1)দেয় strictly ছোট value-র best dp, তারপরupdate(r, cur)।r - 1— strict increasing চাই বলে নিজের সমান value বাদ।brute— O(n^2) DP, নির্ভুল reference।
17. Output walkthrough¶
[3,1,4,1,5,9,2,6]-এ BIT version দেয় 4, brute DP-ও 4। তারপর 400-টা random array-তে — duplicate আর negative-সহ — দুই পদ্ধতি সবসময় একমত। [7,7,7] strict বলে 1 দেয়। শেষে print: সব test pass!।
18. Time complexity¶
O(n log n): প্রতি element-এ একটা prefix_max + একটা update, প্রতিটা O(log n); compression-এর sort-ও O(n log n)।
19. Space complexity¶
O(n): max-BIT array + rank map।
20. Common mistakes¶
prefix_max(r)লেখা যেখানেprefix_max(r - 1)দরকার — তাহলে নিজের সমান value-ও ধরা পড়ে, strict ভেঙে যায়।- max-BIT-এ arbitrary range-max চাওয়া — max invertible নয়, শুধু prefix [1..i] valid; arbitrary range-এ segment tree লাগত।
- Coordinate compression বাদ দেওয়া — বড়/negative value-তে BIT index ভেঙে পড়ে।
- element-গুলো index-order ছাড়া process করা — তখন ভবিষ্যতের element BIT-এ ঢুকে subsequence-এর order ভাঙে।
21. Which basic problem this inherits from¶
ভিত্তি: folder 12-র LIS DP + এই folder-এর BIT-over-values (../fenwick-tree.md section 9)। এটা দেখায় কীভাবে একটা DP transition-কে range query-তে দেখে data structure দিয়ে accelerate করা যায়।
22. Similar harder problems¶
- Number of Longest Increasing Subsequence (length নয়, count — BIT-এ (max_len, count) pair জমাও) — https://leetcode.com/problems/number-of-longest-increasing-subsequence/
- Russian Doll Envelopes (2D LIS, sort + LIS) — https://leetcode.com/problems/russian-doll-envelopes/
- Longest Increasing Subsequence (patience-sorting O(n log n)) — https://leetcode.com/problems/longest-increasing-subsequence/
23. Pattern learned¶
BIT accelerates DP transitions: যখন একটা DP-র ভেতরের loop আসলে "value-axis-এর উপর prefix max/sum", তখন value-indexed BIT সেটা O(log n)-এ করে পুরো DP-কে O(n log n)-এ তোলে। DP + data-structure সংযোগের প্রথম স্বাদ।
24. Final summary¶
ভবিষ্যতের আমাকে: LIS-এর ভেতরের max(dp[j] : a[j] < a[i]) একটা prefix-max over compressed values — value-indexed max-BIT দিয়ে O(log n)। মূল শিক্ষা: একটা DP transition-কে range query হিসেবে চিনতে পারলে BIT/segment tree দিয়ে সেটা accelerate করা যায়।
মনে রাখার নিয়ম (legal): সব নিজের ভাষায় লেখা; problem statement copy করা হয়নি; official link শুধু attribution-এর জন্য; "asked by Google/Amazon" দাবি নেই — এটা একটা common interview pattern।