اذهب إلى المحتوى

محمد بغات

الأعضاء
  • المساهمات

    177
  • تاريخ الانضمام

  • تاريخ آخر زيارة

  • عدد الأيام التي تصدر بها

    6

آخر يوم ربح فيه محمد بغات هو سبتمبر 17 2019

محمد بغات حاصل على أكثر محتوى إعجابًا!

آخر الزوار

7181 زيارة للملف الشخصي

إنجازات محمد بغات

عضو نشيط

عضو نشيط (3/3)

80

السمعة بالموقع

  1. السلام عليكم. مرحبا. حاولت الدخول إلى صفحة marketplace.zoom.us لأجل الحصول على API KEY. لكن المشكلة أني حصلت على صفحة بيضاء وفارغة. ولما بحثت لاحظت أن هذه المشكلة تحصل للمستخدمين من الدول العربية. سألت بعض الأصدقاء من السعودية ومصر، ويبدو أن المشكلة لديهم هم أيضا. يبدو أن شركة زوم تمنع العرب عن عمد من الوصول إلى هذه الصفحة، لأن المشكلة ليست موجودة في الدول الأخرى. ما يؤكد هذه النظرية أنهم يتحاشون الإجابة عن هذا في منتدى الدعم الفني الخاص بزوم. المشكلة أن هذه الصفحة مهمة، لأنه لا يمكن الحصول على ZOOM API KEY لربط المواقع بخدمات زوم بدونها. هل هذه المشكلة موجودة في دولتك أيضا، أي عندما تحاول الوصول إلى صفحة marketplace.zoom.us، هل ترى صفحة بيضاء (جرب على الديسكتوب أما الجوال فيمكن الوصول إلى الصفحة، لكنها لا تتيح الوصول إلى API KEY)؟ وفي حال كانت زوم تحظر العرب عن عمد فما السبب في رأيك؟ وما الحل للوصول إلى ZOOM API KEY.
  2. هل يفتح عندك؟ وإ كان يفتح فمن أي دولة أنت؟ لأني قرأت أن هذه المشكلة موجودة في السعودية ومصر، وأنا من المغرب. أي ثلاث دول.
  3. السلام عليكم. مرحبا. حاولت الدخول إلى صفحة marketplace.zoom.us لأجل الحصول على API KEY. لكن المشكلة أني حصلت على صفحة بيضاء وفارغة. ولما بحثت لاحظت أن هناك اتهامات من العرب بأن هذا مُتعمذ، وأن هذه المشكلة موجودة في الدول العربية الأخرى. أتساءل هل هناك استهداف للعالم العربي ومنع الزوار من الدول العربية من الوصول إلى هذه الصفحة. علما أنها ضرورية، وبدونها لا يمكن الحصول على المفتاح API KEY. هل هناك من جرب الوصول إلى هذه الصفحة (من الديسكتوب أما الجوال فيمكن الوصول إليها لكنها لا تتيح الوصول إلى API KEY)
  4. يُستخدم تحويل فورييه المتقطع Discrete Fourier Transforms أو DFT بشكليه الحقيقي والمركّب لتحليل ترددات الإشارات المتقطعة والدورية. تحويل فورييه السريع Fast Fourier Transform أو FFT هو تطبيق خاص لتحويل فورييه المتقطع DFT، يتميّز بالسرعة ويناسب وحدات المعالجة المركزية الحديثة. تحويل فورييه السريع من الأساس 2 لعلّ أبسط طرق حساب تحويل فورييه السريع FFT وأشهرها هي خوارزمية التعشير الزمني من الأساس 2 أي Radix-2 Decimation in Time، وينبني عمل هذه الخوارزمية على تفكيك إشارة معيّنة ذات نطاق زمني مؤلف من N نقطة إلى N إشارة أحادية (أي ذات نطاق زمني مؤلف من نقطة واحدة). يمكن تفكيك الإشارة -أو التعشير الزمني decimation in time- عن طريق عكس بتّات المؤشّرات في مصفوفة بيانات النطاق الزمني، فإن كانت الإشارة مؤلفة من ستة عشر نقطة مثلًا، فستُبدّل العينة 1 (ترميزها الثنائي 0001) بالعينة 8 (ترميزها الثنائي 1000)، وتبديل العينة 2 (0010) بـ 4 (0100) وهكذا دواليك. يمكن إجراء مبادلة العينات Sample swapping باستخدام تقنية عكس البتات bit reverse، بيْد أنّ ذلك سيحدّ إمكانية استخدام خوارزمية Radix 2 FFT على الإشارات التي يمكن كتابة أطوالها وفق الصيغة N = 2 ^ M. تتطابق قيم الإشارات الأحادية داخل النطاق الزمني time domain مع قيمها في نطاق التردد frequency domain، لهذا لا تتطلب هذه المصفوفة المؤلّفة من نقاط النطاق الزمني المفكّكة أيّ تحويل لتصبح مصفوفة لترددات النطاق. بيْد أنّه يجب إعادة بناء النقاط الفردية، وعددها N كما تعلم ، إلى طيف ترددات مؤلف من N نقطة N-point frequency spectra. تُعدّ طريقة الفراشة أفضل طريقة لإعادة بناء طيف الترددات. تتجنّب خوارزمية تحويل فورييه السريع الحسابات الزائدة التي يُجريها تحويل فورييه المتقطع DFT، وذلك عبر استغلال التواتر الدوري periodicity لـ Wn ^ R. تستغرق عملية إعادة البناء الطيفي عدد log2 (N)‎‎ مرحلة من مراحل حسابات الفراشة لتُعيد قيمة X [K]‎‎؛ أي بيانات نطاق التردد وفق الشكل الجبري. ولكي نحول الإحداثيات الجبرية إلى إحداثيات قطبية فإننا نحسب القيمة المطلقة التي تساوي: ‎√‎(Re^2 + Im^2)، حيث يمثّل Re الجزء الحقيقي من العدد المركب، ويمثّل Im الجزء التخيلي. سيكون علينا إيضًا حساب وسيط العدد المركّب عبر الصيغة: tan-1 (Im / Re)‎‎. حيث: N: عدد النقاط في تحويل فورييه السريع R: قيمة WN الحالية، وتعتمد على قيمة N و المرحلة الحالية من تحويل فورييه السريع وقيمة حسابات الفراشة في هذه المرحلة. يمثل الرسم البياني التالي مخطّط الفراشة لخوارزمية فورييه السريع الثُمانِي من الأساس 2 - أي eight point Radix 2 FFT- ولاحظ أن الإشارات المُدخلة رُتَِبت وفقًا لقيم التعشير الزمني المشار إليه سابقًا. يأخذ تحويل فورييه السريع أعدادًا مركّبة ويعيد أعدادًا مركّبة، فإن أردت تمرير إشارات حقيقية (أي تنتمي لمجموعة الأعداد الحقيقية R)، فيجب أن تعيّن الجزء التخيلي على القيمة 0، وتعطي للجزء الحقيقي قيمة الإشارة المُمرَّرة، أي x [n]‎‎. يمكن تحديد قيم Wn ^ R المستخدمة خلال أطوار إعادة البناء باستخدام معادلة أسّية موزونة exponential weighting equation. تُحدَِّد قيمة R (القوة الأسية الموزونة) المرحلة الحالية في عملية إعادة البناء الطيفية ومرحلة حسابات الفراشة. هذه شيفرة مكتوبة بلغة C / C++‎ لحساب تحويل فورييه السريع من الأساس 2. تعمل هذه الشيفرة مهما كان الحجم N، شرط أن يكون N قوة للعدد 2. هذه الطريقة أبطأ 3 مرات تقريبًا من أسرع تقديم لتحويل فورييه السريع، ولكنّها تقنية واعدة ويمكن تحسينها مستقبلًا، كما أنّها مناسبة لتعليم أساسيات خوارزمية تحويل فورييه السريع. #include <math.h> #define PI 3.1415926535897932384626433832795 // PI قيمة #define TWOPI 6.283185307179586476925286766559 // 2*PI قيمة // ‫معامل التحويل من Degrees إلى Radians #define Deg2Rad 0.017453292519943295769236907684886 // ‫معامل التحويل من Radians إلى Degrees #define Rad2Deg 57.295779513082320876798154814105 / #define log10_2 0.30102999566398119521373889472449 // Log10 of 2 #define log10_2_INV 3.3219280948873623478703194294948 // 1/Log10(2) // بنية لتمثيل الأعداد المركبة struct complex { public: double Re, Im; }; // ‫تعيد true إن كان N قوة للعدد 2 bool isPwrTwo(int N, int *M) { // ‫تمثل M عدد المراحل التي يجب إجراؤها،‪‫ 2‪^M = N *M = (int)ceil(log10((double)N) * log10_2_INV); int NN = (int)pow(2.0, *M); // ‫تحقق من أنّ N قوة للعدد 2 if ((NN != N) || (NN == 0)) return false; return true; } void rad2FFT(int N, complex *x, complex *DFT) { int M = 0; // ‫تحقق من أنّه قوة للعدد 2، وإن كان خلاف ذلك فأنهِ البرنامج if (!isPwrTwo(N, &M)) throw "Rad2FFT(): N must be a power of 2 for Radix FFT"; // متغيرات صحيحة // ‫لتخزين المسافة في الذاكرة بين حسابات الفراشة int BSep; // لتخزين المسافة بين الطرفين المتقابلين في حسابات الفراشة int BWidth; // المتماثلة التي ستُستخدم في هذه المرحلة Wn عدد قيم P يمثل المتغير int P; // في حلقة التكرار لإجراء كل الحسابات في هذه المرحلة j يُستخدم int j; // رقم المرحلة الحالية في تحليل فورييه السريع stage يمثل المتغير // في المجموعة M هناك int stage = 1; // الخاصة بالقيمة العليا DFT يمثَّل فهرس مصفوفة // في كل عملية من حسابات الفراشة int HiIndex; // يُستخدم في قلب القناع البتي unsigned int iaddr; // يُستخدم في التعشير الزمني int ii; int MM1 = M - 1; unsigned int i; int l; unsigned int nMax = (unsigned int)N; // ‫يمثل TwoPi_N ثابتة لتخزين مدّة الحسابات = ‪ 2*PI / N double TwoPi_N = TWOPI / (double)N; double TwoPi_NP; // أعداد مركبة complex WN; // ‫يمثل دالة الوزن الأسية وفق الشكل ‪ a + jb complex TEMP; // ‫يُستخدم لتخزين نتائج حسابات الفراشة complex *pDFT = DFT; // ‫مؤشر يشير إلى العنصر الأول في مصفوفة DFT complex *pLo; // ‫ مؤشّر يشير إلى lo / hi في حسابات الفراشة complex *pHi; complex *pX; // x[n] مؤشر إلى for (i = 0; i < nMax; i++, DFT++) { // ‫حساب قيمة x[n]‎‎ انطلاقا من العنوان ‎‎*x والفهرس i pX = x + i; // DFT[n] إعادة تعيين عنوان جديد لـ ii = 0; iaddr = i; for (l = 0; l < M; l++) // ii وتخزين الناتج في i قلب بتات { if (iaddr & 0x01) // تحديد البتة الأقل أهمية ii += (1 << (MM1 - l)); iaddr >>= 1; operations for speed increase if (!iaddr) break; } DFT = pDFT + ii; DFT->Re = pX->Re; DFT->Im = pX->Im; // العدد التخيلي يساوي 0 دائما } for (stage = 1; stage <= M; stage++) { //‫تقسيم حسابات الفراشة إلى ‪ 2^stage BSep = (int)(pow(2, stage)); // ‫قيم Wn المتماثلة في هذه المرحلة = ‪N/Bsep P = N / BSep; // ‫عرض المرحلة الحالية في حسابات الفراشة // (المسافة بين النقاط المتقابلة) BWidth = BSep / 2; TwoPi_NP = TwoPi_N*P; for (j = 0; j < BWidth; j++) { //‫حفظ الحسابات إن كان R = 0، حيث ‪ WN^0 = (1 + j0) if (j != 0) { //WN.Re = cos(TwoPi_NP*j) // (‫حساب Wn (الجزء الحقيقي والتخيلي WN.Re = cos(TwoPi_N*P*j); WN.Im = -sin(TwoPi_N*P*j); } for (HiIndex = j; HiIndex < N; HiIndex += BSep) { pHi = pDFT + HiIndex; // التأشير إلى القيمة العليا pLo = pHi + BWidth; // التأشير إلى القيمة السفلى if (j != 0) { //CMult(pLo, &WN, &TEMP); TEMP.Re = (pLo->Re * WN.Re) - (pLo->Im * WN.Im); TEMP.Im = (pLo->Re * WN.Im) + (pLo->Im * WN.Re); //CSub (pHi, &TEMP, pLo); pLo->Re = pHi->Re - TEMP.Re; pLo->Im = pHi->Im - TEMP.Im; //CAdd (pHi, &TEMP, pHi); pHi->Re = (pHi->Re + TEMP.Re); pHi->Im = (pHi->Im + TEMP.Im); } else { TEMP.Re = pLo->Re; TEMP.Im = pLo->Im; //CSub (pHi, &TEMP, pLo); pLo->Re = pHi->Re - TEMP.Re; pLo->Im = pHi->Im - TEMP.Im; //CAdd (pHi, &TEMP, pHi); pHi->Re = (pHi->Re + TEMP.Re); pHi->Im = (pHi->Im + TEMP.Im); } } } } pLo = 0; pHi = 0; pDFT = 0; DFT = 0; pX = 0; } تحويل فورييه السريع المعكوس من الأساس 2 يمكن الحصول على تحويل فورييه المعكوس عبر تعديل ناتج تحويل فورييه السريع العادي، ويمكن تحويل البيانات في نطاق الترددات frequency domain إلى النطاق الزمني time domain بالطريقة التالية: اعثر على مرافق العدد المركب لبيانات نطاق الترددات. طبّق تحويل فورييه السريع العادي على مرافق بيانات نطاق التردّد conjugated frequency domain data. اقسم كل مخرجات نتيجة تحويل فورييه السريع على N للحصول على القيمة الصحيحة للنطاق الزمني. احسب مرافق العدد المركب للعنصر الناتج عبر عكس المكوّن التخيلي لبيانات النطاق الزمني لجميع قيم n. ملاحظة: تُعدّ كل من بيانات نطاق التردد وبيانات النطاق الزمني متغيّراتٍ مركبة، وعادة ما يساوي المكونّ التخيلي للنطاق الزمني للإشارة التي تعقُب تحويل فورييه السريع المعكوس القيمة 0، وإلا فستُعد خطأ تقريبيًا وتُتجاهل. وإنّ زيادة دِقة المتغيرات من 32 بتّة عشرية 32-bit float إلى 64 بتّة مزدوجة 64-bit double أو إلى 128 بتّة مزدوجة سيقلّص أخطاء التقريب الناتجة عن تعاقب عدّة تحويلات فورييه سريعة. هذا تطبيق مكتوب بلغة C / C++‎: #include <math.h> #define PI 3.1415926535897932384626433832795 #define TWOPI 6.283185307179586476925286766559 #define Deg2Rad 0.017453292519943295769236907684886 #define Rad2Deg 57.295779513082320876798154814105 #define log10_2 0.30102999566398119521373889472449 // Log10 of 2 #define log10_2_INV 3.3219280948873623478703194294948 // 1/Log10(2) // بنية لتمثل الأعداد المركبة struct complex { public: double Re, Im; }; void rad2InverseFFT(int N, complex *x, complex *DFT) { // ‫تمثل M عدد المراحل التي يجب إجراؤها،‪‫ 2‪^M = N double Mx = (log10((double)N) / log10((double)2)); int a = (int)(ceil(pow(2.0, Mx))); int status = 0; if (a != N) { x = 0; DFT = 0; throw "rad2InverseFFT(): N must be a power of 2 for Radix 2 Inverse FFT"; } complex *pDFT = DFT; complex *pX = x; double NN = 1 / (double)N; // تعديل معامل تحويل فورييه السريع المعكوس for (int i = 0; i < N; i++, DFT++) DFT->Im *= -1; // حساب مرافق التردد الطيفي DFT = pDFT; rad2FFT(N, DFT, x); // حساب تحويل فورييه السريع العادي int i; complex* x; for ( i = 0, x = pX; i < N; i++, x++){ x->Re *= NN; // ‫تقسيم النطاق الزمني على N لتصحيح السعة x->Im *= -1; // ImX تغيير إشارة } } ترجمة -بتصرّف- للفصل 55 من كتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال السابق: خوارزميات للتعامل مع المصفوفات matrix algorithms المرجع الشامل إلى: تعلم الخوارزميات للمبتدئين خوارزميات البحث في النصوص أمثلة عن أنواع الخوارزميات خوارزميات الترتيب وأشهرها
  5. المصفوفات matrices هي إحدى مفاهيم الجبر الخطي، ولها تطبيقات في الكثير من المجالات، وسوف نستعرض في هذه المقالة خوَارزميتان من خوارزميات المصفوفات. طباعة المصفوفات سنبدأ بخوارزمية بسيطة لطباعة المصفوفات. تأخذ الخوارزمية مصفوفة ثنائية الأبعاد وتطبع عناصرها الواحد تلو الآخر وفق ترتيب خاص، وإليك مثال على ذلك: Input: الدخل 14 15 16 17 18 21 19 10 20 11 54 36 64 55 44 23 80 39 91 92 93 94 95 42 Output: الخرج print value in index // اطبع القيم 14 15 16 17 18 21 36 39 42 95 94 93 92 91 64 19 10 20 11 54 80 23 44 55 or print index // أو اطبع الفهارس 00 01 02 03 04 05 15 25 35 34 33 32 31 30 20 10 11 12 13 14 24 23 22 21 هذه شيفرة عامة للخوارزمية: function noOfLooping(m,n) { if(m > n) { smallestValue = n; } else { smallestValue = m; } if(smallestValue % 2 == 0) { return smallestValue/2; } else { return (smallestValue+1)/2; } } function squarePrint(m,n) { var looping = noOfLooping(m,n); for(var i = 0; i < looping; i++) { for(var j = i; j < m - 1 - i; j++) { console.log(i+''+j); } for(var k = i; k < n - 1 - i; k++) { console.log(k+''+j); } for(var l = j; l > i; l--) { console.log(k+''+l); } for(var x = k; x > i; x--) { console.log(x+''+l); } } } squarePrint(6,4); ضرب المصفوفات ضرب المصفوفات هي إحدى العمليات الأساسية على المصفوفات، ولها تطبيقات عديدة، مثل حلّ نِظمات المعادلات التفاضلية الخطية وغيرها، سوف نستعرض في هذه الفقرة أحد تطبيقات ضرب المصفوفات، وهو حساب أعداد فيبوناتشي. نعلم أنّ العثور على العنصر رقم n في متتالية فيبوناتشي سهل للغاية عندما يكون n صغيرًا نسبيًا، إذ يكفي أن نستخدم منظورًا عوديًا بسيطًا، حيث أنّ f‎(n)=‎f‎(n-1)+‎f‎(n-2)‎، أو يمكننا استخدام منظور البرمجة الديناميكية لتجنب تكرار الحسابات. لكنّ هذه الطرق لا تنفع لحل المشاكل المعقّدة، فماذا ستفعل مثلًا إن طُلِب منك حلّ هذه المشكلة: حيث تمثّل mod عملية حساب الباقي. لن تنجح البرمجة الديناميكية هنا، لذا علينا أن نبحث عن حل أفضل لهذه المشكلة، وإحدى الحلول الممكنة لهذه المشكلة هي استخدام ضرب المصفوفات لتمثيل العلاقة العَودية. قبل أن نواصل، يُستحبّ أن تكون لك معرفة أولية بالمفاهيم الرياضية الآتية. لتكن A و B مصفوفتين، يجب أن تكون قادرًا على حساب ضرب هاتين المصفوفتين، أي BxA. لتكن A و B مصفوفتين، يجب أن تكون قادرًا على إيجاد المصفوفة T التي تحقق B = TxA. لتكن A مصفوفة أبعادها d X d، يجب أن تكون قادرًا على إيجاد قوّتها إلى n (في مدّة O(d3log(n))‎‎). سنحتاج في البداية إلى إنشاء مصفوفة M تمثّل العلاقة العَودية، بحيث يمكننا انطلاقًا منها حساب الحالة / القيمة المطلوبة انطلاقًا من الحالات / القيم المعروفة سلفًا من المتتالية. لنفترض أنّنا نعرف قيمة k حالة سابقة من المتتالية العودية، نريد أن نحسب قيمة الحالة رقم (k +1) من المتتالية انطلاقًا من الحالات المعروفة. لتكن M مصفوفة حجمها k X k، سنبني مصفوفة A حجمها k X 1 تحتوى الحالات المعروفة لعلاقة العوديّة، وسننشئ أيضًا مصفوفة B حجمها k X 1 تحتوي قيم الحالات الموالية، أي تحقق MXA = B: | f(n) | | f(n+1) | | f(n-1) | | f(n) | M X | f(n-2) | = | f(n-1) | | ...... | | ...... | | f(n-k) | | f(n-k+1) | إن استطعنا تصميم المصفوفة M وفقًا لهذه الخاصيات، فسنكون قادرين على حل المشكلة بيُسر. سنجرّب هذه الطريقة على عدةّ أنواع من علاقات العوديّة. النوع 1 لنبدأ بالحالة الأبسط: ‎f(n) = f(n-1) + f(n-2)‎، سنحصل على: ‎f(n+1) = f(n) + f(n-1)‎. لنفترض أنّنا نعرف قيمتي ‎f(n)‎ و ‎f(n-1)‎، ونريد أن نحسب قيمة ‎f(n+1)‎ انطلاقًا من القيمتين السابقتين. نبدأ بإنشاء المصفوفتين A و B كما وضّحنا سابقًا: Matrix A Matrix B | f(n) | | f(n+1) | | f(n-1) | | f(n) | لاحظ أن المصفوفة A تُصمّم بحيث تحتوي كل الحالات التي تعتمد عليها ‎f(n+1)‎، وعلينا الآن تصميم مصفوفة M حجمها 2X2 تحقّق MxA = B. العنصر الأول من B هو ‎f(n+1)‎، ويساوي ‎f(n) + f(n-1)‎ بحسب علاقة العوديّة. نحن نعلم أنّ العنصر الأول من B يساوي الصف الأول من M مضروبًا في A حسبَ قواعد ضرب المصفوفات، أي أنّ ‎f(n)*a + f(n-1)*b = f(n+1)‎، حيث [a b‏‏] يمثّل الصف الأول من المصفوفة M، من جهة أخرى نعلم من علاقة التعود أنّ ‎f(n+1)=f(n)+f(n-1)‎، لهذا نستنتج أنّ الصفّ الأول من M سيكون [1 1]. | 1 1 | X | f(n) | = | f(n+1) | | ----- | | f(n-1) | | ------ | ملاحظة: يشير الرمز ----- إلى عدم اهتمامنا بهذه القيمة حاليًا. يساوي العنصر الثاني من B القيمة ‎f(n)‎، والتي يمكن الحصول عليها عن طريق العملية f(n) x 1، ومن ثم فإنّ الصف الثاني من M هو [1 0]. | ----- | X | f(n) | = | ------ | | 1 0 | | f(n-1) | | f(n) | لقد حصلنا الآن على المصفوفة M المطلوبة. | 1 1 | X | f(n) | = | f(n+1) | | 1 0 | | f(n-1) | | f(n) | النوع 2 سنحاول الآن حساب: ‎f(n) = a X f(n-1) + b X f(n-2)‎، حيث a و b ثابتتان. تكافئ الصيغة أعلاه المعادلة: ‎f(n+1) = a X f(n) + b X f(n-1)‎. قد تعلم الآن أنّه ينبغي أن تتساوي أبعاد المصفوفات مع عدد التبعيات، والتي تساوي 2 في هذا المثال، لذا سنبني المصفوفتين A و B، وكلاهما من الحجم 2x1: Matrix A Matrix B | f(n) | | f(n+1) | | f(n-1) | | f(n) | نحتاج أن تكون [a, b] في الصفّ الأول من المصفوفة M بالنسبة لـ ‎f(n+1) = a X f(n) + b X f(n-1)‎، وبالنسبة للعنصر الثاني فيB، أي ‎f(n)‎، فهو موجود في المصفوفة A، لذلك سنأخذه، ولهذا نضع [1 0] في الصف الثاني من M لنحصل على: | a b | X | f(n) | = | f(n+1) | | 1 0 | | f(n-1) | | f(n) | النوع 3 نأخذ الآن الحالة التالية: احسب قيمة ‎f(n) = a x f ‎(n-1) +‎ c x f ‎(n-3)‎. لقد كانت الحالات -القيم السابقة التي تُحسب العودية انطلاقًا منها- في المثالين السابقين متجاورات، أمّا هنا فهي غير متجاورة، فالحالة f (n-2)‎‎ مفقودة، فكيف نتعامل مع هذا؟ يمكننا تحويل علاقة العودية إلى العلاقة التالية: ‎f(n) = a X f(n-1) + 0 X f ‎(n-2) + c X f ‎(n-3) // أو ‎f(n + 1) = a X f(n) + 0 X f ‎(n-1) + c X f ‎(n-2) شبه هذه الصيغةُ صيغةَ النوع 2. هذه هي المصفوفة M، والتي ستكون من الحجم 3x3. لقد حسبنا المصفوفة M بطريقة مشابهة للطريقة التي استخدمناها في النوع 2، ويمكنك تجربتها بالورقة والقلم إن وجدت صعوبة في فهمها. | a 0 c | | f(n) | | f(n+1) | | 1 0 0 | X | f(n-1) | = | f(n) | | 0 1 0 | | f(n-2) | | f(n-1) | النوع 4 نأخذ الآن مثالًا أكثر تعقيدًا: ‎f(n) = f(n-1) + f(n-2) + c‎، حيث تكون c ثابتًا ما. يختلف هذا المثال عن سابقيه، فقد كنا نحوّل في السابق كل حالة في A إلى الحالة التالية في B عبر عملية الضرب. f(n) = f(n-1) + f(n-2) + c f(n+1) = f(n) + f(n-1) + c f(n+2) = f(n+1) + f(n) + c ................................ أما هنا فلا تصلح الطريقة التي اعتمدنَاها سابقًا، وعلينا أن نبحث في طريقة أخرى. ماذا لو جرّبنا إضَافة c كحالة على النحو التالي: | f(n) | | f(n+1) | M X | f(n-1) | = | f(n) | | c | | c | يمكننا الآن إنشاء M بسهولة: | 1 1 1 | | f(n) | | f(n+1) | | 1 0 0 | X | f(n-1) | = | f(n) | | 0 0 1 | | c | | c | النوع 5 لنأخذ الآن مثالًا آخر: ‎f(n) = a X f(n-1) + c X f(n-3) + d X f(n-4) + e‎ هذا تمرين لك، حاول أولاً تحديد حالات العوديّة، وأنشئ المصفوفتين A و B، ثمّ احسب المصفوفة M: هذه قيمة M: | a 0 c d 1 | | 1 0 0 0 0 | | 0 1 0 0 0 | | 0 0 1 0 0 | | 0 0 0 0 1 | النوع 6 هناك أشكال كثيرة من العودية، منها الشكل التالي: f(n) = f(n-1) -> if n is odd // في الحالات الفردية f(n) = f(n-2) -> if n is even // في الحالات الزوجية أو باختصار: f(n) = (n&1) X f(n-1) + (!(n&1)) X f(n-2) يمكننا تقسيم الدوال على أساس الفردية أو الزوجية، وإنشاء مصفوفتين تمثّل إحداهما الفردية، وتمثّل الأخرى الزوجية، ثمّ نحُسب كل واحدة منهما على حدة. النوع 7 قد تحتاج أحيانًا إلى الحفاظ على أكثر من علاقة عودية واحدة كما في المثال التالي: g(n) = 2g(n-1) + 2g(n-2) + f(n) لاحظ أنّ قيمة g (n)‎‎ تعتمد على ‎f(n)‎. يمكننا استخدام ضرب المصفوفات كما في الأمثلة السابقة ولكن بأبعاد أكبر. ننشئ أولا المصفوفتين A و B. Matrix A Matrix B | g(n) | | g(n+1) | | g(n-1) | | g(n) | | f(n+1) | | f(n+2) | | f(n) | | f(n+1) | لدينا: ‎g(n+1) = 2g(n-1) + f(n+1)‎‎ و f(n+2) = 2f(n+1) + 2f(n)‎. لننشئ المصفوفة M بالطريقة نفسها التي استخدمناها آنفًا: | 2 2 1 0 | | 1 0 0 0 | | 0 0 2 2 | | 0 0 1 0 | ترجمة -بتصرّف- للفصلين 51 و 52 من الكتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال السابق: خوارزميات المتتاليات subsequences algorithms مدخل إلى تحليل الخوارزميات أمثلة عن أنواع الخوارزميات
  6. تستعرض هذه المقالة بعض الخوارزميات المتعلقة بالمتتاليات الرياضية، مثل خوارزمية أطول متتالية جزئية مشتركة، وخوارزمية أطول متتالية متزايدة، وخوارزمية الحيد الزمني الديناميكي. أطول متتالية جزئية مشتركة Longest Common Subsequence سوف نعرّف بعض المصطلحات الأساسية أولًا: المتتالية الجزئية Subsequence: في الرياضيات، المتتالية الجزئية هي متتالية مشتقة من متتالية أخرى، بحيث يمكن الحصول عليها بحذف بعض عناصر المتتالية الضامّة دون المساس بترتيب عناصرها. لتكن ABC سلسلة نصية، سنحصل عند حذف حرف واحد أو أكثر من هذه السلسلة النصية على متتالية جزئية من هذه السلسلة النصية، لذا فإنّ السلاسل النصية {‎ ‎A - B - C - AB - AC - BC - ABC …إلخ} كلها متتاليات جزئية من ABC، بل حتى السلسلة الفارغة (الناتجة عن إزالة جميع الأحرف) تُعدّ أيضًا متتالية جزئية. وهناك طريقة سهلة لاستخراج المتتاليات الجزئية من سلسلة نصية، بالمرور على كل حرف من حروف السلسلة النصية، وأمامك خياران، إمّا أن تأخذه أو تتركه، ومهما كانت خياراتك ستكون المتتالية المؤلّفة من الحروف التي أخذتها متتالية جزئية، كذلك إن كان طول السلسلة النصية يساوي n، فيمكن أن نستخرج منها 2n متتالية جزئية. أطول متتالية جزئية مشتركة longest Common Subsequence: لتكن س و ش سلسلتين نصيتين، ولتكن ج المجموعة المؤلفة من كل المتتاليات الجزئية في س، ولتكن ح المجموعة المؤلفة من جميع المتتاليات الجزئية في ش. هناك على الأقل متتالية جزئية واحدة مشتركة بين هاتين المجموعتين، وهي المتتالية الفارغة، لأنّ المتتالية الفارغة تُعدّ دائما متتالية جزئية من كل المتتاليات. أطول متتالية جزئية مشتركة LCS بين السلسلتين النصيتين س و ش هي أطول عنصر مشترك بين المجموعتين ج و ح، فمثلًا: المتتاليات الجزئية المشتركة بين السلسلتين النصيتين HELLOM وHMLD هي H و HL و HM، والسلسلة النصية HLL هي الأطول من بين كل هذه المتتاليات، لذا فهي أطول متتالية جزئية مشتركة بين HMLD و HELLOM. هناك عدّة طرق للعثور على أطول متتالية جزئية مشتركة، منها الطريقة العنيفة وطريقة البرمجة الديناميكية. الطريقة العنيفة: يمكننا إنشاء جميع المتتاليات الجزئية للسلسلتين النصيتين ثم نوازن بينها لنخرج بالمتتاليات الجزئية المشتركة بينهما، ثم سنحاول العثور على أطول عنصر / متتالية مشتركة. بما أننا قد رأينا أنّ هناك 2n متتالية جزئية لكل سلسلة نصية طولها n، فنحن نعلم أننا سنستغرق أعوامًا من أجل حل المشكلة إن كانت n أكبر من 20، وبناءً على ذلك فهي غير عملية، وهناك طريقة أفضل لإيجاد أطول متتالية جزئية مشتركة، وهي البرمجة الديناميكية. طريقة البرمجة الديناميكية: لنفترض أنّ لدينا سلسلتين نصيتين abcdaf و acbcf نرمز لهما بالرمزين s1 و s2، وأطول متتالية جزئية مشتركة بين هاتين السلسلتين النصيتين هي abcf، وطولها يساوي 4. بما أن المتتاليات الجزئية لا يشترط أن تكون متواصلة، فيمكن إنشاء المتتالية abcf عبر تجاهل da في s1 و c في s2. والآن، كيف نعرف هذا باستخدام البرمجة الديناميكية؟ سنبدأ بإنشاء جدول (مصفوفة ثنائية الأبعاد) تحتوي جميع أحرف s1 في أحد صفوفها، وتحتوي جميع أحرف s2 في أحد أعمدتها، ونفهرس الجدول انطلاقًا من 0، ونضع الأحرف في المصفوفة ابتداءً من الفهرس 1، وسيبدو الجدول كما يلي: 0 1 2 3 4 5 6 +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | chʳ | | a | b | c | d | a | f | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 0 | | | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | a | | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 2 | c | | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 3 | b | | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 4 | c | | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 5 | f | | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ تمثل كل خانة Table[‎i][j]‎‎ من خانات الجدول طول أطول متتالية جزئية مشتركة بين السلسلتين النصيتين t1 و t2، حيث تساوي t1 سابقة السلسلة النصية s1 التي تبدأ من بدايتها وحتى الحرف رقم j، وتساوي t2 سابقة السلسلة النصية s2 التي تبدأ من بدايتها وحتى الحرف رقم i. على سبيل المثال: تمثّل الخانة Table[2][3]‎‎ طول أطول متتالية جزئية مشتركة بين ac و abc. يمثّل العمود رقم 0 المتتالية الجزئية الفارغة من s1، وبالمثل فإن الصفّ رقم 0 يمثّل المتتالية الجزئية الفارغة من s2. إذا أخذنا متتالية جزئية فارغة من سلسلة نصية وحاولنا مطابقتها بسلسلة نصية أخرى، فسيساوي طول المتتالية الجزئية المشتركة 0 مهما كان طول السلسلة النصية الفرعية الثانية، لهذا نملأ خانات الصف الأوّل والعمود الأوّل بالقيمة 0، ونحصل على: 0 1 2 3 4 5 6 +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | chʳ | | a | b | c | d | a | f | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | a | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 2 | c | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 3 | b | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 4 | c | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 5 | f | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ نريد الآن ملء الخانة Table[1][1]‎‎، إن كان لدينا سلسلتين نصيتين a و a أخرى، ما هي أطول متتالية جزئية مشتركة يمكننا الحصول عليها هنا؟ نحن نعلم أنّ طول أطول متتالية جزئية مشتركة بين السلسلة النصية a و a هو 1، فنذهب الآن إلى Table[1][2]‎‎ التي تمثّل طول أطول متتالية جزئية مشتركة بين ab و a، والتي تساوي 1، وكما ترى فإن جميع قيم الصف رقم 1 تساوي القيمة 1، وذلك لأنه يحتوي على طول أطول متتالية جزئية مشتركة بين a والسلاسل abcd و abcda و abcdaf. سيبدو الجدول كما يلي: 0 1 2 3 4 5 6 +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | chʳ | | a | b | c | d | a | f | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | a | 0 | 1 | 1 | 1 | 1 | 1 | 1 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 2 | c | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 3 | b | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 4 | c | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 5 | f | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ نذهب الآن إلى الصف رقم 2 الذي يتضمن الحرف c. تحتوي الخانة Table[2][1]‎‎ طول أكبر متتالية جزئية مشتركة بين السلسلتين النصيتين ac و a والذي يساوي 1، وقد حصلنا على هذه القيمة من القيمة الموجودة في الصف الأعلى، ذلك أنّه إذا كان الحرفان s1 [2]‎‎ و s2 [1]‎‎ غير متساويين فلا بدّ أن يساوي طول أكبر متتالية جزئية مشتركة الحد الأقصى لقيمة LCS (أطول متتالية جزئية مشتركة) الموجودة في الأعلى أو على اليسار. إن أخذنا قيمة LCS الموجودة في الأعلى فذلك يعني أنّنا تجاهلنا الحرف الحالي في السلسلة النصية s2، وبالمثل إن أخذنا قيمة LCS الموجودة على اليسار فذلك يعني أنّنا تجاهلنا الحرف الحالي في s1. نحصل على هذا الجدول: 0 1 2 3 4 5 6 +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | chʳ | | a | b | c | d | a | f | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | a | 0 | 1 | 1 | 1 | 1 | 1 | 1 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 2 | c | 0 | 1 | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 3 | b | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 4 | c | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 5 | f | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ هذه صيغة الحالة الأولى: if s2[i] is not equal to s1[j] Table[i][j] = max(Table[i-1][j], Table[i][j-1] endif ننتقل الآن إلى Table[2][2]‎‎ التي تحوي طول أطول متتالية جزئية مشتركة بين ab و ac. بما أن الحرفين الأخيرين من هاتين السلسلتين مختلفان (c و b)، فسنَأخذ الحدّ الأقصى الموجود في الأعلى أو على اليسار، وهو 1 في هذه الحالة. ثم بعد ذلك ننتقل إلى Table[2][3]‎‎ والتي تحتوي طول أكبر متتالية جزئية مشتركة بين abc و ac، ستتساوى القيمتان الحاليتان في كل من الصف والعمود هذه المرة، لذا فإن طول المتتالية الجزئية المشتركة الأطول يساوي قيمة LCS القصوى الحالية + 1. ولكي تحصل على قيمة LCS القصوى الحالية عليك أن تتحقق من القيمة القطرية diagonal value، والتي تمثل أفضل تطابق بين ab و a، ومن هذه الحالة نضيف محرفًا من محارف s1 وآخر من s2، وقد وجدنا أنّهما متساويين -أي s1 و s2-، لذا سيزيد طول أكبر متتالية جزئية مشتركة حتمًا. نضع القيمة 1 + 1 = 2 في الخانة Table[2][3]‎‎ لنحصل على: 0 1 2 3 4 5 6 +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | chʳ | | a | b | c | d | a | f | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | a | 0 | 1 | 1 | 1 | 1 | 1 | 1 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 2 | c | 0 | 1 | 1 | 2 | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 3 | b | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 4 | c | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 5 | f | 0 | | | | | | | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ هذه صيغتنا الثانية: if s2[i] equals to s1[j] Table[i][j] = Table[i-1][j-1] + 1 endif لقد عرّفنا كلا الحالتين، سنملأ الآن الجدول كله باستخدام هاتين الصيغتين، وسيبدو بعد أن نتمّ ملأه كما يلي: 0 1 2 3 4 5 6 +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | | chʳ | | a | b | c | d | a | f | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 1 | a | 0 | 1 | 1 | 1 | 1 | 1 | 1 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 2 | c | 0 | 1 | 1 | 2 | 2 | 2 | 2 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 3 | b | 0 | 1 | 2 | 2 | 2 | 2 | 2 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 4 | c | 0 | 1 | 2 | 3 | 3 | 3 | 3 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ | 5 | f | 0 | 1 | 2 | 3 | 3 | 3 | 4 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+ نستنتج من الجدول أنّ طول أكبر متتالية جزئية مشتركة بين s1 و s2 هو Table[5][6] = 4. لاحظ أنّ 5 و 6 هما طولا s2 و s1 على التوالي. انظر المثال التوضيحي التالي: Procedure LCSlength(s1, s2): Table[0][0] = 0 for i from 1 to s1.length Table[0][i] = 0 endfor for i from 1 to s2.length Table[i][0] = 0 endfor for i from 1 to s2.length for j from 1 to s1.length if s2[i] equals to s1[j] Table[i][j] = Table[i-1][j-1] + 1 else Table[i][j] = max(Table[i-1][j], Table[i][j-1]) endif endfor endfor Return Table[s2.length][s1.length] التعقيد الزمني لهذه الخوارزمية يساوي O(mn)‎‎، حيث يمثّل m و n طولي السلسلتين النصيتين. ها قد عرفنا طول أكبر متتالية جزئية مشتركة، فكيف نستخرج هذه المتتالية الجزئية؟ من أجل معرفة أطول متتالية جزئية مشتركة، سننشئ مكدسًا لتخزين محارفها، وسنبدأ من الزاوية اليمنى السفلى باحثين عن المصدر الذي أتت منه القيمة، فإذا كانت القيمة قادمة من القطر diagonal -أي إذا كانت Table[‎i][j] - 1 = Table[i-1][j-1]‎‎- فسندفع إمّا [‎i]s2 ‎‎ أو s1 [j]‎‎ (كلاهما متساويان) إلى المكدّس ثمّ نتحرك قُطريًا. أمّا إذا كانت القيمة آتية من الأعلى -أي إن كانت Table[‎i][j] = Table[i-1][j]‎‎- فإننا ننتقل إلى الأعلى، وإذا كانت القيمة آتية من اليسار، أي إن كانت Table[‎i][j] = Table[‎i][j-1]‎‎، فإننا ننتقل إلى اليسار. ينتهي بحثنا عندما نصل إلى العمود الموجود في الأعلى أو في أقصى اليسار، وننزع القيم المخزّنة في المكدّس ثمّ نطبعها. انظر المثال التوضيحي: Procedure PrintLCS(LCSlength, s1, s2) temp := LCSlength S = stack() i := s2.length j := s1.length while i is not equal to 0 and j is not equal to 0 if Table[i-1][j-1] == Table[i][j] - 1 and s1[j]==s2[i] S.push(s1[j]) //or S.push(s2[i]) i := i - 1 j := j - 1 else if Table[i-1][j] == Table[i][j] i := i-1 else j := j-1 endif endwhile while S is not empty print(S.pop) endwhile ملاحظة: إذا كان كلّ من Table[i-1][j]‎‎ و Table[‎i][j-1]‎‎ يساويان Table[‎i][j]‎‎، ولم يكن Table[i-1][j-1]‎‎ مساويًا للقيمة Table[‎i][j] - 1، فذلك يعني أنّه قد تكون هناك أكثر من متتالية جزئية مشتركة طولى (أي كل واحدة منها تعدّ أطول متتالية جزئية مشتركة)، ولا يأخذ المثال التوضيحي المذكور آنفًا هذا الأمر في الحسبان. إن أردت العثور على جميع المتتاليات الجزئية المشتركة القصوية، فسيكون عليك استخدام العوديّة. التعقيد الزمني لهذه الخوارزمية هو: O (max (m, n))‎‎. أطول متتالية جزئية متزايدة Longest Increasing Subsequence أطول متتالية جزئية متزايدة، هي أطول متتالية جزئية عناصرها مرتبة تصاعديًا أو تنازليًا. تُستخدم خوارزميات حساب أطول متتالية جزئية متزايدة في العديد من التطبيقات مثل أنظمة إدارة الإصدارات Git وغيرها، وفيما يلي وصفٌ مبسّط للخوارزمية التي تعتمدها أنظمة إدارة الإصدارات للموازنة بين ملفّين يمثّلان إصدارين مختلفين من الملف نفسه: ابحث عن الأسطر المشتركة في كلا الملفين. خذ كل هذه الأسطر من الملف الأوّل ثم رتّبها بحسب ظهورها في الملف الثاني. اعثر على أطول متتالية جزئية متزايدة LIS للمتتالية الناتجة (باستخدام خوارزمية ترتيب الصبر patience sorting). ستحصل على أطول متتالية متطابقة من الأسطر، والتي تمثّل التقابلات بين السطور المشتركة في الملفّين. كرّر الخوارزمية عوديًا على كل مجال من الأسطر بين السطور المُتطابقة. لننظر الآن في مثال بسيط على مشكلة أطول متتالية جزئية متزايدة: مدخلات المشكلة هي متتالية من الأعداد الصحيحة المختلفة a1,a2,...,an.‎‎، ونريد أن نجد أطول متتالية جزئية متزايدة. إذا كانت المدخلات تُساوي 7,3,8,4,2,6 مثلًا، فإنّ أطول متتالية جزئية متزايدة فيها هي 3,4,6. أسهل طريقة للحصول على أطول متتالية جزئية متزايدة هي ترتيب عناصر المدخلات ترتيبًا تزايديًا ثمّ تطبيق خوارزمية LCS على التسلسلات الأصلية والمرتّبة، غير أنّه إذا ألقيت نظرة سريعة على المصفوفة الناتجة، فستلاحظ أنّ العديد من قيمها متساوية إذ تحتوي المصفوفة الكثير من العناصر المكرّرة، هذا يعني أنّه يمكن حل مشكلة LIS بالبرمجة الديناميكية باستخدام مصفوفة من بعد واحد. المثال التوضيحي: صِف مصفوفة القيم التي نريد حسابها: لكل ‎1 <= i <= n‎، ليكن A (i)‎‎ طول أكبر متتالية جزئية متزايدة في المتتالية المؤلفة من أوّل i عنصر في المدخلات. لاحظ أنّ طول أكبر متتالية جزئية متزايدة في المدخلات (بالكامل) يحقق العبارة ‎max{A(i)|1 ≤ i ≤ n}‎، أي أنّه: إن كان طول أكبر متتالية جزئية متزايدة في المدخلات هو L، فإنّه يوجد عدد k يحقّق A(k) = L، و لكل 1 =< n >= j لدينا: L > A(j)‎‎. نحسب قيم A (i)‎‎ عوديًا على النحو التالي: For 1 <= i <= n, A(i) = 1 + max { A(j)|1 ≤ j < i and input(j) < input(i) } احسب قيم A باستخدام الصيغة السابقة. ابحث عن الحل الأمثل. يستخدم البرنامج التالي A لحساب الحل الأمثل، ويحسب الجزء الأول منه قيمة m حيث تساوي A (m)‎‎ طول متتالية جزئية متزايدة مثلى في المدخلات، فيما يحاول الجزء الثاني العثور على متتالية جزئية متزايدة مثلى. سنطبع المتتالية بترتيب عكسي لجعلها أوضَح. يستغرق هذا البرنامج O (n)‎‎، وتستغرق الخوارزمية إجمالًا O (n ^ 2)‎‎. الجزء الأول: m ← 1 for i : 2..n if A(i) > A(m) then m ← i end if end for الجزء الثاني: put a while A(m) > 1 do i ← m−1 while not(ai < am and A(i) = A(m)−1) do i ← i−1 end while m ← i put a end while الحل العودي، المنظور الأول: LIS(A[1..n]): if (n = 0) then return 0 m = LIS(A[1..(n − 1)]) B is subsequence of A[1..(n − 1)] with only elements less than a[n] (* let h be size of B, h ≤ n-1 *) m = max(m, 1 + LIS(B[1..h])) Output m التعقيد الزمني للمنظور الأول: ‎O(n*2^n)‎. المنظور الثاني: LIS(A[1..n], x): if (n = 0) then return 0 m = LIS(A[1..(n − 1)], x) if (A[n] < x) then m = max(m, 1 + LIS(A[1..(n − 1)], A[n])) Output m MAIN(A[1..n]): return LIS(A[1..n], ∞) التعقيد الزمني للمنظور الثاني: ‎O(n^2)‎. المنظور الثالث: LIS(A[1..n]): if (n = 0) return 0 m = 1 for i = 1 to n − 1 do if (A[i] < A[n]) then m = max(m, 1 + LIS(A[1..i])) return m MAIN(A[1..n]): return LIS(A[1..i]) التعقيد الزمني للمنظور الثالث: ‎O(n^2)‎. الخوارزمية التكرارية Iterative Algorithm: تحسب هذه الخوارزمية القيم بطريقة متكررة من أسفل إلى أعلى. LIS(A[1..n]): Array L[1..n] (* L[i] = value of LIS ending(A[1..i]) *) for i = 1 to n do L[i] = 1 for j = 1 to i − 1 do if (A[j] < A[i]) do L[i] = max(L[i], 1 + L[j]) return L MAIN(A[1..n]): L = LIS(A[1..n]) return the maximum value in L التعقيد الزمني للمنظور التكراري: ‎O(n^2)‎. المساحة الإضافية: ‎O(n)‎. على سبيل المثال، لو كانت المدخلات تساوي: {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15} فستكون أطول متتالية جزئية متزايدة في المدخلات هي: {0, 2, 6, 9, 11, 15}. الحيد الزمني الديناميكي Dynamic Time Warping خوارزمية الحيد الزمني الديناميكي Dynamic Time Warping -أو DTW اختصارًا- هي خوارزمية لقياس مدى التشابه بين متتاليتين زمنيتين قد تختلفان في السرعة، فمثلًا يمكن رصد أوجه التشابه في مشية شخصين باستخدام خوارزمية DTW حتى لو كان أحدهما يمشي أسرع من الآخر، أو كان هناك تسارع وتباطؤ أثناء المراقبة. وتُستخدم هذه الخوارزمية لمطابقة مقطع صوتي قياسي مع مقطع صوتي لشخص آخر، حتى لو كان ذلك الشخص يتحدث أسرع من صوت العينة القياسية المسجّلة أو أبطأ منها. تُطبَّق خوارزمية DTW على التسلسلات الزمنية لبيانات الفيديو والصوت والرسومات، أو أيّ نوع من البيانات ما دام يمكن تحويلها إلى متتالية خطية. تحاول هذه الخوارزمية إيجاد التطابق الأمثل بين متتاليتين زمنيتين مع الأخذ بالحسبان قيودًا معيّنة، فمثلًا لنفترض أنّ لدينا متتاليتين صوتيتين Sample و Test، ونريد أن نتحقق من تطابق هاتين المتتاليتين الزمنيتين. يشير مصطلح متتالية صوتية هنا إلى الإشارة الرقمية للصوت، وقد تُمثَّل بسعة الصوت amplitude أو تردّده frequency. لنفترض أنّ: Sample = {1, 2, 3, 5, 5, 5, 6} Test = {1, 1, 2, 2, 3, 5} نريد العثور على أمثل تطابق بين هاتين المتتاليتين، نبدأ بتعريف دالة المسافة d (x، y)‎‎، حيث تمثل x و y النقطتين اللتان نودّ حِساب المسافة الفاصلة بينهما. تكون صيغة حساب المسافة كما يلي: d(x, y) = |x - y| // القيمة المطلقة للفرق ننشئ الآن مصفوفة ثنائية الأبعاد Table باستخدام قيم المتتاليتين Sample و Test، وسنحسب المسافات بين كل نقطة في Sample وكل نقطة في Test ونبحث عن أفضل تطابق بينهما. +------+------+------+------+------+------+------+------+ | | 0 | 1 | 1 | 2 | 2 | 3 | 5 | +------+------+------+------+------+------+------+------+ | 0 | | | | | | | | +------+------+------+------+------+------+------+------+ | 1 | | | | | | | | +------+------+------+------+------+------+------+------+ | 2 | | | | | | | | +------+------+------+------+------+------+------+------+ | 3 | | | | | | | | +------+------+------+------+------+------+------+------+ | 5 | | | | | | | | +------+------+------+------+------+------+------+------+ | 5 | | | | | | | | +------+------+------+------+------+------+------+------+ | 5 | | | | | | | | +------+------+------+------+------+------+------+------+ | 6 | | | | | | | | +------+------+------+------+------+------+------+------+ تحتوي كل خانة Table[‎i][j]‎‎ من خانات الجدول المسافة المثلى بين المتتاليتين الزمنيتين الجزئيتين t1 و t2، حيث تساوي t1 المتتالية الجزئية من Test التي تبدأ من بدايتها وحتى العنصر رقم j، وتساوي t2 المتتالية الجزئية من Sample التي تبدأ من بدايتها وحتى العنصر رقم i. إذا لم نأخذ أيّ قيمة من Sample في الصف الأول فستكون المسافة بينها وبين Test لا نهائية، لذا نضع ما لا نهاية في الصف الأول، وكذلك نفعل مع العمود الأول لأنّنا إن لم نأخذ أيّ قيمة من Test فستكون المسافة بينها وبين Sample لا نهائية كذلك، والمسافة بين 0 و 0 تساوي 0. نحصل على ما يلي: +------+------+------+------+------+------+------+------+ | | 0 | 1 | 1 | 2 | 2 | 3 | 5 | +------+------+------+------+------+------+------+------+ | 0 | 0 | in | inf | inf | inf | inf | inf | +------+------+------+------+------+------+------+------+ | 1 | inf | | | | | | | +------+------+------+------+------+------+------+------+ | 2 | inf | | | | | | | +------+------+------+------+------+------+------+------+ | 3 | inf | | | | | | | +------+------+------+------+------+------+------+------+ | 5 | inf | | | | | | | +------+------+------+------+------+------+------+------+ | 5 | inf | | | | | | | +------+------+------+------+------+------+------+------+ | 5 | inf | | | | | | | +------+------+------+------+------+------+------+------+ | 6 | inf | | | | | | | +------+------+------+------+------+------+------+------+ سنحسب في كل خطوة المسافة بين النقاط الحالية ثمّ نضيفها إلى الحد الأدنى للمسافة التي وجدناها حتى الآن، وسينتج عن هذا المسافة المثلى بين المتتاليتين الجزئيتين t1 و t2 (انظر تعريفيهمَا في الأعلى). هذه هي الصيغة التي سنعمل بها: Table[i][j] := d(i, j) + min(Table[i-1][j], Table[i-1][j-1], Table[i][j-1]) يكون لدينا في البداية d(1, 1) = 0، كما تحتوي الخانة Table[0][0] ‎‎ القيمةَ الصغرى، لذا نضع Table[1][1] = 0 + 0 = 0‎‎. لدينا في الحالة الثانية d(1, 2) = 0، وتحتوي الخانة Table[1][1] ‎‎ القيمةَ الصغرى، لذا نضع Table[1][2] = 0 + 0 = 0. نستمر على هذا المنوال إلى أن نملأ الجدول، الذي سيبدو بعد الانتهاء كما يلي: +------+------+------+------+------+------+------+------+ | | 0 | 1 | 1 | 2 | 2 | 3 | 5 | +------+------+------+------+------+------+------+------+ | 0 | 0 | inf | inf | inf | inf | inf | inf | +------+------+------+------+------+------+------+------+ | 1 | inf | 0 | 0 | 1 | 2 | 4 | 8 | +------+------+------+------+------+------+------+------+ | 2 | inf | 1 | 1 | 0 | 0 | 1 | 4 | +------+------+------+------+------+------+------+------+ | 3 | inf | 3 | 3 | 1 | 1 | 0 | 2 | +------+------+------+------+------+------+------+------+ | 5 | inf | 7 | 7 | 4 | 4 | 2 | 0 | +------+------+------+------+------+------+------+------+ | 5 | inf | 11 | 11 | 7 | 7 | 4 | 0 | +------+------+------+------+------+------+------+------+ | 5 | inf | 15 | 15 | 10 | 10 | 6 | 0 | +------+------+------+------+------+------+------+------+ | 6 | inf | 20 | 20 | 14 | 14 | 9 | 1 | +------+------+------+------+------+------+------+------+ تمثل قيمة الخانة Table[7][6]‎‎ المسافة القصوى بين المتتاليتين Sample و Test، وتشير القيمة 1 هنا إلى أنّ المسافة القصوى بين Sample و Sample تساوي 1. الآن إذا ارتددنا من النقطة الأخيرة وحتى نقطة البداية (0 , 0) فسنحصل على خط طويل يتحرك أفقيًا وعموديًا وقطريًا. هذا مثال توضيحي لعملية الارتداد: if Table[i-1][j-1] <= Table[i-1][j] and Table[i-1][j-1] <= Table[i][j-1] i := i - 1 j := j - 1 else if Table[i-1][j] <= Table[i-1][j-1] and Table[i-1][j] <= Table[i][j-1] i := i - 1 else j := j - 1 end if نواصل على هذا المنوال حتى نصل إلى النقطة (0 , 0)، واعلم أن كل حركة لها معنى خاص: تمثل الحركة الأفقية حذفًا، وهذا يعني أنّ المتتالية Test قد تسرّعت أثناء هذه المدة. تمثل الحركة الرأسية إدراجًا، وهذا يعني تباطؤ المتتالية Test أثناء هذه المدة. تمثل الخطوة القطرية تطابقًا، أي أنّ Test تتطابق مع Sample أثناء هذه المدة. انظر المثال التوضيحي التالي: Procedure DTW(Sample, Test): n := Sample.length m := Test.length Create Table[n + 1][m + 1] for i from 1 to n Table[i][0] := infinity end for for i from 1 to m Table[0][i] := infinity end for Table[0][0] := 0 for i from 1 to n for j from 1 to m Table[i][j] := d(Sample[i], Test[j]) + minimum(Table[i-1][j-1], // مطابقة Table[i][j-1], // إدراج Table[i-1][j]) // حذف end for end for Return Table[n + 1][m + 1] يمكن أيضًا أن نضيف قيودًا محلية، كأن نشترط أنّه إذا تطابقت ‎Sample[‎i]‎ مع ‎Test[j]‎، فيجب أن لا تكون قيمة ‎|i - j|‎ أكبر من w (معامل نافذة - window parameter). التعقيد: التعقيد الزمني لخوارزمية DTW هو O (m * n)‎‎، حيث يمثل كلّ من m و n طولي المتتاليتين. ويجدر التنويه إلى أنّ هناك تقنيات أخرى أسرع لحساب DTW مثل: PrunedDTW و SparseDTW و FastDTW. ترجمة -بتصرّف- للفصول 47 و 48 و 54 من كتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال السابق: أمثلة على خوارزميات لحل مشكلات بسيطة تطوير الخوارزميات في جافا أمثلة عن أنواع الخوارزميات خوارزميات الترتيب وأشهرها
  7. نستعرض في هذه المقالة ثلاث خوارزميات بسيطة، وهي خوارزمية الحقيبة، وخوارزمية أخرى للتحقق من الألفاظ المقلوبة وخوارزمية لطباعة مثلث باسكال. مشكلة الحقيبة Knapsack Problem في مشكلة الحقيبة، نفترض أنّ لدينا مجموعة من العناصر، لكل منها قيمة ووزن، وعليك اختيار مجموعة من تلك العناصر بحيث يكون وزنها الإجمالي أقلّ من حدّ معيّن أو يساويه، وتكون قيمتها الإجمالية أكبر ما يمكن. هذا مثال توضيحي لحل مشكلة الحقيبة: v: مصفوفة تمثّل قيم العناصر. w: مصفوفة تمثّل أوزان العناصر. n: مصفوفة تحتوي العناصر غير المكررة. W: تمثّل سعة Capacity الحقيبة. for j from 0 to W do: m[0, j] := 0 for i from 1 to n do: for j from 0 to W do: if w[i] > j then: m[i, j] := m[i-1, j] else: m[i, j] := max(m[i-1, j], m[i-1, j-w[i]] + v[i]) هذا تطبيق implementation للشيفرة السالفة بلغة بايثون: def knapSack(W, wt, val, n): K = [[0 for x in range(W+1)] for x in range(n+1)] for i in range(n+1): for w in range(W+1): if i==0 or w==0: K[i][w] = 0 elif wt[i-1] <= w: K[i][w] = max(val[i-1] + K[i-1][w-wt[i-1]], K[i-1][w]) else: K[i][w] = K[i-1][w] return K[n][W] val = [60, 100, 120] wt = [10, 20, 30] W = 50 n = len(val) print(knapSack(W, wt, val, n)) احفظ هذه الشيفرة في ملفّ يُسمّى knapSack.py، ثمّ نفّذه على النحو التالي: $ python knapSack.py 220 التعقيد الزمني: يساوي تعقيد الشيفرة السابقة ‎O(nW)‎، حيث يمثّل n عدد العناصر، وتمثّل W سعة الحقيبة. هذا تطبيق آخر بلغة C#‎‎: public class KnapsackProblem { private static int Knapsack(int w, int[] weight, int[] value, int n) { int i; int[,] k = new int[n + 1, w + 1]; for (i = 0; i <= n; i++) { int b; for (b = 0; b <= w; b++) { if (i==0 || b==0) { k[i, b] = 0; } else if (weight[i - 1] <= b) { k[i, b] = Math.Max(value[i - 1] + k[i - 1, b - weight[i - 1]], k[i - 1, b]); } else { k[i, b] = k[i - 1, b]; } } } return k[n, w]; } public static int Main(int nItems, int[] weights, int[] values) { int n = values.Length; return Knapsack(nItems, weights, values, n); } } التحقق من العبارات المقلوبة لتكن س سلسلة نصية، ولتكن ع سلسلة نصية أخرى نحصل عليها بتغيير مواضع بعض حروف س، فتُسمّى السلسلة النصية ع لفظًا مقلوبًا anagram للسلسلة النصية س. مثلًا، تعدّ الكلمتان لكم و كلم لفظان مقلوبان للكلمة ملك. بتعبير آخر، تكون سلسلة نصية لفظًا مقلوبًا من سلسلة نصية أخرى إن كانت السلسِلتان مؤلّفتان من مجموعة الحروف نفسها، وبالتردّدات نفسها. لتكن str1 و str2 سلسلتين نصيتين، نريد أن نتحقّق ممّا إذا كانت str2 لفضًا مقلوبًا للسلسلة str1. هذه خطوات الخوارزمية التي سنعتمدها: أنشئ قاموسًا hashMap مفاتيحه تساوي حروف str1 (غير مكرّرة)، وقيمة كل مفتاح (أو حرف) في القاموس تساوي عدد مرّات تكرار ذلك الحرف. امرر على كل حرف من حروف str2 وتحقّق من أنّه موجود في القاموس hashMap. إن وجدت الحرف في القاموس فانقص قيمته في القاموس بواحد. إن لم تجد الحرف في القاموس فذلك يدل على أنّه غير موجود في السلسلة النصية str1، وإذن لا يمكن أن تكون str2 لفضًا مقلوبًا للسلسلة str1. تنتهي الخوارزمية هنا وتعيد false. إن كانت جميع قيم مفاتيح القاموس hashMap تساوي الصفر بعد المرور على كل حروف str2، فذلك يدل على أنّ str2 لفظًا مقلوبًا للسلسلة str1، نعيد إذن true. خلاف ذلك نعيد false. انظر المثال التالي: let str1 = 'stackoverflow'; let str2 = 'flowerovstack'; هاتان السلسِلتان النصيتَان مُتقالبتان. وهذا هو قاموس الترددات خاصّتها: hashMap = { s : 1, t : 1, a : 1, c : 1, k : 1, o : 2, v : 1, e : 1, r : 1, f : 1, l : 1, w : 1 } لاحظ أنّ القيمة المرتبطة بالمفتاح o تساوي 2، ذلك أنّ o تكرّرت مرّتين في السلسلة النصية. سنمرّ على كل حرف من حروف str2 لنتحقق من أنّه موجود في hashMap، فإن كان كذلك نقصنا قيمة الحرف في hashMap واحدًا، وإلا أعدنا false دلالة على أنّ str2 ليست لفظًا مقلوبًا عن str1. hashMap = { s : 0, t : 0, a : 0, c : 0, k : 0, o : 0, v : 0, e : 0, r : 0, f : 0, l : 0, w : 0 } نمرّ الآن على كل قيم hashMap ونتحقق من أنّها تساوي جميعًا 0. وبما أن جميع قيم hashMap تساوي 0 في المثال الذي ضربناه آنفًا، فإنّ str2 لفظٌ مقلوب عن str1. إليك مثال توضيحي لخوارزمية التحقق من الألفاظ المقلوبة: (function(){ var hashMap = {}; function isAnagram (str1, str2) { if(str1.length !== str2.length){ return false; } createStr1HashMap(str1); var valueExist = createStr2HashMap(str2); return isStringsAnagram(valueExist); } function createStr1HashMap (str1) { [].map.call(str1, function(value, index, array){ hashMap[value] = value in hashMap ? (hashMap[value] + 1) : 1; return value; }); } function createStr2HashMap (str2) { var valueExist = [].every.call(str2, function(value, index, array){ if(value in hashMap) { hashMap[value] = hashMap[value] - 1; } return value in hashMap; }); return valueExist; } function isStringsAnagram (valueExist) { if(!valueExist) { return valueExist; } else { var isAnagram; for(var i in hashMap) { if(hashMap[i] !== 0) { isAnagram = false; break; } else { isAnagram = true; } } return isAnagram; } } isAnagram('stackoverflow', 'flowerovstack'); // true isAnagram('stackoverflow', 'flowervvstack'); // false })(); التعقيد الزمني: 3n أو O (n)‎‎. مثلث باسكال مثلث باسكال هو منظومة عددية تُرتّب على هيئة مثلث، ولها استخدامات كثيرة في الرياضيات. هذه شيفرة مكتوبة بلغة C لطباعة مثلث باسكال: int i, space, rows, k=0, count = 0, count1 = 0; row=5; for(i=1; i<=rows; ++i) { for(space=1; space <= rows-i; ++space) { printf(" "); ++count; } while(k != 2*i-1) { if (count <= rows-1) { printf("%d ", i+k); ++count; } else { ++count1; printf("%d ", (i+k-2*count1)); } ++k; } count1 = count = k = 0; printf("\n"); } الخرج الناتج: 1 2 3 2 3 4 5 4 3 4 5 6 7 6 5 4 5 6 7 8 9 8 7 6 5 ترجمة -بتصرّف- للفصول 45 و 49 و 50 من كتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال السابق: خوارزميات حل المعادلات الرياضية المرجع الشامل إلى: تعلم الخوارزميات للمبتدئين أمثلة على أشهر خوارزميات المخططات والأشجار خوارزميات البحث وآلية عملها حل المشكلات وأهميتها في احتراف البرمجة
  8. حل المعادلات هي من المسائل الشائعة في الرياضيات، وهناك بحث مستمر عن طرق جديدة وسريعة لحل المعادلات عبر الحاسوب، وسنستعرض في هذه المقالة بعض خوارزميات حل المعادلات الخطية وغير الخطية. المعادلات الخطية Linear Equations هناك نوعان من الطرق لحل المعادلات الخطية: الطرق المباشرة: يسعى هذا النوع من الطرق إلى تحويل المعادلة الأصلية إلى معادلة مكافئة أيسر حلًّا، أي أنّنا نسعى في هذا النوع إلى إيجاد الحل مباشرة من معادلة. الطرق التكرارية Iterative Method: تبدأ هذه الطرق بتخمين قيمة أولية للحل، ثم تُجري عمليات تكرارية تقرِّب من الحل، وتستمر إلى حين الاقتراب من الحل بمقدار محدّد سلفًا. تعدّ الطرق التكرارية أقل فعالية على العموم من نظيراتها المباشرة لأنّها تجري الكثير من العمليات الإضافية، ولدينا بعض الأمثلة على الطرق التكرارية مثل طريقة جاكوبي التكرارية Jacobi's Iteration Method، وطريقة جاوس - سيدل Gauss-Seidal. إليك تطبيق لطريقة جاكوبي بلغة C: // تطبيق لطريقة جاكوبي void JacobisMethod(int n, double x[n], double b[n], double a[n][n]){ double Nx[n]; // شكل مُعدَّل من المتغيرات int rootFound=0; // راية int i, j; while(!rootFound){ for(i=0; i<n; i++){ Nx[i]=b[i]; for(j=0; j<n; j++){ if(i!=j) Nx[i] = Nx[i]-a[i][j]*x[j]; } Nx[i] = Nx[i] / a[i][i]; } rootFound=1; // التحقق من قيمة الراية for(i=0; i<n; i++){ if(!( (Nx[i]-x[i])/x[i] > -0.000001 && (Nx[i]-x[i])/x[i] < 0.000001 )){ rootFound=0; break; } } for(i=0; i<n; i++){ // تقييم x[i]=Nx[i]; } } return ; } وإليك تطبيق لطريقة جاوس-سيدل بلغة C أيضًا: // تطبيق لطريقة جاوس سيدل void GaussSeidalMethod(int n, double x[n], double b[n], double a[n][n]){ double Nx[n]; // شكل معدّل من المتغيرات int rootFound=0; // راية int i, j; for(i=0; i<n; i++){ //تهيئة Nx[i]=x[i]; } while(!rootFound){ for(i=0; i<n; i++){ Nx[i]=b[i]; for(j=0; j<n; j++){ if(i!=j) Nx[i] = Nx[i]-a[i][j]*Nx[j]; } Nx[i] = Nx[i] / a[i][i]; } rootFound=1; // التحقق من قيمة الراية for(i=0; i<n; i++){ if(!( (Nx[i]-x[i])/x[i] > -0.000001 && (Nx[i]-x[i])/x[i] < 0.000001 )){ rootFound=0; break; } } for(i=0; i<n; i++){ // تقييم x[i]=Nx[i]; } } return ; } // طباعة المصفوفة void print(int n, double x[n]){ int i; for(i=0; i<n; i++){ printf("%lf, ", x[i]); } printf("\n\n"); return ; } int main(){ // تهيئة المعادلة int n=3; // عدد المتغيرات double x[n]; // المتغيرات double b[n], // الثوابت a[n][n]; // المعاملات //تعيين القيم a[0][0]=8; a[0][1]=2; a[0][2]=-2; b[0]=8; //8x₁+2x₂-2x₃+8=0 a[1][0]=1; a[1][1]=-8; a[1][2]=3; b[1]=-4; //x₁-8x₂+3x₃-4=0 a[2][0]=2; a[2][1]=1; a[2][2]=9; b[2]=12; //2x₁+x₂+9x₃+12=0 int i; for(i=0; i<n; i++){ // تهيئة x[i]=0; } JacobisMethod(n, x, b, a); print(n, x); for(i=0; i<n; i++){ //تهيئة x[i]=0; } GaussSeidalMethod(n, x, b, a); print(n, x); return 0; } المعادلات غير الخطية Non-Linear Equations تُصنَّف المعادلات من النوع ‎‎f(x)= 0 إلى صنفين: إما جبرية أو غير جبرية -أو متسامية transcendental- ويمكن حل المعادلات غير الخطية باستخدام أحد أسلوبين: الأسلوب المباشر: يعطي هذا الأسلوب القيمة الدقيقة لكل جذور f (حلول المعادلة) مباشرة في عدد محدود من الخطوات. الأسلوب غير المباشر أو التكراري: هذا النوع أصلح من النوع الأول لحل المعادلات عبر الحاسوب، ويُبنى على مبدأ التقريب المتتالي، ولدينا طريقتين لحل المعادلة في الأسلوب التكراري: طريقة الحصار Bracketing Method: نأخذ نقطتين أوليّتين نعلم أنّ الجذر يقع بينهما، ثم نستمر في تضييق طول المجال الذي يحاصر الجذر إلى أن نصل إلى طول تقريبي معيّن. تُعد خوارزمية التنصيف من أشهر الخوارزميات التي تستخدم طريقة الحصار. طريقة النهاية المفتوحة Open End Method: نأخذ قيمة أولية أو قيمتين، ولا يُشترط أن تحاصر هاتان القيمتان جذر المعادلة، ثم نكرّر إجراء عمليات حسابية على هاتين القيمتين. وعادة ما يحدث هنا أحد أمرين، إمّا أن تتباعد القيمتان مع تكرار العمليات، أو تتقاربان -أي تؤُولان إلى نقطة واحدة، فإن كانتا متقاربتين فإنّ نقطة التقارب ستكون هي الحل. هذه الطريقة أسرع عمومًا من طريقة الحصار، ويُعد أسلوب نيوتن-رافسون Newton-Raphson، وأسلوب التقريب المتتالي Successive Approximation Method، وأسلوب القاطع Secant Method من الأمثلة على هذه الطريقة. هذا تطبيق بلغة C للحلول السابقة كلها على معادلات وضعناها في بداية الشيفرة: // دوال مساعدة #define f(x) ( ((x)*(x)*(x)) - (x) - 2 ) #define f2(x) ( (3*(x)*(x)) - 1 ) #define g(x) ( cbrt( (x) + 2 ) ) /** * نأخذ قيمتية أوليتين ونقصّر المسافة من كلا الجانبين **/ double BisectionMethod(){ double root=0; double a=1, b=2; double c=0; int loopCounter=0; if(f(a)*f(b) < 0){ while(1){ loopCounter++; c=(a+b)/2; if(f(c)<0.00001 && f(c)>-0.00001){ root=c; break; } if((f(a))*(f(c)) < 0){ b=c; }else{ a=c; } } } printf("It took %d loops.\n", loopCounter); return root; } /** * نأخذ قيمتية أوليتين ونقصّر المسافة من جانب واحد **/ double FalsePosition(){ double root=0; double a=1, b=2; double c=0; int loopCounter=0; if(f(a)*f(b) < 0){ while(1){ loopCounter++; c=(a*f(b) - b*f(a)) / (f(b) - f(a)); /*/printf("%lf\t %lf \n", c, f(c));/**////test if(f(c)<0.00001 && f(c)>-0.00001){ root=c; break; } if((f(a))*(f(c)) < 0){ b=c; }else{ a=c; } } } printf("It took %d loops.\n", loopCounter); return root; } /** * نستخدم قيمة أولية واحدة، ثمّ نتدرّج نحو القيمة الصحيحة **/ double NewtonRaphson(){ double root=0; double x1=1; double x2=0; int loopCounter=0; while(1){ loopCounter++; x2 = x1 - (f(x1)/f2(x1)); /*/printf("%lf \t %lf \n", x2, f(x2));/**////test if(f(x2)<0.00001 && f(x2)>-0.00001){ root=x2; break; } x1=x2; } printf("It took %d loops.\n", loopCounter); return root; } /** * نستخدم قيمة أولية واحدة، ثمّ نتدرّج نحو القيمة الصحيحة **/ double FixedPoint(){ double root=0; double x=1; int loopCounter=0; while(1){ loopCounter++; if( (x-g(x)) <0.00001 && (x-g(x)) >-0.00001){ root = x; break; } /*/printf("%lf \t %lf \n", g(x), x-(g(x)));/**////test x=g(x); } printf("It took %d loops.\n", loopCounter); return root; } /** *استخدام قيمتين أوليتين، بحيث تقترب كلاهما من الحل الصحيح **/ double Secant(){ double root=0; double x0=1; double x1=2; double x2=0; int loopCounter=0; while(1){ loopCounter++; /*/printf("%lf \t %lf \t %lf \n", x0, x1, f(x1));/**////test if(f(x1)<0.00001 && f(x1)>-0.00001){ root=x1; break; } x2 = ((x0*f(x1))-(x1*f(x0))) / (f(x1)-f(x0)); x0=x1; x1=x2; } printf("It took %d loops.\n", loopCounter); return root; } int main(){ double root; root = BisectionMethod(); printf("Using Bisection Method the root is: %lf \n\n", root); root = FalsePosition(); printf("Using False Position Method the root is: %lf \n\n", root); root = NewtonRaphson(); printf("Using Newton-Raphson Method the root is: %lf \n\n", root); root = FixedPoint(); printf("Using Fixed Point Method the root is: %lf \n\n", root); root = Secant(); printf("Using Secant Method the root is: %lf \n\n", root); return 0; } ترجمة -بتصرّف- للفصل 46 من كتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال التالي: أمثلة على خوارزميات لحل مشكلات بسيطة المقال السابق: مفهوم دوال التقطيع Hash Functions في الخوارزميات دليل شامل عن تحليل تعقيد الخوارزمية
  9. تكون دالةُ ‎h()‎ دالةَ تقطيع Hash Functions إذا كانت تأخذ عنصرًا ‎x ∈ X‎ من أيّ حجم، وتعيد قيمة ‎y ∈ Y‎ من حجمٍ ثابت ‎y‎ = h (x)‎‎. تتميز دوّال التقطيع النموذجية بالخصائص التالية: تتصرف مثل توزِيعات منتظمة uniform distribution دوال التقطيع حتمية deterministic، حيث ينبغي أن تعيد الدالة ‎h(x)‎ القيمة نفسها دائمًا لعنصر ‎x‎ محدد. ينبغي أن تكون سريعة الحساب (ذات تعقيد زمني O (1)‎‎). حجم قيمة التقطيع عمومًا أصغر من حجم البيانات المُدخلة: ‎|y| < |x|‎، علاوة على أنّ دوال التقطيع غير قابلة للعكس، لأنه يجوز أن تكون لقيمتين مختلفتين قيمة التقطيع نفسها، أو بتعبير رياضي: ∃ x1, x2 ∈ X, x1 ≠ x2: h(x1) = h(x2) قد تكون المجموعة X منتهية أو غير منتهية، أما ‎Y‎ فهي دائمًا مجموعة منتهية. وتُستخدم دوال التقطيع في الكثير من مجالات الحوسبة مثل هندسة البرمجيات والتشفير وقواعد البيانات والشبكات وتعلُُّم الآلة ونحو ذك، وهناك العديد من أنواع دوال التقطيع، ولكل منها خصائص مميزة. تكون قيمة التقطيع عددًا صحيحًا في الغالب، ومعظم لغات البرمجة لديها توابع ودوال خاصة لحساب قيم التقطيع، مثل الدالة GetHashCode()‎ في لغة C#‎‎ التي تعيد قيمة من النوع ‎Int32‎ (عدد صحيح من 32 بتة) لكل الأنواع. وكذلك في لغة جافا هناك تابع ‎hashCode()‎ يحسب قيمة التقطيع -من النوع int- لكل صنف من أصناف جافا. طرق تعريف دوال التقطيع Hash methods هناك عدة طرق لتعريف دوال التقطيع، يمكننا أن نفترض دون الإخلال بالعمومية، أنّ المجموعة X تساوي مجموعة الأعداد الصحيحة الموجبة، وx عنصر منها: أي ‎x ∈ X = {z ∈ ℤ: z ≥‎ 0}‎‎. وليكن m عددًا، ويُفضّل أن يكون أوليًا (شرط أن لا تكون قيمته قريبة جدًا من أي قوّة للعدد 2). فيما يلي طريقتان لحساب قيم التقطيع الخاصة بعناصر X: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } الطريقة دالة التقطيع طريقة القسمة h(x) = x mod m طريقة الضرب h(x) = ⌊m (xA mod 1)⌋, A ∈ {z ∈ ℝ: 0 < z < 1} جداول التقطيع تُستخدم دوال التقطيع مع جداول التقطيع hash tables لحساب الفهارس في مصفوفة من الحجرات array of slots، وجدول التقطيع هي هيكلية بيانات لتنفيذ القواميس dictionaries وهي بيانات من نوع مفتاح-قيمة. التعقيد الزمني لتطبيقات لجداول التقطيع يساوي في العادة O (1)‎‎ للعمليات التالية: إدراج البيانات بحسب قيمة المفتاح. حذف البيانات بحسب قيمة المفتاح. يمكن أن يكون لعدة مفاتيح نفس التقطيع (الحجرة)، وعندئذ فهناك طريقتان لحل هذه المشكلة: السلسلة Chaining: تُستخدم القوائم المترابطة لتخزين العناصر التي لها نفس قيمة التقطيع في الحجرة. العنونة المفتوحة Open addressing: يُخزّن عنصر واحد على الأكثر في كل حجرة. تُستخدم الطرق التالية لحساب تسلسلات المسبار probe sequences المطلوبة في العنونة المفتوحة: الطريقة الصيغة السبر الخطي h(x, i) = (h”(x) + i) mod m السبر التربيعي h(x, i) = (h”(x) + c1*i + c2*i^2) mod m السبر المضاعف h(x, i) = (h1(x) + i*h2(x)) mod m الدوال h"(x)‎‎ و h1(x)‎‎ و h2(x)‎‎ هي دوال مساعدة، وi ينتمي إلى المجموعة {0, 1, ..., m-1}، وc1 و c2 ثابتتان موجبتان. يمكنك معرفة المزيد من التفاصيل عن هذه الطرق الثلاث وغيرها من المفاهيم المتعلقة بدوال التقطيع من موسوعة حسوب. لنأخذ المثال التالي، ليكن ‎x ∈ U{1, 1000}‎‎ و h = x mod m‎. يوضح الجدول التالي قيم التقطيع في حالة كان العدد m أوليا أو غير أولي، تشير النصوص الغليظة إلى قيم التقطيع المتساوية. x m = 100 - غير أولي m = 101 - أولي 723 23 16 103 3 2 738 38 31 292 92 90 61 61 61 87 87 87 995 95 86 549 49 44 991 91 82 757 57 50 920 20 11 626 26 20 557 57 52 931 31 23 619 19 13 قيم التقطيع للأنواع الشائعة في C#‎‎ سنستعرض في هذه الفقرة قيم التقطيع hash codes التي ينتجها التابع ‎GetHashCode()‎ لأنواع C#‎‎المضمّنة في فضاء الاسم ‎System‎. يمكنك الاطلاع على توثيقات هذه الأنواع من github. القيم المنطقية Boolean 1 إذا كانت القيمة صحيحة true، أو 0 خلاف ذلك. الأنواع Byte و UInt16 و Int32 و UInt32 و Single قيمة العنصر (تُحوّل عند الضرورة إلى النوع Int32). النوع SByte ((int)m_value ^ (int)m_value << 8); النوع Char (int)m_value ^ ((int)m_value << 16); النوع Int16 ((int)((ushort)m_value) ^ (((int)m_value) << 16)); النوعان Int64 و Double تطبيق المعامل Xor بين أدنى 32 بتّة وأعلى 32 بتة في العدد المؤلف من 64 بتة. (unchecked((int)((long)m_value)) ^ (int)(m_value >> 32)); الأنواع UInt64 و DateTime و TimeSpan ((int)m_value) ^ (int)(m_value >> 32); النوع Decimal ((((int *)&dbl)[0]) & 0xFFFFFFF0) ^ ((int *)&dbl)[1]; Object النوع Object هو الصنف الجذري الذي تنحدر منه جميع الكائنات. RuntimeHelpers.GetHashCode(this); استخدمنا التطبيق الافتراضي فهرس كتلة المزامنة sync block index. السلاسل النصية String يعتمد حساب قيمة التقطيع على نوع نظام التشغيل (Win32 أو Win64)، وإمكانية استخدام تقطيع السلاسل النصية العشوائية randomized string hashing، إضافة إلى الإصدار ووضعية المنقّح. في حالة المنصة Win64: int hash1 = 5381; int hash2 = hash1; int c; char *s = src; while ((c = s[0]) != 0) { hash1 = ((hash1 << 5) + hash1) ^ c; c = s[1]; if (c == 0) break; hash2 = ((hash2 << 5) + hash2) ^ c; s += 2; } return hash1 + (hash2 * 1566083941); ValueType يتم البحث عن أول حقل غير ساكن non-static field، وتُحسب قيمة التقطيع خاصته، إذا لم يكن للنوع أيّ حقل غير ساكن، تُعاد قيمة التقطيع الخاصة بذلك النوع. لا يمكن الاعتماد على قيمة التقطيع الخاصة بمتغير عضو ساكن static member، لأنه قد تنتجُ حلقة أبدية إذا كان نوع ذلك العضو يساوي النوع الأصلي. النوع Nullable‎‎ return hasValue ? value.GetHashCode() : 0; المصفوفات Array int ret = 0; for (int i = (Length >= 8 ? Length - 8 : 0); i < Length; i++) { ret = ((ret << 5) + ret) ^ comparer.GetHashCode(GetValue(i)); } ترجمة -بتصرّف- للفصل 43 من كتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال السابق: أمثلة على أشهر خوارزميات المخططات والأشجار مدخل إلى الخوارزميات أمثلة عن أنواع الخوارزميات تعقيد الخوارزميات Algorithms Complexity
  10. تستعرض هذه المقالة أربعةً من خوارزميات الأشجار والمخططات، وهي خوارزمية البحث بالعرض أولا BFS، وخوارزمية البحث بالعمق أولا DFS، والبائع المتجول، وخوارزمية كثيرة الحدود للعثور على أصغر غطاء رأسي في مخطط ما. البحث بالعرض أولا Breadth-First Search سنحاول استعمال هذه الخوارزمية في عدة تطبيقات عمليات بحث. البحث عن أقصر مسار من المصدر إلى العقد الأخرى خوارزمية البحث بالعرض أولا BFS هي خوارزمية لتسلق مخطط أو البحث فيها. تبدأ الخوارزمية من جذر الشجرة (أو أيّ عقدة من المخطط، ويُشار إليها أحيانًا باسم "مفتاح البحث" - search key)، ثمّ تستكشف العقد المجاورة أولاً قبل الانتقال إلى المستوى التالي من العقد. اكتُشفت خوارزمية BFS في أواخر الخمسينيات من قبل إدوارد فورست مور Edward Forrest Moore، الذي استخدمها للعثور على أقصر مسار للخروج من متاهة، وقد اكتُشفت أيضًا بشكل مستقل من قبل CY Lee الذي استخدمها في مجال التوجيه السلكي في عام 1961. تفترض خوارزمية BFS الأمور التالية: لن نجتاز أي عقدة أكثر من مرة. تقع العقدة المصدرية (أي العقدة التي نبدأ منها) في المستوى 0. العقد التي يمكننا الوصول إليها مباشرة من العقدة المصدرية ستكون في المستوى 1، والعقد التي يمكننا الوصول إليها مباشرة من عقد المستوى 1 ستكون في المستوى 2 وهكذا دواليك. يشير مستوى عقدة ما إلى أقصر مسار يصل إلى تلك العقدة انطلاقًا من المصدر. انظر المثال التالي: لنفترض أنّ هذا المخطط يمثل طرقا بين عدة مدن حيث تمثل كل عقدة مدينة واحدة، فيما يشير كل ضلع بين عقدتين إلى طريق يربط بينهما، ونحن نريد الانتقال من العقدة 1 إلى العقدة 10. ستكون العقدة 1 هي المصدر، وهذا يجعلها في المستوى 0. نحدد (نلوّن) العقدة 1 لنشير إلى أننا زرناها. يمكننا الذهاب إلى العقدة 2 أو العقدة 3 أو العقدة 4 من العقدة المصدر، أي العقدة 1، لذلك ستكون هناك 3 عقد من المستوى 1. نحدد هذه العقد لنبيّن أنّها مُزارة، ثمّ نعمل عليها. نلوّن العقد المُزارة، حيث نلوّن العقد التي نعمل عليها حاليًا باللون الوردي، تذكّر أنّنا لن نزور العقدة نفسها مرتين. يمكننا الذهاب إلى العقد 6 و 7 و 8 عبر كل من العقدة 2 والعقدة 3 والعقدة 4، وسنحددهذه العقد لنبيّن أنّها مُزارة. مستوى هذه العقد سيساوي (1 +1) = 2. يشير مستوى العقد إلى أقصر مسافة بينها وبين المصدر. على سبيل المثال: بما أن العقدة 8 موجودة في المستوى 2، فهذا يعني أنّ المسافة من المصدر إلى العقدة 8 هي 2. لم نصل بعدُ إلى العقدة المستهدفة وهي العقدة 10، لذا ننتقل إلى العقد التالية. نستطيع الانطلاق من أيّ من عقد المستوى 2، وهي العقدة 6 والعقدة 7 والعقدة 8. ها قد وجدنا العقدة 10 عند المستوى 3، هذا يعني أنّ أقصر مسار من المصدر إلى العقدة 10 هو 3، وقد بحثنا في كل مستوى من مستويات المخطط إلى أن وجدنا أقصر مسار، وإذ ذاك سنمسح الأضلاع التي لم نستخدمها: بعد إزالة تلك الأضلاع التي لم نستخدمها نحصل على شجرة تُسمى شجرة البحث بالعرض أولًا BFS. تُظهر هذه الشجرة أقصر مسار من المصدر إلى جميع العقد الأخرى. مهمتنا الآن هي الانتقال من العقدة المصدر إلى العقد الموجودة في المستوى 1، ثم من عقد المستوى 1 إلى عقد المستوى 2، وهكذا حتى نصل إلى وجهتنا. يمكننا استخدام الطوابير Queues لتخزين العقد التي سنعالجها. لكل عقدة نعمل عليها فإننا ندفع (نضيف) إلى الطابور جميع العُقد الأخرى التي من الممكن اجتيَازها مباشرة لكنّنا لم نفعل بعد. هذه محاكاة للمثال: أولاً، ندفع العقدة المصدر إلى الطابور، فيبدو هكذا: front +-----+ | 1 | +-----+ مستوى العقدة 1 يساوي 0، أي level[1] = 0، نبدأ الآن البحث بالعرض أولا BFS. في البداية، ننزع عقدة من الطابور فنحصل على العقدة 1، يمكننا -انطلاقًا من هذه العقدة- أن نذهب إلى العقدة 4 أو 3 أو 2، لهذا فهذه العقد الثلاث من المستوى الأول، أي level[4] = level[3] = level[2] = level[1] + 1 = 1. سنحددالآن هذه العقد لنبيّن أنّها مُزارَة، ثمّ ندفعها إلى الطابور. front +-----+ +-----+ +-----+ | 2 | | 3 | | 4 | +-----+ +-----+ +-----+ ننزع الآن العقدة 4 من الطابور ونعالجها، نستطيع الذهاب منها إلى العقدة 7، لهذا يكون لدينا: level[7] = level[4] + 1 = 2. والآن نحددالعقدة 7، ثمّ ندفعها إلى الطابور. front +-----+ +-----+ +-----+ | 7 | | 2 | | 3 | +-----+ +-----+ +-----+ من العقدة 3، يمكننا الذهاب إلى العقدة 7 أو العقدة 8، وبما أن العقدة 7 محددة من قبل (أي مُزارة سابقًا)، فإننا نحدد العقدة 8، ونضع level[8] = level[3] + 1 = 2. والآن ندفع العقدة 8 إلى الطابور. front +-----+ +-----+ +-----+ | 6 | | 7 | | 2 | +-----+ +-----+ +-----+ ستستمر هذه العملية حتى نصل إلى وجهتنا أو نُفرِغ الطابور، وتزوّدنا مصفوفة المستويات level بطول أقصر مسار من المصدر. نستطيع تهيئة عناصر مصفوفة المستويات بقيمة اللانهاية، كناية إلى أنّ العقد لم تُزر بعد. انظر المثال التوضيحي لهذا: Procedure BFS(Graph, source): Q = queue(); level[] = infinity level[source] := 0 Q.push(source) while Q is not empty u -> Q.pop() for all edges from u to v in Adjacency list if level[v] == infinity level[v] := level[u] + 1 Q.push(v) end if end for end while Return level يمكننا معرفة المسافة التي تفصل كل عقدة عن المصدر من خلال التكرار عبر مصفوفة المستويات، على سبيل المثال: المسافة إلى العقدة 10 من المصدر مخزّنة في level[10]‎‎. قد نريد أحيانًا أن نعرف المسارات الأخرى التي يمكن أن تُوصلنا إلى العقدة انطلاقًا من المصدر، وذلك إلى جانب المسار الأقصر. لفعل هذا نحتاج إلى مصفوفة جديدة نسميها parent، قيمة parent[source]‎‎ ‏(source هنا تعني العقدة المصدر‏) ستكون معدومة ‎‎NULL‎‎. نضيف التعليمة ‎parent[v] := u‎ إلى المثال الوهمي عند كل تحديث لمصفوفة المستويات level في حلقة for. لكي تعثر على المسار بعد انتهاء خوارزمية البحث العرضي أولًا، يكفي أن تجتاز المصفوفة parent خلفيًا إلى أن تصل إلى المصدر، والذي يحمل القيمة NULL في هذه المصفوفة. هذا مثال توضيحي يستخدم العودية recursion: Procedure PrintPath(u): if parent[u] is not equal to null PrintPath(parent[u]) end if print -> u وهذا مثال لا يستخدمها وإنما يستخدم التكرارية فقط iteration: Procedure PrintPath(u): S = Stack() while parent[u] is not equal to null S.push(u) u := parent[u] end while while S is not empty print -> S.pop end while تعقيد الخوارزمية: سوف نزور كل عقدة وكل ضلع مرّة واحدة بالضبط، لذا سيكون تعقيد الخوارزمية الزمني O (V + E)‎‎، حيث يمثل V عدد العقد، ويمثّل E عدد الأضلاع. البحث عن أقصر مسار ينطلق من المصدر في مخطط ثنائية الأبعاد قد نحتاج أحيانًا إلى العثور على أقصر مسار من مصدر معيّن إلى جميع العقد الأخرى، أو إلى عقدة محدّدة في مخطط ثنائية الأبعاد. على سبيل المثال: نريد أن نعرف عدد التحركات المطلوبة للفارس للوصول إلى مربع معين في رقعة الشطرنج، وهذه حالة خاصة من مشكلة أكبر، حيث تكون لدينا رقعة بعض مربعاتها محظورة (الفارس لا يمكنه الانتقال لبعض المربعات على الرقعة)، وعلينا العثور على أقصر مسار من مربع إلى آخر لا يمر عبر المربعات المحظورة. في مثل هذه الحالات، يمكننا تحويل المربعات أو الخلايا إلى عقد، ونحل هذه المشكلة بسهولة باستخدام خوارزمية البحث بالعرض أولًا BFS. ستكون المصفوفات visited و parent و level جميعها مصفوفات ثنائية الأبعاد. نأخذ جميع التحركات الممكنة لكل عقدة، ونحسب المسافة إلى العقد الأخرى عبر تحليل مسارات التحرك، ثم نضيف مصفوفة أخرى نسميها direction، والتي ستخزّن جميع التركيبات الممكنة للاتجاهات التي يمكننا الذهاب إليها. مثلا، هذه مصفوفة الاتجاهات الخاصة بالتحركات الأفقية والعمودية: +----+-----+-----+-----+-----+ | dx | 1 | -1 | 0 | 0 | +----+-----+-----+-----+-----+ | dy | 0 | 0 | 1 | -1 | +----+-----+-----+-----+-----+ تمثل dx الحركة على المحور الأفقي x بينما تمثل dy الحركة على المحور العمودي y، ولا شك أن هناك طرق أخرى لتمثيل اتجاهات الحركة لكن يُفضل استخدام مصفوفة الاتجاهات لأنها أسهل وأبسط. كذلك هناك الكثير من التركيبات الممكنة للحركة، إذ يمكن أن تكون التحركات قُطرية أو قد تكون أكثر تعقيدًا مثل تحركات الحصان على رقعة الشطرنج. وينبغي أن نضع في حسباننا الأمور التالية: إذا كانت أيّ من الخلايا محظورة فسيكون علينا أن نتحقق في كل حركة محتملة مما إذا كانت الخلية الحالية محظورة أم لا. علينا أن نتحقق أيضًا ممّا إذا كنا قد تجاوزنا الحدودَ، أي حدودَ المصفوفة. سنُعطى عدد الصفوف والأعمدة. انظر المثال التوضيحي التالي: Procedure BFS2D(Graph, blocksign, row, column): for i from 1 to row for j from 1 to column visited[i][j] := false end for end for visited[source.x][source.y] := true level[source.x][source.y] := 0 Q = queue() Q.push(source) m := dx.size while Q is not empty top := Q.pop for i from 1 to m temp.x := top.x + dx[i] temp.y := top.y + dy[i] if temp is inside the row and column and top doesn't equal to blocksign visited[temp.x][temp.y] := true level[temp.x][temp.y] := level[top.x][top.y] + 1 Q.push(temp) end if end for end while Return level ناقشنا سابقًا أن خوارزمية البحث بالعرض أولا BFS لا تصلح إلا للمخططات غير الموزونة unweighted graphs، أما بالنسبة للمخططات الموزونة فسنحتاج إلى خوارزمية ديكسترا. وبالنسبة لدورات الأضلاع السلبية negative edge cycles فسنحتاج إلى خوارزمية بِِلمان - فورد. هناك مسألة أخرى ينبغي الانتباه إليها، وهي أنّ هذه الخوارزمية مخصّصة لإيجاد أقصر مسار من مصدر واحد، فإذا أردت العثور على المسافة من كل عقدة إلى جميع العقد الأخرى ستحتاج إلى استخدام خوارزمية بِلمان - فورد كذلك. البحث عن المكونات المتصلة لمخطط غير موجهة باستخدام خوارزمية BFS يمكن استخدام خوارزمية BFS للعثور على المكونات المتصلة connected components في مخطط غير موجه، ويمكن استخدامها أيضًا للتحقق مما إذا كان المخطط متصلًا أم لا، وسنفترض فيما يلي أنّنا نتعامل مع مخططات غير موجهة. تعريف: يكون المخطط متصلًا connected إذا كان هناك مسار بين كل زوج من الحروف في المخطط. هذا مثال على مخطط متصل: المخطط التالي غير متصل، ولكن يحتوي على مكوّنتين متصلتين: المكونة المتصل الأولى: {a، b، c، d، e} . المكون المتصلة الثانية: {f} خوارزمية BFS هي خوارزمية لاجتياز المخططات، لذا إن بدأت الخوارزمية من عقدة مصدرية ما وانتقلت من عقدة إلى أخرى، ولم تترك عقدة إلا زارتها قبل انتهائها، فيكون المخطط في هذه الحالة متصلًا، أما إن بقيت بعض بعض العقد غير مُزارة، فسيكون المخطط غير متصل. انظر المثال التوضيحي التالي للخوارزمية: boolean isConnected(Graph g) { BFS(v) // هي عقدة مصدرية v if(allVisited(g)) { return true; } else return false; } وهذا تطبيق بلغة C للتحقق مما إذا كان مخططًا متصلًأ غير موجّه أم لا: #include<stdio.h> #include<stdlib.h> #define MAXVERTICES 100 void enqueue(int); int deque(); int isConnected(char **graph,int noOfVertices); void BFS(char **graph,int vertex,int noOfVertices); int count = 0; struct node { int v; struct node *next; }; typedef struct node Node; typedef struct node *Nodeptr; Nodeptr Qfront = NULL; Nodeptr Qrear = NULL; char *visited;// مصفوفة لتتبع العقد المُزارة int main() { int n,e;// ‫ يمثل n عدد الحروف، ويمثل e عدد الأضلاع int i,j; char **graph;//adjacency matrix printf("Enter number of vertices:"); scanf("%d",&n); if(n < 0 || n > MAXVERTICES) { fprintf(stderr, "Please enter a valid positive integer from 1 to %d",MAXVERTICES); return -1; } graph = malloc(n * sizeof(char *)); visited = malloc(n*sizeof(char)); for(i = 0;i < n;++i) { graph[i] = malloc(n*sizeof(int)); visited[i] = 'N';// في البداية، تكون جميع العقد غير مزارة for(j = 0;j < n;++j) graph[i][j] = 0; } printf("enter number of edges and then enter them in pairs:"); scanf("%d",&e); for(i = 0;i < e;++i) { int u,v; scanf("%d%d",&u,&v); graph[u-1][v-1] = 1; graph[v-1][u-1] = 1; } if(isConnected(graph,n)) printf("The graph is connected"); else printf("The graph is NOT connected\n"); } void enqueue(int vertex) { if(Qfront == NULL) { Qfront = malloc(sizeof(Node)); Qfront->v = vertex; Qfront->next = NULL; Qrear = Qfront; } else { Nodeptr newNode = malloc(sizeof(Node)); newNode->v = vertex; newNode->next = NULL; Qrear->next = newNode; Qrear = newNode; } } int deque() { if(Qfront == NULL) { printf("Q is empty , returning -1\n"); return -1; } else { int v = Qfront->v; Nodeptr temp= Qfront; if(Qfront == Qrear) { Qfront = Qfront->next; Qrear = NULL; } else Qfront = Qfront->next; free(temp); return v; } } int isConnected(char **graph,int noOfVertices) { int i; // نختار العقدة 0 لتكون عقدة مصدرية BFS(graph,0,noOfVertices); for(i = 0;i < noOfVertices;++i) if(visited[i] == 'N') return 0;//0 implies false; return 1;//1 implies true; } void BFS(char **graph,int v,int noOfVertices) { int i,vertex; visited[v] = 'Y'; enqueue(v); while((vertex = deque()) != -1) { for(i = 0;i < noOfVertices;++i) if(graph[vertex][i] == 1 && visited[i] == 'N') { enqueue(i); visited[i] = 'Y'; } } } للعثور على جميع المكونات المتصلة لمخطط غير موجه، يكفي أن نضيف سطرين إلى شيفرة الدالة BFS. الفكرة هي أن نستدعي الدالة BFS إلى أن تُزار جميع الحروف. هذا هو السطر الأول، المتغير count هو متغير عام مهيء على القيمة 0، وهذا السطر يوضع في بداية دالة BFS: printf("\nConnected component %d\n",++count); وهذا هو السطر الثاني، اجعله في بداية حلقة while في دالة BFS: printf("%d ",vertex+1); نعرّف الآن الدالة التالية: void listConnectedComponents(char **graph,int noOfVertices) { int i; for(i = 0;i < noOfVertices;++i) { if(visited[i] == 'N') BFS(graph,i,noOfVertices); } } خوارزمية البحث بالعمق أولا Depth First Search خوارزمية البحث بالعمق أولًا DFS هي خوارزمية لتسلق مخطط أو البحث فيها، وتبدأ من الجذر وتستكشف العقد على طول كل فرع وتتعمق فيه إلى آخر حدّ قبل أن ترتد backtrack، وقد استُخدِمت نسخة أولية من هذه الخوارزمية من قبل عالم الرياضيات الفرنسي تشارلز بيير Tr‎é‎maux في القرن التاسع عشر لإيجاد حلول للمتاهات. البحث بالعمق أولًا هي طريقة تحاول العثور على جميع العقد التي يمكن الوصول إليها من العقدة المصدر، وتجتاز خوارزمية DFS عقد مُكوّنة متصلة connected component ما من مخطط خوارزمية البحث بالعرض أولًا، ثم تعرّف شجرة ممتدة spanning tree. كذلك تستكشف خوارزمية البحث بالعمق أولا كل الأضلاع بطريقة منهجية. إذ تبدأ من العقدة المصدرية، وبمجرد الوصول إلى عقدة من المخطط تبدأ خوارزمية DFS في الاستكشاف انطلاقًا منها (على عكس خوارزمية BFS، التي تضعها في طابور لأجل استكشافها لاحقًا). انظر المثال التالي: نجتاز المخطط باستخدام القواعد التالية: نبدأ من المصدر. لن نزور أيّ عقدة مرتين. نلوّن العقد التي لم نزرها بعد باللون الأبيض. نلوّن بالرمادي العقد التي زرناها ولكن لم نزر بعد جميع العقد المتفرّعة منها. نلوّن بالأسود العقد التي اجتزناها بالكامل هي والعقد المتفرعة منها. تبيّن الرسوم التالية هذه الخطوات: الكلمة المفتاحية التي تهمنا والتي نراها فيما سبق هي الضلع الخلفي backedge، فالضلع 5-1 هو ضلع خلفي backedge، لأننا لم ننته بعد من العقدة 1، فالعودة إلى العقدة المصدرية 1 يعني وجود دورة في المخطط. إذا كنا نستطيع الانتقال من عقدة رمادية إلى أخرى في خوارزمية البحث بالعمق أولا، فذلك دليل على أنّ المخطط يحتوي على دورة، وهذه إحدى طرق رصد الدورات في المخططات. يمكننا أن نجعل أيّ ضلع في الدورة كضلع خلفي اعتمادًا على العقدة المصدرية، وترتيب زيارة العقد. على سبيل المثال: لو انتقلنا إلى العقد 5 من العقدة 1 أولا، لوجدنا أنّ الضلع 2-1 أصبح ضلعًا خلفيًا. تسمى الأضلاع التي تنتقل من عقدة رمادية إلى عقدة بيضاء أضلاع الشجرة أو أضلاعًا شجرية tree edge، وإذا أبقينا على أضلاع الشجرة وحذفنا الأضلاع الأخرى، فسوف نحصل على شجرة البحث بالعمق أولا DFS. إذا استطعنا زيارة عقدة مزارة سابقًا في مخطط غير موجه فذلك يعني وجود ضلع خلفي، أمّا بالنسبة للمخططات الموجّهة فسيكون علينا أن نتحقق من الألوان، حيث يكون الضلع خلفيًا فقط إذا كان بمقدورنا الانتقال من عقدة رمادية إلى عقدة رمادية أخرى. أيضًا، في خوارزمية البحث بالعمق أولا، نستطيع تخزين علامات زمنية timestamps لكل عقدة، تخزن هذه العلامات الزمنية معلومات عما حدث للعقد أثناء المعالجة، ونخزّنها في مصفوفتين، مصفوفة f لتخزين أوقات الانتهاء، ومصفوفة d لتخزين أوقات استكشاف العقد: عندما يتغيّر لون العقدة v من الأبيض إلى الرمادي نسجّل الوقت في الموضع d[v]‎‎. عند يتغيّر لون عقدة v من الرمادي إلى الأسود، نسجّل الوقت في f [v]‎‎. سيبدو المثال التوضيحي لتخزين العلامات الزمنية كما يلي: Procedure DFS(G): for each node u in V[G] color[u] := white parent[u] := NULL end for time := 0 for each node u in V[G] if color[u] == white DFS-Visit(u) end if end for Procedure DFS-Visit(u): color[u] := gray time := time + 1 d[u] := time for each node v adjacent to u if color[v] == white parent[v] := u DFS-Visit(v) end if end for color[u] := black time := time + 1 f[u] := time التعقيد: تُزار كل عقدة وكل ضلع مرة واحدة، لذا فإن تعقيد خوارزمية DFS هو O (V + E)‎‎، حيث يمثّل V عدد العقد في المخطط، ويمثل E عدد الأضلاع. التطبيقات على خوارزمية البحث بالعمق أولًا كثيرة، منها على سبيل المثال لا الحصر: العثور على أقصر مسار بين كل زوج من العقد في مخطط غير موجه. رصد الدورات في المخططات. العثور على المسارات. الترتيب التخطيطي (الطوبولوجي). التحقق ممّا إذا كان المخطط ثنائي التجزئة. العثور على المكونات شديدة الاتصال Strongly Connected Component. حل الألغاز التي لها حل واحد. خوارزمية البائع المتجول إن إنشاء مسار يمر عبر كل رأس من رؤوس المخطط مرة واحدة يكافئ ترتيبًا يرتّب حروف ذلك المخطط، ويمكننا استغلال هذه الخاصية لحساب التكلفة الدنيا لعبور كل رأس مرة واحدة بالضبط عبر تجريب كل التبديلات لمجموعة الأعداد من ‎1‎ إلى ‎N‎، وعددها ‎N!‎. انظر الشيفرة العامة لهذا: minimum = INF for all permutations P // كل التبديلات current = 0 for i from 0 to N-2 // أضف تكلفة الانتقال من عقدة إلى إلى أخرى current = current + cost[P[i]][P[i+1]] // أضف تكلفة الانتقال من العقدة الأخيرة إلى إلى العقدة الأولى current = current + cost[P[N-1]][P[0]] if current < minimum // إن كان ذلك ضروريا minimum حدِّث minimum = current output minimum التعقيد الزمني: هناك ‎N!‎ تبديلة ينبغي معالجتها، وحساب تكلفة كل مسار تستغرق ‎O(N)‎، من ثم فإنّ هذه الخوارزمية تستغرق مدة O ‎(‎N ‎*‎ N!)‎. استخدام البرمجة الديناميكية انظر المسار: (1,2,3,4,6,0,5,7) والمسار (1,2,3,5,0,6,7,4) تبقى تكلفة الانتقال من العقدة ‎1‎ إلى العقدة ‎2‎ ثمّ إلى العقدة ‎3‎ كما هي، لذا ليس علينا إعادة حسابها، إذ يمكن أن نحفظ هذه النتيجة لاستخدامها لاحقًا. لنفترض أنّ ‎dp[bitmask][vertex]‎ تمثل الحد الأدنى لتكلفة المسارات التي تعبر جميع الحروف التي قيمة البتّات المقابلة لها فيها ‎bitmask‎ تساوي ‎1‎، والتي تنتهي عند الحرف ‎vertex‎. على سبيل المثال: dp[12][2] 12 = 1 1 0 0 ^ ^ vertices: 3 2 1 0 نظرًا لأنّ ‎1100‎ هي التمثيل الثنائي لـ 12، فإنّ ‎dp[12][2]‎ تمثل الانتقال عبر الحرفين ‎2‎ و ‎3‎ في المخطط عبر مسار ينتهي عند الحرف 2. هذا تطبيق على الخوارزمية بلغة C++‎: int cost[N][N]; // إن كان ذلك ضروريا N تعديل قيمة int memo[1 << N][N]; // -1 تهيئة كل العناصر بالقيمة int TSP(int bitmask, int pos){ int cost = INF; if (bitmask == ((1 << N) - 1)){ // استكشاف كل العقد return cost[pos][0]; // تكلفة الرجوع } if (memo[bitmask][pos] != -1){ // إن كنا قد حسبنا هذه القيمة سابقا return memo[bitmask][pos]; // فسنعيد القيمة السابقة، فلا داعي لإعادة حسابها } for (int i = 0; i < N; ++i){ if ((bitmask & (1 << i)) == 0){ //إن لم تُكن العقدة مُزارة بعد cost = min(cost,TSP(bitmask | (1 << i) , i) + cost[pos][i]); // زيارة العقدة } } memo[bitmask][pos] = cost; // حفظ النتيجة return cost; } //Call TSP(1,0) قد يكون هذا السطر مبهمًا، لذا سنفصِّله من أجل التوضيح: cost = min(cost,TSP(bitmask | (1 << i) , i) + cost[pos][i]); هنا، تعيّن العبارة ‎bitmask | (1 << i)‎ البتة رقم i في ‎bitmask‎ وتعطيها القيمة 1 دلالة على أنّ الحرف رقم i قد تمت زيارته. تمثّل i الموضوعة بعد الفاصلة قيمة الموضع pos الجديد في هذا الاستدعاء، والذي يمثل الحرف الأخير الجديد. وتضيف العبارة‎cost[pos][i‎]‎ تكلفة الانتقال من الحرف الموجود في الموضع pos إلى الحرف i. يُحدّث هذا السطر قيمة ‎cost‎، ويعطيها أدنى قيمة ممكنة للمرور عبر كل الحروف الأخرى التي لم تُزر بعد. التعقيد الزمني: هناك ‎2^N‎ قيمة ممكنة لـ ‎bitmask‎ و ‎N‎ قيمة ممكنة لـ ‎pos‎ في الدالة ‎TSP(bitmask,pos)‎. ويستغرق تنفيذ كل دالة ‎O(N)‎ (الحلقة for). وبالتالي يستغرق هذا التطبيق إجمالا مدة ‎O(N^2 * 2^N)‎ لإيجاد الحل النهائي. خوارزمية كثيرة الحدود ومقيدة زمنيًا للعثور على أصغر غطاء رأسي تعريف: ليكن G مخطط، و C مجموعة من حروفها، فنقول أنّ C غطاء رأسي للمخطط G إن كانت تحتوي طرفًا واحدًا على الأقل من كل ضلع من أضلاع المخطط. وفي هذه الفقرة نستعرض خوارزمية كثيرة الحدود للحصول على أصغر غطاء رأسي vertex cover لمخطط متصل غير موجّه. التعقيد الزمني لهذه الخوارزمية يساوي O (n2)‎‎. هذا مثال توضيحي لخوارزمية أصغر غطاء رأسي، إذ نُدخل إليها مخططًا متصلًا G لتعيد لنا مجموعة C تمثّل أصغر غطاء رأسي لهذا المخطط. Set C <- new Set<Vertex>() Set X <- new Set<Vertex>() X <- G.getAllVerticiesArrangedDescendinglyByDegree() for v in X do List<Vertex> adjacentVertices1 <- G.getAdjacent(v) if !C contains any of adjacentVertices1 then C.add(v) for vertex in C do List<vertex> adjacentVertices2 <- G.adjacentVertecies(vertex) if C contains any of adjacentVertices2 then C.remove(vertex) return C يمكنك استخدام ترتيب الدلو لترتيب الحروف بحسب درجاتها، وبما أن القيمة القصوى للدرجات تساوي (n-1)، حيث يمثّل n عدد الحروف، فستستغرق عمليات الترتيب O(n)‎‎. ترجمة -بتصرّف- للفصول 41 و 42 و 44 و 53 من كتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال السابق: خوارزميات البحث في النصوص تطبيقات الخوارزميات الشرهة خوارزمية ديكسترا Dijkstra’s Algorithm
  11. هذه المقالة تتمة للمقالة السابقة خوارزميات البحث وآلية عملها، إذ نستعرض فيها بعض خوارزميات البحث، لكننا سنركز هذه المرة على البحث داخل النصوص، وسنستعرض اثنتين من أشهر خوارزميات البحث في النصوص، وهما خوارزمية رابين-كارب وخوارزمية KMP. خوارزمية KMP لنفترض أن لدينا نصًّا ونمطًا (سلسلة نصية)، ونريد أن نعرف ما إذا كان النمط موجودًا في النص أم لا. انظر المثال التالي: +-------+----+----+----+----+----+----+----+----+ | Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +-------+----+----+----+----+----+----+----+----+ | Text | a | b | c | b | c | g | l | x | +-------+----+----+----+----+----+----+----+----+ +----------+----+----+----+----+ | Index | 0 | 1 | 2 | 3 | +----------+----+----+----+----+ | Pattern | b | c | g | l | +----------+----+----+----+----+ هذا النمط Pattern موجود فعلًا في النص Text، لذا يجب أن تعيد خوارزمية البحث العدد 3، وهو فهرس الموضع الذي يبدأ منه هذا النمط داخل النص. الطريقة البدهية هي أن نبدأ من الفهرس 0 في النص والفهرس 0 في النمط، ونوازن Text[0]‎‎ وPattern[0]‎‎. وبما أنهما غير متساويين، سننتقل إلى الفهرس التالي في النص، ونوازن Text[1]‎‎ وPattern[0]‎‎. وبما أنهما متطابقين، فإنّنا نزيد قيمة الفهرس في النمط والنص معًا. بعد ذلك نوازن Text[2]‎‎ وPattern[1]‎‎ فنجد أنّهما متطابقين، ثم نتابع ونوازن الآن Text[3]‎‎ وPattern[2]‎‎، واللذين نجد أنّهما غير متطابقين، فنبدأ الآن من الموضع حيث بدأ التطابق بين النص والنمط، أي الفهرس 2 في النص، ونوازن بين كل من Text[2]‎‎ وPattern[0]‎‎، وبما أنهما لا يتطابقان، فسنزيد الفهرس النصي، ونوازن بين كل من Text[3]‎‎ وPattern[0]‎‎. واللذان نجد أنهما متطابقان، كما يتطابق كل من: Text[4]‎‎. Pattern[1]‎‎. Text[5]‎‎. Pattern[2]. Text[6]‎‎. Pattern[3]‎‎. لقد استنفدنا حروف النمط، وهذا يعني أنه موجود في النص. نعيد الآن الفهرس الذي بدأ التطابق عنده، وهو 3. إن لم يكن النمط موجودًا في النص (مثلا إن كان يساوي ‎bcgll‎)، فنتوقّع أن يُرفع اعتراض exception أو يُعاد -1 أو أيّ قيمة أخرى محدّدة مسبقًا. تستغرق الخوارزمية في أسوأ الحالات ‎O(mn)‎، حيث يمثّل m طول النص، و n طول النمط. السؤال الآن هو: هل هناك خوارزمية أسرع؟ نعم، وهي خوارزمية KMP. تبحث خوارزمية نوث-موريس-برات Knuth-Morris-Pratt أو KMP اختصارًا، عن ظهور نمط معيّن داخل سلسلة نصّية، وتستغل هذه الخوارزمية حقيقة أنّه عند حدوث عدم التطابق فإنّ الكلمة نفسها تقدّم معلومات كافية لتقدير المكان التالي الذي يمكن أن يبدأ التطابق عنده، وبالتالي تجاوز بعض الحروف التي تحقّقنا منها سابقًا. صُمِّمت هذه الخوارزمية سنة 1970 على يد دونالد نوث Donuld Knuth وفوهان برات Vaughan Pratt، وبشكل مستقل على يد جيمس مورِس James H. Morris، وقد نَشر هذا الثلاثي بحثًا مشتركًا عنها سنة 1977. لنوسّع مثالنا السابق كي نفهمه أكثر: +-------+---+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | Index |0 |1 |2 |3 |4 |5 |6 |7 |8 |9 |10|11|12|13|14|15|16|17|18|19|20|21|22| +--------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | Text |a |b |c |x |a |b |c |d |a |b | x | a | b | c |d | a | b | c | d | a | b | c | y | +--------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +----------+---+---+---+---+---+---+---+---+ | Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +----------+---+---+---+---+---+---+---+---+ | Pattern | a | b | c | d | a | b | c | y | +---------+---+---+---+---+---+---+---+---+ في البداية، يتطابق النص والنمط حتى الفهرسَ 2، ثمّ ينهار التطابق عند الفهرس 3، إذ لا تتطابق Text[3]‎‎ وPattern[3]‎‎. ونحن نهدف إلى عدم الرجوع للخلف في النص، إذ لا نريد أن نبدأ المطابقة مرةً أخرى من الموضع الذي بدأنا المطابقة عنده سابقًا في حالة عدم التطابق. ولتحقيق ذلك، نبحث عن لاحقةٍ suffix في الجزء الحالي من النمط قبل انهيار التطابق والتي تكون أيضًا سابقةً في النمط (السلسلة النصية الجزئية abc). على سبيل المثال، بما أنّ جميع الحروف فريدة غير مكرّرة، فلن يكون هناك أيّ سلسلة نصية تكون لاحقة وسابقة في الوقت نفسه في السلسلة النصية الجزئية التي طَابقناها للتو، هذا يعني أنّ الموازنة التالية ستبدأ من الفهرس 0. سنوازن الآن بين كل من Text[3]‎‎ وPattern[0]‎‎، إلا أنّهما لا يتطابقان. بعد ذلك نجد تطابقًا ابتداءً من الفهرس 4 وحتى الفهرس 9 في النص، ومن الفهرس 0 إلى 5 في النمط. غير أنّ التطابق ينهار بعد ذلك في Text[10]‎‎ وPattern[6]‎‎. نأخذ الآن السلسلة النصية الجزئية المُطابِقة من النمط قبل نقطة الانهيار (أي abcdabc)، ونبحث عن لاحقة فيها تكون أيضًا سابقة للنمط. تستطيع ملاحظة أنّ ab هي لاحقة لهذه السلسلة النصية الجزئية وسابقة لها في الوقت نفسه. وبما أنّ المطابقة السابقة استمرت حتى Text[10]‎‎، فإنّ الحرفين اللذين يسبقان موضع عدم التطابق هما ab. هنا نستنتج أنّه لما كانت ab أيضًا سابقة للسلسلة النصية الجزئية التي أخذناها، فلن يكون علينا التحقق من ab مرة أخرى، ويمكن أن يبدأ الفحص التالي من Text[10]‎‎ وPattern[2]‎‎، كذلك ليس علينا أن نعود حيث كنا إذ يمكننا أن نبدأ مباشرة من موضع انهيار التطابق. علينا الآن التحقق من Text[10]‎‎ وPattern[2]‎‎، بما أنّهما غير متطابقتين وليس للجزء المطابَق من النمط أي لاحقة هي نفسها سابقة للنمط، فسيكون علينا أن نتحقق من Text[10]‎‎ وPattern[0]‎‎، ولكنّها لا يتطابقان أيضًا. بعد ذلك نجد تطابقًا ابتداءً من الفهرس 11 وحتى الفهرس 17 في النص، ومن الفهرس 0 إلى 6. لكن ينهار التطابق مجددًا في Text[18]‎‎ وPattern[7]‎‎. مرّة أخرى علينا التحقق من السلسلة النصية الجزئية قبل انهيار التطابق (أي abcdabc) لنجد أنّ abc هي لاحقة وسابقة في الوقت نفسه، لذا ينبغي أن تكون abc قبل Text[18]‎‎ بما أنّ التطابق السابق استمر حتى الموضع Pattern[7]‎‎، وهذا يعني أننا لسنا بحاجة إلى الموازنة حتى الموضع Text[18]‎‎، إذ ستبدأ الموازنة من Text[18]‎‎ وPattern[3]‎‎. سنجد الآن تطابقًا، ونعيد 15، وهو الفهرس الذي يبدأ عنده التطابق. هذه هي طريقة عمل خوارزمية KMP. والآن، كيف نتحقّق مما إذا كانت اللاحقة تساوي السابقة، وعند أي نقطة نبدأ التحقق مما إذا كان هناك عدم تطابق للحروف بين النص والنمط؟ لنلق نظرة على المثال التالي: +-------+----+----+----+----+----+----+----+----+ | Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +-------+----+----+----+----+----+----+----+----+ | Text | a | b | c | d | a | b | c | a | +-------+----+----+----+----+----+----+----+----+ سننشئ مصفوفةً تحتوي المعلومات الضرورية وسنسميها المصفوفة S، حجم هذه المصفوفة يساوي طول النمط، ولا يمكن أن يكون الحرف الأول من النمط لاحقة لأيّ سابقة، لذا نضع S[0] = 0. نأخذ i = 1 و j = 0 في البداية، نوازن في كل خطوة بين ‎Pattern[i‎]‎‎‎ وPattern[j]‎‎ ونزيد قيمة i بواحد، فإذا كان هناك تطابق نضع S[i‎] = j + 1 ونزيد قيمة j، وإن لم يكن فنتحقق من قيمة الموضع السابق لـ j -إن وُجد- ونضع j = S [j-1]‎‎ -إذا كانت j تخالف 0-، ونستمر في هذا إلى أن يحدث عدم تطابق بين S[i‎] [j]‎‎ و S ‎‎، أو تخالف j القيمة 0. في الحالة الأخيرة نضع S[i‎] = 0. نعود إلى المثال السابق: j i +-------+----+----+----+----+----+----+----+----+ | Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +-------+----+----+----+----+----+----+----+----+ | Text | a | b | c | d | a | b | c | a | +-------+----+----+----+----+----+----+----+----+ لا يتطابق ‎Pattern[i‎]‎ وPattern[j]‎‎، لذا نزيد قيمة i بواحد، وبما أنّ j تساوي 0، فلن نتحقق من القيمة السابقة، وسنضع ‎Pattern[i‎] = 0‎. سنحصل على تطابق عند الموضع i = 4 إذا واصلنا زيادة i، لذلك نضع [i‎] = S[4] = j + 1 = 0 + 1 = 1 ثمّ نزيد قيمتي j و i. ستبدو المصفوفة هكذا: j i +-------+----+----+----+----+----+----+----+----+ | Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +-------+----+----+----+----+----+----+----+----+ | Text | a | b | c | d | a | b | c | a | +-------+----+----+----+----+----+----+----+----+ | S | 0 | 0 | 0 | 0 | 1 | | | | +-------+----+----+----+----+----+----+----+----+ بما أن ‎Pattern[5]‎ وPattern[1]‎‎ متطابقتين، فإننا نضع S[i‎] = S [5] = j + 1 = 1 + 1 = 2. إذا تابعنا سنجد عدم تطابق في الموضعين j = 3 و i = 7. وبما أنّ j لا تساوي 0، فإننا نضع j = S [j-1]‎‎ ونوازن الحرفين الموجودين عند الموضعين i و j، ولأنها متماثلان فإننا نضع S[i‎] = j + 1. ستبدو المصفوفة النهائية كما يلي: +---------+---+---+---+---+---+---+---+---+ | S | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 1 | +---------+---+---+---+---+---+---+---+---+ هذه هي المصفوفة المطلوبة. إن كانت S[i‎]‎‎ تخالف الصفر فذلك يعني أنّ هناك سلسلة نصية طولها S[i‎]‎‎ هي لاحقة وسابقة في الوقت نفسه للسلسلة النصية الجزئية من النمط التي تبدأ من 0 وتنتهي عند i. نبدأ الموازنة التالية من الموضع S[i‎] + 1 في النمط. هذه شيفرة عامة لخوارزمية توليد المصفوفة: Procedure GenerateSuffixArray(Pattern): i := 1 j := 0 n := Pattern.length while i is less than n if Pattern[i] is equal to Pattern[j] S[i] := j + 1 j := j + 1 i := i + 1 else if j is not equal to 0 j := S[j-1] else S[i] := 0 i := i + 1 end if end if end while التعقيد الزمني لعملية بناء المصفوفة هو ‎O(n)‎، والتعقيد المساحي يساوي أيضًا ‎O(n)‎. للتأكد من أنك قد فهمت الخوارزمية، حاول إنشاء مصفوفة للنمط ‎aabaabaa‎، وتحقق مما إذا كانت النتيجة التي حصلت عليها تتطابق مع هذه النتيجة: +---------+---+---+---+---+---+---+---+---+---+ | S | 0 | 1 | 0 | 1 | 2 | 3 | 4 | 5 | 2 | +---------+---+---+---+---+---+---+---+---+---+ انظر إلى المثال التالي: +---------+---+---+---+---+---+---+---+---+---+---+---+---+ | Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 | +---------+---+---+---+---+---+---+---+---+---+---+---+---+ | Text | a | b | x | a | b | c | a | b | c | a | b | y | +---------+---+---+---+---+---+---+---+---+---+---+---+---+ +---------+---+---+---+---+---+----+ | Index | 0 | 1 | 2 | 3 | 4 | 5 | +---------+---+---+---+---+---+----+ | Pattern | a | b | c | a | b | y | +---------+---+---+---+---+---+----+ | S | 0 | 0 | 0 | 1 | 2 | 0 | +---------+---+---+---+---+---+----+ لدينا سلسلة نصية ونمط ومصفوفة S محسوبة قبليًا باستخدام الطريقة التي شرحناها سابقًا. سوف نوازن ‎Text[0]‎ وPattern[0]‎‎ وهما متطابقان، كما أنّ ‎Text[1]‎ وPattern[1]‎‎ متطابقان كذلك؛ أمّا ‎Text[2]‎ وPattern[2]‎‎، فهما غير متطابقين. نتحقق من القيمة الموجودة عند الموضع الذي يسبق موضع عدم التطابق. ولمّا كانت ‎‎S [1] ‎‎ تساوي 0، فذلك يعني أنّه لا توجد لاحقة تطابق سابقة في السلسلة النصية الجزئية الحالية، لذا تبدأ الموازنة عند الموضع S [1]‎‎، وهو 0. وهكذا نجد أنّ ‎Text[2]‎ وPattern[0]‎‎ غير متطابقتين، لذا نستمر لنجد أنّ ‎Text[3]‎ وPattern[0]‎‎ متطابقتان، ويستمر التطابق حتى ‎Text[8]‎ وPattern[5]‎‎. نعود الآن خطوةً واحدةً إلى الوراء في المصفوفة S لنجد القيمة 2، هذا يعني أنّ هناك سابقة طولها 2 وهي أيضًا لاحقة في السلسلة النصية الجزئية الحالية abcab، وهي ab. هذا يعني أنّ السلسلة النصية ab موجودة قبل ‎Text[8]‎. وعلى ذلك نستطيع تجاهلPattern[1]‎‎ وPattern[0]‎‎ وبدء الموازنة التالية من الموضعين ‎Text[8]‎ وPattern[2]‎‎. إذا تابعنا بذلك النسق فسنجد النمط داخل النص، وستبدو الشيفرة لتلك الإجراءات كما يلي: Procedure KMP(Text, Pattern) GenerateSuffixArray(Pattern) m := Text.Length n := Pattern.Length i := 0 j := 0 while i is less than m if Pattern[j] is equal to Text[i] j := j + 1 i := i + 1 if j is equal to n Return (j-i) else if i < m and Pattern[j] is not equal t Text[i] if j is not equal to 0 j = S[j-1] else i := i + 1 end if end if end while Return -1 التعقيد الزمني لهذه الخوارزمية -بصرف النظر عن العمليات الحسابية المتعلقة بحساب مصفوفة اللاحقات- هو ‎O(m)‎، وبما أنّ دالة GenerateSuffixArray تستغرق وقتًا قدره O ‎(n)‎، فإن التعقيد الزمني الإجمالي لخوارزمية KMP هو: O ‎(m+n)‎. خوارزمية رابين كارب Rabin-Karp خوارزمية رابين-كارب‏ Rabin-Karp هي خوارزمية بحث في السلاسل النصية أُنشِئت على يد ريتشارد كارب Richard M. Karp ومايكل رابِن Michael Rabin، وتستخدم هذه الخوارزمية دالة التعمية hashing للتحقق من وجود مجموعة من الأنماط في نص ما. ونقول أنّ s سلسلة نصية جزئية من S إذا كانت s مُتضمّنة في S. على سبيل المثال، ver هي سلسلة نصية جزئية من stackoverflow. لكن لا ينبغي الخلط بين هذا المفهوم، وبين مفهوم التتابع الجزئئ subsequence الذي لا يشترط أن تكون حروفه متلاصقةً في النص، فمثلًا، تُعَد cover تتابعًا جزئيًا في stackoverflow، لكنها ليست سلسلةً نصيةً جزئيةً منها. في خوارزمية Rabin-Karp، سنحسب تجزئة النمط الذي نبحث عنه، ثمّ نتحقق مما إذا كانت هناك تعمية تداولية rolling hash في النص تتوافق مع النمط أم لا، وإذا لم تتطابق فذلك يضمن لنا أنّ النمط غير موجود في النص، أما في حال كان هناك تطابق فربّما يكون النمط موجودًا في النص. دعنا نلقي نظرةً على المثال التالي: لنفترض أنّ النص يساوي yeminsajid، وأننا نريد أن نعرف إن كان هذا النص يحتوي النمط nsa. لكي نحسب التجزئة والتجزئة التداولية سنحتاج إلى استخدام عدد أولي، ويمكنك هنا اختيار أيّ عدد أولي وليكن العدد Prime = 11. نحسب قيمة التجزئة باستخدام هذه الصيغة: (1st letter) X (prime) + (2nd letter) X (prime)¹ + (3rd letter) X (prime)² X + ...... لكل حرف رقم يميزه في قائمة الحروف الهجائية: a -> 1 g -> 7 m -> 13 s -> 19 y -> 25 b -> 2 h -> 8 n -> 14 t -> 20 z -> 26 c -> 3 i -> 9 o -> 15 u -> 21 d -> 4 j -> 10 p -> 16 v -> 22 e -> 5 k -> 11 q -> 17 w -> 23 f -> 6 l -> 12 r -> 18 x -> 24 قيمة تجزئة nsa هي: 14 X 11⁰ + 19 X 11¹ + 1 X 11² = 344 الآن نحسب التجزئة التداولية للنص، أي نحسب تجزئة كل سلسلة نصية جزئية مؤلفة من 3 حروف في النص، فإذا تطابقت التجزئة التداولية مع قيمة تجزئة النمط، نتحقق مما إذا كانت السلسلتان النصيّتان متطابقتان أم لا، وبما أنّ النمط يحتوي على 3 أحرف، فإننا نأخذ أوّل ثلاثة أحرف من النص yem ونحسب قيمة تجزئتها، فنحصل على: 25 X 11⁰ + 5 X 11¹ + 13 X 11² = 1653 لا تتطابق هذه القيمة مع قيمة تجزئة النمط، لذا فإنّ السلسلة النصية غير موجودة هنا، وسننتقل الآن إلى الخطوة التالية ونحسب قيمة التجزئة الخاصة بالسلسلة النصية emi التالية باستخدام الصيغة أعلاه. مشكلة هذا المنظور هي أنّ حساب التجزئة في كل مرة مكلف جدًا، لكن هناك تقنية يمكن أن تسرّع العملية برمّتها، وخطواتها كالآتي: نطرح قيمة الحرف الأول من السلسلة النصية السابقة (التي حسبنا تجزئتها قبيل قليل) من قيمة التجزئة الحالية، والتي هي فـي هـذه الحالـة y. وسنحصل على ‎1653 - 25 = 1628‎. نقسّم الناتج على العدد الأولي 11 الذي اخترناه سابقًا، وسنحصل على ‎1628 / 11 = 148‎. نضيف ناتج العملية (ترتيب الحرف الجديد) X‏ 11m-y، حيث يمثّل m طول النمط، أما ترتيب الحرف الجديد فيساوي ترتيب i، وهو 9. نحصل على 112 = 1237‎ قيمة التجزئة الجديدة لا تساوي قيمة التجزئة الخاصة بالنمط، لذا نستمر بالبحث على المنوال نفسه، وبالنسبة للحرف n نحصل على: Previous String: emi // السلسلة النصية السابقة First Letter of Previous String: e(5) // الحرف الأول من السلسلة النصية السابقة New Letter: n(14) // الحرف الجديد New String: "min" // السلسلة النصية الجديدة 1237 - 5 = 1232 1232 / 11 = 112 112 + 14 X 11² = 1806 ليس هناك تطابق أيضًا، لذا سنأخذ الآن الحرف s: Previous String: min // السلسلة النصية السابقة First Letter of Previous String: m(13) // الحرف الأول من السلسلة النصية السابقة New Letter: s(19) // الحرف الجديد New String: "ins" // السلسلة النصية الجديدة 1806 - 13 = 1793 1793 / 11 = 163 163 + 19 X 11² = 2462 ليس هناك تطابق أيضًا. بعد ذلك، نأخذ a فنحصل على: Previous String: ins // السلسلة النصية السابقة First Letter of Previous String: i(9) // الحرف الأول من السلسلة النصية السابقة New Letter: a(1) // الحرف الجديد New String: "nsa" // السلسلة النصية الجديدة 2462 - 9 = 2453 2453 / 11 = 223 223 + 1 X 11² = 344 هنا حصل تطابق، لذا سنوازن الآن النمط بالسلسلة النصية الحالية، وبما أنّ كلا السلسلتين متطابقتين نستنج أنّ السلسلة النصية الجزئية موجودة في النص، ونعيد موضع بداية السلسلة النصية الجزئية. انظر المثال التوضيحي التالي: حساب التجزئة: Procedure Calculate-Hash(String, Prime, x): hash := 0 // ‫هنا، تمثل x الطول for m from 1 to x // البحث عن قيمة التجزئة hash := hash + (Value of String[m])⁻¹ end for Return hash إعادة حساب التجزئة، ستمثل Curr هنا الحرف الأول من السلسلة النصية السابقة: Procedure Recalculate-Hash(String, Curr, Prime, Hash): Hash := Hash - Value of String[Curr] Hash := Hash / Prime m := String.length New := Curr + m - 1 Hash := Hash + (Value of String[New])⁻¹ Return Hash مطابقة السلسلة النصية: Procedure String-Match(Text, Pattern, m): for i from m to Pattern-length + m - 1 if Text[i] is not equal to Pattern[i] Return false end if end for Return true منظور رابين كارب: Procedure Rabin-Karp(Text, Pattern, Prime): m := Pattern.Length HashValue := Calculate-Hash(Pattern, Prime, m) CurrValue := Calculate-Hash(Pattern, Prime, m) for i from 1 to Text.length - m if HashValue == CurrValue and String-Match(Text, Pattern, i) is true Return i end if CurrValue := Recalculate-Hash(String, i+1, Prime, CurrValue) end for Return -1 ستعيد الخوارزمية -1 إذا لم تعثر على أي تطابق. تُستخدم هذه الخوارزمية للكشف عن الانتحال، إذ نعطيها مادةً نصيةً مصدريةً، فتبحث بسرعة في مقالة عما إذا كانت المقالة تحتوي جملًا أو عبارات من المادة المصدرية متجاهلةً تفاصيلًا معينةً من قبيل علامات الترقيم وحالة الحروف (أو تشكيلها). عادةً ما تكون هناك الكثير من السلاسل النصية (الجمل) التي ينبغي أن نبحث عنها، لذا فإنّ خوارزميات البحث التقليدية التي تبحث عن سلسلة نصية واحدة مثل خوارزمية Knuth- Morris-Pratt أو خوارزمية Boyer-Moore String، لن تكون عمليّةً هنا، لهذا يُفضّل استخدام خوارزمية Rabin-Karp. لكن من المهم أن تنتبه إلى أنّ خوارزمية Knuth- Morris-Pratt وخوارزمية Boyer-Moore String أسرع من خوارزمية Rabin-Karp. هناك تطبيقات أخرى كثيرة لخوارزمية Rabin-Karp، حيث يمكنك استخدامها مثلًا للعثور على السلاسل النصية الرقمية من طول معين في النص وموجودة بعدد ما وليكن k، وذلك عبر إجراء بعض التعديلات البسيطة على الخوارزمية. بالنسبة لنص طوله n، و p نمط مجموع أطوالها يساوي m، فإنّ حالتي التعقيد المتوسطة والفضلى تساويانO (n + m) ‎‎، كما تحتاج الخوازمية إلى مساحة O (p)‎‎؛ أما حالة التعقيد الأسوأ فتُساوي O (nm)‎‎. هذا تطبيق بلغة بايثون على خوارزمية KMP: Haystack: السلسلة النصية التي سنبحث فيها. Needle: النمط المراد البحث عنه. التعقيد الزمني: التعقيد الزمني للجزء الذي يُجري عمليات البحث (التابع strstr) هو O (n)‎‎، حيث يمثّل ‎n‎ طول الوسيط haystack، ولكن لمّا كان الوسيط needle يُحلَّل قبليا لأجل بناء جدول السوابق prefix table، فسنحتاج مدة O (m)‎‎، وهي المدة المطلوب لبناء الجدول، حيث يمثل ‎m‎ طول needle، وهكذا فإنّ التعقيد الزمني الكلي لخوارزمية KMP يساوي O (n + m)‎‎. تعقيد المساحة: تحتاج الخوارزمية إلى مساحة بحجم O (m)‎‎ لأجل بناء جدول السوابق. def get_prefix_table(needle): prefix_set = set() n = len(needle) prefix_table = [0]*n delimeter = 1 while(delimeter<n): prefix_set.add(needle[:delimeter]) j = 1 while(j<delimeter+1): if needle[j:delimeter+1] in prefix_set: prefix_table[delimeter] = delimeter - j + 1 break j += 1 delimeter += 1 return prefix_table def strstr(haystack, needle): # ‫يمثل ‫m الموضع في S حيث يبدأ التطابق القادم لـ W # ‫يمثل i فهرس المحرف الحالي W haystack_len = len(haystack) needle_len = len(needle) if (needle_len > haystack_len) or (not haystack_len) or (not needle_len): return -1 prefix_table = get_prefix_table(needle) m = i = 0 while((i<needle_len) and (m<haystack_len)): if haystack[m] == needle[i]: i += 1 m += 1 else: if i != 0: i = prefix_table[i-1] else: m += 1 if i==needle_len and haystack[m-1] == needle[i-1]: return m - needle_len else: return -1 if __name__ == '__main__': needle = 'abcaby' haystack = 'abxabcabcaby' print strstr(haystack, needle) هذا تطبيق على خوارزمية KMP في لغة C، الوسيط txt يمثل النص، فيما يمثل الوسيط pat النمط، يطبع هذا البرنامج كل ظهور للنمط pat في النص txt. مثلا، إن كان الدخل يساوي: txt[] = "THIS IS A TEST TEXT" pat[] = "TEST" فسيطبع البرنامج الخرج التالي: Pattern found at index 10 بالنسبة للدخل: txt[] = "AABAACAADAABAAABAA" pat[] = "AABA" يطبع البرنامج الخرج التالي: Pattern found at index 0 Pattern found at index 9 Pattern found at index 13 هذا هو المثال التطبيقي على استخدام خورازمية KMP في لغة C: #include<stdio.h> #include<string.h> #include<stdlib.h> void computeLPSArray(char *pat, int M, int *lps); void KMPSearch(char *pat, char *txt) { int M = strlen(pat); int N = strlen(txt); هنا ستنشئ []lps لتخزين أطول قيم سابقة لاحقة للنمط، نتابع … int *lps = (int *)malloc(sizeof(int)*M); int j = 0; // pat[] فهرس لـ الحساب القبلي للنمط، أي حساب مصفوفة []lps، نتابع … computeLPSArray(pat, M, lps); int i = 0; // txt[] فهرس while (i < N) { if (pat[j] == txt[i]) { j++; i++; } if (j == M) { printf("Found pattern at index %d \n", i-j); j = lps[j-1]; } // تطابق j العثور على عدم تطابق بعد else if (i < N && pat[j] != txt[i]) { // lps[0..lps[j-1]] لا تحاول التحقق من مطابقة الحروف // لأننا نعلم أنها مطابقة if (j != 0) j = lps[j-1]; else i = i+1; } } free(lps); // لتجنب تسرب الذاكرة } void computeLPSArray(char *pat, int M, int *lps) { int len = 0; // حجم أطول سابقة-لاحقة حالية int i; lps[0] = 0; // يساوي 0 دائما lps[0] i = 1; while (i < M) { if (pat[i] == pat[len]) { len++; lps[i] = len; i++; } else // (pat[i] != pat[len]) { if (len != 0) { // هنا فخ، انظر المثال التالي // AAACAAAA و i = 7. len = lps[len-1]; // هنا i لاحظ أيضا أننا لم نزد قيمة } else // if (len == 0) { lps[i] = 0; i++; } } } } الخرج الناتج هو الآتي: Found pattern at index 10 ترجمة -بتصرّف- للفصل 40 من الكتاب Algorithms Notes for Professionals. اقرأ أيضًا خوارزميات الترتيب وأشهرها أمثلة عن أنواع الخوارزميات خوارزميات تحليل المسارات في الأشجار خوارزمية ديكسترا Dijkstra’s Algorithm
  12. سوف نستعرض في هذه المقالة خوارِزميتان من خوارزميات البحث الشهيرة، وهما خوارزمية البحث الثنائي، وخوارزمية البحث الخطي، مع تحليلهما وشرح آلية عملهما. البحث الثنائي Binary Search خوارزمية البحث الثنائي هي خوارزمية بحث في المصفوفات المرتبة تعتمد منظور فرق تسد، وتحتاج هذه الخوارزمية مدة قدرها ‎O(log n)‎ للعثور على موقع العنصر المبحوث عنه في المصفوفة المرتبة، حيث يمثّل ‎n‎ مساحة المَبحث، أي المصفوفة التي نبحث فيها. تبدأ خوارزمية البحث الثنائي في كل خطوة بالتحقق من القيمة الموجودة في منتصف المصفوفة mid، فإن كانت تساوي العنصر المبحوث عنه فإنها تتوقف وتعيد فهرسه؛ وإلا تقسّم المصفوفة إلى اثنتين هما مصفوفة [mid,b] ومصفوفة [a,mid]، حيث يمثل a و b طرفي المصفوفة. وإن كان العنصر المبحوث عنه موجودا في المصفوفة وكان أكبر من mid، فهذا يعني أنّه سيكون لا محالة في الجزء [mid,b] لأنّ المصفوفة مرتبة؛ أما إذا كان العنصر المبحوث عنه أصغر من mid فهذا يعني أنّه سيكون في الجزء [a,mid]. نختار الآن الجزء المناسب، ونعيد تطبيق الخوارزمية عليه، ونستمر في هذه العملية إلى أن نجد العنصر المبحوث عنه أو تتساوى قيمة mid مع قيمة a، فإن لم تكن قيمة mid تساوي العنصر المبحوث، فهذا يعني أنّه غير موجود في المصفوفة. إليك مثالًا توضيحيًا؛ لنفترض أنّك خبير اقتصادي وقد تمّ تكليفك بمهمة تحديد سعر التوازن equilibrium price (أي السعر الذي يحقق المعادلة العرض = الطلب) لسلعة الأرز. تذكّر أنه كلما زاد السعر، زاد العرض وقلّ الطلب. لنفترضّ أنّ الشركة التي تعمل فيها توفر لك جميع المعطيات المتعلقة بالعرض والطلب المتعلقين بالأرز عندما يستقر سعر الأرز عند قيمة معيّنة ‎p‎، فعندئذ تستطيع الحصول على العرض والطلب بوحدات الأرز. ويريد رئيسك أن تحسب سعر التوازن في أسرع وقت ممكن، لكن سينبّهك إلى أنّ سعر التوازن يجب أن يكون عددًا صحيحًا موجبًا لا يتجاوز ‎10^17‎، كما انّه يضمن لك أنّه سيكون هناك حل عددي صحيح موجب واحد بالضبط في هذا النطاق. يُسمح لك باستدعاء الدالتين ‎getSupply(k)‎ و ‎getDemand(k)‎، وَاللتان تعيدان كمية العرض وحجم الطلب بالنسبة للسعر K على التوالي. المبحث أو مساحة البحث هنا هي مصفوفة الأعداد الصحيحة من ‎1‎ إلى ‎10^17‎. ويكون البحث الخطي في هذه الحالة غير مناسب لأنّ المصفوفة مرتّبة. لاحظ أنه كلما ارتفعت قيمة ‎k‎، ستزيد قيمة ‎getSupply(k)‎، وتنخفض قيمة ‎getDemand(k)‎، ومن ثم تكون لدينا المتراجحة التالية: أي أن زيادة السعر يصاحبها زيادة في الفرق بين العرض والطلب، وهذا منطقي ومتوقع. أيضًا، بما أنّ مساحة المبحث (المصفوفة) مرتّبة، فيمكننا استخدام خوارزمية البحث الثنائي. المثال التوضيحي التالي يبين كيفية استخدام خوارزمية البحث الثنائي: high = 100000000000000000 <- الحد الأقصى لمساحة المبحث low = 1 <- الحد الأدنى لمساحة المبحث while high - low > 1 mid = (high + low) / 2 <- خذ القيمة الوسطى supply = getSupply(mid) demand = getDemand(mid) if supply > demand high = mid <- الحل موجود في النصف السفلي من مساحة المبحث else if demand > supply low = mid <- الحل موجود في النصف العلوي من مساحة المبحث else <- الشرط: العرض == الطلب return mid <- العثور على الحل تستغرق هذه الخوارزمية وقتًا مقداره ‎~O(log 10^17)‎. وعمومًا، يساوي تعقيد خوارزمية الترتيب الثنائي ‎~O(log S)‎، حيث يمثّل S حجم مصفوفة البحث، لأنّنا نقلّص مساحة البحث إلى النصف في كل تكرار للحلقة while، (من [low: high] إلى [low: mid] أو [mid: high] ). فيما يلي تطبيق على خوارزمية البحث الثنائي يستخدم التكرارية، في لغة 😄 int binsearch(int a[], int x, int low, int high) { int mid; if (low > high) return -1; mid = (low + high) / 2; if (x == a[mid]) { return (mid); } else if (x < a[mid]) { binsearch(a, x, low, mid - 1); } else { binsearch(a, x, mid + 1, high); } } خوارزمية رابِن كارب Rabin Karp خوارزمية رابِن كارب هي خوارزمية بحث نصية تستخدم دالة تعمية hashing للعثور على نمط معيّن من مجموعة من الأنماط النصية في سلسلة نصية، ومتوسط أوقات تشغيلها وأفضلها يساوي المدة O(n + m)‎‎ في المساحة (O(p، حيث يمثّل n طول السلسلة النصية، فيما يمثّل m طول النمط. بالمقابل، فأسوأ أوقات تشغيلها يساوي O(nm)‎‎. هذا تطبيق لخوارزمية مطابقة السلاسل النصية في لغة جافا، حيث يمثل q عددًا أوليًا، و p يمثل قيمة التجزئة الخاصة بالنمط، و t قيمة التجزئة الخاصة بالنص، و d يمثل عدد الحروف الأبجدية. void RabinfindPattern(String text,String pattern){ int d=128; int q=100; int n=text.length(); int m=pattern.length(); int t=0,p=0; int h=1; int i,j; //دالة حساب قيمة التجزئة for (i=0;i<m-1;i++) h = (h*d)%q; for (i=0;i<m;i++){ p = (d*p + pattern.charAt(i))%q; t = (d*t + text.charAt(i))%q; } // البحث عن النمط for(i=0;i<end-m;i++){ if(p==t){ // إن تطابقت قيمة التجزئة، فقارن النمط حرفا حرفا for(j=0;j<m;j++) if(text.charAt(j+i)!=pattern.charAt(j)) break; if(j==m && i>=start) System.out.println("Pattern match found at index "+i); } if(i<end-m){ t =(d*(t - text.charAt(i)*h) + text.charAt(i+m))%q; if(t<0) t=t+q; } } } نقسّم قيمة التجزئة على عدد أولي لتجنب وقوع أيّ تداخل، إذ أنّ القسمة على الأعداد الأولية تقلّل احتمال حدوث تداخل، ولكنّها لا تعدمه، فلا يزال احتمال أن يكون لسلسلتين نصيتين مختلفتين قيمة التجزئة نفسها قائمًا. ولأجل هذا، يجب أن تتحقق من النمط حرفًا حرفًا عند العثور على تطابق، من أجل التأكد أنك حصلت على تطابق تام. تعيد المعادلة التالية حساب قيمة التجزئة الخاصة بالنمط، أولًا عن طريق إزالة الحرف الموجود في أقصى اليسار، ثم إضافة الحرف الجديد من النص. t =(d*(t - text.charAt(i)*h) + text.charAt(i+m))%q; تحليل الحالات التعقيدية لخوارزمية البحث الخطي كل خوارزمية لها ثلاث حالات تعقيد: أسوأ حالة. الحالة المتوسطة. أفضل حالة. #include <stdio.h> // ‫البحث عن x في arr[]‎‎، في حال العثور على x، تعيد الدالة الفهرس // وإلا أعِد -1 int search(int arr[], int n, int x) { int i; for (i=0; i<n; i++) { if (arr[i] == x) return i; } return -1; } برنامج مشغِّل لاختبار الدوال أعلاه: int main() { int arr[] = {1, 10, 30, 15}; int x = 30; int n = sizeof(arr)/sizeof(arr[0]); printf("%d is present at index %d", x, search(arr, n, x)); getchar(); return 0; تحليل أسوأ حالة عند تحليل الحالة الأسوأ نحسب الحد الأعلى لوقت تشغيل الخوارزمية، ويجب أن نعرف الحالة التي تجعل الخوارزمية تنفّذ أقصى عدد من العمليات. وبالنسبة لخوارزمية البحث الخطي، تحدث أسوأ حالة عندما لا يكون العنصر المبحوث عنه (x) موجودًا في المصفوفة، فحينئذ سوف تقارنه دالة البحث search()‎‎ مع جميع عناصر المصفوفة المبحوث فيها واحدًا تلو الآخر. وعليه، فإنّ أسوأ حالة تعقيدية في خوارزمية البحث الخطي تساوي ‎Θ‎(n)‎‎. تحليل الحالة التعقيدية المتوسطة عند تحليل الحالة التعقيدية المتوسطة، نأخذ جميع المدخلات الممكنة ونحسب أوقات تنفيذها بالنسبة لجميع المدخلات، ثمّ نجمع كل القيم المحسوبة ونقسّم المجموع على إجمالي عدد المدخلات. سيكون علينا أن نقدّر توزيع الحالات الممكنة. لنفترض أنّ جميع الحالات مُوزّعة توزيعًا منتظمًا uniformly distributed -بما في ذلك الحالات التي لا يكون فيها x ضمن المصفوفة-، ثم نجمع جميع تلك الحالات ونقسّم المجموع على (n +1). هذه صيغة الحالة التعقيدية المتوسطة: تحليل أفضل حالة عند تحليل أفضل حالة تعقيدية، نحسب الحد الأدنى لأوقات تشغيل الخوارزمية، ويجب أن نعرف الحالة التي تجعل الخوارزمية تنفّذ أقل عدد من العمليات. تحدث أفضل حالة في مشكلة البحث الخطي عندما يكون x في الموضع الأول، ففي هذه الحالة، سيكون عدد العمليات المُنفّذة ثابتًا (مهما كانت قيمة n)، لذا فإنّ التعقيد الزمني لأفضل حالة يساوي Θ(1)‎‎. عادةً ما نحلل أداء الخوارزمية بناءً على الحالة الأسوأ، إذ أنّ الحالة الأسوأ تعطينا فكرةً عن أقصى وقت يمكن أن تستغرقه الخوارزمية، وهذه معلومة حيوية؛ بالمقابل، لا يكون حساب الحالة المتوسطة سهلًا في معظم الحالات، وعليه فنادرًا ما نلجأ إليه، وذلك لأنه من أجل إجراء تحليل الحالة المتوسطة سيكون علينا أن نعرف التوزيع الرياضي لجميع المدخلات الممكنة، وذلك من الصعوبة بمكان. بالمقابل، لا يُعَد تحليل أفضل حالة مجديًا، إذ لن نستفيد شيئًا من معرفة الحد الأدنى الذي تستغرقه الخوارزمية، لأنه في الحالة الأسوأ، قد تستغرق الخوارزمية سنوات. في بعض الخوارزميات، تكون جميع الحالات متماثلة تقريبًا، إذ لا توجد حالة أسوأ وحالة فضلى، نجد هذا على سبيل المثال في خوارزمية الترتيب بالدمج، إذ ينفّذ الترتيب بالدمج عدد ‎Θ‎(nLogn)‎ عملية في جميع الحالات. تتمايز الحالة الأسوأ عن الفضلى في معظم خوارزميات الترتيب الأخرى، ففي التطبيق التقليدي مثلًا لخوارزمية الترتيب السريع -حيث يُختار المحور في الركن-، تحدث أسوأ حالة عندما تكون المصفوفة المراد ترتيبها مرتّبة سلفا، فيما تحدث أفضل حالة عندما يقسّم المحور المصفوفة إلى نصفين دائمًا؛ أما بالنسبة لخوارِزمية الترتيب بالإدراج، تحدث الحالة الأسوأ عندما تكون المصفوفة المراد ترتيبها مرتبةً ترتيبًا معكوسًا، فيما تحدث أفضل حالة عند تكون المصفوفة مُرتبةً في الاتجاه المراد. تطبيق خوارزمية البحث الثنائي على أعداد مرتبة هذا مثال توضيحي لتطبيق خوارزمية البحث الثنائي على الأعداد: int array[1000] = { sorted list of numbers }; int N = 100; // عدد المدخلات في مساحة المبحث int high, low, mid; // قيم مؤقتة int x; // القيمة المبحوث عنها low = 0; high = N -1; while(low < high) { mid = (low + high)/2; if(array[mid] < x) low = mid + 1; else high = mid; } if(array[low] == x) // low العثور على العنصر عند الفهرس else // لم نعثر على العنصر لا تحاول العودة مبكرًا بموازنة العنصر الأوسط array[mid]‎‎ بـ x، فذلك سيضيف عملية موازنة زائدة تبطئ الخوارزمية. لاحظ أن عليك إضافة 1 إلى قيمة low لتجنب أن يُقرّب ناتج القسمة الصحيحة إلى أسفل rounding down. يتيح الإصدار أعلاه من البحث الثنائي إمكانية إيجاد أصغر فهرس لظهور x في المصفوفة في حال كانت تحتوي عدة نسخ من x، ويمكن تعديل الخوارزمية لجعلها تعيد أكبر فهرس كما تبيّن الشيفرة التالية: while(low < high) { mid = low + ((high - low) / 2); if(array[mid] < x || (array[mid] == x && array[mid + 1] == x)) low = mid + 1; else high = mid; } لاحظ أنه بدلًا من إجراء العملية التالية: ‎mid = (low + high) / 2‎ فقد يكون الأفضل إجراء العملية الآتية: ‎mid = low + ((high - low) / 2)‎ في التطبيقات التي تستهدف لغات مثل Java لتقليل خطر حصول طفح overflow في حال كانت المدخلات كبيرة الحجم. البحث الخطي linear search البحث الخطي هي خوارزمية بسيطة، فهي تمرّ على جميع عناصر المصفوفة حتى تعثر على العنصر المبحوث عنه. تعقيد هذه الخوارزمية يساوي O(n)‎‎، حيث يمثّل n عدد العناصر. قد تسأل لماذا O(n)‎‎؟ لأنّه في أسوأ حالة، سيكون عليك المرور على جميع عناصر المصفوفة، أي على n عنصر. يمكن موازنة منظور خوارزمية البحث الخطي بعملية البحث عن كتاب في رفّ من الكتب، إذ سيكون عليك الاطلاع عليها جميعًا حتى تجد الكتاب الذي تريده. هذا تطبيق لخوارزمية البحث الخطي بلغة بايثون: def linear_search(searchable_list, query): for x in searchable_list: if query == x: return True return False linear_search(['apple', 'banana', 'carrot', 'fig', 'garlic'], 'fig') #returns True ترجمة -بتصرّف- للفصل 39 من الكتاب Algorithms Notes for Professionals. اقرأ أيضًا أمثلة عن أنواع الخوارزميات خوارزمية ديكسترا Dijkstra’s Algorithm خوارزميات تحليل المسارات في الأشجار الخوارزميات الشرهة Greedy Algorithms
  13. سنتحدث في هذه المقالة عن بعض المفاهيم العامة المتعلقة بخوارزميات الترتيب، ثم نستعرض 10 من أشهر خوارزميات ترتيب المصفوفات. قبل أن نواصل، سنعطي بعض التعاريف العامة. خوارزميات الترتيب المستقرة: تكون خوارزمية ترتيب ما مستقرةً stable إذا كانت تحافظ على الترتيب النسبي للعناصر التي لها نفس قيمة المفتاح الذي يُجرى الترتيب وفقًا له. خوارزميات الترتيب الموضعية In place algorithms: نقول أنّ خوارزمية ترتيب ما هي خوارزمية موضعية In place إذا كانت الذاكرة الإضافية التي تستخدمها أثناء الترتيب تساوي ‎O(1)‎ (دون احتساب المصفوفة المراد ترتيبها). أسوء حالة تعقيدية Worst case complexity: نقول أنّ أسوء حالة تعقيدية Worst case complexity لخوارزمية تساوي ‎O(T(n))‎ إذا كان وقت تشغيلها لا يزيد عن ‎T(n)‎ مهما كانت المدخلات. أفضل حالة تعقيدية Best case complexity: نقول أنّ أفضل حالة تعقيدية Best case complexity لخوارزمية ما تساوي ‎O(T(n))‎ إذا كان وقت تشغيلها لا يقل عن ‎T(n)‎ مهما كانت المدخلات. أوسط حالة تعقيدية Average case complexity: نقول أنّ أوسط حالة تعقيدية Average case complexity لخوارزمية ترتيب ما تساوي ‎O(T(n))‎ إذا كان متوسط أوقات تشغيلها بالنسبة لجميع المدخلات الممكنة يساوي ‎T(n)‎. استقرار الترتيب Stability in Sorting تكون خوارزمية الترتيب مستقرةً إذا كانت تحافظ على الترتيب النسبي للقيم ذات المفاتيح المتساوية في المصفوفة المُدخلة الأصلية بعد ترتيبها. أي إن كانت خوارزمية الترتيب مستقرةً وكان هناك كائنَان لهما مفاتيح متساوية، فسيكون ترتيبُهما في الخرج المُرتّب مماثلًا لتَرتيبهما في المصفوفة المدخلة غير المرتبة. انظر مصفوفة الأزواج التالية: (1, 2) (9, 7) (3, 4) (8, 6) (9, 3) سنحاول ترتيب المصفوفة حسب العنصر الأول في كل زوج. سيخرج الترتيب المستقر لهذه القائمة ما يلي: (1, 2) (3, 4) (8, 6) (9, 7) (9, 3) وهو ترتيبٌ مستقر لأنّ ‎(9, 3)‎ تظهر بعد ‎(9, 7)‎ في المصفوفة الأصلية أيضًا. أما الترتيب غير مستقر فيكون كما يلي: (1, 2) (3, 4) (8, 6) (9, 3) (9, 7) قد ينتج عن الترتيب غير المستقر الخرج نفسه الذي يٌنتجه الترتيب المستقر أحيانًا، لكن ليس دائمًا. هذه بعض أشهر أنواع التراتيب المستقرة: الترتيب بالدمج Merge sort. الترتيب بالإدراج Insertion sort. ترتيب بالجذر Radix sort . ترتيب Tim. الترتيب بالفقاعات Bubble Sort. هذه بعض أنواع التراتيب غير المستقرة: الترتيب بالكومة Heap sort. الترتيب السريع Quick sort. سوف نستعرض في بقية هذه المقالة 10 خوارزميات ترتيب شهيرة. الترتيب بالفقاعات Bubble Sort table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المعامِل الوصف ترتيب مستقر نعم ترتيب موضعي نعم أفضل حالة تعقيد (O(n أسوء حالة تعقيد (O(n^2 أوسط حالة تعقيد (O(n^2 تعقيد المساحة (O(1 توازن خوارزمية الفقاعات ‎BubbleSort‎ كل زوج متتالي من العناصر في المصفوفة غير المرتبة، ثمّ تبدل مواضعهما إن كان تَرتيبهما خاطئًا. ويوضّح المثال التالي كيفية عمل خوارزمية الفقاعات على المصفوفة ‎{6,5,3,1,8,7,2,4}‎ (تُحاط الأزواج قيد المقارنة في كل خطوة بالرمز "**"): {6,5,3,1,8,7,2,4} {**5,6**,3,1,8,7,2,4} -- 5 < 6 -> تبديل {5,**3,6**,1,8,7,2,4} -- 3 < 6 -> تبديل {5,3,**1,6**,8,7,2,4} -- 1 < 6 -> تبديل {5,3,1,**6,8**,7,2,4} -- 8 > 6 -> لا تبديل {5,3,1,6,**7,8**,2,4} -- 7 < 8 -> تبديل {5,3,1,6,7,**2,8**,4} -- 2 < 8 -> تبديل {5,3,1,6,7,2,**4,8**} -- 4 < 8 -> تبديل نحصل بعد الدورة الأولى على المصفوفة ‎{5,3,1,6,7,2,4,8}‎، لاحظ أنّ أكبر قيمة غير مرتبة في المصفوفة (8 في هذه الحالة) ستصل دائمًا إلى موضعها النهائي. للتأكد من أنّ المصفوفة مرتّبة ترتيبًا صحيحًا وأنّ كل عناصرها في المواضع الصحيحة، سيكون علينا تكرار هذه العملية n-1 مرّة، حيث يمثّل n طول المصفوفة المراد ترتيبها. خوارزمية الترتيب بالفقاعات التي تُعرف أيضًا باسم الترتيب المتدرّج Sinking Sort، هي خوارزمية ترتيب بسيطة تمرّ مرارًا وتكرارًا على المصفوفة حتى ترتّبها، وتوازن كل زوج متتال من العناصر، ثمّ تبدّلهما إن كان ترتيبهما خاطئًا. الرسم البياني التالي يمثّل آلية عمل خوارزمية الترتيب بالفقاعات: هذه تطبيقات لخوارزمية الترتيب بالفقاعات بعدة لغات برمجة. لغة C++‎: void bubbleSort(vector<int>numbers) { for(int i = numbers.size() - 1; i >= 0; i--) { for(int j = 1; j <= i; j++) { if(numbers[j-1] > numbers[j]) { swap(numbers[j-1],numbers(j)); } } } } لغة C: void bubble_sort(long list[], long n) { long c, d, t; for (c = 0 ; c < ( n - 1 ); c++) { for (d = 0 ; d < n - c - 1; d++) { if (list[d] > list[d+1]) { /* تبديل */ t = list[d]; list[d] = list[d+1]; list[d+1] = t; } } } } هذا تطبيق يستخدم المؤشرات: void pointer_bubble_sort(long * list, long n) { long c, d, t; for (c = 0 ; c < ( n - 1 ); c++) { for (d = 0 ; d < n - c - 1; d++) { if ( * (list + d ) > *(list+d+1)) { /* تبديل */ t = * (list + d ); * (list + d ) = * (list + d + 1 ); * (list + d + 1) = t; } } } } لغة C#‎‎: public class BubbleSort { public static void SortBubble(int[] input) { for (var i = input.Length - 1; i >= 0; i--) { for (var j = input.Length - 1 - 1; j >= 0; j--) { if (input[j] <= input[j + 1]) continue; var temp = input[j + 1]; input[j + 1] = input[j]; input[j] = temp; } } } public static int[] Main(int[] input) { SortBubble(input); return input; } } بايثون: #!/usr/bin/python input_list = [10,1,2,11] for i in range(len(input_list)): for j in range(i): if int(input_list[j]) > int(input_list[j+1]): input_list[j],input_list[j+1] = input_list[j+1],input_list[j] print input_list جافا: public class MyBubbleSort { public static void bubble_srt(int array[]) {//main logic int n = array.length; int k; for (int m = n; m >= 0; m--) { for (int i = 0; i < n - 1; i++) { k = i + 1; if (array[i] > array[k]) { swapNumbers(i, k, array); } } printNumbers(array); } } private static void swapNumbers(int i, int j, int[] array) { int temp; temp = array[i]; array[i] = array[j]; array[j] = temp; } private static void printNumbers(int[] input) { for (int i = 0; i < input.length; i++) { System.out.print(input[i] + ", "); } System.out.println("\n"); } public static void main(String[] args) { int[] input = { 4, 2, 9, 6, 23, 12, 34, 0, 1 }; bubble_srt(input); } } جافاسكربت: function bubbleSort(a) { var swapped; do { swapped = false; for (var i=0; i < a.length-1; i++) { if (a[i] > a[i+1]) { var temp = a[i]; a[i] = a[i+1]; a[i+1] = temp; swapped = true; } } } while (swapped); } var a = [3, 203, 34, 746, 200, 984, 198, 764, 9]; bubbleSort(a); console.log(a); // [ 3, 9, 34, 198, 200, 203, 746, 764, 984 ] الترتيب بالدمج Merge Sort الترتيب بالدمج هي خوارزمية تعتمد مبدأ فرّق تسد، إذ تقسم المصفوفةَ المدخلةَ إلى نصفين بشكل متتابع إلى أن تصبح لدينا n مصفوفة أحادية، حيث n يمثل حجم المصفوفة المُدخلة. بعد ذلك، تُدمج المصفوفات الجزئية المرتبة مثنى مثنى، إذ يُضاف أصغر العنصرين المتقابلين في المصفوفتين المراد دمجهما في كل خطوة. تستمر هذه العملية إلى حين الانتهاء من بناء المصفوفة المُرتّبة. يوضح المثال التالي آلية عمل خوارزمية الترتيب بالدمج: التعقيد الزمني للخوارزمية: ‎T(n) = 2T(n/2) + Θ(n)‎. يمكن تطبيق التكرارية أعلاه إما باستخدام طريقة الشجرة التكرارية Recurrence Tree method أو الطريقة الرئيسية (Master method)، يمكنك معرفة تفاصيل هاتين الطريقتين من هنا. تعقيد الطريقة الرئيسية يساوي ‎Θ(nLogn)‎ في جميع الحالات الثلاث (الأسوأ والمتوسطة والفضلى)، ذلك أنّ الترتيب بالدمج يقسّم المصفوفة تعاوديًا إلى أنصاف، ويستغرق وقتًا خطيًا لدمج نصفين، حيث لدينا: المساحة الإضافية: ‎O(n)‎. نموذج الخوارزمية: فرّق تسد. الموضعيّة: على العموم، التطبيقات الشائعة للخوارزمية لا تكون موضعية. مستقرة: نعم. هذه بعض تطبيقات خوارزمية الدمج في بعض لغات البرمجة: لغة Go: package main import "fmt" func mergeSort(a []int) []int { if len(a) < 2 { return a } m := (len(a)) / 2 f := mergeSort(a[:m]) s := mergeSort(a[m:]) return merge(f, s) } func merge(f []int, s []int) []int { var i, j int size := len(f) + len(s) a := make([]int, size, size) for z := 0; z < size; z++ { lenF := len(f) lenS := len(s) if i > lenF-1 && j <= lenS-1 { a[z] = s[j] j++ } else if j > lenS-1 && i <= lenF-1 { a[z] = f[i] i++ } else if f[i] < s[j] { a[z] = f[i] i++ } else { a[z] = s[j] j++ } } return a } func main() { a := []int{75, 12, 34, 45, 0, 123, 32, 56, 32, 99, 123, 11, 86, 33} fmt.Println(a) fmt.Println(mergeSort(a)) } لغة C int merge(int arr[],int l,int m,int h) { int arr1[10],arr2[10]; // مصفوفتان مؤقتتان hold the two arrays to be merged int n1,n2,i,j,k; n1=m-l+1; n2=h-m; for(i=0; i<n1; i++) arr1[i]=arr[l+i]; for(j=0; j<n2; j++) arr2[j]=arr[m+j+1]; arr1[i]=9999; // لتحديد نهاية كل مصفوفة مؤقتة arr2[j]=9999; i=0; j=0; for(k=l; k<=h; k++) { // دمج مصفوفتين مرتبتين if(arr1[i]<=arr2[j]) arr[k]=arr1[i++]; else arr[k]=arr2[j++]; } return 0; } int merge_sort(int arr[],int low,int high) { int mid; if(low<high) { mid=(low+high)/2; // فرق تسد merge_sort(arr,low,mid); merge_sort(arr,mid+1,high); // Combine merge(arr,low,mid,high); } return 0; } ‎لغةC# ‎‎ public class MergeSort { static void Merge(int[] input, int l, int m, int r) { int i, j; var n1 = m - l + 1; var n2 = r - m; var left = new int[n1]; var right = new int[n2]; for (i = 0; i < n1; i++) { left[i] = input[l + i]; } for (j = 0; j < n2; j++) { right[j] = input[m + j + 1]; } i = 0; j = 0; var k = l; while (i < n1 && j < n2) { if (left[i] <= right[j]) { input[k] = left[i]; i++; } else { input[k] = right[j]; j++; } k++; } while (i < n1) { input[k] = left[i]; i++; k++; } while (j < n2) { input[k] = right[j]; j++; k++; } } static void SortMerge(int[] input, int l, int r) { if (l < r) { int m = l + (r - l) / 2; SortMerge(input, l, m); SortMerge(input, m + 1, r); Merge(input, l, m, r); } } public static int[] Main(int[] input) { SortMerge(input, 0, input.Length - 1); return input; } } جافا: هذا تطبيق بلغة جافا يستخدم المقاربة العامة. public interface InPlaceSort<T extends Comparable<T>> { void sort(final T[] elements); } public class MergeSort < T extends Comparable < T >> implements InPlaceSort < T > { @Override public void sort(T[] elements) { T[] arr = (T[]) new Comparable[elements.length]; sort(elements, arr, 0, elements.length - 1); } // نتحقق من كلا الجانبين، ثم ندمجهما private void sort(T[] elements, T[] arr, int low, int high) { if (low >= high) return; int mid = low + (high - low) / 2; sort(elements, arr, low, mid); sort(elements, arr, mid + 1, high); merge(elements, arr, low, high, mid); } private void merge(T[] a, T[] b, int low, int high, int mid) { int i = low; int j = mid + 1; // b نختار أصغر عنصر منهما، ثم نضعه في for (int k = low; k <= high; k++) { if (i <= mid && j <= high) { if (a[i].compareTo(a[j]) >= 0) { b[k] = a[j++]; } else { b[k] = a[i++]; } } else if (j > high && i <= mid) { b[k] = a[i++]; } else if (i > mid && j <= high) { b[k] = a[j++]; } } for (int n = low; n <= high; n++) { a[n] = b[n]; }}} Java: هذا تطبيق آخر للخوارزمية بلغة Java، ولكن وفق منظور تصاعدي (من الأسفل إلى الأعلى) public class MergeSortBU { private static Integer[] array = { 4, 3, 1, 8, 9, 15, 20, 2, 5, 6, 30, 70, 60,80,0,9,67,54,51,52,24,54,7 }; public MergeSortBU() { } private static void merge(Comparable[] arrayToSort, Comparable[] aux, int lo,int mid, int hi) { for (int index = 0; index < arrayToSort.length; index++) { aux[index] = arrayToSort[index]; } int i = lo; int j = mid + 1; for (int k = lo; k <= hi; k++) { if (i > mid) arrayToSort[k] = aux[j++]; else if (j > hi) arrayToSort[k] = aux[i++]; else if (isLess(aux[i], aux[j])) { arrayToSort[k] = aux[i++]; } else { arrayToSort[k] = aux[j++]; } } } public static void sort(Comparable[] arrayToSort, Comparable[] aux, int lo, int hi) { int N = arrayToSort.length; for (int sz = 1; sz < N; sz = sz + sz) { for (int low = 0; low < N; low = low + sz + sz) { System.out.println("Size:"+ sz); merge(arrayToSort, aux, low, low + sz -1 ,Math.min(low + sz + sz - 1, N - 1)); print(arrayToSort); } } } public static boolean isLess(Comparable a, Comparable b) { return a.compareTo(b) <= 0; } private static void print(Comparable[] array) {http://stackoverflow.com/documentation/algorithm/5732/merge-sort# StringBuffer buffer = new StringBuffer();http://stackoverflow.com/documentation/algorithm/5732/merge-sort# for (Comparable value : array) { buffer.append(value); buffer.append(' '); } System.out.println(buffer); } public static void main(String[] args) { Comparable[] aux = new Comparable[array.length]; print(array); MergeSortBU.sort(array, aux, 0, array.length - 1); } } بايثون def merge(X, Y): " دمج مصفوفتين مرتبتين " p1 = p2 = 0 out = [] while p1 < len(X) and p2 < len(Y): if X[p1] < Y[p2]: out.append(X[p1]) p1 += 1 else: out.append(Y[p2]) p2 += 1 out += X[p1:] + Y[p2:] return out def mergeSort(A): if len(A) <= 1: return A if len(A) == 2: return sorted(A) mid = len(A) / 2 return merge(mergeSort(A[:mid]), mergeSort(A[mid:])) if __name__ == "__main__": # Generate 20 random numbers and sort them A = [randint(1, 100) for i in xrange(20)] print mergeSort(A) الترتيب بالإدراج الترتيب بالإدراج هي إحدى خوارزميات الترتيب البسيطة، إذ ترتّب العناصر واحدًا تلو الآخر بنفس الطريقة التي ترتّب فيها أوراق اللعب يدويًّا. الرسم البياني التالي يوضّح آلية عمل خوارزمية الترتيب بالإدراج: المصدر: ويكيبديا يمكنك معرفة المزيد من التفاصيل حول آلية عمل الخوارزمية من ويكي حسوب. هذا تطبيق لخوارزمية الترتيب بالإدراج بلغة هاسكل Haskell: insertSort :: Ord a => [a] -> [a] insertSort [] = [] insertSort (x:xs) = insert x (insertSort xs) insert :: Ord a => a-> [a] -> [a] insert n [] = [n] insert n (x:xs) | n <= x = (n:x:xs) | otherwise = x:insert n xs الترتيب بالدلو Bucket Sort الترتيب بالدلو هي خوارزمية ترتيب توزّع عناصر المصفوفة المراد ترتيبها على عدد من الدلاء (مصفوفات)، ثمّ تُرتّب عناصر كل دلو على حدة باستخدام خوارزمية ترتيب مختلفة، أو بتطبيق خوارزمية الترتيب بالدلو تكراريًا، يمكنك معرفة المزيد من التفاصيل عن هذه الخوارزمية من موسوعة حسوب. هذا تطبيق لخوارزمية الترتيب بالدلو بلغة C#‎‎ public class BucketSort { public static void SortBucket(ref int[] input) { int minValue = input[0]; int maxValue = input[0]; int k = 0; for (int i = input.Length - 1; i >= 1; i--) { if (input[i] > maxValue) maxValue = input[i]; if (input[i] < minValue) minValue = input[i]; } List<int>[] bucket = new List<int>[maxValue - minValue + 1]; for (int i = bucket.Length - 1; i >= 0; i--) { bucket[i] = new List<int>(); } foreach (int i in input) { bucket[i - minValue].Add(i); } foreach (List<int> b in bucket) { if (b.Count > 0) { foreach (int t in b) { input[k] = t; k++; } } } } public static int[] Main(int[] input) { SortBucket(ref input); return input; } } الترتيب السريع Quicksort خوارزمية الترتيب السريع هي خوارزمية ترتيب تنتقي عنصرًا من عناصر المصفوفة وتجعله محورًا، ثمّ تقسّم المصفوفة المعطاة حول ذلك العنصر، بحيث تأتي جميع العناصر الأصغر من المحور قبله، وجميع العناصر الأكبر منه تأتي بعده. تُطبّق الخوارزمية تكراريًا على الأقسام حتى تُرتّب المصفوفة بالكامل، وهناك طريقتان لتقديم هذه الخوارزمية. طريقة لوموتو Lomuto في هذه الطريقة يُنتقى أحد العناصر ليكون محور الترتيب، وغالبًا ما يكون العنصر الأخير من المصفوفة، ويُحفظ فهرس المحور ثم تُتعقّب المواقع التي تحتوي على العناصر التي تساوي قيمة المحور أو تصغُره، ويُجرى التبديل بين موقعي العنصرين إن عُثِر على عنصر أصغر من المحور، ويُزاد فهرس المحور بواحد. فيما يلي شيفرة عامة للخوارزمية: partition(A, low, high) is pivot := A[high] i := low for j := low to high – 1 do if A[j] ≤ pivot then swap A[i] with A[j] i := i + 1 swap A[i] with A[high] return i آلية الترتيب السريع: quicksort(A, low, high) is if low < high then p := partition(A, low, high) quicksort(A, low, p – 1) quicksort(A, p + 1, high) يوضّح المثال التالي آلية عمل خوارزمية الترتيب السريع: طريقة هور Hoare تستخدم طريقة هور مؤشرين يبدآن بالإشارة إلى طرفي المصفوفة المُقسّمة، ثم يتحركان نحو بعضهمًا إلى أن يحصل انقلاب، أي تصبح القيمة الصغرى في الجانب الأيسر من المحور، والكبرى في الجانب الأيمن منه؛ وعند حصول الانقلاب تبدّل الخوارزمية بين موقعي القيمتين وتعاد العملية مرةً أخرى. عندما تلتقي الفهارس، تتوقف الخوارزمية ويُعاد الفهرس النهائي. تُعد طريقة هور أكثر فعاليةً من طريقة Lomuto، لأنّ عدد التبديلات التي تجريها أقل بثلاث مرات في المتوسط من طريقة Lomuto، كما أنّها أكثر فعاليةً في إنشاء الأقسام حتى عندما تكون جميع القيم متساوية. quicksort(A, lo, hi) is if lo < hi then p := partition(A, lo, hi) quicksort(A, lo, p) quicksort(A, p + 1, hi) شيفرة عامة لتقسيم المصفوفة: partition(A, lo, hi) is pivot := A[lo] i := lo - 1 j := hi + 1 loop forever do: i := i + 1 while A[i] < pivot do do: j := j - 1 while A[j] > pivot do if i >= j then return j swap A[i] with A[j] هذا تطبيق لخوارزمية الترتيب السريع بلغة بايثون: def quicksort(arr): if len(arr) <= 1: return arr pivot = arr[len(arr) / 2] left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quicksort(left) + middle + quicksort(right) print quicksort([3,6,8,10,1,2,1]) الخرج الناتج: [1 ، 1 ، 2 ، 3 ، 6 ، 8 ، 10] وهذا تطبيق لطريقة Lomuto بلغة جافا: public class Solution { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int[] ar = new int[n]; for(int i=0; i<n; i++) ar[i] = sc.nextInt(); quickSort(ar, 0, ar.length-1); } public static void quickSort(int[] ar, int low, int high) { if(low<high) { int p = partition(ar, low, high); quickSort(ar, 0 , p-1); quickSort(ar, p+1, high); } } public static int partition(int[] ar, int l, int r) { int pivot = ar[r]; int i =l; for(int j=l; j<r; j++) { if(ar[j] <= pivot) { int t = ar[j]; ar[j] = ar[i]; ar[i] = t; i++; } } int t = ar[i]; ar[i] = ar[r]; ar[r] = t; return i; } الترتيب بالعد Counting Sort المساحة الإضافية: ‎O(n+k)‎ التعقيد الزمني: الحالة الأسوأ: ‎O(n+k)‎. الحالة الأفضل: ‎O(n)‎. الحالة المتوسطة ‎O(n+k)‎. الترتيب بالعد Counting sort هي خوارزمية لترتيب الكائنات وفقًا لمفاتيحها. خطوات الخوارزمية: أنشئ مصفوفة C حجمها يساوي عدد العناصر الفريدة في المصفوفة المُدخلة A. املأ المصفوفة 😄 لكل x عنصر فريد من المصفوفة المُدخلة، تحتوي C[x]‎‎ تردّد ذلك العنصر في المصفوفة المُدخلة A. حوّل C إلى مصفوفة بحيث يشير C[x]‎‎ إلى عدد القيم التي تصغُر x عبر التكرار في المصفوفة، وعيّن مجموع القيمة السابقة لكل C [x]‎‎، وكذلك جميع القيم في C التي تظهر قبله. كرِّر خلفيًا على المصفوفة A مع وضع كل قيمة في مصفوفة B جديدة مرتبة في الفهرس المسجّل في C. ولكل A [x]‎‎، نعيّن قيمة B [C [A [x]]]‎‎ إلى A [x]‎‎، مع إنقاص قيمة C [A [x]]‎‎ بمقدار واحد في حال وجود قيم مكرّرة في المصفوفة الأصلية غير المرتبة. يوضّح المثال التالي آلية عمل خوارزمية الترتيب بالعد: وفي ما يلي مثال توضيحي للخوارزمية: for x in input: count[key(x)] += 1 total = 0 for i in range(k): oldCount = count[i] count[i] = total total += oldCount for x in input: output[count[key(x)]] = x count[key(x)] += 1 return output يمكنك الاطلاع على المزيد من التفاصيل والأمثلة التوضيحية عن خوارزمية الترتيب بالعدّ من موسوعة حسوب الترتيب بالكومة Heap Sort المساحة الإضافية: ‎O(1)‎. التعقيد الزمني:O(nlogn)‎. الترتيب بالكومة هي خوارزمية تستند على الكومة الثنائية Binary Heap، وهي مشابهة لخوارزمية الترتيب بالتحديد Selection Sort التي تعتمد على اختيار العنصر الأكبر في المصفوفة في البداية، ثمّ تضعه في نهاية المصفوفة، ثمّ تعيد العملية على بقية العناصر. هذا مثال توضيحي لخوارزمية الترتيب بالكومة: function heapsort(input, count) heapify(a,count) end <- count - 1 while end -> 0 do swap(a[end],a[0]) end<-end-1 restore(a, 0, end) function heapify(a, count) start <- parent(count - 1) while start >= 0 do restore(a, start, count - 1) start <- start - 1 وهذا مثال على آلية عمل خوارزمية الترتيب بالكومة على المصفوفة [2,3,7,1,8,5,6]: هذا تطبيق لخوارزمية الترتيب بالكومة بلغة C#‎‎: public class HeapSort { public static void Heapify(int[] input, int n, int i) { int largest = i; int l = i + 1; int r = i + 2; if (l < n && input[l] > input[largest]) largest = l; if (r < n && input[r] > input[largest]) largest = r; if (largest != i) { var temp = input[i]; input[i] = input[largest]; input[largest] = temp; Heapify(input, n, largest); } } public static void SortHeap(int[] input, int n) { for (var i = n - 1; i >= 0; i--) { Heapify(input, n, i); } for (int j = n - 1; j >= 0; j--) { var temp = input[0]; input[0] = input[j]; input[j] = temp; Heapify(input, j, 0); } } public static int[] Main(int[] input) { SortHeap(input, input.Length); return input; } } الترتيب بالتدوير Cycle Sort الترتيب بالتدوير هي خوارزمية موضعية غير مستقرة، وتُعدّ مثاليةً من حيث عدد مرات الكتابة في المصفوفة الأصلية وعدد مرات الكتابة في الذاكرة، فكل عنصر يُكتب إمّا مرةً واحدةً إن لم يكن في موضعه الصحيح، أو لا يُكتب على الإطلاق. يمكنك معرفة المزيد من التفاصيل والأمثلة التوضيحية عن خوارزمية الترتيب بالتدوير من موسوعة حسوب هذا مثال توضيحي عن تطبيق على الخوارزمية: (input) output = 0 for cycleStart from 0 to length(array) - 2 item = array[cycleStart] pos = cycleStart for i from cycleStart + 1 to length(array) - 1 if array[i] < item: pos += 1 if pos == cycleStart: continue while item == array[pos]: pos += 1 array[pos], item = item, array[pos] writes += 1 while pos != cycleStart: pos = cycleStart for i from cycleStart + 1 to length(array) - 1 if array[i] < item: pos += 1 while item == array[pos]: pos += 1 array[pos], item = item, array[pos] writes += 1 return outout ترتيب الفردي-الزوجي Odd-Even Sort المساحة الإضافية: ‎O(n)‎ التعقيد الزمني: ‎O(n)‎ خوارزمية ترتيب الفردي-الزوجي Odd-Even Sort (أو الترتيب بالطوب Brick sort هي خوارزمية ترتيب بسيطة طُوِّرت لتُستخدم مع المعالجات المتوازية ذات التقاطعات المحلية parallel processors with local interconnection. توازن هذه الخوارزمية بين جميع أزواج العناصر المتجاورة ذات الفهارس الفردية / الزوجية في المصفوفة، وتبدل العناصر إذا وجدت أنّ الزوج مرتّب ترتيبًا خاطئًا. تكرر الخوارزمية الخطوة نفسها، لكن هذه المرة على الأزواج ذات الفهارس الزوجية / الفردية، وتستمر في المناوبة بين النمط زوجي / فردي وفردي / زوجي إلى أن تُرتّب المصفوفة. هذا مثال توضيحي لخوارزمية الطوب Brick: if n>2 then 1. apply odd-even merge(n/2) recursively to the even subsequence a0, a2, ..., an-2 and to the odd subsequence a1, a3, , ..., an-1 2. comparison [i : i+1] for all i element {1, 3, 5, 7, ..., n-3} else comparison [0 : 1] هذا رسم توضيحي لآلية عمل خوارزمية الترتيب فردي/زوجي على مجموعة عشوائية: المصدر: ويكيبيديا وهذا مثال توضيحي على الخوارزمية: وفيما يلي تطبيق بلغة C#‎‎ لخوارزمية الترتيب بقوالب الطوب: public class OddEvenSort { private static void SortOddEven(int[] input, int n) { var sort = false; while (!sort) { sort = true; for (var i = 1; i < n - 1; i += 2) { if (input[i] <= input[i + 1]) continue; var temp = input[i]; input[i] = input[i + 1]; input[i + 1] = temp; sort = false; } for (var i = 0; i < n - 1; i += 2) { if (input[i] <= input[i + 1]) continue; var temp = input[i]; input[i] = input[i + 1]; input[i + 1] = temp; sort = false; } } } public static int[] Main(int[] input) { SortOddEven(input, input.Length); return input; } } الترتيب بالتحديد Selection Sort المساحة الإضافية: ‎O(n)‎ التعقيد الزمني: ‎O(n^2)‎ الترتيب بالتحديد هي خوارزمية موضعية لترتيب المصفوفات، تعقيدها الزمني يساوي O (n2)‎‎، ما يجعلها غير فعالة مع المصفوفات الكبيرة، وعادةً ما يكون أداؤها أسوأ من خوارزميات الإدراج المماثلة. بالمقابل، تتميّز خوارزمية الترتيب بالتحديد بالبساطة وقد تتفوّق أداءً على بعض الخوارزميات الأعقد في بعض الحالات، خاصّةً عندما تكون الذاكرة الإضافية محدودة. تقسّم الخوارزمية المصفوفة إلى قسمين هما مصفوفة تضم العناصر المرتبة فعليًا، والتي تُبنى من اليسار إلى اليمين من مقدمة المصفوفة (اليسرى)؛ فيما تضم المصفوفة الأخرى العناصر المتبقية التي تنتظر أن تُرتّب، والتي تشغل بقية المصفوفة. وتكون المصفوفة المرتّبة فارغةً في البداية، بينما تحتوى المصفوفة غير المرتّبة كل عناصر المصفوفة المُدخلة. تبحث الخوارزمية عن أصغر عنصر (أو أكبر عنصر، بحسب غرض الترتيب) في المصفوفة غير المرتّبة وتبدّله بالعنصر غير المرتّب الموجود في أقصى اليسار (أي تضعه في المكان الصحيح)، ثمّ تنقل حدود المصفوفة الفرعية عنصرًا واحدًا إلى اليمين . هذا مثال توضيحي للخوارزمية: function select(list[1..n], k) for i from 1 to k minIndex = i minValue = list[i] for j from i+1 to n if list[j] < minValue minIndex = j minValue = list[j] swap list[i] and list[minIndex] return list[k] وهذا تمثيل بصري للخوارزمية: وهذا مثال على خوارزمية الترتيب بالتحديد: فيما يلي تطبيق على لخوارزمية بلغة C #‎‎: public class SelectionSort { private static void SortSelection(int[] input, int n) { for (int i = 0; i < n - 1; i++) { var minId = i; int j; for (j = i + 1; j < n; j++) { if (input[j] < input[minId]) minId = j; } var temp = input[minId]; input[minId] = input[i]; input[i] = temp; } } public static int[] Main(int[] input) { SortSelection(input, input.Length); return input; } } وهذا تطبيق على الخوارزمية بلغة إكسير Elixir: defmodule Selection do def sort(list) when is_list(list) do do_selection(list, []) end def do_selection([head|[]], acc) do acc ++ [head] end def do_selection(list, acc) do min = min(list) do_selection(:lists.delete(min, list), acc ++ [min]) end defp min([first|[second|[]]]) do smaller(first, second) end defp min([first|[second|tail]]) do min([smaller(first, second)|tail]) end defp smaller(e1, e2) do if e1 <= e2 do e1 else e2 end end end Selection.sort([100,4,10,6,9,3]) |> IO.inspect ترجمة -بتصرّف- للفصول من 28 إلى 38 من الكتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال السابق: أمثلة عن أنواع الخوارزميات خوارزميات تحليل المسارات في الأشجار خوارزمية ديكسترا Dijkstra’s Algorithm الخوارزميات الشرهة Greedy Algorithms
  14. نستعرض في هذه المقالة أمثلةً على مجموعة من الخوارزميات، مثل خوارزمية الأعداد الكاتالانية وخوارزمية بريزنهام لرسم المستقيمات، وخوارزميات إدارة ذاكرة التخزين المؤقت، إضافة إلى بعض الخوارزميات متعددة الخيوط. خوارزمية رسم المستقيمات تُرسم المستقيمات على شاشة الحاسوب بِمدِّ نقاط متتابعة ومتقاربة على طول المسار الذي يمرّ منه المستقيم بين النقطتين اللتين تحدّدان طرفي المستقيم، وسنستعرض في هذه الفقرة إحدى أفضل الخوارزميات التي يستخدمها المبرمجون لحساب إحداثيات هذه النقاط وهي خوارزمية بريزنهام لرسم المستقيمات، التي هي عبارة عن خوارزمية لرسم الخطوط بفعالية ودقّة، طورها جاك إيلتون بريزنهام Jack E. Bresenham، وهي دقيقة وسريعة لأنها تتم بإجراء عمليات على الأعداد الصحيحة فقطـ، وهي تسع أيضًا رسم الدوائر والمنحنيات الأخرى. وفي خوارزمية بريزنهام، لنقل أنّ ميل المستقيم الذي نريد رسمه هو m. إن كان ‎‎|m|<1: إما تُزاد قيمة الإحداثية x. أو تُزاد قيمتا الإحداثيتان x و y معًا باستخدام معامل قرار decision parameter خاصّ. إن كان ‎‎|m|<1‎‎: إما أن تُزاد قيمة y. أو تُزاد قيمتا x و y معا باستخدام معامل قرار خاصّ. والآن، نستعرض خطوات الخوارزمية اعتمادًا على ميل المستقيم المراد رسمه. الحالة الأولى: ‎‎| m | <1 أدخِل طرفي المستقيم (x1، y1) و (x2، y2). ارسم النقطة الأولى (x1، y1). احسب القيمتين: Delx = | x2 ‎–‎ x1 | ‎‎. Dely = | y2 –‎ y1 | ‎‎. احسب معامل القرار الأولي عبر الصيغة التالية: P = 2 * dely –‎ delx لكل I من 0 إلى delx: إذا كان p <0، فسيكون: X1 = x1 + 1. ارسم (x1، y1). P = p + 2dely. وإلا: X1 = x1 + 1. Y1 = y1 + 1. ارسم (x1، y1). P = p + 2dely –‎ 2 * delx. نهاية عبارة إن. نهاية عبارة لكل. النهاية. فيما يلي شيفرة الخوارزمية، وهي برنامج مكتوب بلغة C لتطبيق خوارزمية بريزنهام في حالة ‎‎| m | <1: #include<stdio.h> #include<conio.h> #include<graphics.h> #include<math.h> int main() { int gdriver=DETECT,gmode; int x1,y1,x2,y2,delx,dely,p,i; initgraph(&gdriver,&gmode,"c:\\TC\\BGI"); printf("Enter the intial points: "); scanf("%d",&x1); scanf("%d",&y1); printf("Enter the end points: "); scanf("%d",&x2); scanf("%d",&y2); putpixel(x1,y1,RED); delx=fabs(x2-x1); dely=fabs(y2-y1); p=(2*dely)-delx; for(i=0;i<delx;i++){ if(p<0) { x1=x1+1; putpixel(x1,y1,RED); p=p+(2*dely); } else { x1=x1+1; y1=y1+1; putpixel(x1,y1,RED); p=p+(2*dely)-(2*delx); } } getch(); closegraph(); return 0; } الحالة الثانية: ‎‎| m | > 1: أدخل طرفي المستقيم (x1، y1) و (x2، y2). ارسم النقطة الأولى (x1، y1). احسب القيمتين: Delx = | x2 –‎ x1 | ‎‎. Dely = | y2 ‎–‎ y1 | ‎‎. احسب معامل القرار الأول عبر الصيغة التالية: P = 2 * dely –‎ delx. لكل I من 0 إلى delx: إذا كان p <0 فسيكون: Y1 = y1 + 1. ارسم (x1، y1). P = p + delx. وإلا: X1 = x1 + 1. Y1 = y1 + 1. ارسم (x1، y1) P = p + 2delx –‎ 2 * dely. نهاية عبارة إن. نهاية عبارة لكل. النهاية. انظر إلى الشيفرة أدناه، وهي برنامج مكتوب بلغة C لتطبيق خوارزمية بريزنهام في حالة ‎‎| m | > 1: #include<stdio.h> #include<conio.h> #include<graphics.h> #include<math.h> int main() { int gdriver=DETECT,gmode; int x1,y1,x2,y2,delx,dely,p,i; initgraph(&gdriver,&gmode,"c:\\TC\\BGI"); printf("Enter the intial points: "); scanf("%d",&x1); scanf("%d",&y1); printf("Enter the end points: "); scanf("%d",&x2); scanf("%d",&y2); putpixel(x1,y1,RED); delx=fabs(x2-x1); dely=fabs(y2-y1); p=(2*delx)-dely; for(i=0;i<delx;i++){ if(p<0) { y1=y1+1; putpixel(x1,y1,RED); p=p+(2*delx); } else { x1=x1+1; y1=y1+1; putpixel(x1,y1,RED); p=p+(2*delx)-(2*dely); } } getch(); closegraph(); return 0; } خوارزمية الأعداد الكاتالانية خوارزمية الأعداد الكاتالانية Catalan Number Algorithm هي إحدى خوارزميات البرمجة الديناميكية، وتشكل الأعداد الكاتلانية في الرياضيات التوافقية combinatorial mathematics سلسلةً من الأعداد الطبيعية التي تظهر في العديد من المسائل العددية المختلفة، وخاصةً المقادير التي تُعرّف بطريقة تكرارية، كما تظهر في مشاكل عدّ الأشجار. ويمكن استخدام خوارزمية الأعداد الكاتالانية لحساب مقادير مثل: عدد الطرق التي يمكن تكديس القطع النقدية بها في صف سفلي يتكون من n قطعة نقدية متتالية في المستوى، بحيث لا يُسمح بوضع أي قطعة نقدية على جانبي القطع النقدية السفلية، كما يجب أن تكون كل قطعة نقدية فوق قطعنين نقديتين أُخريين، الجواب هو العدد الكتالاني رقم n. عدد التعابير التي تحتوي على n زوج من الأقواس الهلالية، والتي تطابق بعضها البعض تطابقًا صحيحًا هو العدد الكتالاني رقم n. عدد الطرق الممكنة لقطع مضلّع محدّب ذو n+2 وجهًا‏ n+2-sided convex polygon في مستوى ما إلى مثلثات عبر ربط الحروف ذات الخطوط المستقيمة غير المتقاطعة هو العدد الكاتالاني رقم n. يُحصَل على العدد الكاتالاني رقم n مباشرةً عبر المعادلة التالية التي تستخدم المعاملات الثنائية. إليك مثال على عدد كاتالاني: n = 4 المساحة الإضافية = ‎O(n)‎. تعقيد الوقت = ‎O(n^2)‎. الخوارزميات متعددة الخيوط Multithreaded Algorithms سنستعرض في هذه الفقرة أمثلة على بعض الخوارزميات متعددة الخيوط. مثال عن ضرب المصفوفة المربعة Square matrix multiplication: multiply-square-matrix-parallel(A, B) n = A.lines C = Matrix(n,n) //n*n إنشاء مصفوفة من الحجم parallel for i = 1 to n parallel for j = 1 to n C[i][j] = 0 pour k = 1 to n C[i][j] = C[i][j] + A[i][k]*B[k][j] return C مثال عن خوارزمية ضرب مصفوفة ومتجه متعددة الخيوط: matrix-vector(A,x) n = A.lines y = Vector(n) //create a new vector of length n parallel for i = 1 to n y[i] = 0 parallel for i = 1 to n for j = 1 to n y[i] = y[i] + A[i][j]*x[j] return y خوارزمية دمج وترتيب متعددة الخيوط لتكن A وB مصفوفتان، وليكن p و r فهرسان للمصفوفة A، سنحاول ترتيب المصفوفة الجزئية A [p..r]‎‎. وسنملأ المصفوفة B عند ترتيب المصفوفة الجزئية. في الشيفرة التالية، ترتب الدالة p-merge-sort (A، p، r، B، s)‎‎ عناصر A [p..r]‎‎ وتضعها في B [s..s + rp]‎‎. p-merge-sort(A,p,r,B,s) n = r-p+1 if n==1 B[s] = A[p] else T = new Array(n) //create a new array T of size n q = floor((p+r)/2)) q_prime = q-p+1 spawn p-merge-sort(A,p,q,T,1) p-merge-sort(A,q+1,r,T,q_prime+1) sync p-merge(T,1,q_prime,q_prime+1,n,B,s) في الشيفرة أدناه، تجري الدالة المساعدة p-merge عملية الدمج بالتوازي، وتفترض هذه الدالة أنّ المصفوفتين الجزئيتين المراد دمجهما موجودتان في المصفوفة نفسها، ولكنّها لا تفترض أنهما متجاورتان في المصفوفة، لذا علينا تزويدها بالفهارس p1 وr1 p2 وr2. p-merge(T,p1,r1,p2,r2,A,p3) n1 = r1-p1+1 n2 = r2-p2+1 if n1<n2 // n1>=n2 التحقق من أنّ permute p1 and p2 permute r1 and r2 permute n1 and n2 if n1==0 // كلاهما فارغ؟ return else q1 = floor((p1+r1)/2) q2 = dichotomic-search(T[q1],T,p2,r2) q3 = p3 + (q1-p1) + (q2-p2) A[q3] = T[q1] spawn p-merge(T,p1,q1-1,p2,q2-1,A,p3) p-merge(T,q1+1,r1,q2,r2,A,q3+1) sync الشيفرة أدناه تمثل دالة البحث الانقسامي المساعِدة auxillary function dichotomic-search، ويكون x فيها هو المفتاح المراد البحث عنه في المصفوفة الفرعية T [p..r]‎‎. dichotomic-search(x,T,p,r) inf = p sup = max(p,r+1) while inf<sup half = floor((inf+sup)/2) if x<=T[half] sup = half else inf = half+1 return sup خوارزمية KMP خوارزمية KMP هي خوارزمية لمطابقة الأنماط، إذ تبحث عن مواضع ظهور كلمةٍ ما W في سلسلة نصية S. ومبدأ هذه الخوارزمية أنّه في كلّ مرة نرصد فيها عدم تطابق، فإنّنا سنكون على علم ببعض الحروف الموجودة في النص والتي ستكون مطابقة في الخطوة القادمة. سنستغل هذه المعلومة لتجنب مطابَقة الحروف التي نعلم بأنّها ستكون متطابقة بأي حال. ولا يتجاوز تعقيد هذه الخوارزمية O (n)‎‎ في أسوأ الحالات. سنأخذ مثالًا على هذا النوع من الخوارزميات؛ تتألف هذه الخوارزمية من خطوتين: الخطوة الأولى: التجهيز نعالج النمط معالجةً مسبقة، وننشئ مصفوفة مساعدة lps []‎‎ تُستخدم لتخطي المحارف أثناء المطابقة. تشير lps[]‎‎ هنا إلى أطول سابقة ملائمة proper prefix تكون لاحقة suffix أيضًا. نقصد بالسابقة المناسبة أيّ سابقة تبدأ بها السلسلة النصية المبحوث عنها، شرط ألا تحتوي كامل السلسلة النصية. مثلاً، سوابق "ABC" الملائمة هي "" و "A" و "AB"، أما اللواحق الخاصة بهذه السلسلة النصية فهي: "" و "C" و "BC" و "ABC". الخطوة الثانية: البحث نستمر في مطابقة المحارف txt ‎‎ و ‎pat [j]‎، ونستمر في زيادة i و j طالما تطابق pat [j]‎‎ و txt‎‎. عندما نرصد عدم تطابق فإننا نعلم أنّ الأحرف pat[0..j-1] تتطابق مع txt [i-j +1… i-1]‎‎. ونعلم أيضًا أنّ lps[j-1]‎‎ يساوي تعداد محارف pat[0 … j-1]‎‎ التي هي سوابق ملائمة ولواحق على حدّ سواء، لهذا لا نحتاج إلى مطابقة أحرفlps [j-1]‎‎ مع txt [ij… i-1]‎‎، لأنّنا نعلم سلفًا أنّها مُتطابقة. فيما يلي تطبيق على ذلك بلغة جافا: public class KMP { public static void main(String[] args) { // TODO Auto-generated method stub String str = "abcabdabc"; String pattern = "abc"; KMP obj = new KMP(); System.out.println(obj.patternExistKMP(str.toCharArray(), pattern.toCharArray())); } public int[] computeLPS(char[] str){ int lps[] = new int[str.length]; lps[0] = 0; int j = 0; for(int i =1;i<str.length;i++){ if(str[j] == str[i]){ lps[i] = j+1; j++; i++; }else{ if(j!=0){ j = lps[j-1]; }else{ lps[i] = j+1; i++; } } } return lps; } public boolean patternExistKMP(char[] text,char[] pat){ int[] lps = computeLPS(pat); int i=0,j=0; while(i<text.length && j<pat.length){ if(text[i] == pat[j]){ i++; j++; }else{ if(j!=0){ j = lps[j-1]; }else{ i++; } } } if(j==pat.length) return true; return false; } } خوارزمية مسافة التعديل الديناميكية Edit Distance Dynamic Algorithm بيان المشكلة: إذا أُعطيت سلسلتين نصيتين str1 و str2، فما هو العدد الأدنى للعمليات التي يمكن إجراؤها على str1 لتحويلها إلى str2. علمًا بأنّ العمليات الممكنة هي: الإدراج Insert. الحذف Remove. الاستبدال Replace. إليك مثال: إن كانت str1 = "geek"‎‎ و str2 = "gesek"‎‎ فالجواب هو 1، لأنّنا نحتاج فقط إلى إدراج الحرف s في السلسلة النصية str1. إن كانت str1 = "march"‎‎ و str2 = "cart"‎‎ فالجواب هو 3، لأنّنا نحتاج إلى 3 عمليات هي وضع c مكان m، ثمّ حذف c، ثمّ وضع t مكان h. ولحل هذه المشكلة، سنستخدم المصفوفة dp[n+1] [m+1]‎‎ ثنائية الأبعاد، حيث يمثّل n طول السلسلة النصية الأولى، و m طول السلسلة النصية الثانية. على سبيل المثال، إذا كانت str1 تساوي "azcef" و str2 تساوي "abcdef"، فستكون المصفوفة من البعدين dp [6] [7]‎‎، وستُخزّن الإجابة النهائية في الموضع dp [5] [6]‎‎. (a) (b) (c) (d) (e) (f) +---+---+---+---+---+---+---+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +---+---+---+---+---+---+---+ (a) | 1 | | | | | | | +---+---+---+---+---+---+---+ (z) | 2 | | | | | | | +---+---+---+---+---+---+---+ (c) | 3 | | | | | | | +---+---+---+---+---+---+---+ (e) | 4 | | | | | | | +---+---+---+---+---+---+---+ (f) | 5 | | | | | | | +---+---+---+---+---+---+---+ بالنسبة للفهرس dp [1] [1]‎‎، ليس علينا فعل شيء لتحويل a إلى a لأنّهما متساويان، لذا نضع 0 هناك. وبالنسبة للفهرس dp [1] [2]‎‎، ما الذي علينا أن نفعله لتحويل a إلى ab؟ سنحتاج إلى عملية واحدة، وهي إدراج الحرف b. ستبدو المصفوفة كما يلي بعد التكرار الأول: (a) (b) (c) (d) (e) (f) +---+---+---+---+---+---+---+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +---+---+---+---+---+---+---+ (a) | 1 | 0 | 1 | 2 | 3 | 4 | 5 | +---+---+---+---+---+---+---+ (z) | 2 | | | | | | | +---+---+---+---+---+---+---+ (c) | 3 | | | | | | | +---+---+---+---+---+---+---+ (e) | 4 | | | | | | | +---+---+---+---+---+---+---+ (f) | 5 | | | | | | | +---+---+---+---+---+---+---+ بالنسبة للفهرس dp [2] [1]‎‎، لأجل تحويل az إلى a، نحتاج إلى إزالة z، ومن ثم نضع 1 في dp [2] [1]‎‎. أما بالنسبة لـ dp [2] [2]‎‎، نحتاج إلى استبدال z بـ b، ومن ثم نضع 1 في dp [2] [2]‎‎. هكذا ستبدو المصفوفة الآن: (a) (b) (c) (d) (e) (f) +---+---+---+---+---+---+---+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +---+---+---+---+---+---+---+ (a) | 1 | 0 | 1 | 2 | 3 | 4 | 5 | +---+---+---+---+---+---+---+ (z) | 2 | 1 | 1 | 2 | 3 | 4 | 5 | +---+---+---+---+---+---+---+ (c) | 3 | | | | | | | +---+---+---+---+---+---+---+ (e) | 4 | | | | | | | +---+---+---+---+---+---+---+ (f) | 5 | | | | | | | +---+---+---+---+---+---+---+ نستمر في إعادة العملية إلى أن نحوّل السلسلة النصية الأولى إلى الثانية ("azcef" ‏ ← "abcdef")، وستبدو المصفوفة النهائية هكذا: (a) (b) (c) (d) (e) (f) +---+---+---+---+---+---+---+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +---+---+---+---+---+---+---+ (a) | 1 | 0 | 1 | 2 | 3 | 4 | 5 | +---+---+---+---+---+---+---+ (z) | 2 | 1 | 1 | 2 | 3 | 4 | 5 | +---+---+---+---+---+---+---+ (c) | 3 | 2 | 2 | 1 | 2 | 3 | 4 | +---+---+---+---+---+---+---+ (e) | 4 | 3 | 3 | 2 | 2 | 2 | 3 | +---+---+---+---+---+---+---+ (f) | 5 | 4 | 4 | 2 | 3 | 3 | 3 | +---+---+---+---+---+---+---+ هذه هي الصيغة العامة لتحديث المصفوفة: if characters are same dp[i][j] = dp[i-1][j-1]; // الحرفان متطابقان else dp[i][j] = 1 + Min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) هذا تطبيق مكتوب بلغة جافا: public int getMinConversions(String str1, String str2){ int dp[][] = new int[str1.length()+1][str2.length()+1]; for(int i=0;i<=str1.length();i++){ for(int j=0;j<=str2.length();j++){ if(i==0) dp[i][j] = j; else if(j==0) dp[i][j] = i; else if(str1.charAt(i-1) == str2.charAt(j-1)) dp[i][j] = dp[i-1][j-1]; else{ dp[i][j] = 1 + Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1])); } } } return dp[str1.length()][str2.length()]; } التعقيد الزمني لهذه الخوارزمية هو O(n^2)‎‎. الخوارزميات المتصلة Online algorithms تنشأ مشكلة التخزين المؤقت caching من محدودية مساحة التخزين، وسنفترض أنّ ذاكرة التخزين المؤقت ‎C‎ تحتوي على ‎k‎ صفحة، ونحن نريد معالجة سلسلة من طلبات الصفحات عددها ‎m‎ طلب، ويجب تخزينها في ذاكرة التخزين المؤقت قبل معالجتها. إذا كانت ‎m<=k‎ فلن تكون هناك مشكلة إذ سنضع جميع العناصر في ذاكرة التخزين المؤقت، ولكن في العادة تكون ‎m>>k‎. تكون طلبيّة ما مقبولة في ذاكرة التخزين المؤقت (نقول أنها cache hit) إذا كان العنصر موجودًا في ذاكرة التخزين المؤقت بالفعل، وإلا نقول أنّها مُفوَّتة من ذاكرة التخزين المؤقت cache miss، وفي مثل هذه الحالة سيكون علينا إحضار العنصر المطلوب إلى ذاكرة التخزين المؤقت وإخراج عنصر آخر وذلك على افتراض أنّ ذاكرة التخزين المؤقت ممتلئة. إن الهدف هنا هو تصميم جدول إخلاء eviction schedule يقلّل عدد عمليات الإخلاء الضرورية قدر الإمكان. هناك العديد من الاستراتيجيات الممكنة لحلّ هذه المشكلة، منها: من دخل أولًا، يخرج أولًا First in, first out أو FIFO: تُخلى أقدم صفحة. من دخل آخرًا، يخرج أولًا Last in, first out أو LIFO: تُخلى أحدث صفحة. الأقل استخدامًا مؤخرًا LRU: تخلى الصفحة التي كان آخر وصول لها هو الأبكر. الأقل طلبًا LFU: تخلى الصفحة التي طُلِبت أقل عدد من المرّات. أطول مسافة أمامية LFD: تخلى الصفحة التي لن تُطلَب إلا بعد أطول مدة. التفريغ عند الامتلاء FWF: مسح ذاكرة التخزين المؤقت كاملة بمجرد حدوث فوات في ذاكرة التخزين المؤقت. هناك طريقتان للتعامل مع هذه المشكلة: الطريقة غير المتصلة offline: يكون فيها تسلسل الطلبات معروف منذ البدء. الطريقة المتصلة online: يكون تسلسل الطلبات غير معروف مسبقًا. وقبل أن نواصل، سنقدم بعض التعاريف الرياضية المساعدة. التعريف 1: نقول أنّ المشكلة ‎Π‎ هي مشكلة تحسين optimization problem إن كانت تتألف من مجموعة من الحالات ‎ΣΠ‎، بحيث يكون لكل حالة ‎σ∈ΣΠ‎ مجموعة من الحلول ودالة تقييمf σ: Ζσ → ℜ≥ 0 تعطي قيمة حقيقية موجبة لكلّ حل من تلك الحلول، ونرمز لقيمة الحل الأمثل (optimal solution) من بين كل حلول الحالة σ بالرمز OPT (σ‎)‎‎. ولكل خوارزمية A، يشير الرمز A (σ‎)‎‎ إلى حل الخوارزمية A للمشكلة ‎Π‎ في الحالة σ، بينما يساوي التعبير wA (‎σ‎) = fσ‎ (A (‎σ))‎‎ قيمة الخوارزمية. التعريف 2: نقول أنّ خوارزمية متصلة A لمشكلة تصغير ‎Π‎ ‏minimization problem لديها معدل تنافسي competetive ratio قيمته r ‎≥ 1 إذا كان هناك ثابت ‎τ∈ℜ‎ يحقّق: wA(σ) = fσ(A(σ)) ≤ r ⋅ OPT(σ) + τ لكل الحالات ‎σ∈ΣΠ‎، نقول أنّ الخَوارزمية A خوارزمية متصلة ذات r تنافسية r-competitive online algorithm إن كان لدينا: wA(σ) ≤ r ⋅ OPT(σ) لكل عنصر ‎σ∈ΣΠ‎، نقول أنّ A خوارزمية متصلة ذات r تنافسية قطعًا strictly r-competitive online algorithm. تعريف 3: نقول أنّ خوارزمية A خوارزمية واسمة Marking Algorithm فقط إذا لم تكن تُخلي أيّ صفحة موسومة من ذاكرة التخزين المؤقت، مما يعني أنّ الصفحات التي تُستخدم أثناء أيّ مرحلة لن تُخلى. خاصية 1.3: الخوارِزميتان LRU -الأقل استخدامًا مؤخرًا Least recently used، أي إخلاء الصفحة التي لم يُدخل إليها منذ أطول مدة-، و FWF -التفريغ عند الملء Flush when full، وتعني مسح ذاكرة التخزين المؤقت بمجرد وقوع فوات في ذاكرة التخزين المؤقت cache miss- هما خوارِزميتان واسمَتان. دعنا نفترض أن LRU ليست خوارزميةًواسمة، إذًا ستكون هناك حالة ‎σ‎، بحيث تخلي الخوازرمية LRU صفحة x موسومة marked page في المرحلة i. وإن كان ‎σt‎ هو الطلب في المرحلة i حيث أُخلِيت الصفحة x، وبما أن صفحة x موسومة، فلا بدّ أن يكون هناك طلب سابق ‎σt*‎ للصفحة x في المرحلة نفسها، لذا تكون t * <t. بعد t* ‎، ستكون x هي الصفحة الأحدث في ذاكرة التخزين المؤقت، لذا ينبغي أن يطلب التسلسل σt*+1,...,σt على الأقل k صفحة مختلفة عن x، وذلك لأجل إخلائها عند الوقت t. هذا يعني أنّ المرحلة i قد طلبت على الأقل k +1 صفحة مختلفة، وهذا يتعارض مع تعريف المرحلة. وعليه نستنتج أنّ LRU خوارزمية واسمة. خاصية 1.4: كل الخوارزميات الواسمة هي خوارزميات ذات k تنافسية قطعًا. إذا افترضنا أنّ l ≥ 2، فستكون تكلفة جميع الخوارزميات الواسمة للحالة ‎σ‎ أصغر من l ⋅ k، لأنه لا يمكن للخوارزمية الواسمة أن تخلي في كل مرحلة أكثر من k صفحة دون إخلاء صفحة واحدة واسمة على الأقل. سنحاول الآن أن نبيّن أنّ الخوارزمية غير المتصلة المثلى optimal offline algorithm تخلي على الأقل k + l-2 صفحةً في الحالة ‎σ‎، حيث تُخلي k في المرحلة الأولى، إضافةً إلى صفحة واحدة على الأقل في كل مرحلة تالية خلال المرحلة الأخيرة: نعرّف الآن l - 2 تسلسلًا فرعيًا منفصلا للحالة ‎σ‎، يبدأ التسلسلi ∈ {1,…,l-2} ‎‎ في الموضع الثاني من المرحلة i + 1 وينتهي عند الموضع الأول للمرحلة i + 2. لتكن x الصفحة الأولى من المرحلة i +1، في بداية التسلسل i توجد صفحة x، إضافةً إلى k-1 صفحة مختلفة على الأكثر في ذاكرة التخزين المؤقت الخاصة بالخوارزميات غير المتصلة المثلى. هناك k صفحة مطلوبة في التسلسل i مختلفة عن x، لذا يتوجّب على الخوارزمية غير المتصلة المثلى إخلاء صفحة واحدة على الأقل في كل تسلسل. وبما أن ذاكرة التخزين المؤقت في المرحلة الأولى فارغة، فإنّ الخوارزمية غير المتصلة المثلى تجري k عملية إخلاء خلال المرحلة الأولى. وهذا يبيّن أنّ: wA(σ) ≤ l⋅k ≤ (k+l-2)k ≤ OPT(σ) ⋅ k خاصية 1.5: الخوارزمِيتان LRU و FWF خوارِزميتان ذواتا k تنافسية قطعا. لتكن A خوارزمية متصلة، إذا لم تكن هناك أيّ ثابتة r بحيث تكون هذه الخوارزمية ذات r تنافسية، نقول أنّ A غير تنافسية not competitive. خاصية 1.6: الخوارزميتان LFU و LIFO غير تنافُسيتان. الصفحة الأولى مطلوبة عدد l مرّة، وكذلك الصفحة 2، وهكذا دواليك، وفي النهاية سيكون لدينا (l-1) طلبًا للصفحة k و k +1. تملأ الخوارِزميتان LFU و LIFO صفحات المخزن المؤقت بـالصفحات من 1 إلى k، وعند طلب الصفحة k +1 تُخلى الصفحة k، والعكس صحيح. هذا يعني أنّ كل طلب من التسلسل ‎‎(k,k+1)l-1 يخلي صفحة واحدة. أيضًا، هناك مرات فوات قدرها k-1 فواتًا في ذاكرة التخزين المؤقت cache misses في أول استخدام للصفحات من 1 إلى (k-1)، وهذا يعني أنّ الخوارزميتَين LFU و LIFO تخليان k-1 +2 (l-1)‎‎ صفحة بالضبط. سنحاول الآن أن نثبت أنّه لكل ثابتة ‎τ∈ℜ‎، ولكل ثابتة r ‎≤ 1، يوجد l يحقق: والذي يساوي: لتحقيق هذه المتراجحة، يكفي أن تختار عدد I كبيرًا بما يكفي. نستنتج من كل هذا أنّ الخوارزميتَين LFU و LIFO غير تنافسيتَين. خاصية 1.7: لكل r <k، لا توجد أيّ خوارزمية حتمية متصلة ذات r تنافسية لحل مشكلة التصفيح r-competetive deterministic online algorithm for paging. المنظور غير المتصل Offline Approach قبل المواصلة، راجع مقالة تطبيقات خوارزميات الشَّرِهة، وراجع فقرة التخزين المؤقت غير المتصل والاستراتيجيات الخمس المستخدمة فيها. لقد وسّعنا برنامج المثال بإضافة استراتيجية FWF: class FWF : public Strategy { public: FWF() : Strategy("FWF") { } int apply(int requestIndex) override { for(int i=0; i<cacheSize; ++i) { if(cache[i] == request[requestIndex]) return i; // بعد أول صفحة فارغة، يجب أن تكون كل الصفحات الأخرى فارغة else if(cache[i] == emptyPage) return i; } // لا صفحات حرة return 0; } void update(int cachePos, int requestIndex, bool cacheMiss) override { // لا صفحات حرة -> فوات -> مسح المخزن المؤقت if(cacheMiss && cachePos == 0) { for(int i = 1; i < cacheSize; ++i) cache[i] = emptyPage; } } }; يمكنك تحميل الشيفرة المصدرية كاملة من pastebin. إذا أعدنا استخدام المثال، فسنحصل على النتيجة التالية: Strategy: FWF Cache initial: (a,b,c) Request cache 0 cache 1 cache 2 cache miss a a b c a a b c d d X X x e d e X b d e b b d e b a a X X x c a c X f a c f d d X X x e d e X a d e a f f X X x b f b X e f b e c c X X x Total cache misses: 5 على الرغم من أنّ خوارزمية LFD مثلى، إلا أنّ خوارزمية FWF تتميّز بأنّ عدد الفوات فيها أقل، غير أن الهدف الرئيسي هو تقليل عدد عمليات الإخلاء إلى الحد الأدنى، وفي خوارزمية FWF تحدث 5 حالات فوات، مما يعني أنّه ستكون هناك 15 عملية إخلاء، ما يجعلها الخيار الأسوء. المنظور المتصل Online Approach نريد الآن أن نبحث عن حل لمشكلة التصفيح المتصل online problem of paging، نحن نعلم أنّه لا يمكن لأيّ خوارزمية متصلة أن تكون أفضل من الخوارزمية غير المتصلة المثلى، لأنّه في الخوارزمية غير المتصلة تكون سلسلة الطلبات معروفة منذ البداية، وذلك على خلاف الخوارزميات المتصلة التي لا تعرف مسبقًا إلا الطلبات السابقة وليس اللاحقة، لهذا فإنّ الخوارزميات غير المتصلة لديها دائما معلومات أكثر موازنةً بالخوارزميات المتصلة، ما يجعلها أكثر كفاءة. سنحتاج إلى بعض التعريفات الدقيقة قبل أن نواصل: التعريف 1: نقول أنّ المشكلة ‎Π‎ هي مشكلة تحسين optimization problem إن كانت تتألف من مجموعة من الحالات ‎ΣΠ‎. بحيث يكون لكل حالة ‎σ∈ΣΠ‎ مجموعة من الحلول، ودالة تقييمf σ: Ζσ → ℜ≥ 0 تعطي قيمة حقيقية موجبة لكلّ حل من تلك الحلول. نرمز للحل الأمثل optimal solution من بين كل حلول الحالة σ بالرمز OPT (σ‎)‎‎. لكل خوارزمية A، يشير الرمز A (σ‎)‎‎ إلى حل الخوارزمية A للمشكلة ‎Π‎ في الحالة σ، فيما يساوي التعبير wA (‎σ‎) = fσ‎ (A (‎σ))‎‎ قيمة الخوارزمية. التعريف 2: نقول أنّ خوارزمية متصلة A لمشكلة تصغير ‎Π‎minimization problem لديها معدل تنافسي competetive ratio قيمته r ‎≥ 1 إذا كان هناك عدد ‎τ∈ℜ‎ ثابت يحقّق: wA(σ) = fσ(A(σ)) ≤ r ⋅ OPT(σ) + τ لكل الحالات ‎σ∈ΣΠ‎، نقول أنّ الخَوارزمية A خوارزمية r-تكرارية متصلة r-competitive online algorithm إن كان لدينا: wA(σ) ≤ r ⋅ OPT(σ) لكل عنصر ‎σ∈ΣΠ‎، نقول أنّ A خوارزمية متصلة تنافسية قطعا strictly r-competitive online algorithm. لذا فإن السؤال هو مدى تنافسية خوارزميتنا المتصلة موازنةً بالخوارزمية غير المتصلة المثلى. وصف كل من آلان بورودين Allan Borodin وران الينيف Ran El-Yaniv في كتابهما الشهير خوارزميةَ التصفيح المتصل بالتعبير الطريف التالي: إذا أردت أن تكون مكان الخصم، فيمكنك تجربة لعبة الخصم (حاول التغلب على استراتيجيات التصفيح). الخوارزميات الواسمة Marking Algorithms بدلاً من تحليل كل الخوارزميات على حدة، سنحلّل عائلةً خاصةً من الخوارزميات المتصلة الخاصة بمشكلة التصفيح تسمّى خوارزميات الوسم أو الخوارزميات الواسمة marking algorithms. تعريف: نقول أنّ خوارزمية A خوارزمية واسمةً، فقط إذا لم تكن تُخلي أيّ صفحة موسومة من ذاكرة التخزين المؤقت، مما يعني أنّ الصفحات التي تُستخدم أثناء أيّ مرحلة لن تُخلى. لتكن ‎σ=(σ1,...,σp)‎‎ حالة من المشكلة، و k حجم ذاكرة التخزين المؤقت، فعندئذ يمكن تقسيم ‎σ‎ إلى مراحل: المرحلة 1 هي أقصى تتابع لـ ‎σ‎ من البداية حتى أقصى k صفحة مختلفة مطلوبة. المرحلة i ‎≥‎ 2 هي أقصى تتابع لـ ‎σ‎ من نهاية المرحلة i-1 حتى أقصى k صفحة مختلفة مطلوبة على سبيل المثال: في الحالة k = 3، نحصل على: تتذكّر الخوارزميات الواسمة (ضمنيًا أو صراحة) ما إذا كانت الصفحة موسومةً أم لا، وتكون كل الصفحات غير موسومة في بداية كل مرحلة، وتوسم الصفحة بمجرد أن تُطلب أثناء مرحلة ما. خاصية 1.3: الخوارِزميتان LRU (وتعني الأقل استخدامًا مؤخرًا - Least recently used، أي إخلاء الصفحة التي لم يُدخل إليها منذ أطول مدة)، و FWF (أي التفريغ عند الملء - Flush when full، وتعني مسح ذاكرة التخزين المؤقت بمجرد وقوع فوات في ذاكرة التخزين المؤقت cache miss) هما خوارِزميتان واسمَتان marking algorithm. لنفترض أنّ LRU ليست خوارزميةً واسمة، إذًا ستكون هناك حالة σ بحيث تخلي الخوازرمية LRU صفحة x موسومة marked page في المرحلة i. ليكن *σt‏ هو الطلب في المرحلة i، حيث أُخلِيت الصفحة x، عندئذ وبما أن الصفحة x موسومة، فلا بدّ أن يكون هناك طلب سابق *σt‏ للصفحة x في المرحلة نفسها، لذلك فإنّ t * <t. بعد * t‎‎، ستكون x هي الصفحة الأحدث في ذاكرة التخزين المؤقت، لذلك ينبغي أن يطلب التسلسل σt*+1,...,σt على الأقل k صفحة مختلفة عن x لأجل إخلائها عند الوقت t، وهذا يعني أنّ المرحلة i قد طلبت على الأقل k +1 صفحة مختلفة، مما يتعارض مع تعريف المرحلة، وعليه نستنتج أنّ LRU خوارزمية واسمة. خاصية 1.4: كل الخوارزميات الواسمة هي خوارزميات ذات k تنافسية قطعًا. إذا افترضنا أنّ ‎ l ≥ 2، فستكون تكلفة جميع الخوارزميات الواسمة للحالة ‎σ‎ أصغر من ‎‎ l ⋅ k، لأنه لا يمكن للخوارزمية الواسمة أن تخلي في كل مرحلة أكثر من k صفحة دون إخلاء صفحة واحدة واسمة على الأقل. سنحاول الآن أن نبيّن أنّ الخوارزمية غير المتصلة المثلى optimal offline algorithm تخلي على الأقل k + l-2 صفحة في الحالة ‎σ‎، حيث تُخلي k في المرحلة الأولى، إضافةً إلى صفحة واحدة على الأقل في كل مرحلة تالية عدا المرحلة الأخيرة. نعرّف الآن l - 2 تسلسلًا فرعيًا منفصلا للحالة ‎σ‎، يبدأ التسلسل i ∈ {1,…,l-2}‎ في الموضع الثاني من المرحلة i + 1 وينتهي عند الموضع الأول للمرحلة i + 2. لتكن x الصفحة الأولى من المرحلة i +1. في بداية التسلسل i توجد صفحة x، إضافةً إلى k-1 صفحة مختلفة على الأكثر في ذاكرة التخزين المؤقت الخاصة بالخوارزميات غير المتصلة المثلى. في التسلسل i، هناك عدد k صفحة مطلوبة مختلفة عن x، لذا يتوجّب على الخوارزمية غير المتصلة المثلى إخلاء صفحة واحدة على الأقل في كل تسلسل. وبما أن ذاكرة التخزين المؤقت في المرحلة الأولى فارغة، فإنّ الخوارزمية غير المتصلة المثلى تجري k عملية إخلاء خلال المرحلة الأولى، وهذا يبيّن أنّ: wA(σ) ≤ l⋅k ≤ (k+l-2)k ≤ OPT(σ) ⋅ k خاصية 1.5: الخوارزميتان LRU و FWF خوارزميتان ذواتا k تنافسية قطعًا. تمرين: أثبت أنّ خوارزمية FIFO ليست خوارزميةً واسمة، وإنّما خوارزمية ذات k تنافسية قطعًا. لتكن A خوارزمية متصلة، إذا لم تكن هناك أيّ ثابتة r بحيث تكون هذه الخوارزمية ذات r تنافسية، نقول أنّ A غير تنافسية not competitive. خاصية 1.6: الخوارزميتان LFU و LIFO غير تنافُسيتان. الصفحة الأولى مطلوبة عدد l مرّة، وكذلك الصفحة 2، وهكذا دواليك. في النهاية، سيكون هناك (l-1) طلبًا للصفحة k و k +1. تملأ الخوارِزميتان LFU وLIFO صفحات المخزن المؤقت بـالصفحات من 1 إلى k، وتُخلى الصفحة k عند طلب الصفحة k +1، والعكس صحيح. هذا يعني أنّ كل طلب من التسلسل ‎‎(k,k+1)l-1 يخلي صفحة واحدة. أيضًا، هناك عدد k-1 فواتًا في ذاكرة التخزين المؤقت cache misses في أول استخدام للصفحات من 1 إلى (k-1)، وهذا يعني أنّ الخوارزميتَين LFU و LIFO تخليان k-1 +2 (l-1)‎‎ صفحة بالضبط. الآن، سنحاول أن نثبت أنّه لكل ثابتة ‎τ∈ℜ‎، ولكل ثابتة r ‎≤ 1، يوجد l يحقق: والذي يساوي: لتحقيق هذه المتراجحة، يكفي أن تختار عدد I كبيرًا بما يكفي، من ذلك نستنتج أنّ الخوارزميتَين LFU و LIFO غير تنافسيتَين. خاصية 1.7: لكل r <k، لا توجد أيّ خوارزمية حتمية متصلة ذات r تنافسية لحل مشكلة التصفيح r-competetive deterministic online algorithm for paging. برهان هذه الخاصية طويل نوعًا ما ويستند إلى فكرة أنّ LFD هي خوارزمية مثلى. السؤال الآن هو: هل هناك منظور أفضل؟ للإجابة على هذا السؤال سيكون علينا أن نتجاوز المنظور الحتمي deterministic approach ونستخدم المقاربات العشوائية في خوَارزميتنا، وهو أمر سنعود إليه لاحقًا في هذه السلسلة. ترجمة -بتصرّف- للفصول 21 و 23 و 24 و25 و26 و27 من كتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال التالي: خوارزميات الترتيب وأشهرها المقال السابق: خوارزميات تحليل المسارات في الأشجار مدخل إلى الخوارزميات تعلم بايثون تعلم PHP
  15. نستعرض في هذا المقال بعض أشهر الخوارزميات المستخدمة لتحليل المسارات في الأشجار، مثل خوارزمية بْرِم Prim وخوارزمية فلويد-وورشال Floyd-Warshall وخوارِزمية بلمان-فورد Bellman-Ford. خوارزمية برم Prim's Algorithm لنفترض أنّ لدينا 8 منازل، ونريد إعداد خطوط هاتفية بينها بأقل تكلفة ممكنة. لأجل ذلك سننشئ شجرةً حروفها تمثل المنازل، وأضلَاعها تمثّل تكلفة توصيل الخط الهاتفي بين منزلين. سنحاول وصل الخطوط بين جميع المنازل بأقل كلفة ممكنة، ولتحقيق ذلك سنستخدم خوارزمية برم Prim، وهي خوارزمية شرهة تبحث عن أصغر شجرة ممتدة spanning tree في مخطط موزون غير موجّه undirected weighted graph. وهذا يعني أنها تبحث عن مجموعة من الأضلاع التي تشكل شجرة تتضمّن كل العقد، بحيث يكون الوزن الإجمالي لجميع أضلاع الشجرة أقل ما يمكن. طُوِّرت هذه الخوارزمية عام 1930 من قبل عالم الرياضيات التشيكي Vojtěch Jarník، ثم أعاد عالم الحوسبة روبرت كلاي بْرِم اكتشافها ونشرها في عام 1957، وكذلك إيدجر ديكسترا في عام 1959. تُعرف أيضًا باسم خوارزمية DJP وخوارزمية Jarnik وخوارزمية Prim-Jarnik وخوارزمية Prim-Dijsktra. إذا كانت G مخططًا غير موجّه، فنقول أنّ المخطط S هو مخطط فرعي subgraph من G إذا كانت جميع حروفه وأضلَاعه تنتمي إلى G. ونقول أنّ S شجرة ممتدة فقط إذا كانت: تحتوي جميع عقد G. وكانت شجرة، أي أنّها لا تحتوي أيّ دورة cycle، وجميع عقدها متصلة. تحتوى (n-1) ضلعًا، حيث n يمثّل عدد العقد في G. يمكن أن يكون لمخطط ما أكثر من شجرة ممتدة واحدة، والشجرة الممتدة الصغرى لمخطط موزون غير موجّه هي شجرة مجموع أوزان أضلاعها أقل ما يمكن. سنستخدم خوارزمية Prim لإيجاد الشجرة الممتدة الصغرى، وسيساعدنا هذا على حل مشكلة الخطوط الهاتفية في المثال أعلاه. أولاً، سنختار عقدةً ما لتكون العقدة المصدرية source node، ولتكن العقدة 1 مثلًا. سنضيف الآن الضلع الذي ينطلق من 1 وله أقل كلفة إلى المخطط الفرعي، ونلوّن الأضلاع التي أضفناها إلى المخطط الفرعي باللون الأزرق. وسيكون الضلع 1-5 في مثالنا هو الضلع الذي له أقل كلفة. الآن، سنأخذ الضلع الأقل كلفة من بين جميع الأضلاع المُنطلِقة من العقدة 1 أو العقدة 5. وبما أنّنا لوّنّا 1-5 سلفًا، فالضلع المرشّح الثاني هو الضلع 1-2. نأخذ هذه المرة الضلع الأقل كلفةً من بين جميع الأضلاع (غير المُلوّنة) المُنطلقة من العقدة 1 أو العقدة 2 أو العقدة 5، والذي هو في حالتنا 5-4. تحتاج الخطوة التالية إلى تركيز، وفيها سنحاول أن نفعل ما فعلناه سابقا، ونختار من بين جميع الأضلاع (غير المُلوّنة) التي تنطلق من العقدة 1 أو 2 أو 5 أو 4 الضلع الذي له أقل كلفة، وهو 2-4. ولكن انتبه إلى أنّه في حال اختيار هذا الضلع، فسنخلُق دورةً cycle في المخطط الفرعي، ذلك أنّ العقدتين 2 و4 موجودتان سلفًا فيه، لذا فإنّ أخذ الضلع 2-4 ليس الخيار الصحيح. وبدلًا من ذلك سنختار الضلع 4-8. إذا واصلنا على هذا النحو فسنختَار الأضلاع 8-6 و6-7 و4-3، وسيبدو المخطط الفرعي النهائي هكذا: إذا أزلنا الأضلاع التي لم نَخترها (غير المُلوّنة)، فسنحصل على: هذه هي الشجرة الممتدة الصغرى MST التي نبحث عنها، ونستنتج منها أنّ تكلفة إعداد خطوط الهاتف هي: 4 + 2 + 5 + 11 + 9 + 2 + 1 = 34. يمكن أن تكون هناك عدة أشجار ممتدة صغرى للمخطط نفسه بحسب العقدة المصدرية التي اخترناها. انظر شيفرة توضيحية لهذه الخوارزمية، حيث تمثل Graph مخططًا فارغًا متصلًا غير موزون، وتمثل Vnew مخططًا فرعيًا جديدًا عقدته المصدرية هي x: Procedure PrimsMST(Graph): Vnew[] = {x} Enew[] = {} while Vnew is not equal to V u -> a node from Vnew v -> a node that is not in Vnew such that edge u-v has the minimum cost // إذا كان لعقدتين الوزن نفسه، فاختر أيًّا منهما add v to Vnew add edge (u, v) to Enew end while Return Vnew and Enew التعقيد التعقيد الزمني للمنظور البسيط أعلاه هو ( O (V 2‎، يمكننا تقليل التعقيد باستخدام طابور أولويات priority queue، فإذا أضفنا عقدةً جديدةً إلى المخطط الفرعي الجديد Vnew، فيمكننا إضافة أضلاعها المجاورة إلى رتل الأولويات، ثم إخراج الضلع الموزون ذو الكلفة الأدنى منه. وحينئذ يصبح التعقيد مساويًا للقيمة O (ElogE)‎‎، حيث يمثّل E عدد الأضلاع. يمكنك أيضًا إنشاء كَومة ثنائية Binary Heap لتخفيض التعقيد إلى O (ElogV)‎‎. هذه شيفرة توضيحية تستخدم طابور الأولويات: Procedure MSTPrim(Graph, source): for each u in V key[u] := inf parent[u] := NULL end for key[source] := 0 Q = Priority_Queue() Q = V while Q is not empty u -> Q.pop for each v adjacent to i if v belongs to Q and Edge(u,v) < key[v] // edge(u, v) كلفة Edge(u, v) تمثل parent[v] := u key[v] := Edge(u, v) end if end for end while تخزّن key[]‎‎ الكلفة الأقل لعبور العقدة v، فيما تُستخدم parent[]‎‎ لتخزين العقدة الأصلية parent node، وهذا مفيد لتسلّق الشجرة وطباعتها. فيما يلي برنامج بسيط بلغة Java: import java.util.*; public class Graph { private static int infinite = 9999999; int[][] LinkCost; int NNodes; Graph(int[][] mat) { int i, j; NNodes = mat.length; LinkCost = new int[NNodes][NNodes]; for ( i=0; i < NNodes; i++) { for ( j=0; j < NNodes; j++) { LinkCost[i][j] = mat[i][j]; if ( LinkCost[i][j] == 0 ) LinkCost[i][j] = infinite; } } for ( i=0; i < NNodes; i++) { for ( j=0; j < NNodes; j++) if ( LinkCost[i][j] < infinite ) System.out.print( " " + LinkCost[i][j] + " " ); else System.out.print(" * " ); System.out.println(); } } public int unReached(boolean[] r) { boolean done = true; for ( int i = 0; i < r.length; i++ ) if ( r[i] == false ) return i; return -1; } public void Prim( ) { int i, j, k, x, y; boolean[] Reached = new boolean[NNodes]; int[] predNode = new int[NNodes]; Reached[0] = true; for ( k = 1; k < NNodes; k++ ) { Reached[k] = false; } predNode[0] = 0; printReachSet( Reached ); for (k = 1; k < NNodes; k++) { x = y = 0; for ( i = 0; i < NNodes; i++ ) for ( j = 0; j < NNodes; j++ ) { if ( Reached[i] && !Reached[j] && LinkCost[i][j] < LinkCost[x][y] ) { x = i; y = j; } } System.out.println("Min cost edge: (" + + x + "," + + y + ")" + "cost = " + LinkCost[x][y]); predNode[y] = x; Reached[y] = true; printReachSet( Reached ); System.out.println(); } int[] a= predNode; for ( i = 0; i < NNodes; i++ ) System.out.println( a[i] + " --> " + i ); } void printReachSet(boolean[] Reached ) { System.out.print("ReachSet = "); for (int i = 0; i < Reached.length; i++ ) if ( Reached[i] ) System.out.print( i + " "); //System.out.println(); } public static void main(String[] args) { int[][] conn = {{0,3,0,2,0,0,0,0,4}, // 0 {3,0,0,0,0,0,0,4,0}, // 1 {0,0,0,6,0,1,0,2,0}, // 2 {2,0,6,0,1,0,0,0,0}, // 3 {0,0,0,1,0,0,0,0,8}, // 4 {0,0,1,0,0,0,8,0,0}, // 5 {0,0,0,0,0,8,0,0,0}, // 6 {0,4,2,0,0,0,0,0,0}, // 7 {4,0,0,0,8,0,0,0,0} // 8 }; Graph G = new Graph(conn); G.Prim(); } } صرّف الشيفرة أعلاه باستخدام التعليمة ‎javac Graph.java‎، وسيكون الخرج الناتج كما يلي: $ java Graph * 3 * 2 * * * * 4 3 * * * * * * 4 * * * * 6 * 1 * 2 * 2 * 6 * 1 * * * * * * * 1 * * * * 8 * * 1 * * * 8 * * * * * * * 8 * * * * 4 2 * * * * * * 4 * * * 8 * * * * ReachSet = 0 Min cost edge: (0,3)cost = 2 ReachSet = 0 3 Min cost edge: (3,4)cost = 1 ReachSet = 0 3 4 Min cost edge: (0,1)cost = 3 ReachSet = 0 1 3 4 Min cost edge: (0,8)cost = 4 ReachSet = 0 1 3 4 8 Min cost edge: (1,7)cost = 4 ReachSet = 0 1 3 4 7 8 Min cost edge: (7,2)cost = 2 ReachSet = 0 1 2 3 4 7 8 Min cost edge: (2,5)cost = 1 ReachSet = 0 1 2 3 4 5 7 8 Min cost edge: (5,6)cost = 8 ReachSet = 0 1 2 3 4 5 6 7 8 0 --> 0 0 --> 1 7 --> 2 0 --> 3 3 --> 4 2 --> 5 5 --> 6 1 --> 7 0 --> 8 خوارزمية بلمان- فورد Bellman–Ford خوارزمية بِلمان - فورد ‏Bellman–Ford هي خوارزمية تحاول حساب أقصر المسارات من رأس مصدري source vertex إلى جميع الحروف الأخرى في مخطط موجّه موزون، ورغم أنّ هذه الخوارزمية أبطأ من خوارزمية Dijkstra، إلا أنها تعمل في الحالات التي تكون فيها أوزان الأضلاع سالبة، كما أنّها قادرة على العثور على الدورات ذات الوزن السالب في المخطط، على خلاف خوارزمية Dijkstra التي لا تعمل في حال كانت هناك دورة سالبة، إذ ستستمر في المرور عبر الدورة مرارًا وتكرارًا، وتستمر أيضًا في تقليل المسافة بين الرأسيْن. تتمثل فكرة هذه الخوارزمية في المرور عبر جميع أضلاع المخطط واحدًا تلو الآخر بترتيب عشوائي، شرط أن يحقق الترتيب المعادلة التالية: بعد تحديد الترتيب، سنخفّف الضلع -تخفيف الضلع هو تخفيض تكلفة الوصول إلى رأس معيّن عبر استخدام رأس آخر وسيط- وفقًا لصيغة التخفيف التالية. لكل ضلع u-v من u إلى v: if distance[u] + cost[u][v] < d[v] d[v] = d[u] + cost[u][v] بمعنى أنّه إذا كانت المسافة من المصدر إلى أيّ رأس u + وزن الضلع u-v < المسافة من المصدر إلى رأس ٍآخر v، فسنحدّث المسافة من المصدر إلى v. نحتاج إلى تخفيف الأضلاع (V-1) مرّة على الأكثر، حيث V هو عدد الأضلاع في المخطط. سنشرح لاحقًا لماذا اخترنا العدد (V-1)، ونخزّن أيضًا الرأس الأب parent vertex الخاص بكل الرأس، ونكتب ما يلي في كل مرّة نخفّف ضلعًا: parent[v] = u هذا يعني أننا وجدنا مسارًا آخر أقصر للوصول إلى v عبر u. سنحتاج أيضًا هذه القيمة المُخزّنة لاحقًا لطباعة أقصر مسار من المصدر إلى الرأس المنشود. انظر المثال التالي: لقد اخترنا 1 ليكون الرأس المصدري، والآن نريد العثور على أقصر مسار من هذا المصدر إلى جميع الحروف الأخرى. نكتب في البداية d[1] = 0‎‎، لأنّ 1 هو المصدر؛ أما البقيّة فستساوي اللانهاية لأنّنا لا نعرف مسافاتها بعد. سنخفّف الأضلاع في هذا التسلسل: +--------+--------+--------+--------+--------+--------+--------+ | التسلسل | 1 | 2 | 3 | 4 | 5 | 6 | +--------+--------+--------+--------+--------+--------+--------+ | الضلع | 4->5 | 3->4 | 1->3 | 1->4 | 4->6 | 2->3 | +--------+--------+--------+--------+--------+--------+--------+ يمكنك أن تختار أيّ تسلسل تريد، إذا خفّفنا الأضلاع مرّةً واحدة، فسنحصل على المسافات من المصدر إلى جميع الرؤوس الأخرى للمسار الذي يستخدم ضلعًا واحدًا على الأكثر. لنخفّف الآن الأضلاع ونحدث قيم d[]‎‎: d[4] + cost[4][5] = infinity + 7 = infinity. لا يمكننا تحديث هذا. d[2] + cost[3][4] = infinity. لا يمكننا تحديث هذا. d[1] + cost[1][3] = 0 + 2 = 2 < d[2]‎‎ إذًا d[3] = 2 و parent[1] = 1. d[1] + cost[1][4] = 4. إذن d[4] = 4 < d[4]. parent[4] = 1. d[4] + cost[4][6] = 9. d[6] = 9 < d[6]. parent[6] = 4. d[2] + cost[2][3] = infinity لا يمكننا تحديث هذا. تعذّر تحديث بعض الرؤوس نتيجة عدم تحقّق الشرط ‎d[u‎] + cost[u‎][v] < d[v]‎. وكما قلنا سابقًا، فقد حصلنا على المسارات من المصدر إلى العقد الأخرى باستخدام ضلع واحد على الأكثر. سيزوّدنا التكرار الثاني بمسار يستخدم عقدتين: d[4] + cost[4][5] = 12 < d[5]. d[5] = 12. parent[5] = 4. d[3] + cost[3][4] = 1 < d[4]. d[4] = 1. parent[4] = 3 d[3] تبقى بلا تغيير. d[4] تبقى بلا تغيير d[4] + cost[4][6] = 6 < d[6]. d[6] = 6. parent[6] = 4. d[3] تبقى بلا تغيير. هكذا سيبدو المخطط: التكرار الثالث سيحدّث الرأس 5 فقط، حيث سيضع القيمة 8 في d[5]‎‎. وسيبدو المخطط هكذا: بعد هذا، ستبقى المسافة كما هي مهما زِدنا من التكرارات، لذلك سنخزّن راية flag للتحقق من وقوع أيّ تحديث أم لا، فإذا لم يقع أيّ تحديث، أوقفنا حلقة التكرار. هذه شيفرة توضيحية: Procedure Bellman-Ford(Graph, source): n := number of vertices in Graph for i from 1 to n d[i] := infinity parent[i] := NULL end for d[source] := 0 for i from 1 to n-1 flag := false for all edges from (u,v) in Graph if d[u] + cost[u][v] < d[v] d[v] := d[u] + cost[u][v] parent[v] := u flag := true end if end for if flag == false break end for Return d لتتبع الدورات السالبة، سنعدّل الشيفرة كما يلي: Procedure Bellman-Ford-With-Negative-Cycle-Detection(Graph, source): n := number of vertices in Graph for i from 1 to n d[i] := infinity parent[i] := NULL end for d[source] := 0 for i from 1 to n-1 flag := false for all edges from (u,v) in Graph if d[u] + cost[u][v] < d[v] d[v] := d[u] + cost[u][v] parent[v] := u flag := true end if end for if flag == false break end for for all edges from (u,v) in Graph if d[u] + cost[u][v] < d[v] Return "Negative Cycle Detected" end if end for Return d لأجل طباعة أقصر مسار إلى رأس معين، سنكّرر خلفيًا إلى الأب parent إلى أن نعثر على القيمة المعدومة NULL، ثمّ نطبع الحروف. انظر الشيفرة التوضيحية التالية: Procedure PathPrinting(u) v := parent[u] if v == NULL return PathPrinting(v) print -> u سيساوي التعقيد الزمني لهذه الخوارزمية O (V * E)‎‎ إن استخدمنا قائمة تجاور adjacency list بما أننا سنحتاج إلى تخفيف الأضلاع بحد أقصى (V-1) مرة، حيث تشير E إلى عدد الأضلاع؛ أما إن استخدمنا مصفوفة تجاور لتمثيل المخطط، فسيكون التعقيد الزمني O (V ^ 3‎‎)‎‎، ذلك أننا سنستطيع التكرار على جميع الأضلاع عند استخدام قائمة التجاور خلال زمن قدره O(E)‎‎، بينما نستغرق زمنًا قدره O (V ^ 2) ‎‎ إن استخدمنا مصفوفة التجاور. رصد الدورات السالبة في المخططات نستطيع رصد أي دورة سالبة في المخطط باستخدام خوارزمية بِلمَن-فورد Bellman-Ford. ونحن نعرف أنه يجب تخفيف جميع أضلاع المخطط عدد (V-1) مرة من أجل العثور على أقصر مسار، حيث تمثل V عدد الرؤوس في المخطط، وقد رأينا أنه لا يمكن تحديث d[]‎‎ مهما كان عدد التكرارات التي أجريناها. إذا كانت هناك دورة سالبة في المخطط، فسيمكننا تحديث d[]‎‎ حتى بعد التكرار (V-1)، ذلك أن كل تكرار، سيقلل العبور خلال دورة سالبة تكلفةَ المسار الأقصر، وهذا سبب أن خوارزمية بِلمَن فورد تحد عدد التكرارات بـ (V-1) مرة، لأننا سنعلق داخل حلقة أبدية لا تنتهي إن استخدمنا خوارزمية ديكسترا. سنركز الآن على كيفية إيجاد دورة سالبة، انظر المخطط التالي: لنختر الرأس1 ليكون المصدر، بعد تطبيق خوارزمية بلمان-فورد لأقصر مسار ذي مصدر وحيد على المخطط، سنجد المسافات التي تفصل المصدر عن جميع الرؤوس الأخرى. انظر: هكذا سيبدو المخطط بعد ‎‎(V-1) = 3 تكرار، وينبغي أن تكون هذه هي النتيجة الصحيحة، لأنّنا سنحتاج 3 تكرارات على الأكثر للعثور على أقصر مسار ما دامت هناك 4 أضلاع في المخطط، لذا إمّا أنّ هذه هي الإجابة الصحيحة، وإمّا أنّ هناك دورة ذات وزن سالب في المخطط. ولنعرف ذلك، سنضيف تكرارًا جديدًا بعد التكرار (V-1)، فإذا استمرت المسافة في الانخفاض، فذلك يعني أنّ هناك دورة سالبة في المخطط. ولهذا المثال فإنه بالنسبة للضلع 2-3، تكون نتيجة d[2] + cost [2] [3]‎‎ مساوية لـ 1، وهو ‎‎ أقل من d[3]‎‎، لذا يمكننا أن نستنتج أنّ هناك دورةً سالبةً في المخطط. والسؤال الآن هو كيف نجد هذه الدورة السالبة؟ سنجري تعديلًا بسيطًا على خوارزمية Bellman-Ford للعثور على الدورة السالبة: Procedure NegativeCycleDetector(Graph, source): n := number of vertices in Graph for i from 1 to n d[i] := infinity end for d[source] := 0 for i from 1 to n-1 flag := false for all edges from (u,v) in Graph if d[u] + cost[u][v] < d[v] d[v] := d[u] + cost[u][v] flag := true end if end for if flag == false break end for for all edges from (u,v) in Graph if d[u] + cost[u][v] < d[v] Return "Negative Cycle Detected" end if end for Return "No Negative Cycle" بهذه الطريقة نستطيع التحقق مما إذا كانت هناك أيّ دورة سالبة في المخطط، كذلك نستطيع تعديل خوارزمية Bellman-Ford لتخزين الدورات السالبة وحفظ سجل لها. لماذا نحتاج إلى تخفيف جميع الأضلاع بحد أقصى (V-1) مرة نحتاج إلى تخفيف جميع أضلاع المخطط في خوارزمية Bellman-Ford للعثور على أقصر مسار، وتُكرّر هذه العملية (V-1) مرّةً بحد أقصى، حيث يمثل V عدد الرؤوس في المخطط. ويعتمد عدد التكرارات اللازمة للعثور على أقصر مسار من المصدر إلى جميع الرؤوس الأخرى على الترتيب الذي اخترناه لتخفيف الأضلاع. انظر المثال التالي: لقد اخترنا الرأس 1 ليكون المصدر، سنبحث عن أقصر مسافة تفصل بين المصدر وجميع الحروف الأخرى. ونستطيع رؤية أننا سنحتاج في أسوأ الأحوال إلى (V-1) ضلع للوصول إلى الرأس 4، ووفقًا للترتيب الذي اكتُشفت من خلاله الأضلاع، فقد نحتاج إلى (V-1) مرة. للتوضيح، سنستخدم خوارزمية Bellman-Ford في مثال توضيحي للعثور على أقصر مسار. انظر التسلسل التالي: +--------+--------+--------+------+ | التسلسل | 1 | 2 | 3 | +--------+--------+--------+------+ | الضلع | 3->4 | 2->3 | 1->2 | +--------+--------+--------+------+ في التكرار الأول: d[3] + cost[3][4]‎‎ = infinity لن يحدث أيّ تغيير. d[2] + cost[2][3]‎‎ = infinity لن يحدث أيّ تغيير. d[1] + cost[1][2] = 2 < d[2]. d[2] = 2. parent[2] = 1. لاحظ أنّ عملية التخفيف لم تغيّر إلا قيمة d[2]‎‎ فقط. سيبدو المخطط خاصتنا هكذا: التكرار الثاني: d[3] + cost[3][4]‎‎ = infinity لن يحدث أيّ تغيير. d[2] + cost[2][3] = 5 < d[3]. d[3] = 5. parent[3] = 2. لن يحدث أيّ تغيير. غيّرت عمليةُ التخفيف قيمةَ d[3]‎‎ في هذه المرّة. وسيبدو المخطط هكذا: التكرار الثالث: d[3] + cost[3][4] = 7 < d[4] . d[4] = 7 . parent[4] = 3 . لن يحدث أيّ تغيير. لن يحدث أيّ تغيير. وجدنا في التكرار الثالث أقصر مسار إلى 4 من المصدر 1، حيث سيبدو المخطط هكذا: احتجنا إلى 3 تكرارات للعثور على أقصر مسار. ستظل قيمة d[]‎‎ كما هي بعد ذلك مهما حاولنا تخفيف الأضلاع. إليك تسلسلًا آخر: +--------+--------+--------+------+ | التسلسل | 1 | 2 | 3 | +--------+--------+--------+------+ | الضلع | 1->2 | 2->3 | 3->4 | +--------+--------+--------+------+ سنحصل على: d[1] + cost[1][2] = 2 < d[2] . d[2] = 2 . d[2] + cost[2][3] = 5 < d[3]. d[3] = 5. d[3] + cost[3][4] = 7 < d[4]. d[4] = 5. وجدنا أقصر مسار من المصدر إلى جميع العقد الأخرى منذ التكرار الأول، يمكننا إجراء التسلسلات الإضافية 1-> 2 و3-> 4 و 2-> 3، والتي ستعطينا أقصر مسار بعد تكرارين. يقودنا هذا إلى استنتاج أنه مهما كان ترتيب التسلسل، فلن نحتاج أكثر من 3 تكرارات للعثور على أقصر مسار من المصدر. نستنتج أيضًا أننا قد لا نحتاج في بعض الحالات إلّا إلى تكرار واحد فقط للعثور على أقصر مسار من المصدر. وسنحتاج في أسوأ الحالات إلى (V-1) تكرار. أرجو أن تكون قد فهمت الآن لماذا ينبغي أن نكرّر عملية التخفيف (V-1) مرّة. خوارزمية فلويد وورشال Floyd-Warshall تُستخدم خوارزمية فلويد وورشال Floyd-Warsha ll لإيجاد أقصر المسارات في مخطط موزون قد تكون أوزان أضلاعه موجبة أو سالبة. عند تنفيذ الخوارزمية مرّةً واحدة، سنحصل على أطوال -مجاميع أوزان- أقصر المسارات بين كل أزواج الرؤوس، ويمكنها -بقليل من التعديل- طباعة أقصر مسار، كما يمكنها رصد الدورات السالبة في المخطط. وخوارزمية Floyd- Warshall هي من خوارزميات البرمجة الديناميكية. لنطبق هذه الخوارزمية على المخطط التالي: أول شيء نفعله هو أخذ مصفوفتين ثنائيتَي الأبعاد لتكونا مصفُوفتي تجاور adjacency matrices يساوي حجماهما العدد الإجمالي للرؤوس. وفي مثالنا، سنأخذ مصفوفتين من الحجم 4*4، الأولى هي مصفوفة المسافات Distance Matrix، وسنخزّن فيها أقصر مسافة عثرنا عليها حتى الآن بين رأسين. بدايةً، إذا كان هناك ضلع بين u-v وكانت المسافة / الوزن = w، فإننا نضع ‎distance[u‎][v] = w‎، وسنضع قيمة ما لا نهاية للأضلاغ غير الموجودة. المصفوفة الثانية هي مصفوفة المسارات Path Matrix، ونستخدمها لتوليد أقصر مسار بين رأسين، لذا إذا كان هناك مسار بين u وv، فسنضع ‎path[u‎][v] = u‎، وهذا يعني أنّ أفضل طريقة للوصول إلى الرأس v انطلاقًا من u هو باستخدام الضلع الذي يربط v بـ u، وإذا لم يكن هناك مسار بين الرأسين، فسنعطيه القيمة N كناية على عدم وجود مسار متاح حاليًا. سيبدو جدولا المخطط كما يلي: +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | | 1 | 2 | 3 | 4 | | | 1 | 2 | 3 | 4 | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | 1 | 0 | 3 | 6 | 15 | | 1 | N | 1 | 1 | 1 | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | 2 | inf | 0 | -2 | inf | | 2 | N | N | 2 | N | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | 3 | inf | inf | 0 | 2 | | 3 | N | N | N | 3 | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | 4 | 1 | inf | inf | 0 | | 4 | 4 | N | N | N | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ المسار المسافة وقد وضعنا الرأس N في المواضع القطرية diagonals في مصفوفة المسارات نظرًا لعدم وجود حلقة loop، ولما كانت المسافة من كل رأس إلى نفسه تساوي 0، فقد وضعنا القيمة 0 في المواضع القطرية في مصفوفة المسافات. سنختار رأسًا وسطيًا k لأجل تطبيق خوارزمية Floyd-Warshall، وسنتحق بعد ذلك لكلّ رأس i مما إن كنا نستطيع الانتقال من i إلى k ثم من k إلى j، حيث j يمثّل رأسًا آخر، لتقليل تكلفة الانتقال من i إلى j. إذا كانت المسافة distance [j]‎‎ أكبر من المسافة distance [k] + distance[k] [j]‎‎، فسنحدّث قيمة distance [j]، ونضع فيها مجموع هاتين المسافتين، كما سنحدّث path [j]‎‎ ونعطيها القيمة path[k] [j]‎‎، لأنّ الانتقال من i إلى k، ثم من k إلى j أفضل. كما ستٌختار جميع الرؤوس بنفس طريقة اختيار k. وهكذا نحصل على 3 حلقات متداخلة: k من 1 إلى 4. i من 1 إلى 4. j من 1 إلى 4. هذه شيفرة توضيحية لذلك: if distance[i][j] > distance[i][k] + distance[k][j] distance[i][j] := distance[i][k] + distance[k][j] path[i][j] := path[k][j] end if السؤال الآن هو: لكل زوج u,v من الرؤوس، هل يوجد رأس يمكن أن نمرّ عبره لتقصير المسافة بين u وv؟ سيكون العدد الإجمالي لعمليات المخطط هو 4 * 4 * 4 = 64. وهذا يعني أنّنا سنجري هذا الاختبار 64 مرة، لنلقي نظرةً على بعض هذه الحالات: إذا كانت k = 1 وi = 2 وj = 3، فإن [distance [j ستساوي -2، وهي ليست أكبر من distance [k] + distance[k] [j] = -2 + 0 = -2‎‎، لذلك ستبقى دون تغيير. ومرةً أخرى، إذا كانت k = 1 وi = 4 وj = 2، فستكون distance [j] = infinity‎‎، وهي أكبر من distance [k] + distance[k] [j] = 1 + 3 = 4‎‎‎‎‎‎، لذا نضع distance [j] = 4‎‎، وpath [j] = path[k] [j] = 1‎‎. وهذا يعني أنّه للانتقال من الرأس 4 إلى 2، فإنّ المسار 4-> 1-> 2 يكون أقصر من المسار الحالي. انظر هذا الرابط الأجنبي للاطلاع على حسابات كل خطوة، وستبدو مصفوفتنا كما يلي بعد إجراء التعديلات اللازمة. بعد إجراء التغييرات اللازمة، ستبدو المصفوفتان كما يلي: +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | | 1 | 2 | 3 | 4 | | | 1 | 2 | 3 | 4 | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | 1 | 0 | 3 | 1 | 3 | | 1 | N | 1 | 2 | 3 | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | 2 | 1 | 0 | -2 | 0 | | 2 | 4 | N | 2 | 3 | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | 3 | 3 | 6 | 0 | 2 | | 3 | 4 | 1 | N | 3 | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ | 4 | 1 | 4 | 2 | 0 | | 4 | 4 | 1 | 2 | N | +-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+ المسار المسافة وهكذا نكون قد حصلنا على مصفوفةِ أقصر المسافات، فمثلًا، تكون أقصر مسافة من 1 إلى 4 هي 3، وأقصر مسافة من 4 إلى 3 هي 2. انظر إلى الشيفرة التوضيحية التالية، حيث تمثل V عدد الرؤوس: Procedure Floyd-Warshall(Graph): for k from 1 to V for i from 1 to V for j from 1 to V if distance[i][j] > distance[i][k] + distance[k][j] distance[i][j] := distance[i][k] + distance[k][j] path[i][j] := path[k][j] end if end for end for end for سنتحقق من مصفوفة المسارات Path من أجل طباعة المسار، ولطباعة المسار من u إلى v، سنبدأ من path[u‎] [v]‎‎. ثمّ نستمر في تغيير v = path[u‎] [v]‎‎ إلى أن نحصل على path[u‎] [v] = u‎‎، وندفع كل قيم path[u‎] [v]‎‎ إلى مكدّس. بعد العثور على الرأس u، سنطبع u ونبدأ بإخراج العناصر من المكدّس وطباعتها. هذا الأمر ممكن لأنّ مصفوفة المسارات تخزّن قيمة الرأس الذي يشارك في أقصر مسار إلى v انطلاقًا من أيّ عقدة أخرى. انظر إلى الشيفرة التوضيحية لذلك: Procedure PrintPath(source, destination): s = Stack() S.push(destination) while Path[source][destination] is not equal to source S.push(Path[source][destination]) destination := Path[source][destination] end while print -> source while S is not empty print -> S.pop end while من أجل التحقق من وجود دورة سالبة، سيكون علينا التحقق من القطر diagonal الرئيسي لمصفوفة المسافات، وإذا كانت أيٌّ من القيم القطرية diagonal سالبة، فهذا يعني وجود دورة سالبة في المخطط. إن تعقيد خوارزمية فلويد-وورشال Floyd-Warshall هو O (V3)‎‎، وتعقيد المساحة هو: O (V2)‎‎. ترجمة -بتصرّف- للفصول 19 و20 و22 من كتاب Algorithms Notes for Professionals. اقرأ أيضًا المقال السابق: تطبيقات الخوارزميات الشرهة المرجع الشامل إلى تعلم الخوارزميات للمبتدئين دليل شامل عن تحليل تعقيد الخوارزمية مدخل إلى الخوارزميات خوارزمية تحديد المسار النجمية A* Pathfinding خوارزمية ديكسترا Dijkstra’s Algorithm
×
×
  • أضف...