Skip to content

081 — Two Sum Sorted

এই note দিয়ে তুমি two pointers পরিবারের জন্মস্থানে পা রাখছ। আগের level-এ Two Sum-কে আমরা hash map দিয়ে সামলেছি; এখানে array আগে থেকে sorted — আর এই একটা শর্ত পুরো খেলা বদলে দেয়। দুই প্রান্ত থেকে দুটো আঙুল রেখে আমরা প্রতি step-এ search space নিশ্চিতভাবে ছোট করব। ধীরে পড়ো — "কেন একটা move নিরাপদ" এই যুক্তিটাই এখানকার আসল রত্ন।

Source

  • Original source: LeetCode Two Sum II — Input Array Is Sorted; CSES Sum of Two Values
  • Platform: LeetCode — https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/; CSES — https://cses.fi/problemset/task/1640
  • Topic: Two pointers → opposite-end scan on sorted array
  • Difficulty: Easy
  • Pattern: opposite pointers (দুই প্রান্ত থেকে চাপা)
  • Basic idea inherited from: 073 — Subarray Sum Equals K (ওখানে hash map পথ, এখানে sorted-এ pointer পথ — তুলনাটাই শিক্ষা)

1. Problem in simple words

একটা sorted (ছোট থেকে বড়) array a আর একটা target সংখ্যা দেওয়া আছে। তোমাকে এমন দুটো index (i, j) খুঁজতে হবে যাদের a[i] + a[j] == target, যেখানে i < j। ঠিক একটাই উত্তর আছে ধরে নাও; না পেলে None

মূল কথা: array sorted — এই তথ্যটাকে কাজে লাগানোই এই problem-এর প্রাণ। Unsorted হলে এই কৌশল খাটত না (তখন 073-এর hash map পথ লাগত)।

2. What is the math idea?

মূল idea হলো monotonicity (একমুখী আচরণ)। Array sorted বলে:

  • বাঁ পয়েন্টার ডানে সরালে (l += 1) যোগফল বাড়ে (বড় সংখ্যা ঢুকল)
  • ডান পয়েন্টার বাঁয়ে সরালে (r -= 1) যোগফল কমে (বড় সংখ্যা বেরোল)

মানে দুই প্রান্তের sum-টা একটা "নব"-এর মতো — দুই দিকে ঘুরিয়ে target-এর দিকে নেওয়া যায়। এই predictable আচরণই O(n²)-কে O(n)-এ নামায়।

3. Which basic idea does this inherit from?

এটা দাঁড়িয়ে আছে 073 — Subarray Sum Equals K-এর কাঁধে। 073-এ আমরা "দুটো জিনিসের সম্পর্ক" খুঁজতে hash map ব্যবহার করেছি — প্রতিটা element দেখে জিজ্ঞেস করেছি "এর জুটি আগে দেখেছি কি?"।

এখানে array sorted বলে hash map-এর দরকারই নেই — sorted-এর structure নিজেই জুটি খুঁজে দেয়। তাই এই note আসলে একটা তুলনা শেখায়:

  • Unsorted + index দরকার → hash map (073-এর পথ), O(n) time + O(n) space
  • Sorted → opposite pointers (এই পথ), O(n) time + O(1) space

কোন তথ্য হাতে আছে, তার উপর হাতিয়ার বদলায় — এই বিচারটাই বড় শিক্ষা।

4. Real-life analogy

ভাবো একটা লম্বা বইয়ের তাক, বাঁ থেকে ডানে বইগুলো দাম অনুযায়ী সাজানো (সস্তা থেকে দামি)। তোমাকে ঠিক target টাকায় দুটো বই কিনতে হবে।

তুমি এক হাত রাখলে সবচেয়ে সস্তা বইয়ে (বাঁ প্রান্ত), আরেক হাত সবচেয়ে দামি বইয়ে (ডান প্রান্ত)। দুটোর দাম যোগ করলে:

  • যোগফল বেশি? → দামি হাতটা একটু বাঁয়ে সরাও (একটু সস্তা বই নাও)
  • যোগফল কম? → সস্তা হাতটা একটু ডানে সরাও (একটু দামি বই নাও)

