أطول لاحقة مشتركة: Python, C++ مثال

⚡ ملخص ذكي

تُحدد أطول سلسلة فرعية مشتركة أطول نمط عناصر مُرتبة مُشترك بين سلسلتين نصيتين دون اشتراط وجود أحرف مُتجاورة. يُعد هذا الأسلوب الكلاسيكي في البرمجة الديناميكية أساسًا لأدوات المقارنة، ومحاذاة الحمض النووي، والتحكم في الإصدارات، وذلك من خلال مُقارنة التسلسلات بكفاءة في وقت متعدد الحدود.

  • 📘 المبدأ الأساسي: تقوم دالة Longest Common Subsequence بإرجاع أطول مجموعة مرتبة من الأحرف التي تظهر في كلتا سلسلتي الإدخال مع الحفاظ على ترتيبهما النسبي الأصلي.
  • 🐢 النهج الساذج: تقوم القوة الغاشمة بحصر كل تسلسل فرعي من السلسلة الأولى وفحصه مقابل السلسلة الثانية، وتعمل في وقت أسي O(n·2^m).
  • 🔁 الطريقة التكرارية: تُطابق القاعدة التكرارية الأحرف الأخيرة أو تُكرر نفسها على سلاسل فرعية أصغر، ولكنها تُعيد حساب التداخل.ping تتكرر المشاكل الفرعية بشكل متكرر.
  • 🧮 البرمجة الديناميكية: يقوم جدول البرمجة الديناميكية ثنائي الأبعاد بتخزين نتائج المسائل الفرعية، مما ينتج عنه حل نظيف من رتبة O(m·n) مع مساحة مساعدة من رتبة O(m·n).
  • 🐍 التغطية اللغوية: اكتملت Python و C++ توضح التطبيقات كلاً من الأساس التكراري وجدول البرمجة الديناميكية المخزن مؤقتًا للاستخدام العملي.
  • 🌐 تطبيقات عملية: تدعم أداة Longest Common Subsequence أدوات مقارنة البيانات، ومدققات الانتحال، ومصححات الإملاء، ومحاذاة تسلسل المعلوماتية الحيوية عبر الحمض النووي والبروتينات.

أطول نتيجة مشتركة

ما هو أطول لاحقة مشتركة؟

تعني مسألة إيجاد أطول سلسلة فرعية مشتركة (LCS) أنه سيتم إعطاؤك سلسلتين نصيتين أو نمطين أو تسلسلين من العناصر. من بين هذين التسلسلين أو السلسلتين، عليك إيجاد أطول سلسلة فرعية من العناصر بنفس الترتيب الموجودة في كلتا السلسلتين أو النمطين.

مثال

على سبيل المثال، لدينا سلسلتان نصيتان. لنفترض أن:

Pattern_1 = "RGBGARGA"
Pattern_2 = "BGRARG"

  • انطلاقاً من النمط 1، يمكن إنتاج تسلسلات مثل "RGB" و"RGGA" و"RGAR". لإنشاء تسلسل، يجب الحفاظ على الموضع النسبي لكل حرف في السلسلة.
  • انطلاقاً من النمط الثاني، يمكننا إنتاج متواليات مثل "BGR" و"BRAG" و"RARG". ويمكن إنتاج المتواليات طالما حافظت على الموضع النسبي للسلسلة الأصلية.

مصطلح الوضع النسبي يعني النظام.

على سبيل المثال، يُعد التسلسل "BRG" تسلسلاً صحيحاً لأن الحرف "B" ظهر أولاً، ثم "R"، ثم "G" في النمط الأصلي pattern_2. أما إذا كان التسلسل "RBRG"، فهو غير صحيح، لأن الحرف "B" ظهر أولاً في النمط الأصلي (pattern_2).

أمثلة على سلاسل نصية تتضمن أطول تسلسل فرعي مشترك

لدينا خياران للعثور على أطول تسلسل مشترك من التسلسلين أو المصفوفتين المحددتين.

  • طريقة ساذجة
  • حل البرمجة الديناميكية: يُعرف أيضًا أطول تسلسل مشترك باسم LCS.

