Faragiri

همهٔ زبان‌ها

آموزش برنامه‌نویسی پویاالگوریتم‌ها

فاصله ویرایشی دو واژه فارسی، شمرده‌شده. با هر حرف که به دو رشته اضافه می‌شود تعداد فراخوانی حدود پنج برابر می‌شود، و یک دکوراتور آن را به شصت‌ونه می‌رساند.

۴بخش
۳دقیقه خواندن
۴عمق

پنج اردوگاه، یک شیب

تعداد فراخوانی، وقتی هر بار یک حرف اضافه می‌شود

فاصله میان دو اردوگاه آخر از فاصله میان دو اردوگاه اول بسیار بیشتر است، و همین شیب است که مسئله را می‌سازد.

  1. ۰۱چهار حرفدویست و پنجاه فراخوانی، که روی هر ماشینی آنی است
  2. ۰۲پنج حرفهفتصد و پنجاه و سه، و هنوز هیچ نشانه‌ای از دردسر نیست
  3. ۰۳شش حرفدو هزار و هفتصد و هشت، اولین جایی که عدد به چشم می‌آید
  4. ۰۴هفت حرفیازده هزار و سیصد و چهل و چهار، و شیب دیگر پنهان نیست
  5. ۰۵هشت حرفپنجاه و یک هزار و نهصد و هشت، برای دو واژه کوتاه فارسی

این اردوگاه‌ها فراخوانی می‌شمارند و نه ثانیه. با یک سطر کش، همین مسیر به شصت و نه زیرمسئله متمایز جمع می‌شود و شیب کاملا از بین می‌رود.

فاصله ویرایشی می‌گوید یک واژه با چند درج و حذف و جایگزینی به واژه دیگر تبدیل می‌شود، و بازگشتی نوشتنش سه سطر است. مسئله سرعت نیست، تکرار است: همان زیرمسئله بارها حل می‌شود.

روی دو واژه «کتابخانه» و «کارخانه‌ها»، شمارش فراخوانی‌ها وقتی طول پیشوندها یکی‌یکی زیاد می‌شود، اندازه‌گیری‌شده روی پایتون 3.14 در تاریخ ۲۰۲۶-۰۹-۰۳:

  • چهار حرف: ۲۵۰ فراخوانی
  • پنج حرف: ۷۵۳
  • شش حرف: ۲٬۷۰۸
  • هفت حرف: ۱۱٬۳۴۴
  • هشت حرف: ۵۱٬۹۰۸

یک حرف بیشتر، کار را حدود پنج برابر می‌کند. روی دو واژه کامل، نسخه بازگشتی ساده ۳۱۰٬۴۰۶ بار خودش را صدا می‌زند. حالا یک سطر بالای همان تابع اضافه کنید، @lru_cache(maxsize=None)، و همان جواب با ۶۹ زیرمسئله متمایز درمی‌آید. هیچ چیز دیگری از تابع عوض نشد.

شرطی که این کار را ممکن می‌کند

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

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

از بالا یا از پایین

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

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

جایی که کار گیر می‌کند، تطبیق نام فارسی است

هر سامانه‌ای که با نام فارسی کار می‌کند دیر یا زود باید دو املای یک چیز را یکی بداند: نامی که یک بار با «ی» عربی و یک بار با «ی» فارسی ثبت شده، یا نامی که فاصله مجازی دارد و یکی ندارد. کاری که معمولا اول انجام می‌شود نوشتن ده‌ها قاعده جایگزینی است، و هیچ‌وقت تمام نمی‌شود. فاصله ویرایشی همان مسئله را با یک عدد جواب می‌دهد و بقیه کار تصمیم درباره آستانه است. نکته‌ای که باید بدانید این است که این تابع روی رشته‌های بلند بدون حافظه‌دار شدن غیرقابل استفاده است، و همان یک سطر است که آن را از یک تمرین کلاسی به چیزی قابل اجرا تبدیل می‌کند.

ابزار، و سالی که این حرف درست است

در سال ۲۰۲۶ functools.lru_cache در کتابخانه استاندارد و رایگان است و جای نگه داشتن دستی یک دیکشنری از نتیجه‌ها را گرفته؛ متد cache_info() هم دارد که تعداد اصابت و خطای کش را می‌گوید و بهترین راه دیدن این است که آیا اصلا کاری کرده یا نه. اگر سقف اندازه لازم ندارید، functools.cache از پایتون 3.9 همان است با نوشتار کوتاه‌تر. هیچ‌کدام به شبکه نیاز ندارند.

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

کنار این

بالای این