التفريغ الكامل للمحاضرة 6: البحث بالتنصيف
التفريغ الكامل للمحاضرة 6: البحث بالتنصيف
المصدر الأصلي: التفريغ الإنجليزي الرسمي بصيغة PDF، المرتبط بـصفحة المحاضرة والفيديو الرسمي.
النسبة والترخيص: آنا بيل (Ana Bell)، مقرر 6.100L: مقدمة في علوم الحاسوب والبرمجة باستخدام Python، خريف 2022، معهد ماساتشوستس للتكنولوجيا، MIT OpenCourseWare. هذه ترجمة وتكييف عربي غير رسمي، ولا تعني تأييد MIT أو اعتماده. الأصل المشمول والترجمة تحت رخصة CC BY-NC-SA 4.0: الاستخدام غير التجاري، مع النسبة ومشاركة التكييفات بالمثل. لا تشمل الرخصة تلقائيًا المواد الخارجية أو المستثناة من ترخيص المصدر.
منهج المطابقة: ترجمة التفريغ الرسمي المستخرج من PDF، لا الملاحظات ولا كلام مستنتج من الشرائح. عناوين صفحات المصدر تطابق الصفحات السبع عشرة غير الفارغة المفصولة بمحرف فصل الصفحة في الاستخراج. حُفظ ترتيب الفقرات وأدوار المتحدثين والتكرار والتصحيحات والإشارات الصوتية. تبقى المعرّفات والتعبيرات البرمجية، وأي توضيح تحريري موسوم بأنه ملاحظة مترجم.
صفحة المصدر 1
MITOCW | 6.100L Lecture 6 Multi Version 3.mp4
[صرير]
[حفيف]
[نقر]
آنا بيل: لنبدأ محاضرة اليوم. تركتكم في المحاضرة الماضية بوعد بخوارزميات أكبر وأفضل لما نحاول فعله، أي تقريب الجذور التربيعية وما شابه. اليوم آخر خوارزمية لفترة، قبل العودة للمزيد من صياغة Python. نقدّم اليوم خوارزمية البحث بالتنصيف (Bisection Search).
لكن قبل ذلك لنتذكر أين توقفنا. تحدثنا عن أعداد الفاصلة العائمة (Floating Point Numbers) ثم خوارزميات التقريب (Approximation Algorithms). تحدثنا عنها لأننا أردنا خوارزمية أفضل من التخمين والتحقق (Guess and Check)، الذي كان محدودًا جدًا بمجموعة شاملة من الحلول المحتملة.
لم نرد مجموعة شاملة نبحث فيها فقط، بل تقريبًا يحل المسائل. قلنا بدل زيادات صحيحة مثلًا عند البحث عن جذور القيم، لنجرب زيادات أصغر: 0.1 أو 0.25 أو 0.001، ما نشاء.
وبعدما بدأنا بهذه الأعداد كان مهمًا فهم ما يحدث خلف الكواليس. رأينا أنها لا تُمثَّل في الذاكرة مباشرة على نحو دقيق؛ غالبية الأعداد يحدث لها تقريب عند التخزين. وهو صغير جدًا، مثل 10 أس سالب 10 — عفوًا، 2 أس سالب 32، وهو تقريبًا 10 أس سالب 10. يبدو صغيرًا، لكن حتى حلقة تجمع 0.1 مع نفسها عشر مرات أعطت نتائج مفاجئة جدًا.
قدّمت طريقة التقريب فكرة أننا نستطيع تقريب جذر عدد، لكن لا نفحص المساواة. لا نقول إن التقريب مربّعًا، أو ما نفعله لحل المسألة، يساوي العدد المطلوب تمامًا. نحتاج هامشًا صغيرًا، جاء في صورة إبسيلون (Epsilon). نقرّب الحل بالسؤال: هل يقع ضمن زائد أو ناقص إبسيلون من القيمة المطلوبة؟
صنعنا خوارزمية جميلة، خوارزمية التقريب، واختبرنا قيمًا مختلفة. نزيد بمقدار صغير خطوة خطوة. ولتقريب جذر x قلنا نواصل تغيير التخمين بهذه الزيادات حتى يقترب ضمن زائد أو ناقص إبسيلون: مربع التخمين ضمن هذا الهامش من x.
صفحة المصدر 2
آنا بيل: وهذه الشريحة الجميلة كانت ذروة المحاضرة الماضية: علينا الحذر في كتابة خوارزميات التقريب، لأننا قد نتجاوز إبسيلون. إذا كان هذا التخمين وهذا مربعه، يزيد السهم الأزرق بالمقدار المختار، لكن قد يصبح المربع قبل x - epsilon بقليل، ثم بعد الزيادة التالية يتجاوز x + epsilon بقليل.
الشيفرة التي كتبناها، وبدت منطقية لحظتها، انتهت بحلقة لا نهائية لأنها لم تتوقف. لم نكن أبدًا ضمن زائد أو ناقص إبسيلون، فنواصل التخمين إلى الأبد. حصلنا على حلقة لا نهائية.
الحل أن نأخذ قليلًا من التخمين والتحقق ونضيف شرط توقف إضافيًا للتحقق من المعقولية (Sanity Check). كل شيء إلا هذا المربع كان خوارزمية التقريب؛ أضفنا المحاط هنا من التخمين والتحقق، ليقول إن تجاوزنا التخمين المعقول، فكل ما بعده غير معقول أيضًا، فلا داعي للاستمرار. يوقف هذا الحلقة اللا نهائية المحتملة.
guess**2 <= x تعني توقف حين تتجاوز آخر تخمين معقول. هذا الشرط مع شرط التقريب المعتاد، أن يكون المربع ضمن زائد أو ناقص إبسيلون من x، كوّنا خوارزميتي. هي هذه الحلقة فقط، while والزيادة، تبدو بسيطة جدًا.
لدينا الشرطان: أريد أن أكون ضمن إبسيلون وأن أواصل التخمين المعقول — عفوًا، أريد أن أكون خارج حدود إبسيلون وما زلت أخمّن معقولًا. هذا شرط الاستمرار. حين يصير أيهما خاطئًا أتوقف. وهذا ما تقوله if/else هنا: أيهما صار خاطئًا؟ هل أصبحت أخمّن غير المعقول، أم اقتربت ضمن إبسيلون؟
هنا أخمّن غير المعقول، فخرجت لأنني تجاوزت كثيرًا، وأطبع فشلت في إيجاد الجذر. وإلا خرجت لأنني ضمن إبسيلون. لنشغّل لنتذكر: نحاول 54321، تلك القيمة المزعجة، ضمن زائد أو ناقص 0.01، أي مربع التخمين ضمن ذلك من 54321. والزيادة بدت صغيرة جدًا، 0.0001.
لكن التشغيل استغرق ثانيتين تقريبًا و2.3 مليون تخمين. تقول الشيفرة فشلنا في إيجاد الجذر، وتعرض آخر تخمين ومربعه أيضًا.
الحل؟ يمكن تكبير إبسيلون إلى 1؛ نريد ضمن زائد أو ناقص 1 من 54321. نعم تعمل ولا تفشل. ما زلنا نخمن نحو 2.3 مليون مرة، ونحصل على هذا التقدير. بمجرد بلوغ حد إبسيلون أوقفنا البرنامج؛ لم يحاول التحسين أو الاقتراب أكثر من x.
صفحة المصدر 3
آنا بيل: الحل الآخر إن لم يعجبنا الفشل هو تصغير الخطوات. ما المشكلة؟ تذكرون عند التشغيل؟ نعم، يستغرق أطول. هل تقدّرون كم أطول؟
قلّلت حجم الخطوة عشر مرات. مقابل كل خطوة سابقًا أخطو عشرًا الآن. أنتظر نحو 15 أو 20 ثانية إن استغرق السابق ثانيتين. وضاعفت التخمينات — عفوًا، لا ضاعفت؛ أخمّن عشرة أضعاف، 23 مليونًا بدل 2.3 مليون. لكن الشيفرة لم تفشل ووجدت ما يقترب جدًا من جذر 54321.
هنا توقفنا. لا أعرف عنكم، لكن لا أريد انتظار 20 ثانية لجذر 54000. وقت طويل بصورة غير معقولة للحصول على تقريب، ولا ننتظره على الحاسوب أو الآلة الحاسبة. وهذا يقود للبحث بالتنصيف، طريقة أفضل لحل أنواع معينة من المسائل بسرعة أكبر بكثير، لكن أنواع معينة فقط.
لتحفيزه قبل الشيفرة أعطي أمثلة مختلفة. الأول فرصة لكم لربح مال. لنفترض وضعت ورقة 100 دولار عند صفحة في هذا الكتاب. هذه الطبعة السابقة، لا المستخدمة هذا العام؛ لا أملك الحالية في مكتبي للأسف.
الكتاب 448 صفحة، وضعت فيه مالًا. إن خمّنتم موضعه بثمانية تخمينات أو أقل أعطيكم إياه، وإن فشلتم تحصلون على F. ليس حقًا. هل يود أحد اللعب؟ لا، توقعت هذا. فرصة الفوز نحو 1 من 56؛ وأنا أيضًا لا أريد.
لكن لنقل أعطي معلومات إضافية: مع كل تخمين أخبركم هل صحيح أو أقل من المطلوب أو أعلى. هل تودون اللعب الآن؟ من يلعب معي؟ تريد؟ حسنًا، دورك.
سأكتب تخميناتك لأن لك ثمانية فقط؛ تذكر ثمانية فقط. ما الأول؟ 448 صفحة، اختر من 1 إلى 448. ما تخمينك؟
طالب: 224.
آنا بيل: 224، في المنتصف بالضبط، هنا. حسنًا، لا تنظر.
طالب: لا أعرف ما أفعله إطلاقًا.
[ضحك]
آنا بيل: لا مال. لكن معلومات إضافية: التخمين أعلى من المطلوب.
طالب: كان أعلى؟
آنا بيل: نعم، ليس 224.
طالب: 112.
صفحة المصدر 4
آنا بيل: 112، تريد هنا. حسنًا، تخمينان الآن. لا، أعلى أيضًا، ما زال أعلى. 56؟ حسنًا، في مكان هنا. 56، أعلى، ما زال أعلى.
طالب: [غير مسموع]
آنا بيل: 28؟ حسنًا، سأبدأ الكتابة هنا. 28. واحد واثنان وثلاثة، وصلت لأربعة تخمينات. 28. هو — لا، انتظر، عليّ تذكر أين وضعته. أعلى، أعلى، آسفة. [تضحك] ما زال أعلى.
طالب: 14.
آنا بيل: 14، هنا. الآن أقل من المطلوب، بعدما تذكرت — بعدما أتذكر أين وضعته فعلًا، هو أقل.
طالب: سيتبين أنه في صفحة مثل 400.
آنا بيل: بين 14 و28، تعرف الآن — نعم. 21؟ هنا، لنرَ. يا جماعة أنا أرتجف.
[هتاف]
ليس 100، لكن فيه 1 و0، فها أنت. رائع، سعيدة جدًا أنك لعبت. استغرقت فعليًا سبعة تخمينات فقط. كان يمكنني التلاعب أفضل قليلًا، لأن فرصة فوزك بهذه اللعبة نحو 1 من 3. أحسنت جدًا.
ما طريقة تفكيرك؟ أظن بعد بعضها فهم من لم يفكر هكذا. كنت تخمّن المنتصف في كل مرة أخبرك أعلى أو أقل.
البحث بالتنصيف طريقة لمسائل فيها ترتيب لما تبحثون فيه. نعرف المجال (Interval)، في الكتاب من 1 إلى 448؛ حد أدنى (Low) عند 1 وحد أعلى (High) عند 448، والإجابة بينهما. يمكن أعدادًا صحيحة كالكتاب أو أجزاء كسرية. الفكرة تخمين نقطة المنتصف (Midpoint)، تخمين جيد كأي آخر.
لكن بناء على إجابتي، لأنني أعطي الآن هل أعلى أو أقل، تستبعد نصف فضاء البحث (Search Space). عند تخمين 244 استبعدت هذا النصف كله من الكتاب. كأنني آخذ 448 صفحة وأجد الوسط وأمزّقه نصفين وأرمي الصفحات العليا. صار كتابًا أنحف، وهو ما تبحث فيه الآن.
ثم تكرر العملية. الحد الأدنى يبقى الصفحة 1، لا معلومات عن مدى النزول، والحد الأعلى يصبح تخمينك الأخير 224. ثم تخمّن المنتصف في هذا الكتاب الأنحف.
ينبغي أن يحرّك هذا في أذهانكم فكرة الحوسبة وما تعلمناه. بمجرد قول نكرر التخمين في نسخة أصغر من الكتاب، فلدينا حلقة؛ ينبغي التفكير في ذلك.
ملاحظة مترجم: ورد 244 عند وصف التخمين في هذه الفقرة، بينما التخمين الفعلي 224؛ حُفظت الزلة كما في التفريغ.
صفحة المصدر 5
آنا بيل: تكرر هذه الحلقة العملية نفسها مرارًا. بعدما استبعدت النصف الأعلى، تنظر لمنتصف الأدنى. حسب الإجابة تمزّق الكتاب نصفين مجددًا، وتبحث في نسخة أنحف.
نقسم ما علينا البحث فيه نصفين مع كل تخمين، وهذا قوي جدًا. لو استخدمنا التخمين والتحقق أو التقريب في الكتاب، لقلنا هل الصفحة 1 أم 2 أم 3 أم 4؟ ممل. لا نستبعد نصف الكتاب في كل تخمين، بل صفحة واحدة فقط.
هذا هو النمو اللوغاريتمي (Logarithmic Growth)، حين تقسّم فضاء البحث نصفين بكل مرحلة. سنعود إليه في أسابيع المحاضرات الأخيرة حين نقارن الخوارزميات: ماذا تعني كفاءة أكبر، وماذا يعني تشغيل أسرع؟ مجرد ذكر الآن.
أما التخمين والتحقق بصفحة كل مرة، فهو نمو خطي (Linear Growth). لو أعطيتكم كتابًا ضعفي الحجم، ولسوء الحظ وضعت المال في آخره، فستحتاجون ضعفي الأسئلة للوصول للجواب.
لكن مع النمو اللوغاريتمي، لو وضعت المال في كتاب ضعفي الحجم في مكان ما، تحتاجون تخمينًا إضافيًا واحدًا فقط، لا 400 إضافية، لمعرفة المجال. تخمّنون أولًا في الكتاب المضاعف وفجأة نعود لهذا الكتاب.
تشبيه آخر لفهم الاتجاه. ليس الأعداد فقط؛ أي شيء فيه خاصية ترتيب (Ordering Property). لنفترض طلبت عند دخولكم الجلوس أبجديًا: الأمام يسارًا اسم العائلة A، والخلف يمينًا Z، وأبحث عن شخص معين.
أسأل الشخص وسط الصف عن اسم عائلته. بحسب إجابته أستطيع صرف نصف الناس. إن بدأ اسمه بحرف بعد المطلوب في الأبجدية أصرف النصف الأعلى، والعكس بالعكس.
يبقى نصف الأشخاص، وأكرر حتى يبقى واحد، إما هو المطلوب — قلّصت حجم الصف نصفًا مع كل تخمين وأبقيت واحدًا لأسأله — أو الشخص غير موجود أصلًا.
لنطبّق الفكرة في البرمجة، وبالتحديد المسألة التي تربط هذه الخوارزميات: إيجاد جذر عدد تربيعي، لا دقيقًا، بل ما زلنا نبحث عن تقريب فقط.
صفحة المصدر 6
آنا بيل: مجال جذر x بين 0 وx. أعيد استخدام خط الأعداد هذا، وهذان طرفا المجال. في التقريب نبدأ 0 ونتقدم ببطء خطوة خطوة، أما التنصيف فالتخمين الأول عند المنتصف. نعمل بأعداد، فخاصية الترتيب حدسية جدًا.
نسأل عند المنتصف: ما مربع التخمين؟ إن كان هنا أعلى، g**2 هنا، أعرف أن التخمين كبير جدًا، فلا أحتاج المزيد من التخمينات هنا. هذه الحالة.
عند كبره يبقى الحد الأدنى هنا، ويصير هذا الأعلى، وهذا مجال البحث الجديد. لكنه المشكلة نفسها التي بدأتها بالمجال الأكبر: مجال ذو أدنى وأعلى، وتخمين وسط، g جديد، ونسأل هل مربعه أقل أم أكبر من x؟
لنفترض أقل. إن كان التخمين الجديد — مربع g الجديدة — أقل من x، فكل ما دون هذه g لن يكون أقرب للإجابة. أستبعد هذا النصف وأواصل التخمين نفسه: g التالية، ثم g، ثم أحدث g.
مثل أسماء ملفاتكم: الملف الجديد، النهائي، الأحدث، النسخة الثانية وكل ذلك. هذا ما فعلته. عندي أحدث g، المنتصف الجديد، وأواصل السؤال هل مربعها أكبر أو مساويًا أو أقل من x، وأعدّل الحدود وفقًا له.
في كل مرحلة القيم التي أبحث فيها نصف ما كانت في التخمين السابق. يستغل البحث بالتنصيف خاصيتين، ولا تستخدمونه إلا بهما: ترتيب ما، كالأسماء الأبجدية أو نطاق القيم، وتغذية راجعة (Feedback) تقول هل التخمين أقل أو أعلى أو دقيق أو تقريبي، ما تريدون.
فكروا وأجيبوا: تخمّنون رمز PIN من أربعة أرقام لهاتف مثلًا، ويخبر الهاتف فقط هل صحيح أم لا. هل تستخدمون التنصيف لتخمينه سريعًا وصحيحًا؟ لا، لماذا؟ ما الناقص؟
طالب: لا يخبرك هل أكبر أم أصغر.
آنا بيل: نعم. يمكن استخدام التنصيف واختيار نصف تفحصونه، لكن في أسوأ الحالات تحتاجون جميع القيم، فكان الأجدر البدء بـ0000 ثم 0001 ثم 0002 والتقدم تدريجيًا.
وماذا عن لعبة تخمين متطرفة؟ صديق يريد منك تخمين عدد بالضبط. في ذهنه عدد عشري، يمكن أي عدد حقيقي من 0 إلى 10، شاملًا كليهما، وبأي دقة.
صفحة المصدر 7
آنا بيل: التغذية الراجعة أنه صحيح أو أقل أو أعلى. هل يمكن التنصيف لتخمينه سريعًا وصحيحًا؟
طالب: يمكن، لكن قد يكون العدد طويلًا جدًا فيستغرق وقتًا طويلًا.
آنا بيل: وضعت كلمة «بالضبط» هنا. دونها يمكن نعم، لأنكم تلعبون لتقريب إلى منزلتين عشريتين مثلًا. لكن إذا أراد صديقك الاستعراض بالعدد pi، فلن يعمل؛ محاولة إيجاده بدقة لا تصل إليه أبدًا. تستخدمون التنصيف، لكنها خوارزمية لا نهائية لا تنتهي.
هذه الشريحة نفسها من بداية المحاضرة لتذكيرنا بشيفرة التقريب، لا جديد. الذي يعمل هو while: ما دمنا أبعد من إبسيلون وما زلنا نخمن معقولًا، زد التخمين 0.0001.
لنكتب تقريب الجذر التربيعي بالتنصيف. نتبع الإجراء نفسه، ونكتب معًا على الشرائح، وأشرح التفكير وراء كل خطوة.
أولًا نهيّئ بعض الأشياء. العدد الذي نريد جذره، لم لا العدد نفسه الذي أزعجنا؟ وما زلنا نريد ضمن زائد أو ناقص إبسيلون لأننا لا نريد مقارنة floats.
num_guesses تتتبع عدد التخمينات، كما في لعبة المال. وفي الأسفل نطبع العدد والتخمين الذي قربنا ضمن إبسيلون.
أولًا نلاحظ التكرار. while هنا كالتي للتقريب تمامًا: ما دام مربع التخمين بعيدًا زائد أو ناقص إبسيلون عن x، أي abs(guess**2 - x) >= epsilon — يمكن أكبر فقط، تفاصيل — فلنواصل التخمين.
لكن لا نزيد مقدارًا هنا؛ ليست طريقة التقريب. نحتاج تخمينًا ذكيًا. نهيّئ ما يلزم للخوارزمية، كالحدود الأصلية، ثم نفعل المتكرر داخل الحلقة كما لاحظنا. ماذا يتكرر في كل مرة؟
عن التهيئة: نحتاج حدّينا. كي يعمل التنصيف نعرفهما. الأدنى 0، لأننا نريد جذر x، والأعلى x. يمكن الأعلى 2*x أو 3*x أو ما نشاء، لكنه كبير جدًا؛ نعرف بالجبر أنه لن يبلغ ذلك، فنستخدم x.
ثم نبدأ بتخمين أولي، منتصف الأدنى والأعلى: الأعلى زائد الأدنى مقسومًا على 2. يصل بنا هذا إلى ما قبل while هنا.
صفحة المصدر 8
آنا بيل: نكرر ما دمنا أبعد من إبسيلون عن جوابنا. نكرر فحص هل التخمين أقل أم أعلى. لدينا المنتصف الذي بدأ الخوارزمية، ونسأل هل أقل أم أعلى، فهذا ما تحتاجه.
هذا if/else صغير. إذا كان مربع التخمين أقل من x فهو أقل من المطلوب. إن أخذنا المربع إلى هنا، نعرف ذلك. ماذا أفعل؟ ماذا تقول الخوارزمية؟ نعم.
طالب: [غير مسموع]
آنا بيل: العكس، نعم. هذا منخفض جدًا، فلا أريد شيئًا أدنى منه. بالضبط، نضبط الحد الأدنى إلى التخمين الذي صنعناه. نعرف أنه منخفض وكل ما دونه منخفض بالتأكيد، فلا أهتم بهذه القيم.
وإلا — لا نحتاج elif لأننا نعرف العكس — تخميننا أعلى. إن خمّنا المرة التالية هنا، نعرف أنه أعلى، فنضبط الحد الأعلى إلى التخمين. الجميع واضح؟ حسنًا.
ما الباقي؟ غيّرت أحد الحدّين، الأدنى أو الأعلى، إلى التخمين الأخير. ما التالي؟ ماذا تفعل الحلقة كما هي؟ تنتهي من الداخل وتعود لفحص هل مربع التخمين ناقص x أكبر من أو يساوي إبسيلون.
هل غيّرت التخمين داخل الحلقة بعد؟ لا. هذه الخطوة الأخيرة: جعله المنتصف الجديد باستخدام الحد الذي تغيّر. في كل مرور أغيّر الأدنى أو الأعلى إلى التخمين؛ أحد التغييرين، ثم أجد المنتصف الجديد. إذا غيّرت الأدنى أحتاج تخمينًا جديدًا الآن.
بهذا التخمين أسمح لـwhile أن تفحصه ثانية: تأخذ المربع وتقيس بُعده عن x، ثم تعيد تغيير الحدّ. هذا كل شيء، لا أسطر أخرى.
نوعًا ما نثق قليلًا بأن الحلقة تفعل الصحيح. لكن بتتبع قليل في ذهنكم أو Python Tutor سترون أنها صحيحة. نستخدم خط الأعداد نفسه.
لنجد تقريب جذر 36. جعلت إبسيلون 1 كي لا نخطو كثيرًا في Python Tutor. تخيلوا إن كانت أصغر فستعطي تقريبًا أفضل.
صفحة المصدر 9
آنا بيل: نهيّئ x التي نريد جذرها، وإبسيلون، والأدنى والأعلى، 0 و36 هنا. بالتتبع أول تخمين نصف 36 و0، أي 18. هذا التخمين. نبدأ while بسؤال ما مربع 18؟ كبير جدًا وأكبر من 36 بالتأكيد، فأدخل else لأن التخمين عالٍ. يصير الأعلى هنا، ويبقى الأدنى؛ لا معلومات عن الطرف الأدنى بعد.
التخمين التالي الأعلى زائد الأدنى، 0 زائد 18 على 2، أي 9. ترون تحديثه. أجد مربع 9: هل ما زال خارج زائد أو ناقص 1 حول 36؟ نعم، ما زال كبيرًا جدًا. فأجعل الحد الأعلى 9، هكذا.
ثم منتصف 0 و9، أي 4 ونصف. ها هو تحديثه. نربعه ونرى أقل أم أكبر من 36: أقل. فتصير 4.5 الحد الأدنى؛ لدينا معلومات عن الأدنى الآن. أعرف أن الجواب النهائي ضمن هذا المجال الصغير.
أسرع الآن لأننا نتعامل بكسور. الأدنى 4.5، والمنتصف بينه و9 هو 6.75. نواصل العملية مرارًا؛ آمل وضوح تغيير الحدود.
ما دام مربع التخمين خارج حدود 36 زائد أو ناقص 1، أي أصغر من 35 أو أكبر من 37، نواصل. سنصل ربما إلى 6 وشيء. هنا، أظن انتهينا، نعم. التخمين 6.0469 يجعل مربعه ضمن زائد أو ناقص 1. نعم، سؤال؟
طالب: ماذا لو كان تخمينك [غير مسموع]، ننظر في المكتبة؟
آنا بيل: لو كان ماذا؟ لو كان صحيحًا، نخرج فورًا لأن الشرط خاطئ. لا ندخل while حتى.
لنشغّل الشيفرة. هذه التي تتبعتها في Python Tutor وعلى الشرائح، لكن لـ54321. تذكير: طريقة التقريب احتاجت 23 مليون تخمين لتعطينا جذر 23 — جذر 54000 نحو 233.
نشغّل التنصيف ولم أحتج الانتظار. أقل من ثانية بدل 20 ثانية، ولم يفشل. أعطى جوابًا مشابهًا، 233.068 قريب من جذر 54000، بـ30 تخمينًا. وقفة درامية.
23 مليونًا و20 ثانية مقابل 30 تخمينًا وأقل من ثانية. لم نهبط من 23 مليونًا إلى 5 ملايين، بل من الملايين إلى العشرات، رائع جدًا ومثير للإعجاب. هذا معنى النمو اللوغاريتمي وقوته، وإدراك إمكان التنصيف لهذه المسائل.
صفحة المصدر 10
آنا بيل: بطريقة التقريب نقلّص فضاء البحث بمقدار 0.0001 كل تخمين، وبالتنصيف بالنصف. في الكتاب كانت 400 صفحة، بعد الأول 200 وبعد الثاني 100 وبعد التالي 50.
كون التنصيف لوغاريتميًا يأتي من السؤال: كم تخمينًا حتى تبقى صفحة واحدة للمال؟ أو حتى نكون ضمن إبسيلون ونصل للقيمة الوحيدة التي تحقق ذلك؟ هذا العدد من التخمينات يعني قسمة فضاء البحث على 2 أس k؛ حينها نتقارب إلى الجواب (Converge).
التقارب يعني قسمته على 2، k مرات: n / 2**k = 1. بلغتم الجواب الوحيد: المال بهذه الصفحة، الطالب جالس هناك، أو اقتربنا ضمن 0.01 من الجواب الحقيقي.
عند ذلك n = 2**k، ونريد حلها بدلالة n: k = log(n). من هنا النمو اللوغاريتمي لهذه المسألة.
من حيث الحلقة مررنا k مرات بـwhile، ومن حيث حجم البحث احتجنا log(n) مرات للوصول.
لننظر لبعض دقائق الشيفرة. ما الذي يحدث للقيم بين 0 و1؟ مثل جذر 0.5. ما زال يعمل ويعمل؛ كان ينبغي أن يعطينا جوابًا، فلنوقفه. دخلنا حلقة لا نهائية.
لنرَ ما يطبعه. حين تدخلون حلقة لا نهائية، وقت عبارات الطباعة. أفضل مكان داخل الحلقة، لطباعة بعض القيم. هنا أطبع — أوه، أزيح هذا — قيمة الأدنى.
لدينا low equals، ولا أحتاج تحويله لسلسلة، يكفي low، ثم الأعلى والتخمين، هكذا. عند التشغيل هذا ما نحصل عليه، يبدو تكرارًا مرة بعد مرة.
ماذا يحدث لجذر قيمة بين 0 و1؟ هذا المجال من 0 إلى x، لكن جذر x في هذه الحالة أكبر منها نفسها. جذر 0.5 أكبر من 0.5 لا أصغر. يضع البرنامج أول تخمين، الأعلى زائد الأدنى على 2، أي 0. إذا كان المجال 0 إلى x يخمّن هنا، ثم يعلق في الحلقة. يصير الأدنى 0.5 بعد أول تخمين والأعلى أيضًا 0.5، ومنتصفهما 0.5. يعيد إسناد التخمين إلى نفسه مرارًا.
صفحة المصدر 11
آنا بيل: نحتاج إصلاحًا، وسأجعلكم تصلحونه. لا تحتاجون معالجة الحالتين، بل غيّروا الحدّين لهذه المسألة كي تعمل بين 0 و1. لجذر عدد عشري بينهما، أي حدّين تختارون؟ الشيفرة نفسها تمامًا، تختارون حدّين مختلفين فقط. نعم.
طالب: عفوًا، لا أفهم لماذا علقت، كيف الأعلى والأدنى [غير مسموع].
آنا بيل: يمكن تشغيلها في Python Tutor. هذه 0.5، صنعنا تخمينًا هكذا، ثم نغيّر التخمينات، ترون تغيّر الأدنى والأعلى. في البداية فعلت الصحيح، فالتخمينات الأولى تغيّر بصورة مناسبة.
لكن تدخل أخطاء الفاصلة العائمة: هذا 0.4999 وهذا الأدنى الذي نواصل قسمته يصبح 0.5 في لحظة ما. و0.5 قوة من 2، تذكروا تمثيل الفاصلة العائمة. بعد بلوغها لا تؤثر الأخطاء لأنها تُمثَّل بدقة. ربما أضغط Next مرات كثيرة، لكن ترون من أين تأتي 0.5. هل يساعد؟
طالب: نعم، إذن خطأ الفاصلة العائمة [غير مسموع] يحصل [غير مسموع].
آنا بيل: ذلك، وأيضًا أننا لم نعالج هذه القيم فعلًا؛ الشيفرة لا تعمل صحيحًا معها. تدخل حلقة لا نهائية بسبب الخطأ قرب النهاية، فنرى 0.5 و0.5 و0.5. لو بدقة لا نهائية لرأيتم أعدادًا تقترب من 0.5 ولا تبلغها. لكن أظن رأيناها لأن الشيفرة مرّت 100 أو 200 مرة، فنرى النهاية فقط.
هذه شيفرة الإصلاح. ما الحدّان لتعمل بين 0 و1؟ هذه x، أصغر من 1 وأكبر من 0. جذرها هنا أعلى، ونعرف أن أقصى موضع هو 1.
وأدنى موضع للجذر في هذا النطاق؟ سمعت x. هذا الأدنى وهذا الأعلى. نقول فقط low = x وhigh = 1، وأظن تعمل، نعم.
فعلت ذلك هنا، مع السماح للمستخدم بأي قيمة لا بين 0 و1 فقط أو أكبر من 1. لجعلها تعمل ومتينة أضفت if/else في البداية؛ يختار x كما يشاء.
أفحص إن كانت x >= 1 فالحدّان 0 وx لأن الجذر فيهما. وإلا إن أعطى أقل من 1 — ينبغي إضافة أكبر من 0 تحسبًا للسالب — أختار الأدنى x والأعلى 1. if/else بسيطة والبقية كما هي.
هذه كما رأينا في الشرائح، if وelse لاختيار الحدود المناسبة. أسئلة؟ مفهومة؟ نعم.
صفحة المصدر 12
طالب: جعلت الأدنى 0 وما زال يعطيني جواب جذر [غير مسموع] 0.5 نفسه. لماذا؟
آنا بيل: إن جعلته 0 هنا، أظن لا بأس. يعني أدنى أقل مما يحتاج، وتخمينك الأول المنتصف، x نفسها، ثم أظنه يصلح ذلك.
طالب: إذن تخمين إضافي واحد فقط؟
آنا بيل: بالضبط، وهذه قوة التنصيف أيضًا. للقيم فوق 1 لو جعلنا الأعلى 2*x، سيحتاج تخمينًا إضافيًا للوصول إلى x ثم ما دونها وهكذا. تخمين واحد لا شيء للحاسوب.
بعض الملاحظات: يستغرق التنصيف أقل بكثير من التقريب لحل المسائل، ويعطينا تقريبًا للجذر بنفس جودة طريقة التقريب نفسها.
في مثال الكتاب — النقطة الثانية هنا، أسهل لتوضيحها — استبعد أول تخمين صفحات أكثر من التالية: 200 فورًا، ثم 100 ثم 50. في لحظة نتصور استبعاد أربع فقط، ثم اثنتين مع زيادة التخمينات.
يبدو مثيرًا أولًا ثم يخفت، لكن هذا النمو اللوغاريتمي. كلما اقتربتم من الجواب لا تكون الخطوات أو استبعاد صفحات الكتاب بهذا الكبر والإثارة.
التنصيف رائع لكن له حدود: فضاء بحث ذو حدّين، وترتيب أبجدي أو عددي أو غيره، وتغذية راجعة: هل التخمين أقل أم أعلى؟ دونها لا يمكن استخدامه.
أعطيكم قليلًا للعمل وحدكم: اكتبوا التنصيف لجذر مكعب موجب. لا تهتموا بالسالب، افترضوا أن المستخدم يعطي موجبًا. أهيّئ لكم القيم؛ cube هي 27.
أريد ضمن زائد أو ناقص 0.01؛ يجب أن يكون مربع التخمين ضمن ذلك من 27. ابدؤوا بأدنى 0 وأعلى cube، واكتبوا الباقي. لا تنسخوا وتلصقوا شيفرة المربع؛ حاولوا الكتابة وحدكم مجددًا، أولًا للتدرب وثانيًا للتأكد من فهم خطوات الخوارزمية.
لا يلزم من الأعلى للأسفل. اكتبوا داخل while أولًا إن كان أريح. ما دمتم تحاولون وحدكم لجعل البرمجة طبيعة ثانية فأنا أشجعكم. أعطيكم قليلًا ثم نكتب معًا. هي تقريبًا مثل ما رأيناه في الشرائح.
ملاحظة مترجم: تقول المحاضِرة «مربع التخمين» في تمرين الجذر التكعيبي؛ حُفظ القول، بينما شرط التمرين الذي تكتبه لاحقًا يستخدم
guess**3.
صفحة المصدر 13
آنا بيل: هل لدى أحد بداية؟ أتريدون while أم for؟ نسأل هذا أولًا. while، حسنًا. ما شرط التقريب؟ نعم، أحتاج تعريف التخمين، ممتاز. ماذا يكون؟ الأعلى زائد الأدنى على 2، إذن لدي الأول.
ماذا يحدث بالحَلقة؟ أواصل ما دام — التخمين للقوة الثالثة ناقص cube، نعم، القيمة المطلقة، بالضبط — أكبر، أو أكبر أو يساوي كما تريدون، من إبسيلون. ما دمت بعيدة جدًا.
طالب: [غير مسموع] نكتب لا يساوي [غير مسموع]؟
آنا بيل: لا، لأننا نقارن floats. نريد أبعد. مع عدم المساواة تتوقف فقط عندما تصبح المسافة 0.01 تمامًا. لنرسم، أسهل. هذه x وهذه إبسيلون، ومكعب التخمين عند المساواة يكون هنا بالضبط، أو هنا بالضبط.
طالب: آه، إذن [غير مسموع].
آنا بيل: نعم، نكون خارج الحدود لنواصل. ما عملية صنع تخمين جديد بالتنصيف؟ لدينا تخمين، ونقرر هل أقل أم أعلى. هذا ما يقول التنصيف.
التخمين أو مكعبه أقل أم أعلى، بالضبط. إذا كان مكعبه أكبر من cube فهو أعلى. أكتب تعليقًا لنفسي: التخمين أعلى. لا أريد ما هو أكبر، فأضبط الأعلى أم الأدنى؟ نعم، الأعلى يصبح التخمين، لأننا نعرف كبره. وإلا العكس، الأدنى يصبحه.
انتهينا؟ لا. ما المطلوب؟ إعادة تعريف التخمين، وإلا حلقة لا نهائية. كما قبل، الأعلى زائد الأدنى على 2. وفي النهاية، عند إزاحة while نفسها، نطبع التخمين لأنني أعرف أننا نخرج بمجرد الاقتراب ضمن إبسيلون أو مساواتها. رائع، هذا ما توقعنا.
ولا بأس بـ3.000 وشيء. لا أتوقع 3 تمامًا مع أننا كبشر نعرفها، لأن الخوارزمية تقول توقف بمجرد بلوغ إبسيلون. يمكن جواب أفضل لو استمررنا، لكن لم نطلبه؛ طلبنا التوقف عند زائد أو ناقص إبسيلون.
طالب: هل يهم وضع الأعلى داخل if أو الأدنى؟
آنا بيل: لا، ما دمتم متسقين: مع أكبر من تعيدون إسناد الأعلى، ومع أصغر من الأدنى.
سنرى خوارزمية أخرى لتقريب جذر عدد، لأريكم أن هناك شيئًا آخر. تعمل هذه فقط لإيجاد جذور كثيرة حدود (Polynomial). خوارزمية نيوتن–رافسون (Newton–Raphson).
صفحة المصدر 14
آنا بيل: لا نحتاج إثباتها، لكن أثبتوا أنه إذا كانت كثيرة الحدود بهذا الشكل، a*x**2 + b*x + c أو a*x**4 + b*x**3 + c*x + d أو نحوها، تبدأون بأي تخمين تريدون.
وتحصلون على تقريب أفضل للجذر بتخمين جديد: الحالي ناقص قيمة كثيرة الحدود عند التخمين — استبدلوا x بتخمينكم — مقسومة على قيمة مشتقتها عنده، أي اشتقوها واستبدلوا x بالتخمين.
ينبغي أن يبدو مألوفًا؛ نفّذنا هذا الجزء فقط في المحاضرة 2 عندما تعلمنا التعبيرات ودمجها. ذكرت الخوارزمية وقلت لن نكتبها كاملة، لكن ننفّذ الجزء الذي يعطي تخمينًا أفضل لجذر عدد. اليوم نأخذ ذلك السطر ونحيطه بغلاف، حلقة صغيرة تخمّن على التوالي أفضل فأفضل باستخدام التخمين السابق لتقريب الجذر.
الفكرة لإيجاد جذر 24 مثلًا إدراك أننا نطبّق الخوارزمية على كثيرة الحدود x**2 - 24. لأن مساواتها للصفر تعني x**2 = 24.
وحل x هو البحث عن جذر 24. يمكن تطبيق نيوتن–رافسون ببساطة على x**2 ناقص القيمة التي نريد جذرها.
حدس صغير: لدينا تخمين أولي x1 هنا، نأخذ f(x1) لنصل هنا، ونجد المشتقة ونتبع خط المماس (Tangent Line) لمحور x للتخمين التالي. ونكرر: نحسب قيمة التخمين، وهذا المماس، نتبعه للمحور لتخمين أفضل، ونواصل حتى نقترب كما نريد.
للاكتمال بما أنني ربطته، هكذا يبدو: التخمين الأول، وf، والمماس، والتالي. قيّمه، احصل على المماس ثم التالي. قيّمه، المماس، ثم التالي. يعمل أساسًا لأي كثيرة حدود، لكن نطبقه لجذر عدد فكثيرتنا بسيطة جدًا.
لجذر k، كثيرة الحدود x**2 - k. المشتقة — هل درستموها في الرياضيات؟ حسنًا جيد — هي 2*x. نهيّئ التخمين كما نشاء، ولتحسينه نأخذ الحالي ناقص قيمته في كثيرة الحدود: g**2 - k مقسومة على قيمة المشتقة 2*g.
بتكرار هذا مرات كثيرة، كثيرة، كثيرة، نصل في النهاية لتقريب جميل للجذر. وهذه الشيفرة أبسط حتى من التنصيف.
صفحة المصدر 15
آنا بيل: نريد بالتخمين ضمن زائد أو ناقص 0.01 من 24. يمكن أي تخمين، لكن المعقول k / 2. ونتتبع عدد التخمينات مجددًا. مفاجأة، شرط while نفسه الذي رأيناه في التقريب والتنصيف: ما دمنا خارج زائد أو ناقص إبسيلون، نواصل لأن التخمين لا يرضيني.
ما دامت abs(guess**2 - k) أكبر من إبسيلون، وk العدد المطلوب جذره، نتتبع التخمينات ونصنع الجديد. المختلف عن التنصيف والتقريب طريقة التخمين، نيوتن–رافسون. هذا السطر كتبناه في المحاضرة 2 أو 3: الجديد هو القديم ناقص التخمين المقيَّم عند x، أي مربع التخمين ناقص k مقسومًا على المشتقة عنده، 2 * guess.
هذا كل شيء. الحلقة تتولى البقية وتواصل حتى ضمن إبسيلون. هذه الدالة f(guess) وهذه f'(guess). لنشغّل، ها هي.
أربعة تخمينات لجذر 24، نحو 4.9، جيد جدًا؛ ضمن 0.01. وإن جربنا 5 و4 و3 و2 و1، عددنا المفضل حتى الآن، احتجنا عشرة فقط. وأعطى تقريبًا بجودة التنصيف وطريقة التقريب الطويلة بشكل سخيف. نعم؟
طالب: لماذا التخمين [غير مسموع]؟
آنا بيل: لماذا k / 2؟ يمكن أي شيء. بدأنا بمعقول يعتمد على k. يمكن 100 أو ما تريد، الخوارزمية تعمل مهما كان.
رائع، تخمينات أقل، لكنها محدودة جدًا. تستخدم فقط لإيجاد جذور قيم معينة، ولا نطبّقها لإيجاد الشخص في وسط الغرفة. خاصة جدًا بهذه المسألة.
خلاصة قبل تمهيد المحاضرة المقبلة: تحدثنا عن التكرار، الشيء الكبير بعد الشروط، طريقة لتكرار أسطر تنفعنا. ورأينا طرق التخمين والتحقق؛ أضع الآن كل الطرق تحت هذا الاسم لأنها كلها نوعًا ما تخمين وتحقق: نخمن قيمة ونفحص هل دقيقة أو ضمن إبسيلون من المطلوب.
لكل هذه الطرق الأشياء الثلاثة نفسها: حلقة وشيء نكرر فعله، وطريقة لتوليد التخمينات، وهنا تختلف الخوارزميات، وطريقة لفحص صحة التخمين أو كونه ضمن إبسيلون، ثم طريقة لمواصلة التخمين. رأينا التعداد الشامل (Exhaustive Enumeration)، الأصل، حيث أعداد صحيحة أو قيم محددة نفحصها. لأنه شامل نعرف بالضبط كم قيمة نمرّ عليها.
ملاحظة مترجم: الأرقام «5 و4 و3 و2 و1» في الكلام تشير إلى إدخال 54321. والتفريغ يحفظ التعميم الشفهي «مهما كان»؛ لا يعني ذلك ضمانًا رياضيًا لتقارب نيوتن–رافسون من كل تخمين، ولا جواز القسمة على صفر.
صفحة المصدر 16
آنا بيل: أتاحت خوارزميات التقريب زيادات أصغر وتقريب جذور تربيعية أو تكعيبية أو أي مسألة نحلها. التنصيف تحسين، لكن للمسائل ذات الترتيب والتي تعرفون فيها هل التخمين أعلى أم أقل. دون ذلك لا تطبّقونه؛ تبقون مع التقريب أو غيره.
نيوتن–رافسون آخر ما رأينا، خوارزمية محددة جدًا لجذور قيم، لكنها مفيدة لعرض بناء الحلقة والفحص ثم التخمين الجديد. هذا ملخص ما قلته، فلا نحتاج مراجعته. أسئلة عن الخوارزميات الثلاث؟ مفهومة؟ آمل أن ممارسة الشيفرة أثناء المحاضرات ساعدت. أسئلة؟ لا؟ حسنًا.
في الدقائق الخمس الأخيرة تقريبًا أقدّم الدافع للموضوع التالي. تعلمنا كتابة شيفرة: تعبيرات ومتغيرات وشروط وحلقات، والأخيرتان لتدفّق التحكم. لدينا صندوق أدوات جميل لكتابة خوارزميات.
صحيح أن لدينا كل المطلوب لخوارزميات مثيرة وقد كتبناها، لكن لم نعلّمكم مفاهيم مهمة في البرمجة، موجودة في كل اللغات الحديثة. هي التفكيك (Decomposition) والتجريد (Abstraction). أعطي الدافع اليوم فقط، لا شيفرة. أريكم نسخة أبسط مما فعلتموه بالفعل، وفي المقبلة نرى التنفيذ بالشيفرة.
التفكيك أخذ برنامج كبير وتقسيمه لأجزاء أصغر، كل منها مكتفٍ بذاته (Self-contained). لا تتداخل، بمعنى لا تؤثر شيفرة جزء في شيفرة آخر، لكنها تتخاطب وتبادل القيم ذهابًا وإيابًا.
تأخذون برنامجًا كبيرًا متشابك الشيفرة (Spaghetti Code) وتقسمونه لأجزاء مستقلة جميلة، يحل كل منها جزءًا مختلفًا من البرنامج الكبير، ثم تتجمع للحل الأكبر. هذا التفكيك. والتجريد أنه بعد كتابة الأجزاء مرة واحدة وبذل العمل والتفكير بكفاءة، لا يحتاج الآخرون معرفة تفاصيل التنفيذ.
تُجرِّدون التفاصيل كلها إلى نص أو واجهة (Interface) تقول: حللت المسألة؛ أعطني هذا الإدخال وهذا وهذا، وستحل شيفرتي وتعطيك هذا الإخراج.
مثل مشروع جماعي، كل واحد يفعل جزءه. لا يهمني استخدام الإنترنت أو المكتبة، بل أن نعود ونجمّع النتائج. هذا التجريد: تفاصيل غير ضرورية في الشيفرة لا أهتم بها ولا بطريقة الحل، بل بأنه حل، فأخبرني كيف أتفاعل معه.
صفحة المصدر 17
آنا بيل: هذه أمثلة منخفضة المستوى نوعًا ما لاستخدامنا التفكيك والتجريد بالفعل. التفكيك كتابة أجزاء أصغر مكتفية بذاتها. إن أعطيتكم هذا — تحدثت عن الشيفرة المتشابكة، وهذه نسخة أبسط منها.
هذا السطر فوضوي قليلًا: قيمة أعرف أهميتها، خصوصًا إن عرّفتها بمنازل عشرية كثيرة، وقيمتان أنسخهما هنا. ليس أسلوبًا أو ممارسة جيدة.
لكن يمكن أخذ القيم وحفظها أو تفكيكها لأشياء قابلة لإعادة الاستخدام. pi تهمني فأحفظها في متغير. وr، قيمتها 2.2، أحفظها باسم r لاستخدامها في مواضع كثيرة. بدل نسخ ولصق 2.2 هنا وهنا واحتمال الخطأ عند كتابتها، أستخدم المتغير.
فككت هذا الجزء الصغير إلى أجزاء وحداتية (Modular) جميلة: pi وحدة، وr وحدة، ثم أجمعهما لهدف مشترك هو المساحة. سنرى ذلك على نطاق أكبر بالدوال (Functions) في المحاضرة المقبلة.
التجريد فعلناه أيضًا، وآمل عبر التعليقات (Comments). إذا أمضيتم وقتًا بمجموعة المسائل عند إصدارها وكتبتم شيفرة جيدة بطريقة رائعة، ثم بعد أسبوع نسيتم التفاصيل ولم تكتبوا تعليقات، فمشكلة كبيرة لأن عليكم اكتشاف ما تفعل الشيفرة مجددًا.
لو كتبتم تعليقًا في البداية لطريقة مثيرة، أو «استخدمت التنصيف هنا» أو نحوها، لأخفيتم كثيرًا من التفاصيل مع تذكر ما تفعل الشيفرة. إذن إخفاء التفاصيل بالتعليقات، وفي المقبلة نرى إخفاء تفاصيل كتل أكبر أيضًا. هذا التجريد هنا.
الفكرة الكبرى للمقبلة إيقاف كتابة كتل كبيرة ننسخ ونلصق فيها ما يفعل الشيء نفسه مرة بعد مرة، لأن هذا يسبب أخطاء: تغيّرون موضعًا وتنسون آخر. سنكتب وحدات صغيرة اسمها دوال، تكتبونها مرة وتنقّحونها مرة، وتستخدمونها مرة بعد مرة بعد مرة بإدخالات مختلفة لمخرجات مختلفة.
نريد شيفرة يسهل تعديلها وصيانتها وفهمها. إن عدتم بعد أسبوع أو عام تعرفون ما تفعل. هذا دافع المحاضرة المقبلة؛ نبدأ بمثال واقعي ثم ندخل الدوال مباشرة.