الحل البسيط يتطلب وقتاً أطول ولا يُعدّ الحل الأمثل. باستخدام حل البرمجة الديناميكية، نتغلب على مشكلة التعقيد.

طريقة ساذجة

تُعدّ الطريقة البسيطة أسلوبًا سهلًا لحلّ المشكلة، بغض النظر عن تعقيد الوقت وعوامل التحسين الأخرى. وهي تعتمد في أغلب الأحيان على "التجربة والخطأ"، وحلقات تكرارية متعددة، واستدعاءات متكررة. ويُقصد بمصطلح "التجربة والخطأ" استعراض جميع الأنماط الممكنة لمشكلة معينة.

مثال

من المثال أعلاه للنمط 1 والنمط 2، لنفترض أن النمط 1 يبلغ طوله m والنمط 2 يبلغ طوله n. للتحقق من كل حالة محتملة، نحتاج إلى تقييم كل تسلسل لاحق محتمل للنمط 1 مع النمط 2.

إليك سلسلة بسيطة من أربعة أحرف "ABCD". على سبيل المثال، نحتاج إلى إنشاء تسلسل من "ABCD". يمكننا إما أخذ حرف أو عدم أخذه. هذا يعني أنه لكل حرف لدينا خياران:

  • سيتم إضافة الحرف إلى اللاحقة.
  • لن يتم إضافة الحرف إلى اللاحقة.

هنا تظهر الصور جميع التسلسلات التي يمكننا صنعها من السلسلة "ABCD".

تسلسلات الطريقة البسيطة لـ ABCD

تسلسل بحرف واحد:

طريقة السذاجة في تسلسل الأحرف المفردة

تسلسلات مكونة من حرفين:

طريقة بسيطة لتسلسلات الأحرف

تسلسلات مكونة من حرفين:

طريقة بسيطة لتسلسل ثلاثة أحرف

من الرسم البياني أعلاه، نجد 14 متتالية. إذا لم نأخذ أي حروف، أي سلسلة فارغة، يصبح إجمالي المتتاليات 15. علاوة على ذلك، فإن السلسلة "ABCD" نفسها تُعدّ متتالية. لذا، يصبح إجمالي المتتاليات 16.

لذا، من الممكن توليد 2^4 أو 16 متتالية فرعية من "ABCD". ثم، سلسلة بطول m سيكون لها تسلسل فرعي إجمالي قدره 2^m.

لكل تسلسل فرعي، نحتاج إلى التحقق منه للنمط الثاني بأكمله. سيستغرق ذلك وقتًا مقداره O(n). يشير O(n) إلى دالة التعقيد التي تحسب الوقت المستغرق للتنفيذ.

لذا، يصبح التعقيد الزمني الإجمالي O(n*2^m). بالنسبة للمثال الذي رأيناه أعلاه، فإن قيمة m = 8 و n = 5.

فيما يلي خطوات الطريقة الساذجة:

الخطوة 1) خذ تسلسلاً من النمط 1.
الخطوة 2) قم بمطابقة التسلسل من الخطوة 1 مع النمط 2.
الخطوة 3) إذا كان مطابقًا، فقم بحفظ التسلسل اللاحق.
الخطوة 4) إذا بقيت المزيد من التسلسلات في النمط 1، فانتقل إلى الخطوة 1 مرة أخرى.
الخطوة 5) طباعة أطول تسلسل.

البنية التحتية المثلى

يشير مصطلح البنية الفرعية المثلى إلى إمكانية إيجاد الحل الأمثل من خلال حل المسائل الفرعية. على سبيل المثال، في المثال أعلاه، لدينا النمط 1 والنمط 2.

الخطوة 1) اختر أول حرفين من كل نمط.

الخطوة 2) خذ الأحرف الثالثة إلى الخامسة من كل نمط.

الخطوة 3) تواصل بالمثل مع الشخصيات المتبقية.

البنية العودية لمشكلة LCS