প্রতি step-এ তুমি একটা প্রান্তের বই বাদ দিয়ে দিচ্ছ — আর সেই বইটা আর কখনো লাগবে না। এই "বাদ দিলে আর ফিরব না" ব্যাপারটাই two pointers-কে দ্রুত করে।

5. Visual explanation

দুই আঙুল কীভাবে ভেতরের দিকে আসে, একটা example-এ দেখো (a = [1, 3, 4, 6, 9], target = 7):

a = [ 1 ,  3 ,  4 ,  6 ,  9 ]
      ^                   ^
      l                   r        a[l]+a[r] = 1+9 = 10 > 7  → r--

a = [ 1 ,  3 ,  4 ,  6 ,  9 ]
      ^              ^
      l              r            a[l]+a[r] = 1+6 = 7  == 7  → পাওয়া গেল! (0, 3)

প্রতিটা তীর ভেতরে এসে search space ছোট করছে। মোট window-টা প্রতি step-এ অন্তত ১ ঘর সংকুচিত হয়:

sum বড় হলে:  [ l ............... r ]  →  [ l ............ r ]   (r বাঁয়ে)
sum ছোট হলে:  [ l ............... r ]  →  [    l ......... r ]   (l ডানে)
সমান হলে   :  থামো — উত্তর পেয়ে গেছ

6. Example input and output

a (sorted)            target  ->  output
----------------------------------------
[1, 3, 4, 6, 9]         10    ->  (0, 4)   (1 + 9)
[1, 3, 4, 6, 9]          7    ->  (0, 3)   (1 + 6)
[2, 3, 4]                6    ->  (0, 2)   (2 + 4)
[2, 7, 11, 15]          26    ->  (2, 3)   (11 + 15)
[1, 2]                   3    ->  (0, 1)
[1, 2, 3]               7    ->  None      (কোনো জুটি নেই)

Edge case: জুটি না থাকলে None; দুইয়ের কম element থাকলেও None (জুটিই হয় না)।

7. Brute force thinking

প্রথমে যেটা মাথায় আসে — সব জোড়া ধরে ধরে দেখা:

def two_sum_brute(a, target):
    n = len(a)
    for i in range(n):
        for j in range(i + 1, n):
            if a[i] + a[j] == target:
                return (i, j)
    return None

এটা ঠিক উত্তর দেয়, sorted না হলেও চলে। কিন্তু এখানে sorted-এর সুবিধাটা একদম ব্যবহারই করছি না।

8. Why brute force may be slow

বাইরের loop n বার, ভেতরেরটা গড়ে n/2 বার — মোট প্রায় n²/2 জোড়া। n = 100000 হলে প্রায় ৫০০ কোটি operation — interview-তে Time Limit Exceeded।

n = 100000 হলে:
  brute force : ~5,000,000,000 জোড়া যাচাই   (O(n²), ধীর)
  two pointers: বড়জোর 100000 step           (O(n), দ্রুত)

মূল অপচয়: sorted array-তে আমরা জানি কোন দিকে গেলে sum বাড়বে/কমবে — তবু brute force সেই জ্ঞান ফেলে সব জোড়া ঘাঁটছে।

9. Better thinking

মূল insight: sorted বলে দুই প্রান্তের sum দেখে নিশ্চিতভাবে বলা যায় কোন প্রান্ত বাদ দেওয়া নিরাপদ।

a[l] + a[r] > target হলে — a[r] খুব বড়। a[r]-এর জুটি হতে পারে শুধু a[l] বা তার ডানের কেউ, কিন্তু তারা সবাই a[l]-এর সমান বা বড়, তাই sum আরো বেশি হবে। মানে a[r]-এর কোনো বৈধ জুটিই নেই → a[r] চিরতরে বাদ → r -= 1। উল্টোভাবে sum ছোট হলে a[l] বাদ → l += 1

