انتقل إلى المحتوى
Topos Uranos

ملخص

علّمت الدروس الماضية من هذه الوحدة كتابة الحلقات والاستدلال عليها باللامتغير والمتغاير؛ وهذا الدرس يطبّق ذلك الاستدلال على أربع خوارزميات عددية كلاسيكية، أقدم من الحواسيب وما زالت مستعملة. اختبار الأولية بالقسمة التجريبية، الذي يتوقف عند n\sqrt{n} لسبب يُبرهَن عليه، والذي يفيض شرط توقفه إذا كُتب بلا انتباه. وخوارزمية إقليدس للقاسم المشترك الأكبر، بلامتغيرها وتوقفها وفخّها مع الأعداد السالبة ومقارنتها بـstd::gcd. وتغيير الأساس بالقسمة المتتالية وقاعدة هورنر، التي تعيد بناء العدد من أرقامه وتحسب قيمة كثير الحدود بالمخطط نفسه. وطريقة التنصيف، التي تقرّب جذرًا بقسمة فترة إلى نصفين، وتضمنها مبرهنة القيمة الوسطى، وعدد جولاتها يُحسب مسبقًا. ويُعرض كل خوارزمية بوصفها كائنًا رياضيًا: ببرهان على أنها تفعل ما تقول، وبرهان على أنها تتوقف، وعدٍّ أوّلي لخطواتها.

أهداف التعلم

  1. تنفيذ اختبار الأولية بالقسمة حتى n\sqrt{n} وتعليل لماذا يكفي الوصول إلى n\sqrt{n}.
  2. تنفيذ خوارزمية إقليدس، وصياغة لامتغيرها (القاسم المشترك الأكبر لا يتغير) ومقارنتها بـstd::gcd.
  3. تحويل عدد إلى أساس آخر بالقسمة المتتالية، وحساب قيمة تمثيل موضعي، أو قيمة كثير حدود، بقاعدة هورنر.
  4. تقريب جذر بطريقة التنصيف حتى هامش تسامح معيّن، وحساب عدد الجولات التي يتطلبها ذلك.

القواسم والأعداد الأولية

القسمة التجريبية

يكون العدد الصحيح n≥2n \geq 2 أوليًا إذا كان قاسماه الموجبان الوحيدان هما 1 وnn؛ وإذا كان له قاسم آخر فهو مؤلَّف؛ أما 0 و1 فليسا هذا ولا ذاك. ويوحي التعريف فورًا بخوارزمية: تجربة كل مرشَّح dd بين 2 وn−1n - 1، والحكم على العدد بأنه مؤلَّف حالما يقسمه أحدها. وهي حلقة القاسم الأصغر من الدرس الحلقات، وهي صحيحة. غير أنها، مع عدد أولي من عشرة أرقام، تُجري قرابة عشرة مليارات قسمة، وكلها تقريبًا بلا فائدة، للسبب الآتي.

قضيةتكفي التجربة حتى n\sqrt{n}

إذا كان n≥2n \geq 2 مؤلَّفًا، فله قاسم dd يحقق 2≤d≤n2 \leq d \leq \sqrt{n}.

البرهان

  1. n=a⋅b,2≤a≤bn = a \cdot b, \quad 2 \leq a \leq b

    إذا كان nn مؤلَّفًا، فإنه يُكتب حاصلَ ضرب عاملين، لا يساوي أيٌّ منهما 1 ولا nn. ونسمّي aa أصغرَهما.

  2. a2≤a⋅b=na^2 \leq a \cdot b = n

    بضرب طرفي a≤ba \leq b في العدد الموجب aa نحصل على a2≤ab=na^2 \leq ab = n، ومن ثَمّ a≤na \leq \sqrt{n}: العامل الأصغر في كل تحليل يقع على يسار n\sqrt{n}.

  3. d≤n  ⟺  nd≥nd \leq \sqrt{n} \iff \frac{n}{d} \geq \sqrt{n}

    فالقواسم تأتي إذن في أزواج (d,n/d)(d, n/d) متناظرة حول n\sqrt{n}: مع n=36n = 36، الأزواج هي (2,18)(2, 18) و(3,12)(3, 12) و(4,9)(4, 9) و(6,6)(6, 6). وإذا لم يقسم أيُّ مرشَّح حتى n\sqrt{n} العددَ nn، فلن يقسمه أيٌّ مما بعده، لأن قرينه كان سيظهر قبله.

ومن ذلك يلزم أن عدد القسمات ينخفض من نحو nn إلى نحو n\sqrt{n}: فمع nn قريب من ملياري، ينخفض من ملياري قسمة إلى ستة وأربعين ألفًا. ويستعمل البرنامج نمط العَلَم من الدرس تصميم حلقات صحيحة: يبدأ isPrime صادقًا، وتُطفئه الحلقة إذا وجدت قاسمًا.