البنية العودية لمشكلة LCS

نحدد أطول سلسلة مشتركة في السلسلة الفرعية (سلسلة مُولّدة من السلسلة الأصلية). ثم نسجل طول أطول سلسلة مشتركة في السلاسل الفرعية.

الآن، هنا خاصية أخرى مثيرة للاهتمام وهي تداخلping المشاكل الفرعيةيقال إن المشكلة تتضمن تداخلاًping المشاكل الفرعية إذا كان من الممكن تقسيم بيان المشكلة إلى مشاكل فرعية صغيرة واستخدامها عدة مرات في البرنامج.

يوضح الرسم البياني أدناه أن الخوارزمية العودية تستدعي الوظيفة بنفس المعلمة عدة مرات.

التداخل الأمثل للبنية الفرعيةping المشاكل الفرعية

على سبيل المثال، انظر إلى شجرة الاستدعاء الذاتي. في المربع ذي اللون الداكن، يمكنك ملاحظة التداخلping المشاكل الفرعية. يتم استدعاء ("RG"، "RA")، ("RG"، "R")، وغيرها عدة مرات.

ولتحسين ذلك، لدينا نهج البرمجة الديناميكية (موانئ دبي).

طريقة التكرار لأطول سلسلة فرعية مشتركة

الرسم البياني الموضح أعلاه يمثل الطريقة التكرارية. لكل دالة تكرارية حالة توقف لإنهاء التكرار أو لبدء العودة من مكدسها.

في هذا التطبيق، سنستخدم حالة أساسية. لذا، فإن خوارزمية هو مثل التالي:

  • إذا كانت جميع العناصر قبل العنصر الأخير متطابقة، فقم بزيادة الطول بمقدار واحد ثم أعد.
  • مرر نمطين إلى الدالة، ثم خذ القيمة القصوى للقيمة المرجعة.
  • إذا كان طول أحد الأنماط صفرًا، فلن يكون لدينا أي نتيجة لاحقة للمقارنة. العودة 0 في هذه الحالة. هذه هي الحالة الأساسية للتكرار.

كنية Code:

def lcs:
    input: pattern_1, pattern_2, len_1, len_2
    if len_1 or len_2 is zero:
        return 0
    if pattern_1[len_1 - 1] equals pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

التنفيذ في C++

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int lcs(string pattern_1, string pattern_2, int len_1, int len_2) {
  if (len_1 == 0 || len_2 == 0)
    return 0;
  if (pattern_1[len_1 - 1] == pattern_2[len_2 - 1]) {
    return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1);
  } else {
    return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2), lcs(pattern_1, pattern_2, len_1, len_2 - 1));
  }
}
int main() {
  string pattern_1, pattern_2;
  pattern_1 = "RGBGARGA";
  pattern_2 = "BGRARG";
  cout<<"Length of LCS is: "<<lcs(pattern_1, pattern_2, pattern_1.size(), pattern_2.size())<<endl;
}

الإخراج:

Length of LCS is: 5

التنفيذ في Python

def lcs(pattern_1, pattern_2, len_1, len_2):
    if len_1 == 0 or len_2 == 0:
        return 0
    if pattern_1[len_1 - 1] == pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS is: ", lcs(pattern_1, pattern_2, len(pattern_1), len(pattern_2)))

الإخراج:

Length of LCS is:  5

طريقة البرمجة الديناميكية لأطول سلسلة فرعية مشتركة (LCS)

البرمجة الديناميكية تعني تحسين الطريقة التكرارية البسيطة. على سبيل المثال، إذا نظرنا إلى الرسم البياني للطريقة التكرارية أو البسيطة، فسنجد عدة استدعاءات متطابقة للدوال. أما طريقة البرمجة الديناميكية فتسجل جميع العمليات الحسابية في مصفوفة وتعيد استخدامها عند الحاجة.