10. Thinking tweak

আসল "আহা!" মুহূর্ত: সব জোড়া দেখার দরকার নেই — শুধু দুই প্রান্ত দেখে এক প্রান্ত নিরাপদে ছেঁটে ফেলা যায়।

প্রতি step-এ ঠিক একটা element চিরতরে বিদায় নেয়। n টা element, প্রতিটা বড়জোর একবার বাদ পড়ে → মোট n step → O(n)। দ্বিমাত্রিক খোঁজ (সব জোড়া) এক মাত্রায় (এক pass) নেমে এল।

11. Step-by-step dry run

a = [1, 3, 4, 6, 9], target = 7 ধীরে চালাই:

step l r a[l] a[r] sum তুলনা action
1 0 4 1 9 10 10 > 7 r -= 1
2 0 3 1 6 7 7 == 7 পাওয়া গেল → return (0, 3)

দুই step-এই উত্তর। লক্ষ করো প্রথম step-এ আমরা index 4 (মান 9)-কে চিরতরে বাদ দিয়েছি — 9-এর সাথে আর কারো জুটি 7 হতে পারে না, তাই ফিরে দেখার দরকার নেই।

12. Python solution

def two_sum_sorted(a, target):
    """sorted array a-তে a[i]+a[j]==target এমন (i, j) ফেরত দেয় (i < j); না পেলে None।"""
    l, r = 0, len(a) - 1
    while l < r:
        s = a[l] + a[r]
        if s == target:
            return (l, r)
        if s > target:
            r -= 1
        else:
            l += 1
    return None


def two_sum_brute(a, target):
    """ধীর কিন্তু সরল reference — সব জোড়া যাচাই (test মেলানোর জন্য)।"""
    n = len(a)
    for i in range(n):
        for j in range(i + 1, n):
            if a[i] + a[j] == target:
                return (i, j)
    return None


# --- ছোট test গুলো (সব pass করা উচিত) ---
assert two_sum_sorted([1, 3, 4, 6, 9], 10) == (0, 4)
assert two_sum_sorted([1, 3, 4, 6, 9], 7) == (0, 3)
assert two_sum_sorted([2, 3, 4], 6) == (0, 2)
assert two_sum_sorted([2, 7, 11, 15], 26) == (2, 3)
assert two_sum_sorted([1, 2], 3) == (0, 1)
assert two_sum_sorted([1, 2, 3], 7) is None
assert two_sum_sorted([5], 5) is None          # একটামাত্র element, জুটি নেই

# brute force-এর সাথে মিলিয়ে দেখা (যেখানে উত্তর আছে)
for tgt in range(2, 19):
    arr = [1, 3, 4, 6, 9]
    fast = two_sum_sorted(arr, tgt)
    slow = two_sum_brute(arr, tgt)
    # দুটোর যেকোনো এক বৈধ জুটি দিলেই হয়; দুজনই None বা দুজনই জুটি
    assert (fast is None) == (slow is None)
    if fast is not None:
        assert arr[fast[0]] + arr[fast[1]] == tgt

print(two_sum_sorted([1, 3, 4, 6, 9], 10))   # (0, 4)
print(two_sum_sorted([2, 7, 11, 15], 26))    # (2, 3)
print(two_sum_sorted([1, 2, 3], 7))          # None
print("সব test pass!")

13. Line-by-line explanation

l, r = 0, len(a) - 1

দুটো আঙুল বসালাম — l একদম বাঁয়ে (সবচেয়ে ছোট), r একদম ডানে (সবচেয়ে বড়)।

while l < r:

দুজন না মেলা পর্যন্ত চলবে। l == r হলে একই element, জুটি হয় না — তাই strict <

s = a[l] + a[r]
if s == target: return (l, r)

দুই প্রান্তের যোগফল। target-এর সমান হলে সাথে সাথে উত্তর।

if s > target: r -= 1
else:          l += 1

