توضیح طولانی‌ترین زیردنباله مشترک و درخت‌های جستجوی دودویی بهینه

دو مسئله کلاسیک برنامه‌نویسی پویای دیگر، تطبیق‌پذیری این تکنیک را فراتر از بهینه‌سازی عددی نشان می‌دهند: یافتن طولانی‌ترین زیردنباله مشترک بین دو رشته، سنگ‌بنای ابزارهای diff و بیوانفورماتیک، و ساخت یک درخت جستجوی دودویی که هزینه جستجوی مورد انتظار را با فرکانس‌های دسترسی شناخته‌شده کمینه می‌کند. این راهنمای جامع هر دو الگوریتم را با جزئیات کامل، شامل استخراج رابطه بازگشتی، ساخت جدول، و بازسازی راه‌حل، مرور می‌کند.

طولانی‌ترین زیردنباله مشترک LCSدرخت جستجوی دودویی بهینهالگوریتم

~6 min read · Updated Sep 7, 2026

مسئله طولانی‌ترین زیردنباله مشترک

یک Subsequence (زیردنباله) از یک رشته با حذف صفر یا چند کاراکتر بدون تغییر ترتیب کاراکترهای باقی‌مانده استخراج می‌شود. مسئله Longest Common Subsequence (LCS) می‌پرسد: با داشتن دو دنباله، طولانی‌ترین زیردنباله مشترک بین هر دو را پیدا کن.

مثال:
X = "ABCBDAB"
Y = "BDCABA"

یک زیردنباله مشترک: "BCBA" (طول 4)
دیگری: "BDAB" (طول 4)
LCS در این حالت طول 4 دارد

این مسئله زیربنای ابزار diff مورد استفاده برای مقایسه نسخه‌های فایل، مقایسه توالی DNA در بیوانفورماتیک، و ابزارهای تشخیص سرقت ادبی است، که آن را به یکی از عملی‌ترین کاربردهای برنامه‌نویسی پویا تبدیل می‌کند.

ایجاد زیرساختار بهینه

فرض کن Xᵢ نشان‌دهنده اولین i کاراکتر رشته X باشد، و به‌طور مشابه برای Yⱼ. بینش ساختاری کلیدی، آخرین کاراکترهای هر دو دنباله را در نظر می‌گیرد.

اگر X[i] == Y[j]:
    LCS از Xᵢ و Yⱼ، LCS از Xᵢ₋₁ و Yⱼ₋₁ را
    دقیقاً با این کاراکتر متناظر گسترش می‌دهد

اگر X[i] ≠ Y[j]:
    LCS از Xᵢ و Yⱼ طولانی‌تر از این‌هاست:
    - LCS از Xᵢ₋₁ و Yⱼ (آخرین کاراکتر X را حذف کن)
    - LCS از Xᵢ و Yⱼ₋₁ (آخرین کاراکتر Y را حذف کن)

این رابطه بازگشتی برای c[i][j]، طول LCS از Xᵢ و Yⱼ، را می‌دهد:

c[i][j] = 0                              اگر i == 0 یا j == 0
c[i][j] = c[i-1][j-1] + 1                اگر i,j > 0 و X[i] == Y[j]
c[i][j] = max(c[i-1][j], c[i][j-1])      اگر i,j > 0 و X[i] ≠ Y[j]

ساخت راه‌حل پایین-به-بالا

LCS-LENGTH(X, Y, m, n):
  فرض کن c[0..m][0..n] و b[1..m][1..n] جدول‌های جدید باشند
  برای i = 1 تا m:
      c[i][0] = 0
  برای j = 0 تا n:
      c[0][j] = 0
  برای i = 1 تا m:
      برای j = 1 تا n:
          اگر X[i] == Y[j]:
              c[i][j] = c[i-1][j-1] + 1
              b[i][j] = "قطری"
          در غیر این صورت اگر c[i-1][j] ≥ c[i][j-1]:
              c[i][j] = c[i-1][j]
              b[i][j] = "بالا"
          در غیر این صورت:
              c[i][j] = c[i][j-1]
              b[i][j] = "چپ"
  c, b را برگردان

