0Pricing
DSA Interview Prep · درس

البحث الثنائي في فضاء الإجابات

اعتبر نطاقًا متصلًا من الإجابات فضاءً للبحث لحل مسائل مثل minimum-time-to-complete-jobs وcapacity-to-ship-packages

البحث الثنائي في فضاء الإجابات درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

البحث الثنائي في فضاء الإجابات

يعرف معظم الناس البحث الثنائي للعثور على قيمة في مصفوفة مرتبة. لكن البحث الثنائي يصبح أكثر قوة عند تطبيقه على فضاء الإجابات الممكنة. فبدلًا من البحث في مصفوفة، تبحثون في نطاق عددي — مثلًا: «ما أقل عدد من الأيام اللازمة لشحن جميع الطرود؟» — وتستخدمون دالة فحص لتحديد ما إذا كانت إجابة مرشحة قابلة للتحقيق.

تحوّل هذه التقنية العديد من مسائل التحسين من O(n²) أو أسوأ إلى O(n log(max_answer)).

قالب فضاء الإجابات

يتكوّن القالب من ثلاثة عناصر. أولًا، حدّدوا نطاق البحث [lo, hi] الذي يحيط بجميع الإجابات الصالحة. ثانيًا، اكتبوا فحصًا لقابلية التحقيق can_achieve(mid) يعيد True إذا كانت قيمة mid قابلة للتحقيق. ثالثًا، نفّذوا بحثًا ثنائيًا ضمن [lo, hi]: إذا أعادت can_achieve(mid) قيمة صحيحة، فتحرّكوا نحو إجابة أصغر (أو أكبر)؛ وإلا فتحرّكوا في الاتجاه الآخر.

الخاصية الأساسية هي أن دالة قابلية التحقيق يجب أن تكون رتيبة — فبمجرد أن تصبح إجابة ما قابلة للتحقيق، تصبح جميع القيم التي تتجاوزها قابلة للتحقيق أيضًا (أو تصبح جميع القيم الأصغر منها غير قابلة للتحقيق).

# Generic template
def answer_space_search(lo, hi, is_feasible):
    result = hi  # or lo, depending on direction
    while lo <= hi:
        mid = lo + (hi - lo) // 2
        if is_feasible(mid):
            result = mid
            hi = mid - 1   # try to minimise further
        else:
            lo = mid + 1
    return result

مثال: سعة شحن الطرود

LeetCode 1011 'Capacity to Ship Packages Within D Days': بالنظر إلى قائمة من الأوزان وعدد D من الأيام، أوجد الحد الأدنى لسعة الشحن اللازمة لشحن جميع الطرود بالترتيب خلال D من الأيام. تقع الإجابة ضمن [max(weights), sum(weights)]. تكون السعة قابلة للتحقيق إذا تمكّنت محاكاة جشعة من شحن جميع الطرود خلال D من الأيام. يحقق البحث الثنائي ضمن نطاق السعة زمناً قدره O(n log(sum)).

def shipWithinDays(weights, days):
    def can_ship(capacity):
        needed_days, current_load = 1, 0
        for w in weights:
            if current_load + w > capacity:
                needed_days += 1
                current_load = 0
            current_load += w
        return needed_days <= days

    lo, hi = max(weights), sum(weights)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if can_ship(mid):
            hi = mid        # feasible, try smaller
        else:
            lo = mid + 1    # not feasible, need more capacity
    return lo

print(shipWithinDays([1,2,3,4,5,6,7,8,9,10], 5))  # 15
print(shipWithinDays([3,2,2,4,1,4], 3))            # 6

مثال: تناول كوكو للموز

LeetCode 875 'Koko Eating Bananas': تستطيع كوكو تناول K موزة في الساعة؛ وتريد إنهاء H أكوام خلال H ساعة، مع تصغير K إلى أدنى قيمة. نطاق البحث هو [1, max(piles)]. الفحص: عند معدل K، يكون إجمالي الساعات = sum(ceil(pile/K))، ويجب أن يكون <= H. نستخدم البحث الثنائي للعثور على أصغر K يحقق ذلك.

import math

def minEatingSpeed(piles, h):
    def can_finish(k):
        return sum(math.ceil(p / k) for p in piles) <= h

    lo, hi = 1, max(piles)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if can_finish(mid):
            hi = mid      # feasible, try lower speed
        else:
            lo = mid + 1  # too slow
    return lo

print(minEatingSpeed([3,6,7,11], 8))    # 4
print(minEatingSpeed([30,11,23,4,20], 5))  # 30

مثال: الحد الأدنى من الأيام لصنع باقات الزهور

