0Pricing
DSA Interview Prep · درس

تفجير البالونات: البرمجة الديناميكية العكسية للفواصل

حلّ مسألة تفجير البالونات بالتفكير عكسيًا، وذلك باختيار البالون الأخير الذي سيُفجَّر في كل فاصل بدلًا من الأول.

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

مسألة تفجير البالونات

إذا أُعطيت n من البالونات ذات القيم nums، فإن تفجير البالون i يكسبكم nums[i-1] * nums[i] * nums[i+1] من العملات (حاصل ضرب قيمته وقيم جارَيه الحاليين). بعد تفجيره، يصبح الجاران متجاورين. اعثروا على الحد الأقصى من العملات التي يمكنكم جمعها بتفجير جميع البالونات. يصعب تنفيذ المحاكاة الساذجة لأن تفجير البالون يغيّر الجيران — وتتجاوز البرمجة الديناميكية العكسية على الفواصل هذه الصعوبة بأناقة.

لماذا تفشل المحاكاة الأمامية

إذا حاولنا تعريف dp[i][j] على أنه الحد الأقصى من العملات الناتجة عن تفجير البالونات في النطاق [i, j]، وفكرنا في البالون الذي ينبغي تفجيره أولًا، فسنواجه مشكلة: تفجير البالون k أولًا يعني أن nums[k-1] وnums[k+1] يجب أن يكونا الجارين الحاليين، لكن قد يجري تفجير هذين البالونين لاحقًا، ما يغيّر الجيران بصورة ديناميكية. لذلك يصعب تعريف الحالة بصورة واضحة في الاتجاه الأمامي.

الفكرة الأساسية: فكّروا بطريقة عكسية

تكمن الحيلة في التفكير في البالون الذي سيكون آخر بالونات التفجير في الفاصل [i, j]. عندما يكون البالون k هو الأخير الذي يُفجَّر في [i, j]، تكون جميع البالونات الأخرى في [i, j] قد اختفت مسبقًا. لذلك يكون جارَا البالون k هما بالضبط nums[i-1] وnums[j+1] — أي البالونان الحدّيان الموجودان خارج الفاصل مباشرةً. وهذا يجعل حساب عملات التفجير الأخير حتميًا؛ إذ لا يعتمد على ترتيب عمليات التفجير السابقة.

تعريف الحالة والعلاقة التكرارية

أضيفوا بالونات حارسة: أضيفوا 1 في بداية nums ونهايتها لتكوين nums = [1] + nums + [1]. عرّفوا dp[i][j] على أنه الحد الأقصى من العملات الناتجة عن تفجير جميع البالونات الموجودة strictly بين الفهرسين i وj (أي باستثناء الطرفين)، حيث يكون nums[i] وnums[j] هما البالونين الحدّيين الباقيين. العلاقة التكرارية: لكل بالون أخير مرشح k في (i, j): dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]).

# With sentinels: nums = [1] + original + [1]
# dp[i][j] = max coins from bursting all balloons in open interval (i, j)
# k = last balloon to burst in (i,j)
# dp[i][j] = max over k in (i,j): dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]

التنفيذ الكامل

نضيف البالونات الحارسة إلى المصفوفة، ونهيّئ جدول البرمجة الديناميكية إلى الصفر (الفاصل الفارغ = 0 عملة)، ثم نملؤه بترتيب متزايد لطول الفاصل. تكون الإجابة النهائية dp[0][n+1]، وهي تمثل الحد الأقصى من العملات الناتجة عن تفجير جميع البالونات الأصلية مع بقاء البالونات الحارسة حدّين دائمين.

def maxCoins(nums):
    nums = [1] + nums + [1]
    n = len(nums)
    dp = [[0]*n for _ in range(n)]
    
    # length of open interval (i, j) exclusive: j - i - 1 balloons inside
    for length in range(2, n):       # length = j - i
        for i in range(0, n - length):
            j = i + length
            for k in range(i+1, j):  # k is last burst in (i, j)
                coins = dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]
                dp[i][j] = max(dp[i][j], coins)
    
    return dp[0][n-1]

print(maxCoins([3, 1, 5, 8]))  # 167

تتبّع المثال

بالنسبة إلى [3, 1, 5, 8]، نضيف الحارسين فنحصل على [1, 3, 1, 5, 8, 1] (الفهارس من 0 إلى 5). نريد حساب dp[0][5]. بالنسبة إلى الفواصل ذات length=2 (أي التي تحتوي على بالون واحد): dp[0][2] = 1*3*1=3، وdp[1][3]=3*1*5=15، وdp[2][4]=1*5*8=40، وdp[3][5]=5*8*1=40. ومع البناء التدريجي، يكون الترتيب الأمثل هو تفجير 1 أخيرًا من بين {3,1,5,8} بعد تفجير جيرانه أولًا، لنحصل على إجمالي قدره 167 عملة.

تحليل التعقيد

يوجد O(n²) من الفواصل، ونجرّب لكل فاصل O(n) من نقاط التقسيم، ما يعطي تعقيدًا زمنيًا مقداره O(n³). أما المساحة فهي O(n²) لجدول البرمجة الديناميكية. عندما يكون n = 500 بالونًا، فهذا يعني 125 مليون عملية، وهو عدد قابل للتنفيذ ضمن قيود المقابلات. تبسّط إضافة البالونات الحارسة معالجة الحدود؛ فبدونها، ستحتاجون إلى فحوصات صريحة لمعرفة ما إذا كان i-1 وj+1 ضمن الحدود.

بديل تنازلي مع التخزين المؤقت

يمكن كتابة الحل نفسه بأسلوب تنازلي باستخدام @lru_cache، وقد يكون اشتقاقه أكثر بداهة أثناء المقابلة. عرّفوا solve(i, j) على أنه الحد الأقصى من العملات في الفاصل المفتوح (i, j). تجرّب الدالة كل k بوصفه التفجير الأخير، ثم تخزّن النتائج مؤقتًا. يمتلك الأسلوبان التعقيد الزمني وتعقيد المساحة نفسيهما.

from functools import lru_cache

def maxCoins_memo(nums):
    nums = [1] + nums + [1]
    n = len(nums)
    
    @lru_cache(maxsize=None)
    def solve(i, j):
        if j - i < 2:  # no balloons between i and j
            return 0
        return max(
            solve(i, k) + solve(k, j) + nums[i]*nums[k]*nums[j]
            for k in range(i+1, j)
        )
    
    return solve(0, n-1)

print(maxCoins_memo([3, 1, 5, 8]))  # 167

خطأ شائع: تعريف البرمجة الديناميكية الأمامية

من الأخطاء الشائعة تعريف dp[i][j] على أنه عدد العملات عند تفجير أول بالون في [i,j]، لا الأخير. يفشل هذا التعريف لأن حساب عملات التفجير الأول يعتمد على بالونات مجاورة لم تُفجَّر بعد، كما أن حالة هذه الجيران تتغير مع تقدم الخوارزمية. فكّروا دائمًا في العنصر الأخير عند استخدام البرمجة الديناميكية على الفواصل عندما تعتمد الحدود على العناصر المتبقية.

لماذا تكون قيم البالونات الحارسة 1؟

اختيرت قيم البالونات الحارسة لتكون 1 لأنها تعمل بوصفها عناصر محايدة في الضرب. عندما يكون بالون حدّي هو الأخير الذي يُفجَّر، تكون قيمة عملاته boundary * last * boundary = 1 * last * 1 = last. أما استخدام 0 فسيعطي 0 من العملات (وهو خطأ)، واستخدام قيم أخرى سيشوّه الحساب. توحّد حيلة البالونات الحارسة جميع حالات الحدود بصورة نظيفة، من دون معالجة خاصة للبالون الموجود في أقصى اليسار أو أقصى اليمين.

مقارنة مع البرمجة الديناميكية القياسية على الفواصل

في البرمجة الديناميكية القياسية على الفواصل (مثل ضرب سلسلة المصفوفات)، تمثل نقطة التقسيم k الموضع الذي نقسم عنده المسألة إلى مسألتين فرعيتين تُحلان بصورة مستقلة. أما في مسألة Burst Balloons، فإن k هو البالون الأخير الذي يُفجَّر في الفاصل، ما يجعل الفاصلين الفرعيين [i,k] و[k,j] مستقلين، ما دام k لا يزال موجودًا بوصفه حدًا. وهذه الرؤية العكسية هي الفكرة الإبداعية التي تجعل حل مسألة تفجير البالونات ممكنًا باستخدام البرمجة الديناميكية على الفواصل.

تحقق سريع

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

ملخص الدرس

لقد تعلّمتم في هذا الدرس: تفشل المحاكاة التقدمية لأن تفجير البالونات يغيّر الجيران بصورة غير متوقعة، وتحدّد الفكرة العكسية أن k هو البالون الأخير الذي يُفجَّر في فترة، وبذلك يصبح الجاران هما nums[i] وnums[j]، وتوفّر علاقة التكرار dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]) مع إضافة قيم حارسة حلًا بزمن O(n³). بعد ذلك ننتقل إلى برمجة حقيبة الظهر الديناميكية، بدءًا من مشكلة حقيبة الظهر الكلاسيكية 0/1 وتحسين استهلاك الذاكرة فيها.

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

هل درس «تفجير البالونات: البرمجة الديناميكية العكسية للفواصل» مجاني؟

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

ماذا ستتعلم في «تفجير البالونات: البرمجة الديناميكية العكسية للفواصل»؟

حلّ مسألة تفجير البالونات بالتفكير عكسيًا، وذلك باختيار البالون الأخير الذي سيُفجَّر في كل فاصل بدلًا من الأول. تتمرن على 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. تقسيم السلسلة إلى مقاطع متناظرة II
  4. تفجير البالونات: البرمجة الديناميكية العكسية للفواصل
← العودة إلى DSA Interview Prep