ردیابی مثال قبلی با X = "ABCBDAB" و Y = "BDCABA" جدولی را پر می‌کند که در نهایت c[7][6] مقدار ۴ را نگه می‌دارد، که با طول LCS پیدا‌شده با بازرسی بالا مطابقت دارد.

از آنجا که جدول Θ(mn) ورودی دارد و هرکدام O(1) زمان برای پر شدن با توجه به ورودی‌هایی که به آن‌ها بستگی دارد نیاز دارد، الگوریتم در زمان Θ(mn) اجرا می‌شود — بهبود چشمگیری نسبت به تعداد نمایی زیردنباله‌های ممکن که یک رویکرد brute-force ساده نیاز به بررسی آن‌ها دارد.

بازسازی زیردنباله واقعی

جدول کمکی b ثبت می‌کند کدام حالت در هر سلول اعمال شده، که اجازه می‌دهد طولانی‌ترین زیردنباله مشترک واقعی، نه فقط طولش، با ردیابی به‌عقب از b[m][n] تا مبدأ، با دنبال کردن حرکت‌های "قطری" برای جمع‌آوری کاراکترهای متناظر، و حرکت‌های "بالا" یا "چپ" برای پرش از موقعیت‌های غیرمتناظر، بازسازی شود.

درخت‌های جستجوی دودویی بهینه: نوع متفاوتی از بهینه‌سازی

درخت جستجوی دودویی که پیش‌تر در این مجموعه بحث شد فرض می‌کند هر کلید به‌طور برابر احتمال دارد جستجو شود. در بسیاری اپلیکیشن‌های واقعی، برخی کلیدها بسیار مکرر‌تر از دیگران جستجو می‌شوند، و شکل درخت باید این را منعکس کند: کلیدهای پردسترسی باید نزدیک‌تر به ریشه بنشینند، و هزینه جستجویشان را کمینه کنند، حتی اگر این یعنی کلیدهای کم‌دسترسی‌تر عمیق‌تر بنشینند.

با داشتن n کلید متمایز با احتمال‌های جستجوی شناخته‌شده p₁, ..., pₙ، مسئله Optimal Binary Search Tree (درخت جستجوی دودویی بهینه) ساختار درخت جستجوی دودویی‌ای می‌خواهد که هزینه کل جستجوی مورد انتظار را کمینه کند.

هزینه جستجوی مورد انتظار یک درخت T:
E[هزینه جستجو] = Σ (i=1 تا n) (depth_T(kᵢ) + 1) · pᵢ

جایی که depth_T(kᵢ) عمق کلید kᵢ در درخت T است
(ریشه عمق 0 دارد، پس دسترسی به آن 1 مقایسه هزینه دارد)

ایجاد ساختار بازگشتی

بینش کلیدی، مشابه از نظر روحی با ضرب زنجیره‌ای ماتریس که پیش‌تر در این مجموعه بحث شد، این است که یک درخت جستجوی دودویی بهینه روی یک بازه پیوسته از کلیدها یک ساختار بازگشتی دارد: هر کلیدی که به‌عنوان ریشه یک زیردرخت انتخاب شود، کلیدهای کوچک‌تر از آن باید زیردرخت چپ را تشکیل دهند، و کلیدهای بزرگ‌تر از آن باید زیردرخت راست را تشکیل دهند، و هرکدام از این زیردرخت‌ها باید خودشان برای بازه و مجموعه احتمال‌های مربوطه‌شان بهینه باشند.

فرض کن e[i][j] = هزینه مورد انتظار یک BST بهینه
              شامل کلیدهای kᵢ تا kⱼ

