البحث الثنائي في فضاء الإجابات
اعتبر نطاقًا متصلًا من الإجابات فضاءً للبحث لحل مسائل مثل 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 يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- البحث الثنائي الكلاسيكي: اليسار واليمين والوسط
- البحث الثنائي في المصفوفات المدورة وغير المرتبة
- الحد الأدنى والحد الأعلى
- البحث الثنائي في فضاء الإجابات