سنستخدم مصفوفة ثنائية الأبعاد بأبعاد m×n، حيث m و n هما طولا النمط 1 والنمط 2 على التوالي. مجموعة 2Dيمكننا استخدام هياكل بيانات القوائم في Python أو هياكل بيانات المتجهات/المصفوفات في C++.

كنية Code بالنسبة لـ LCS باستخدام البرمجة الديناميكية:

LCS(pattern_1, pattern_2):
    m = length of pattern_1 + 1
    n = length of pattern_2 + 1
    dp[n][m]
    for i in range 0 to n + 1:
        for j in range 0 to m + 1:
            if i or j equals to 0:
                dp[i][j] = 0
            else if pattern_1[i] == pattern_2[j]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[n][m]

فيما يلي جدول LCS الذي يستخدم كبنية بيانات مصفوفة ثنائية الأبعاد لنهج البرمجة الديناميكية.

طريقة البرمجة الديناميكية لجدول LCS ثنائي الأبعاد

دعونا نناقش المنطق الذي استخدمناه هنا. الخطوات هي:

الخطوة 1) إذا كانت قيمة i أو j تساوي صفرًا، فإننا نأخذ سلسلة نصية فارغة من السلسلتين المعطيتين ونحاول إيجاد التسلسلات الفرعية المشتركة. ولكن، بما أن السلسلة الفرعية التي نأخذها فارغة، فإن طول التسلسل الفرعي يساوي صفرًا.

الخطوة 2) إذا تطابق حرفان، فسنقوم بتعيين القيمة للفهرس (i,j) عن طريق زيادة LCS المحسوب مسبقًا، والذي يوجد في الفهرس (i-1,j-1) (من الصف السابق).

الخطوة 3) إذا لم تتطابق القيم، فسنختار أكبر عنصر مشترك قصير (LCS) بين الفهرسين المتجاورين. وبهذه الطريقة، نحتاج إلى ملء جميع القيم في المصفوفة ثنائية الأبعاد.

الخطوة 4) وأخيرًا، سنعيد قيمة الخلية الأخيرة من المصفوفة ثنائية الأبعاد.

باختصار، تحتوي جميع القيم في المصفوفة ثنائية الأبعاد على طول التسلسلات الفرعية المشتركة. ومن بين هذه القيم، تحتوي الخلية الأخيرة على طول أطول تسلسل فرعي مشترك.

التنفيذ في C++