فرض کن w[i][j] = مجموع احتمال‌های pᵢ تا pⱼ
              (این افزایش هزینه اضافه‌کردن یک سطح دیگر
               به هر گره در زیردرخت را در نظر می‌گیرد،
               چون انتخاب یک ریشه عمق هر نواده
               را دقیقاً یک واحد افزایش می‌دهد)

e[i][j] = حداقل روی r (i ≤ r ≤ j) از:
            e[i][r-1] + e[r+1][j] + w[i][j]

حالت پایه: e[i][i-1] = 0  (زیردرخت خالی)

جمله w[i][j] صرف‌نظر از اینکه کدام ریشه r انتخاب شود اضافه می‌شود، چون انتخاب هر گره به‌عنوان ریشه یک زیردرخت، عمق هر گره دیگر در آن زیردرخت را دقیقاً یک سطح افزایش می‌دهد، و w[i][j] را به هزینه مورد انتظار کل اضافه می‌کند صرف‌نظر از اینکه زیردرخت داخلاً چگونه بیشتر ساختاردهی شود.

پر کردن جدول پایین-به-بالا

OPTIMAL-BST(p, n):
  فرض کن e[1..n+1][0..n], w[1..n+1][0..n], root[1..n][1..n] جدول‌های جدید باشند
  برای i = 1 تا n + 1:
      e[i][i-1] = 0
      w[i][i-1] = 0
  برای length = 1 تا n:
      برای i = 1 تا n - length + 1:
          j = i + length - 1
          e[i][j] = infinity
          w[i][j] = w[i][j-1] + p[j]
          برای r = i تا j:
              t = e[i][r-1] + e[r+1][j] + w[i][j]
              اگر t < e[i][j]:
                  e[i][j] = t
                  root[i][j] = r
  e, root را برگردان

الگوریتم با افزایش طول زیردرخت پیش می‌رود، دقیقاً مانند ضرب زنجیره‌ای ماتریس، چون e[i][j] فقط به ورودی‌های شامل بازه‌های دقیقاً کوتاه‌تر بستگی دارد. با Θ(n²) ورودی جدول، هرکدام نیازمند O(n) کار برای امتحان هر ریشه ممکن، زمان اجرای کل Θ(n³) است، که دقیقاً با پیچیدگی ضرب زنجیره‌ای ماتریس مطابقت دارد، انعکاسی از شباهت ساختاری بین دو مسئله.

چرا این دو مسئله دامنه برنامه‌نویسی پویا را نشان می‌دهند

طولانی‌ترین زیردنباله مشترک و درخت‌های جستجوی دودویی بهینه، با وجود حل مسائل عملی بسیار متفاوت — مقایسه رشته در مقابل بهینه‌سازی ساختار داده — هر دو از همان الگوی بنیادین برنامه‌نویسی پویا که پیش‌تر در این مجموعه ایجاد شد پیروی می‌کنند: شناسایی زیرساختار بهینه با در نظر گرفتن آخرین تصمیم گرفته‌شده (کدام کاراکترها متناظرند، یا کدام کلید ریشه می‌شود)، بیان راه‌حل به‌صورت یک رابطه بازگشتی روی زیرمسئله‌ها، و پر کردن یک جدول پایین-به-بالا به ترتیبی که تضمین می‌کند هر وابستگی از قبل محاسبه شده است. تشخیص این الگوی مشترک در سراسر مسائل به‌ظاهر نامرتبط، مهارت کلیدی برای به‌کارگیری برنامه‌نویسی پویا روی مسائل جدید هنوز‌ندیده‌شده است.

Written & researched by Dr. Shahin Siami

Related Articles

الگوریتم‌های نظریه اعداد: GCD، توان‌رسانی پیمانه‌ای، و RSA

رمزنگاری مدرن و بی‌شمار کاربرد الگوریتمی به تعداد کمی الگوریتم ظریف نظریه اعداد متکی است. این راهنمای جامع الگوریتم اقلیدس برای محاسبه بزرگ‌ترین مقسوم‌علیه مشترک، توان‌رسانی پیمانه‌ای سریع برای محاسبه کارآمد توان‌های بزرگ، و پایه ریاضی رمزنگاری RSA، یکی از پراستفاده‌ترین سیستم‌های رمزنگاری در جهان، را پوشش می‌دهد.