বেশি হলে বড় দিক ছাঁটো (r--), কম হলে ছোট দিক ছাঁটো (l++)। প্রতি বার এক প্রান্ত নিশ্চিতভাবে বাদ।

return None

while শেষ মানে দুজন মিশে গেছে, কোনো জুটি মেলেনি।

two_sum_brute শুধু test যাচাইয়ের reference; বাকি assert গুলো নিজেদের চেক করে, সব ঠিক থাকলে শেষে সব test pass! ছাপে।

14. Output walkthrough

চালালে ছাপবে:

(0, 4)
(2, 3)
None
সব test pass!

প্রথম দুই লাইন দুটো সফল খোঁজ থেকে; তৃতীয় লাইন None কারণ [1, 2, 3]-এ যোগফল 7 হওয়ার জুটি নেই। তার আগের সব assert (brute-force মেলানো সহ) চুপচাপ pass করেছে, তাই শেষে সব test pass!

15. Time complexity

O(n) — প্রতি step-এ l বাড়ে বা r কমে, মানে window অন্তত ১ ঘর সংকুচিত হয়। মোট সংকোচন বড়জোর n বার, তাই বড়জোর n step। (Sorted না থাকলে আগে sort লাগত — তখন O(n log n); এখানে input sorted ধরা।)

16. Space complexity

O(1) — শুধু l, r, s — গুটিকয় variable। 073-এর hash map পথে O(n) space লাগত; sorted হওয়ায় এখানে বাড়তি জায়গা প্রায় শূন্য — এটাই pointer পথের বড় সুবিধা।

17. Common mistakes

  • Unsorted array-তে এই কৌশল চালানো — "sum বড় তাই r--" যুক্তিটা শুধু sorted-এ খাটে। আগে sort নিশ্চিত করো (আর sort করলে original index হারায় — index দরকার হলে সাবধান)।
  • while l <= r লেখা — তাহলে l == r-এ একই element নিজের সাথে যোগ হতে পারে, ভুল জুটি। সবসময় l < r
  • পেয়ে যাওয়ার পরেও loop চালিয়ে যাওয়া — match পেলে সাথে সাথে return; না করলে অযথা কাজ বা ভুল।
  • sum বাড়ানো-কমানোর দিক উল্টে ফেলা — sum বেশি হলে l++ করলে sum আরো বাড়বে, কখনো মিলবে না। বড় হলে r--, ছোট হলে l++ — মনে রাখো।

18. Harder problems that inherit this idea

19. Pattern learned

Opposite two pointers on a sorted array — দুই প্রান্ত থেকে শুরু, sum বড় হলে r--, ছোট হলে l++। পেছনের বড় শিক্ষা: monotonic structure থাকলে এক প্রান্ত দেখে নিশ্চিতভাবে অন্য প্রান্ত ছেঁটে ফেলা যায় — তাই O(n²) নয়, O(n)।

20. Final summary

ভবিষ্যতের আমাকে এক লাইনে: "sorted array + জুটি খোঁজা = দুই প্রান্ত থেকে চাপা; sum বড় হলে r--, ছোট হলে l++; প্রতি step-এ এক element চিরতরে বাদ বলে O(n)। Signal: 'sorted' শব্দটা দেখলেই opposite pointers মাথায় আসুক।"

পরের ধাপ → 082 — Pair with Difference K (এবার দুই আঙুল একই দিকে ছুটবে)। তুলনা করো → ../../05-prefix-difference-contribution/problems/073-subarray-sum-equals-k.md (hash পথ vs pointer পথ)।

ফিরে দেখা: এই level-এর map → ../README.md · এই note-টা যে ছকে লেখা → ../../../templates/math-problem-note-template.md


মনে রাখার নিয়ম (legal): সব নিজের ভাষায় লেখো; problem statement copy কোরো না; official link শুধু attribution-এর জন্য রাখো; "asked by Google/Amazon" এমন দাবি কোরো না — বরং "Google-style pattern" / "common interview pattern" লেখো।