#include<iostream>
using namespace std;
int lcs(string pattern_1, string pattern_2) {
  int m = pattern_1.size();
  int n = pattern_2.size();
  // dp will store solutions as the iteration goes on
  int dp[n + 1][m + 1];
  for (int i = 0; i < n + 1; i++) {
    for (int j = 0; j < m + 1; j++) {
      if (i == 0 || j == 0) {
        dp[i][j] = 0;
      } else if (pattern_2[i - 1] == pattern_1[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
  return dp[n][m];
}
int main() {
  string pattern_1 = "RGBGARGA";
  string pattern_2 = "BGRARG";
  cout<<"Length of LCS: "<<lcs(pattern_1, pattern_2)<<endl;
}

الإخراج:

Length of LCS: 5

التنفيذ في Python

def lcs(pattern_1, pattern_2):
    m = len(pattern_1)
    n = len(pattern_2)
    # dp will store solutions as the iteration goes on
    dp = [[None] * (n + 1) for item in range(m + 1)]
    for i in range(m + 1):
        for j in range(n + 1):
            if i == 0 or j == 0:
                dp[i][j] = 0
            elif pattern_1[i - 1] == pattern_2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[m][n]

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS: ", lcs(pattern_1, pattern_2))

الإخراج:

Length of LCS: 5

لذلك، كلا السلسلتين لهما أطول تسلسل مشترك بطول 5.

باختصار، نقوم ببساطة بحساب كل مهمة مرة واحدة في طريقة البرمجة الديناميكية. أما في الطريقة التكرارية، فقد يكون هناك تداخل.ping المشاكل الفرعية.

في خوارزمية البرمجة الديناميكية هذه، نستخدم مصفوفة ثنائية الأبعاد. سيكون هناك سلسلتين محددتين (بافتراض أن كلاهما بطول n). إذن المساحة المطلوبة في المصفوفة هي nx n. إذا كانت السلاسل كبيرة بما يكفي، فسنحتاج إلى إصدار محسّن للذاكرة من حل DP.

المنطق المبسط الذي تم أخذه في الكود هو:

  • قم بتعريف صفيف ثنائي الأبعاد DP[m][n].
  • املأ الصف الأول والعمود الأول من مصفوفة DP بـ 0.
  • خذ i وj للتكرار.
  • إذا كان النمط 1[i] يساوي النمط 2[j]، فقم بتحديث DP[i][j] = DP[i-1][j-1] + 1.
  • إذا لم يكن النمط 1[i] يساوي النمط 2[j]، فإن DP[i][j] سيكون القيمة القصوى بين DP[i-1][j] و DP[i][j-1].
  • استمر حتى يصل i وj إلى m وn.
  • العنصر الأخير، DP[m-1][n-1]، سيحتوي على الطول.

هنا، تتم الإشارة إليه على أنه DP[m-1][n-1] لأن فهرس المصفوفة يبدأ من 0.

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

تستخدم مسارات التعلم الآلي خاصية LCS كميزة تشابه في تصنيف النصوص، وتقييم التسلسلات، وكشف انتحال التعليمات البرمجية. كما أنها تُشكل أساس مقاييس BLEU وROUGE التي تُقيّم النصوص المُولّدة مقارنةً بالمخرجات المرجعية.

نعم. يمكن لمساعدي البرمجة بالذكاء الاصطناعي مثل GitHub Copilot وGPT إنتاج إصدارات البرمجة التكرارية والديناميكية لـ LCS في Python, C++ أو Javaويمكنهم أيضًا إضافة التخزين المؤقت، أو طباعة التسلسل الفرعي الفعلي، أو تحويل الكود إلى شكل تكراري عند الطلب.

يجب أن تكون السلسلة الفرعية متصلة، بينما يكفي أن تحافظ المتتالية الفرعية على الترتيب. بالنسبة لـ "ABCDE"، فإن "ACD" متتالية فرعية صالحة ولكنها ليست سلسلة فرعية، بينما "BCD" سلسلة فرعية ومتتالية فرعية في آن واحد.

يعمل إصدار البرمجة الديناميكية في زمن ومساحة O(m·n)، حيث m و n هما طولا سلسلتي الإدخال. أما الإصدار التكراري البسيط فيعمل في زمن أُسّي O(2^(m+n)) في أسوأ الحالات.

يدعم نظام LCS أدوات مقارنة الملفات، وعمليات دمج Git، ومحاذاة تسلسل الحمض النووي والبروتين في المعلوماتية الحيوية، والكشف عن الانتحال، ومدققات الإملاء، وأدوات مزامنة البيانات التي يجب أن تحافظ على الترتيب المشترك للسجلات.

يتطلب الجدول القياسي مساحة O(m·n). أما تحسين الصفين المتجدد فيقلل المساحة إلى O(min(m, n)) عندما تحتاج فقط إلى الطول، مع العلم أن إعادة بناء التسلسل الفرعي الفعلي لا تزال تتطلب الجدول الكامل.

نعم، يعمل الاستدعاء الذاتي البحت مع السلاسل النصية القصيرة، ولكنه يعيد حساب نفس المسائل الفرعية مرات عديدة، ويصبح غير عملي بعد 20 إلى 25 حرفًا. إضافة التخزين المؤقت أو جدول البرمجة الديناميكية يُعيد الأمور إلى نصابها. tracأداء الجدول.

نعم. تمتد فكرة البرمجة الديناميكية لتشمل k من التسلسلات باستخدام جدول ذي k أبعاد، مع زمن ومساحة من رتبة O(n^k). يظهر هذا النوع في أدوات مقارنة الملفات المتعددة ومحاذاة التسلسلات المتعددة في المعلوماتية الحيوية.

تلخيص هذه التدوينة بـ: