مثلث باسكال
درس 1 من 11
1مثلث باسكال
كيف تبني قائمة من القوائم صفًّا صفًّا، ولماذا كل رقم هو مجموع الاثنين اللذين فوقه، ومن أين تأتي الأخطاء في الحدود.
ماذا تشاهد في الرسم: كل صف يبدأ وينتهي بـ 1، وكل رقم بينهما مجموع الرقمين فوقه، مثل 6 = 3 + 3.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
مثلث باسكال هرم من الأرقام: كل رقم في الصف هو مجموع الرقمين اللذين فوقه مباشرة، وأطراف كل صف واحد. تبنيه صفًّا صفًّا كما تبني جدارًا من الطوب، وكل صف يعتمد على الصف الذي تحته فقط.
تمثّله قائمة من القوائم: الصف i له i + 1 عنصر، والعنصر row[j] = prev[j - 1] + prev[j]. الزمن O(n^2). ويعطي معاملات ذي الحدين C(n, k)، وتنتبه للحالات الحدّية مثل n <= 0.
المتطلبات قبل البداية
القوائم المتداخلة
مثلث باسكال قائمة من القوائم: العنصر الواحد فيها قائمة كاملة. لو أنت مرتاح مع matrix[row][col] فأنت مرتاح مع هذا المشروع.
الحلقة داخل الحلقة
حلقة خارجية تمشي على الصفوف وحلقة داخلية تمشي على عناصر الصف. فهم range(1, row_index) هو نصف الحل.
الفهرسة السالبة
triangle[-1] هي آخر قائمة أضفناها، أي الصف السابق. استخدامها يوفّر عليك متغيّرًا إضافيًا تتابعه بنفسك.
جمع الأعداد الصحيحة
ما فيه رياضيات متقدمة هنا، الجمع فقط. لكن لاحظ إن الأرقام تكبر بسرعة، وبايثون ما يفيض (overflow) لأن أعداده الصحيحة بلا حد أعلى.
المفاهيم الأساسية
مثلث باسكال مثلث من الأعداد يبدأ بـ 1 في القمة، وكل رقم بعد ذلك هو مجموع الرقمين اللذين فوقه مباشرة. الرقم الذي ما فوقه إلا رقم واحد (أي طرف الصف) يكون دائمًا 1.
1القاعدة في سطر واحد
لو سمّينا الصف السابق previous والصف الجديد row، فالقاعدة هي:
row[i] = previous[i - 1] + previous[i]
وهذه هي كل الخوارزمية. الباقي مجرد ترتيب للحلقات والحدود.
2خصائص تساعدك تتأكد أن حلك صحيح
| الخاصية | القيمة | مثال |
|---|---|---|
طول الصف رقم r (بدءًا من صفر) | r + 1 عنصر | الصف 4 طوله 5: [1,4,6,4,1] |
مجموع الصف رقم r | 2 ** r | مجموع [1,3,3,1] = 8 = 2**3 |
| أول وآخر عنصر | دائمًا 1 | لأن ما فوقهما إلا رقم واحد |
| التناظر | row[i] == row[-1-i] | المثلث مرآة حول محوره |
| العلاقة بالتوافيق | row[i] == C(r, i) | C(4,2) = 6 وهو عدد طرق اختيار 2 من 4 |
3الحدود: من أين تأتي أخطاء الواحد الزائد
أشهر خطأ في هذا المشروع هو حلقة داخلية بحدود غلط. تأمّل الصف رقم 4 (الصف الرابع، طوله 5): العنصر الأول والأخير ثابتان 1، والعناصر الوسطى هي ثلاثة فقط: المواضع 1 و2 و3. لهذا الحلقة الداخلية تمشي range(1, row_index)، تبدأ من 1 وتنتهي قبل row_index.
range(1, row_index + 1) في الحلقة الداخلية. هذا يقرأ previous[row_index] في آخر دورة، وهو موضع غير موجود في الصف السابق (لأن الصف السابق أقصر بعنصر)، فيظهر لك IndexError. الصف السابق دائمًا أقصر من الجديد بواحد.4الحالات الحدّية
| المدخل | المتوقع | لماذا |
|---|---|---|
n = 0 | [] | ما فيه صفوف تُبنى |
n سالب | [] | نفس المنطق: n <= 0 تغطي الحالتين |
n = 1 | [[1]] | صف واحد فيه قيمة واحدة، لاحظ القائمة داخل قائمة |
5التعقيد
المخرج نفسه فيه n(n+1)/2 رقمًا تقريبًا، أي O(n²). فمهما كان حلك ذكيًا، لا تستطيع تنزل عن O(n²) زمنًا ولا O(n²) ذاكرة، لأنك مطلوب منك تُرجع كل هذه الأرقام. هذه نقطة مهمة: حد التعقيد الأدنى يحدده حجم المخرج، لا ذكاء الخوارزمية.
كيف يعمل؟
شكلان: الأول مسار الكود من الشرط الحدّي إلى آخر صف، والثاني من أين تأتي أرقام الصف الجديد بالضبط.
شكل 1، مسار البناء: الشرط الحدّي أولًا، ثم صف جديد لكل دورة، أطرافه ثابتة ووسطه محسوب من الصف السابق.
شكل 2، من أين تأتي أرقام الصف: كل رقم وسطي له سهمان من الصف السابق، والطرفان ما لهما إلا سهم واحد فصارا 1.
الحل خطوة بخطوة
نبني الحل ثم نشغّله فعليًا. كل مخرج تحت هو مخرج حقيقي من تشغيل الكود.
الخطوة 1، الحل
#!/usr/bin/python3
"""0-pascal_triangle"""
def pascal_triangle(n):
"""Return the first n rows of Pascal's triangle as a list of lists."""
if n <= 0:
return []
triangle = [[1]]
for row_index in range(1, n):
previous = triangle[-1]
row = [1]
for i in range(1, row_index):
row.append(previous[i - 1] + previous[i])
row.append(1)
triangle.append(row)
return triangleالخطوة 2، التشغيل
$ cat 0-main.py
#!/usr/bin/python3
pascal_triangle = __import__('0-pascal_triangle').pascal_triangle
def print_triangle(triangle):
for row in triangle:
print("[{}]".format(",".join([str(x) for x in row])))
if __name__ == "__main__":
print_triangle(pascal_triangle(5))
$ ./0-main.py
[1]
[1,1]
[1,2,1]
[1,3,3,1]
[1,4,6,4,1]الخطوة 3، الحدود
>>> pascal_triangle(0) [] >>> pascal_triangle(-3) [] >>> pascal_triangle(1) [[1]] >>> len(pascal_triangle(10)[-1]) 10 >>> sum(pascal_triangle(10)[-1]) 512
آخر سطرين هما اختبارك الذاتي: الصف العاشر (فهرس 9) طوله 10، ومجموعه 512 وهو 2**9 بالضبط. لو ظهر عندك غير ذلك فعندك خطأ في الحدود.
الخطوة 4، امشِ معها صفًّا صفًّا
بناء 5 صفوف، دورة بدورة
الصف 0. نبدأ بـ triangle = [[1]] مباشرة. هذه حالة الأساس: ما فيه صف قبلها نحسب منها، فنكتبها بأيدينا.
الصف 1. row_index = 1 وprevious = [1]. الحلقة الداخلية range(1, 1) ما تدور ولا دورة، فما فيه عناصر وسطى. نضيف 1 في الآخر فيظهر [1, 1].
الصف 2. previous = [1, 1]. الحلقة تدور مرة واحدة عند i = 1: previous[0] + previous[1] = 2. فيظهر [1, 2, 1].
الصف 3. previous = [1, 2, 1]. دورتان: 1+2 = 3 و2+1 = 3. فيظهر [1, 3, 3, 1]، ولاحظ التناظر.
الصف 4. previous = [1, 3, 3, 1]. ثلاث دورات: 1+3=4 و3+3=6 و3+1=4، فيظهر [1, 4, 6, 4, 1]. تأكّد سريع: المجموع 16 وهو 2**4 ✓
row[i] = C(r, i)، وتستطيع أن تحسب كل خلية بالمضروب. لكن هذا أبطأ وأكثر عرضة للخطأ: المضروب يولّد أرقامًا ضخمة ثم يقسمها، والجمع البسيط يعطيك نفس النتيجة بعمليات أقل. القاعدة: إذا كان الحل التدريجي يبني على ما حسبته قبل، دعه يبني.اعتقاد شائع × الحقيقة
| الاعتقاد | الحقيقة |
|---|---|
| «يجب استخدام الاستدعاء الذاتي (recursion) لأن المثلث معرّف تكراريًا.» | التعريف تكراري، لكن التنفيذ التدريجي أبسط وأسرع. الاستدعاء الذاتي بدون تخزين مؤقت يعيد حساب نفس الخلايا ملايين المرات ويصل عمقه إلى حد بايثون. |
«[[1]*k for k in range(1, n+1)] يبني لي المثلث بسرعة.» | هذا يبني الأطوال الصحيحة لكن كل القيم 1. الأطوال ليست هي المحتوى، يجب أن تحسب الوسط. |
«ما فيه فرق بين [] و[[]].» | [] قائمة فاضية (صفر صفوف) و[[]] قائمة فيها صف واحد فاضي. المطلوب عند n <= 0 هو الأولى. |
| «أستطيع أن أعدّل الصف السابق مباشرة لكي أوفّر ذاكرة.» | لو عدّلت previous داخل الحلقة فأنت تخرّب صفًّا موجودًا في المخرج النهائي. يجب صف جديد كل دورة. |
«الأرقام تفيض عند n كبير.» | ما تفيض في بايثون 3، أعداده الصحيحة بلا حد أعلى. في لغات مثل C أو Java تفيض فعلًا، وهذا فرق مهم تعرفه. |
أين نستخدمه فعليًا؟
الاحتمالات
صف المثلث هو بالضبط توزيع عدد مرات ظهور الصورة في رمي عملة n مرات. [1,4,6,4,1] يعني 6 طرق من 16 لظهور صورتين في 4 رميات.
فك الأقواس
معاملات (a+b)**n هي صف المثلث رقم n. (a+b)**3 معاملاته 1, 3, 3, 1.
مقدمة للبرمجة الديناميكية
هذه أول مرة تبني فيها جدولًا كل خلية فيه محسوبة من خلايا قبلها. نفس الفكرة بالضبط تحل مسائل أصعب بكثير لاحقًا.
التوافيق بدون مضروب
لو احتجت C(n, k) لقيم كثيرة، بناء المثلث أسرع من حساب المضروب كل مرة، ويتجنّب القسمة على أرقام ضخمة.
جرّب بنفسك
الحلقة الداخلية عندك for i in range(1, row_index + 1). ماذا يصير عند row_index = 3؟
عند row_index = 3 الصف السابق [1,2,1] طوله 3 (مواضع 0,1,2). الحلقة توصل i = 3 فتقرأ previous[3] وهو غير موجود. الحد الصحيح هو range(1, row_index).
بطاقات مراجعة سريعة
row[i] = previous[i-1] + previous[i]، كل رقم مجموع الاثنين اللذين فوقه.r؟r + 1 عنصر، بفهرسة تبدأ من صفر.r؟2 ** r، اختبار ذاتي سريع لصحة حلك.n <= 0؟[]، لا [[]] ولا None.range(1, row_index)، تبدأ من 1 وتنتهي قبل row_index، لأن الطرفين ثابتان.O(n²) زمنًا وذاكرةً، وهذا أدنى ما يمكن لأن المخرج نفسه O(n²) رقمًا.تحقّق من فهمك
1. لماذا نكتب triangle = [[1]] بأيدينا بدل أن نحسبه؟
لأنه حالة الأساس: ما فيه صف سابق نجمع منه. القاعدة previous[i-1] + previous[i] تحتاج صفًّا موجودًا، فيجب واحد يُعطى ابتداءً.
2. صديقك يقول إن [[1]*(k+1) for k in range(n)] يحل المسألة. ما المشكلة؟
الأطوال صحيحة لكن المحتوى غلط: كل القيم 1، بينما المطلوب أرقام محسوبة. الشكل الصحيح لا يعني القيم الصحيحة.
3. (اكتشف الخطأ) حلك يرجع [[1]] لكل n. أين تنظر أولًا؟
في الحلقة الخارجية: غالبًا range(1, 1) بدل range(1, n)، أو أن triangle = [[1]] موضوعة داخل الحلقة فتُعاد تهيئتها كل دورة.
4. لماذا ما ينفع نعدّل previous مكانها؟
لأن previous هو triangle[-1]، نفس الكائن الموجود في المخرج. تعديله يغيّر صفًّا سلّمناه مسبقًا. القوائم في بايثون تُمرَّر بالمرجع لا بالنسخة.
5. (قرار) تحتاج C(n, k) لقيم كثيرة من n وk. تبني المثلث ولا تحسب بالمضروب؟
إذا كانت القيم كثيرة ومتقاربة فالمثلث أفضل: تبنيه مرة وتقرأ منه بـ O(1)، بجمع فقط ولا قسمة على أرقام ضخمة. وإذا احتجت قيمة واحدة لـ n عظيم جدًا، فصيغة المضروب المختصرة أوفر ذاكرة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
مقالات2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.