Continue

اصول هندسه محاسباتی: جهت، تقاطع پاره‌خط، و پوسته محدب

الگوریتم‌های هندسی مسائل شامل نقاط، خطوط، و اشکال را حل می‌کنند، که در گرافیک کامپیوتری، برنامه‌ریزی مسیر رباتیک، و سیستم‌های اطلاعات جغرافیایی ظاهر می‌شوند. این راهنمای جامع آزمون جهت مبتنی بر ضرب خارجی که زیربنای تقریباً هر الگوریتم هندسی است، تشخیص تقاطع پاره‌خط ساخته‌شده روی آن آزمون، و الگوریتم اسکن گراهام برای محاسبه پوسته محدب یک مجموعه از نقاط را پوشش می‌دهد.

Continue

الگوریتم‌های تطبیق رشته: جستجوی ساده، Rabin-Karp، و فراتر از آن

جستجو برای یک الگو درون یک متن بزرگ‌تر یکی از رایج‌ترین عملیات‌ها در محاسبات است، از ویرایشگرهای متن تا تحلیل توالی DNA. این راهنمای جامع الگوریتم تطبیق رشته ساده و بدترین‌حالت درجه‌دومش را پوشش می‌دهد، سپس استفاده ماهرانه الگوریتم Rabin-Karp از هشینگ برای دستیابی به کارایی سریع حالت‌میانگین را توضیح می‌دهد، شامل نحوه مدیریت درست برخوردهای هش.

Continue

الگوریتم‌های تقریبی: نزدیک‌شدن اثبات‌پذیر به بهینه برای مسائل سخت

وقتی یک مسئله NP-complete اثبات شود، یک راه‌حل کارآمد دقیق بعید است وجود داشته باشد، اما این به معنای رهاکردن کامل مسئله نیست. این راهنمای جامع الگوریتم‌های تقریبی را توضیح می‌دهد، که تضمین بهینه‌بودن را در ازای تضمین کارایی معامله می‌کنند، و مسائل پوشش رأس و فروشنده دوره‌گرد را به‌عنوان مثال‌های کلاسیک با نسبت‌های تقریبی اثبات‌پذیر پوشش می‌دهد.

Continue

NP-Completeness توضیح داده شده: P، NP، و چرا برخی مسائل در برابر راه‌حل‌های کارآمد مقاومت می‌کنند

برخی مسائل برای دهه‌ها در برابر هر تلاشی برای یک الگوریتم کارآمد مقاومت کرده‌اند، با این حال هیچ‌کس اثبات نکرده یک راه‌حل کارآمد غیرممکن است. این راهنمای جامع کلاس‌های P و NP، مفهوم تقلیل‌های زمان-چندجمله‌ای مورد استفاده برای مقایسه سختی مسئله، و چگونگی اینکه اثبات NP-complete بودن یک مسئله شواهد قوی، هرچند نه اثبات، فراهم می‌کند که هیچ الگوریتم کارآمدی وجود ندارد را توضیح می‌دهد.

Continue

جریان بیشینه: فورد-فالکرسون و قضیه برش-کمینه/جریان-بیشینه

مسائل جریان بیشینه بیشترین توان عملیاتی ممکن از میان یک شبکه با اتصالات محدود-به-ظرفیت را مدل‌سازی می‌کنند، از لوله‌های آب تا شبکه‌های داده. این راهنمای جامع شبکه‌های جریان را معرفی می‌کند، روش فورد-فالکرسون برای یافتن جریان بیشینه با استفاده از مسیرهای تقویتی را مرور می‌کند، و قضیه ظریف برش-کمینه/جریان-بیشینه که دو مسئله به‌ظاهر متفاوت را به یکی متصل می‌کند را توضیح می‌دهد.

Continue