LeetCode 1482 'Minimum Number of Days to Make m Bouquets': تحتاج إلى m باقات، تحتوي كل منها على k زهور متتالية متفتحة. تتفتح الزهرة i في اليوم bloomDay[i]. نستخدم البحث الثنائي على اليوم: النطاق هو [1, max(bloomDay)]. يتحقق اختبار إمكانية التنفيذ من خلال عدّ الزهور المتفتحة المتتالية ومعرفة ما إذا كان بالإمكان تكوين m باقات. الخاصية الرتيبة: إذا نجح اليوم d، فإن اليوم d+1 ينجح أيضاً.

def minDays(bloomDay, m, k):
    if m * k > len(bloomDay):
        return -1  # impossible

    def can_make(day):
        bouquets = consecutive = 0
        for bd in bloomDay:
            if bd <= day:
                consecutive += 1
                if consecutive == k:
                    bouquets += 1
                    consecutive = 0
            else:
                consecutive = 0
        return bouquets >= m

    lo, hi = 1, max(bloomDay)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if can_make(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

print(minDays([1,10,3,10,2], 3, 1))  # 3
print(minDays([1,10,3,10,2], 3, 2))  # -1

تحديد نطاق البحث

اختيار نطاق [lo, hi] الصحيح أمر بالغ الأهمية. يجب أن تكون lo هي الحد الأدنى الممكن للإجابة (مثل أصغر عنصر أو 1 أو 0)، وأن تكون hi هي الحد الأقصى الممكن للإجابة (مثل مجموع جميع العناصر أو أكبر عنصر أو n). إذا جعلت hi صغيرة جداً، فستفقد إجابات صحيحة؛ أما جعلها كبيرة أكثر من اللازم فلا مشكلة فيه، لأن البحث الثنائي سيتقارب مع ذلك خلال O(log(hi - lo)) خطوة.

# Choosing lo and hi for common problems:
# Capacity to ship: lo=max(weights), hi=sum(weights)
# Koko eating:      lo=1,            hi=max(piles)
# Square root:      lo=1,            hi=x
# Allocate books:   lo=max(pages),   hi=sum(pages)

def isqrt_bs(x):
    if x < 2:
        return x
    lo, hi = 1, x
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if mid * mid <= x:
            lo = mid + 1
        else:
            hi = mid
    return lo - 1

for n in [0, 1, 4, 8, 9, 15, 16]:
    print(f'isqrt({n}) = {isqrt_bs(n)}')

التعظيم مقابل التصغير: الاتجاه مهم

للبحث الثنائي في فضاء الإجابات شكلان. تصغير الإجابة: عند نجاح الفحص، جرّب قيمة أصغر (hi = mid)؛ وعند فشله، جرّب قيمة أكبر (lo = mid + 1). تعظيم الإجابة: عند نجاح الفحص، جرّب قيمة أكبر (lo = mid + 1، مع حفظ mid كمرشح)؛ وعند فشله، جرّب قيمة أصغر (hi = mid - 1). وضّح دائماً الاتجاه الذي تبحث فيه قبل كتابة الشيفرة.

# Maximise: largest x such that f(x) is feasible
def max_feasible(lo, hi, is_feasible):
    result = lo - 1   # sentinel: no feasible answer found
    while lo <= hi:
        mid = lo + (hi - lo) // 2
        if is_feasible(mid):
            result = mid
            lo = mid + 1  # try larger
        else:
            hi = mid - 1
    return result

# Example: largest k such that k^2 <= 50
print(max_feasible(1, 50, lambda k: k * k <= 50))  # 7

تخصيص الحد الأدنى من الصفحات (مسألة كلاسيكية)

بالنظر إلى n من الكتب التي تحتوي على pages[] وإلى k من الطلاب، خصّص الكتب في مقاطع متجاورة بحيث يقرأ الطالب الذي لديه أكبر عدد من الصفحات أقل عدد ممكن منها. نستخدم البحث الثنائي على الإجابة (أي الحد الأقصى الأدنى الممكن). يتحقق اختبار إمكانية التنفيذ من خلال إسناد الكتب إلى الطلاب بجشع: عندما تؤدي إضافة كتاب إلى تجاوز الحد الأقصى الحالي، أسنده إلى طالب جديد. إذا كان عدد الطلاب المطلوب <= k، فإن هذا الحد الأقصى قابل للتحقيق.

def allocate_min_pages(pages, k):
    if k > len(pages):
        return -1

    def is_feasible(max_pages):
        students, current = 1, 0
        for p in pages:
            if p > max_pages:
                return False  # single book exceeds limit
            if current + p > max_pages:
                students += 1
                current = 0
            current += p
        return students <= k

    lo, hi = max(pages), sum(pages)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if is_feasible(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

print(allocate_min_pages([12, 34, 67, 90], 2))  # 113
print(allocate_min_pages([10, 20, 30, 40], 2))  # 60

تحليل تعقيد البحث في فضاء الإجابات

التعقيد الزمني هو O(n × log(range))، حيث تمثل n تكلفة اختبار إمكانية التنفيذ (وهو عادةً مرور خطي)، ويمثل range = hi - lo حجم فضاء الإجابات. على سبيل المثال، إذا كان مجموع الصفحات يساوي 10⁹ وكان اختبار إمكانية التنفيذ بتعقيد O(n)، فإن الزمن الإجمالي هو O(n log 10⁹) ≈ O(30n)، وهو أفضل بكثير من الحل بالقوة الغاشمة ذي التعقيد O(n²).

التعقيد المكاني هو O(1) للبحث الثنائي نفسه، بالإضافة إلى ما يستخدمه اختبار إمكانية التنفيذ.

import math

# Compare brute force vs answer-space binary search
# For sum = 10^9 and n = 10^5:
brute_ops = 10**9         # try every possible answer
bsearch_ops = 10**5 * math.log2(10**9)  # n * log(range)
print(f'Brute force: {brute_ops:,.0f} operations')
print(f'Binary search: {bsearch_ops:,.0f} operations')
print(f'Speedup: {brute_ops / bsearch_ops:,.0f}x')

العنصر الأصغر رقم k في مصفوفة مرتبة

LeetCode 378 'Kth Smallest Element in a Sorted Matrix': كل صف وعمود في مصفوفة n×n مرتبان. نستخدم البحث الثنائي على قيمة الإجابة ضمن [matrix[0][0], matrix[n-1][n-1]]. يحسب اختبار إمكانية التنفيذ العناصر التي تكون <= mid باستخدام مؤشر يبدأ من الزاوية السفلية اليسرى، ويعمل بتعقيد O(n). أوجد أصغر قيمة يكون عندها عدد العناصر التي تساوي <= mid هو k على الأقل.

def kthSmallest(matrix, k):
    n = len(matrix)

    def count_le(mid):
        count, row, col = 0, n - 1, 0
        while row >= 0 and col < n:
            if matrix[row][col] <= mid:
                count += row + 1
                col += 1
            else:
                row -= 1
        return count

    lo, hi = matrix[0][0], matrix[n-1][n-1]
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if count_le(mid) >= k:
            hi = mid
        else:
            lo = mid + 1
    return lo

matrix = [[1,5,9],[10,11,13],[12,13,15]]
print(kthSmallest(matrix, 8))  # 13

التعرّف على مسائل فضاء الإجابات

تشترك المسائل المناسبة للبحث الثنائي في فضاء الإجابات في مؤشرات شائعة: يطلب السؤال قيمة دنيا أو قصوى، وتقع الإجابة ضمن نطاق عددي محدود، كما أن زيادة (أو إنقاص) قيمة الإجابة المرشحة يجعل إمكانية التنفيذ تتحسن أو تسوء بصورة رتيبة. من الكلمات المفتاحية الشائعة: «الحد الأقصى الأدنى الممكن»، و«على الأكثر k من العمليات»، و«خلال d من الأيام».

عندما تلاحظ هذه المؤشرات، حدّد lo وhi فوراً، واكتب دالة إمكانية التنفيذ، ثم طبّق القالب. نادراً ما يفشل هذا الأسلوب المنظم في المقابلات.

اختبار سريع

اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

تعلّمت في هذا الدرس أن: البحث الثنائي في فضاء الإجابات يُطبّق عندما تكون دالة إمكانية التنفيذ رتيبة على نطاق عددي، وأن القالب يبحث ضمن [lo, hi] ويستخدم فحص can_achieve لتقسيم فضاء البحث إلى النصف، وأن التعقيد الإجمالي هو O(n log(range))، حيث n تكلفة اختبار إمكانية تنفيذ واحد. ننتقل بعد ذلك إلى القوائم المرتبطة وفئة Node.

الأسئلة الشائعة

هل درس «البحث الثنائي في فضاء الإجابات» مجاني؟

نعم — نص درس «البحث الثنائي في فضاء الإجابات» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «البحث الثنائي في فضاء الإجابات»؟

اعتبر نطاقًا متصلًا من الإجابات فضاءً للبحث لحل مسائل مثل minimum-time-to-complete-jobs وcapacity-to-ship-packages تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟

لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «البحث الثنائي في فضاء الإجابات»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟

نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. البحث الثنائي الكلاسيكي: اليسار واليمين والوسط
  2. البحث الثنائي في المصفوفات المدورة وغير المرتبة
  3. الحد الأدنى والحد الأعلى
  4. البحث الثنائي في فضاء الإجابات
← العودة إلى DSA Interview Prep