المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى
طبّق خوارزمية Kadane على maximum-sum-subarray ومدّدها لتتبع القيمتين العظمى والصغرى في صيغة حاصل الضرب
المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
مسألة المصفوفة الفرعية ذات المجموع الأقصى
تطلب مسألة المصفوفة الفرعية ذات المجموع الأقصى منكم العثور على المصفوفة الفرعية المتجاورة داخل مصفوفة أحادية البعد من الأرقام، والتي لها أكبر مجموع. فمثلًا، في [-2, 1, -3, 4, -1, 2, 1, -5, 4]، تعطي المصفوفة الفرعية [4, -1, 2, 1] أكبر مجموع، وهو 6. يتحقق نهج القوة الغاشمة O(n²) من جميع المصفوفات الفرعية، لكن خوارزمية Kadane تحل المسألة في O(n).
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
# Brute force: O(n^2)
max_sum = float('-inf')
for i in range(len(nums)):
curr = 0
for j in range(i, len(nums)):
curr += nums[j]
max_sum = max(max_sum, curr)
print(max_sum) # 6الحدس وراء خوارزمية Kadane
تمرّ خوارزمية Kadane عبر المصفوفة مرة واحدة، مع الحفاظ على مجموع جارٍ هو current_sum. عند كل عنصر، تقررون: هل من الأفضل تمديد المصفوفة الفرعية الحالية أم البدء من هذا العنصر من جديد؟ إذا أصبح current_sum سالبًا، فلن يفيد أي مصفوفة فرعية لاحقة، لذا نعيد البدء. العلاقة العودية هي current_sum = max(num, current_sum + num).
def max_subarray(nums):
max_sum = current_sum = nums[0]
for num in nums[1:]:
# Extend or start fresh?
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(max_subarray(nums)) # 6تتبّع خوارزمية Kadane
لنتتبّع خوارزمية Kadane على [-2, 1, -3, 4, -1, 2, 1, -5, 4]: نبدأ بـ curr=-2, max=-2. عند 1: curr=max(1,-2+1)=1, max=1. عند -3: curr=max(-3,1-3)=-2, max=1. عند 4: curr=max(4,-2+4)=4, max=4. عند -1: curr=3, max=4. عند 2: curr=5, max=5. عند 1: curr=6, max=6. عند -5: curr=1. عند 4: curr=5, max=6. تحدد الخوارزمية بشكل صحيح أن المصفوفة الفرعية المنتهية عند الفهرس 6 هي الحل الأمثل.
def max_subarray_trace(nums):
curr = max_sum = nums[0]
for i, num in enumerate(nums[1:], 1):
new_curr = max(num, curr + num)
max_sum = max(max_sum, new_curr)
print(f'i={i}, num={num}, curr: {curr}->{new_curr}, max={max_sum}')
curr = new_curr
return max_sum
max_subarray_trace([-2, 1, -3, 4, -1, 2, 1, -5, 4])إرجاع المصفوفة الفرعية الفعلية
إذا طلب منكم المُحاوِر إرجاع المصفوفة الفرعية نفسها، وليس مجموعها فقط، فعليكم تتبّع فهرسي البداية والنهاية. عند إعادة البدء، بسبب num > current_sum + num، حدّثوا temp_start. وعند تحديث max_sum، احفظوا temp_start في start، واحفظوا الفهرس الحالي في end. يضيف ذلك كلفة إضافية مقدارها O(1) إلى الخوارزمية نفسها التي تعمل في O(n).
def max_subarray_indices(nums):
max_sum = curr = nums[0]
start = end = temp_start = 0
for i in range(1, len(nums)):
if nums[i] > curr + nums[i]:
curr = nums[i]
temp_start = i
else:
curr += nums[i]
if curr > max_sum:
max_sum = curr
start, end = temp_start, i
return max_sum, nums[start:end+1]
print(max_subarray_indices([-2, 1, -3, 4, -1, 2, 1, -5, 4]))
# (6, [4, -1, 2, 1])مسألة المصفوفة الفرعية ذات حاصل الضرب الأقصى
تُعد مسألة المصفوفة الفرعية ذات حاصل الضرب الأقصى أكثر تعقيدًا من نظيرتها الخاصة بالمجموع بسبب الأعداد السالبة. فعند ضرب عددين سالبين يكون الناتج موجبًا، ولذلك قد يتحول حاصل ضرب سالب جدًا إلى الأكبر بعد ضربه في عدد سالب آخر. في [2, 3, -2, 4]، تكون الإجابة 6، وهي [2, 3]. وفي [-2, 0, -1]، تكون الإجابة 0. يجب علينا تتبّع حاصلي الضرب الأقصى والأدنى عند كل خطوة.
nums = [2, 3, -2, 4]
# [2,3,-2,4]: products [2, 6, -12, -48]
# subarrays: [2]=2, [2,3]=6, [3]=3, etc.
# max is 6 from subarray [2,3]
nums2 = [-2, 3, -4]
# [-2]*3*[-4] = 24
# negative*negative=positive!
print('Expected:', 24)تتبّع حاصلي الضرب الأقصى والأدنى
الفكرة الأساسية هي أن حاصل الضرب الأقصى الحالي عند كل موضع يكون واحدًا من num أو max_so_far * num أو min_so_far * num؛ إذ يساعد الخيار الأخير عندما يحوّل عدد سالب القيمة الدنيا إلى قيمة قصوى. وينطبق الأمر نفسه على القيمة الدنيا. حدّثوا كلاًّ من cur_max وcur_min في الوقت نفسه، باستخدام القيم السابقة لتجنب استخدام قيم جرى تحديثها بالفعل في الخطوة نفسها.
def max_product(nums):
max_prod = min_prod = result = nums[0]
for num in nums[1:]:
# All three candidates for new max
candidates = (num, max_prod * num, min_prod * num)
max_prod, min_prod = max(candidates), min(candidates)
result = max(result, max_prod)
return result
print(max_product([2, 3, -2, 4])) # 6
print(max_product([-2, 3, -4])) # 24
print(max_product([-2, 0, -1])) # 0
print(max_product([-2])) # -2لماذا يُعد min_prod مهمًا
تأملوا [-3, -10, 5]. بعد معالجة -3: max=-3, min=-3. بعد -10: المرشحون هم (-10, 30, 30) → max=30, min=-10. بعد 5: المرشحون هم (5, 150, -50) → max=150. من دون تتبّع min_prod، ستفوتكم حالة الانقلاب التي تحدث عندما يُضرب الحد الأدنى الكبير السالب في عدد سالب آخر. احسبوا دائمًا كلاً من max وmin باستخدام القيم السابقة نفسها لتجنب خطأ القراءة من قيمة قديمة.
def max_product_traced(nums):
max_p = min_p = result = nums[0]
for num in nums[1:]:
prev_max, prev_min = max_p, min_p
max_p = max(num, prev_max * num, prev_min * num)
min_p = min(num, prev_max * num, prev_min * num)
result = max(result, max_p)
print(f'num={num}: max_p={max_p}, min_p={min_p}')
return result
max_product_traced([-3, -10, 5])
# max_p after -10: 30 (flip!)
# max_p after 5: 150الأصفار تعيد ضبط حاصل الضرب
يعيد الصفر في المصفوفة ضبط حاصلي الضرب الجاريين إلى الصفر، مما يقسم المصفوفة فعليًا إلى مصفوفات فرعية مستقلة. عندما تكون num = 0، فإن كلًا من max_prod * 0 = 0 وmin_prod * 0 = 0، ولذلك تصبح المرشحات الثلاثة كلها 0، مع الاحتفاظ بأقصى قيمة للنتيجة السابقة. لا حاجة إلى كتابة كود لحالة خاصة — فالصيغة العامة تتعامل مع الأصفار بصورة طبيعية.
def max_product(nums):
max_p = min_p = result = nums[0]
for num in nums[1:]:
cands = (num, max_p * num, min_p * num)
max_p, min_p = max(cands), min(cands)
result = max(result, max_p)
return result
# Zero splits array into independent subarrays
print(max_product([3, -1, 4, 0, 2, 5, -1])) # 10 (2*5)
print(max_product([0, 2])) # 2
print(max_product([-1, 0, -2])) # 0بديل: المسح من اليسار إلى اليمين ومن اليمين إلى اليسار
يتمثل نهج بديل في المسح من اليسار إلى اليمين ثم من اليمين إلى اليسار، مع إعادة ضبط حاصل الضرب الجاري إلى 1 عند الوصول إلى الصفر. لا تعبر المصفوفة الفرعية ذات حاصل الضرب الأقصى صفرًا أبدًا، ولذلك إذا جعل عدد سالب النتيجة سيئة في أحد الاتجاهين، فسوف يلتقط المسح العكسي حالة الانقلاب. هذا النهج أنيق، لكن أسلوب تتبّع القيمتين الدنيا والقصوى هو المتوقع غالبًا في المقابلات.
def max_product_sweep(nums):
result = max(nums)
left = right = 1
n = len(nums)
for i in range(n):
left *= nums[i]
right *= nums[n - 1 - i]
result = max(result, left, right)
if left == 0: left = 1
if right == 0: right = 1
return result
print(max_product_sweep([2, 3, -2, 4])) # 6
print(max_product_sweep([-2, 3, -4])) # 24
print(max_product_sweep([-2, 0, -1])) # 0الفروق الأساسية بين Kadane وحاصل الضرب
تختلف المصفوفات الفرعية الخاصة بالمجموع عن تلك الخاصة بحاصل الضرب بطرق مهمة. بالنسبة إلى المجموع، تكون الأعداد السالبة ضارة دائمًا، لذا تعيدون البدء بطريقة جشعة. أما بالنسبة إلى حاصل الضرب، فإن وجود عددين سالبين يساعد، ولذلك يجب تتبّع القيمتين المتطرفتين. بالإضافة إلى ذلك، تنهي الأصفار مسارات حاصل الضرب، لكنها لا تكون سوى ضارة بدرجة محدودة في حالة المجموع. عند شرح الحل في المقابلات، أقرّوا بهذه الفروق صراحةً، واشرحوا سبب ضرورة تتبّع القيمة الدنيا قبل كتابة أي كود.
# Max Sum Subarray: O(n) time, O(1) space
def max_sum(nums):
curr = result = nums[0]
for n in nums[1:]:
curr = max(n, curr + n) # restart or extend
result = max(result, curr)
return result
# Max Product Subarray: O(n) time, O(1) space
def max_prod(nums):
lo = hi = result = nums[0]
for n in nums[1:]:
lo, hi = min(n, lo*n, hi*n), max(n, lo*n, hi*n)
result = max(result, hi)
return result
print(max_sum([-2, 1, -3, 4, -1, 2, 1])) # 6
print(max_prod([-2, 3, -4])) # 24التعقيد ونصائح المقابلات
تعمل كل من خوارزمية Kadane، الخاصة بأقصى مجموع، ونهج تتبّع القيمتين الدنيا والقصوى، الخاص بأقصى حاصل ضرب، في زمن O(n) ومساحة O(1). نصائح مهمة للمقابلات: (1) بالنسبة إلى أقصى مجموع، اذكروا بديل Divide and Conquer بتعقيد O(n log n) لإظهار اتساع معرفتكم. (2) بالنسبة إلى أقصى حاصل ضرب، شدّدوا على تحديث min_prod وmax_prod في الوقت نفسه باستخدام القيم السابقة، لتجنب استخدام بيانات قديمة. (3) وضّحوا دائمًا: هل يمكن أن تكون المصفوفة فارغة؟ وهل يجب أن تكون المصفوفة الفرعية غير فارغة؟ نعم، يجب أن تكون غير فارغة وفقًا للعرف المتبع.
# Both run O(n) time, O(1) space
# Kadane handles: all negative (returns least negative)
# Product handles: zeros (resets naturally), negatives (tracks both extremes)
nums_all_neg = [-5, -2, -8]
print('Max sum (all neg):', max(max(nums_all_neg[0:1]),
max(x for x in nums_all_neg))) # -2
# Correct: return the maximum element when all are negativeاختبار سريع
اختبروا فهمكم للمفاهيم الواردة في هذا الدرس من Data Structures & Algorithms — Coding Interview Prep.
مراجعة الدرس
في هذا الدرس تعلمتم: أن خوارزمية Kadane تحل مسألة المصفوفة الفرعية ذات المجموع الأقصى في O(n) من خلال اختيار التمديد أو إعادة البدء عند كل عنصر، وأن المصفوفة الفرعية ذات حاصل الضرب الأقصى تتطلب تتبّع حاصلي الضرب الجاريين الأدنى والأقصى بسبب انقلاب الإشارة عند وجود أعداد سالبة، وأن الأصفار تعيد ضبط حاصل الضرب الجاري طبيعيًا من دون كود لحالة خاصة. بعد ذلك سنستكشف مسألة Word Break باستخدام جدول DP أحادي البعد.
الأسئلة الشائعة
هل درس «المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى» مجاني؟
نعم — نص درس «المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى»؟
طبّق خوارزمية Kadane على maximum-sum-subarray ومدّدها لتتبع القيمتين العظمى والصغرى في صيغة حاصل الضرب تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- لص المنازل: علاقة تكرار الأخذ أو التخطي
- المصفوفة الجزئية العظمى والمصفوفة الجزئية ذات حاصل الضرب الأقصى
- تقسيم الكلمات وتقسيم السلسلة
- فك الترميز واحتساب المسارات