برنامج بلغة C++
#include <iostream>#include <print>​int main(){    int n{};    std::cin >> n;    bool isPrime{n >= 2};    for (int d{2}; isPrime && d <= n / d; ++d) {        if (n % d == 0) {            isPrime = false;        }    }    if (isPrime) {        std::println("العدد {} أولي", n);    } else {        std::println("العدد {} ليس أوليًا", n);    }}
مدخلات البرنامج
2147483647
المخرجات
العدد 2147483647 أولي

ثلاثة قرارات تستحق التعليق. يُهيَّأ العَلَم بـn >= 2، فيُحكم على 0 و1 والأعداد السالبة بأنها غير أولية دون دخول الحلقة: ونسيان هذه الحالة هو الخطأ النموذجي، لأنه مع n=1n = 1 لا تُجري الحلقة أي جولة، فيحكم البرنامج بأن 1 أولي. وشرط for له جزءان: العَلَم، الذي يوقف الحلقة عند أول قاسم، والحدّ. والحدّ مكتوب d <= n / d، وهو، للأعداد الصحيحة الموجبة، مكافئ لـd2≤nd^2 \leq n (فالقسمة الصحيحة تبتر الكسر، لكن dd لا يتجاوز خارج القسمة المبتور بالضبط حين لا يتجاوز n/dn/d، لأن dd عدد صحيح). والعدد المُدخل، 231−12^{31} - 1، هو أكبر قيمة من نوع int، وهو أولي؛ ويحسم البرنامج ذلك بـ46 33946\,339 باقيًا، هي بواقي قسمته على المرشَّحات من 2 إلى 46 34046\,340.

لامتغير الحلقة هو: لا يقسم أيُّ عدد صحيح من 2 إلى d−1d - 1 العددَ nn، ما دام العَلَم صادقًا. وهو صحيح عند الدخول، حين تكون الفترة من 2 إلى 1 خالية؛ ويُحفظ، لأنه إذا لم يقسم dd العددَ nn فإن الفترة تزداد بعدد لا يقسمه؛ وعند الخروج والعَلَم صادق، يقول اللامتغير والشرط الكاذب، d2>nd^2 > n، معًا إن أيَّ مرشَّح حتى n\sqrt{n} لا يقسم nn، وهذا ما تقتضيه القضية. والمتغاير هو عدد المرشَّحات الباقية للتجربة، أي عدد الأعداد الصحيحة بين dd وn\sqrt{n}: ينقص واحدًا في كل جولة ولا ينزل تحت الصفر.

الشرط الذي يفيض

الطريقة الطبيعية لكتابة الحدّ هي d * d <= n، وهي خطأ. فمع المُدخل نفسه تصل الحلقة إلى d=46 341d = 46\,341، وحينئذ يتجاوز حاصل الضرب 46 3412=2 147 488 28146\,341^2 = 2\,147\,488\,281 القيمةَ العظمى لـint.

برنامج بلغة C++
#include <iostream>#include <print>​int main(){    int n{};    std::cin >> n;    bool isPrime{n >= 2};    for (int d{2}; isPrime && d * d <= n; ++d) {        if (n % d == 0) {            isPrime = false;        }    }    std::println("أولي؟ {}", isPrime);}
مدخلات البرنامج
2147483647
المخرجات
main.cpp:9:33: runtime error: signed integer overflow: 46341 * 46341 cannot be represented in type 'int'

انتهى البرنامج بالرمز 1.

يوقف كاشف السلوك غير المعرَّف البرنامج عند حاصل الضرب. ومن دونه، يكون فيضان الأعداد ذات الإشارة سلوكًا غير معرَّف، ولا يقول المعيار ماذا يحدث. وما يُلاحَظ مع GCC على Linux، مع التحسين ومن دونه، مفيد للتعلم: يلتفّ حاصل الضرب فيصير سالبًا أو أصغر من nn، ولأنه لا توجد قيمة من نوع int تتجاوز القيمة العظمى، فإن الشرط d * d <= n لا يكفّ عن التحقق أبدًا؛ وتظل الحلقة تجرّب المرشَّحات نحو ملياري جولة، حتى تبلغ d=nd = n، الذي يقسم nn، فيحكم البرنامج بأن عددًا أوليًا مؤلَّف. فخطأ الفيضان لا يظهر في صورة فيضان، بل في صورة جواب خاطئ. أما الصورة d <= n / d فلا يمكن أن تفيض، لأنها تقسم بدل أن تضرب؛ وقد عرض الدرس البتات والمكتبة الرياضية مخرجًا آخر، هو حساب الحدّ مرة واحدة بـstd::sqrt، لكن دقة الحدّ تتوقف حينئذ على تقريب الفاصلة العائمة، والقسمة الصحيحة لا تعاني هذه المشكلة.

يبرهن الدرس «الأعداد الأولية والمبرهنة الأساسية في الحساب»، من دورة الجبر والهندسة 1، على أن كل عدد صحيح أكبر من 1 هو حاصل ضرب أعداد أولية بطريقة واحدة فقط؛ والقسمة التجريبية هي أقصر طريق لإيجاد هذا الحاصل، بقسمة nn على كل قاسم يُعثر عليه ومتابعة العمل بخارج القسمة.