17-bo‘lim

Dinamik dasturlash

Optimal qism tuzilma va qoplanuvchi qism masalalar, memoizatsiya va tabulyatsiya, ryukzak va tanga masalalari.

🕑 11 daqiqa o‘qish 📄 756 so‘z 👁 6 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. DP qachon ishlaydi?
  2. Ikki yondashuv
  3. Masala 1: zinapoya
  4. Masala 2: tangalarni maydalash
  5. Masala 3: ryukzak masalasi
  6. Jadval qanday to'ldiriladi?
  7. Masala 4: eng uzun umumiy qism ketma-ketlik
  8. Masala 5: tahrirlash masofasi
  9. DP masalasini yechish tartibi
  10. Mashhur DP masalalari
  11. Xulosa

Dinamik dasturlash (DP) - eksponensial masalalarni polinomial vaqtda yechishning eng kuchli usuli. Nomiga qaramay, u "dinamik" ham, "dasturlash" ham emas - bu shunchaki natijalarni eslab qolish san'ati.

DP qachon ishlaydi? #

Masala ikkita shartga javob berishi kerak:

ShartMa'nosi
Optimal qism tuzilmaKatta masalaning yechimi kichiklarining yechimidan quriladi
Qoplanuvchi qism masalalarBir xil kichik masala qayta-qayta uchraydi

Fibonachchi ikkalasiga ham javob beradi: fib(n) = fib(n-1) + fib(n-2) va fib(3) bir necha marta hisoblanadi.

DP va "bo'l va hukmronlik qil" farqi

Merge sort ham masalani bo'laklarga bo'ladi, lekin uning bo'laklari qoplanmaydi - chap yarim va o'ng yarim umuman bog'liq emas. DP da esa bo'laklar takrorlanadi va aynan shu takrorlanish keshlash imkonini beradi.

Ikki yondashuv #

Memoizatsiya (yuqoridan pastga) fib(5) fib(4) fib(3) fib(1), fib(0) rekursiya + kesh Tabulyatsiya (pastdan yuqoriga) fib(0), fib(1) fib(2) fib(3), fib(4) fib(5) sikl + jadval
Ikkala yondashuv bir xil natija beradi, lekin turli yo'nalishda harakat qiladi
Python
from functools import lru_cache


# 1-usul: memoizatsiya (yuqoridan pastga)
@lru_cache(maxsize=None)
def fib_memo(n):
    if n < 2:
        return n
    return fib_memo(n - 1) + fib_memo(n - 2)


# 2-usul: tabulyatsiya (pastdan yuqoriga)
def fib_jadval(n):
    if n < 2:
        return n

    jadval = [0] * (n + 1)
    jadval[1] = 1

    for i in range(2, n + 1):
        jadval[i] = jadval[i - 1] + jadval[i - 2]

    return jadval[n]


# 3-usul: xotirani optimallashtirish - butun jadval kerak emas
def fib_optimal(n):
    if n < 2:
        return n

    oldingi, joriy = 0, 1
    for _ in range(2, n + 1):
        oldingi, joriy = joriy, oldingi + joriy
    return joriy


for funksiya in [fib_memo, fib_jadval, fib_optimal]:
    print(f"{funksiya.__name__:<14}{funksiya(50):>15,}")
Natija
fib_memo           12,586,269,025
fib_jadval         12,586,269,025
fib_optimal        12,586,269,025
YondashuvVaqtXotiraAfzalligi
MemoizatsiyaO(n)O(n)Yozish oson, faqat kerakli qismlar hisoblanadi
TabulyatsiyaO(n)O(n)Rekursiya cheklovi yo'q, tezroq
OptimallashtirilganO(n)O(1)Eng kam xotira

Masala 1: zinapoya #

Zinapoyada n ta pog'ona bor. Har safar 1 yoki 2 ta pog'ona bosib chiqishingiz mumkin. Yuqoriga chiqishning necha xil usuli bor?

Python
def zinapoya(n):
    """n ta pog'onaga chiqish usullari soni."""
    if n <= 2:
        return n

    ikki_oldin, bir_oldin = 1, 2
    for _ in range(3, n + 1):
        joriy = ikki_oldin + bir_oldin
        ikki_oldin, bir_oldin = bir_oldin, joriy

    return bir_oldin


for pogonalar in range(1, 9):
    print(f"{pogonalar} ta pog'ona -> {zinapoya(pogonalar):>3} xil usul")
Natija
1 ta pog'ona ->   1 xil usul
2 ta pog'ona ->   2 xil usul
3 ta pog'ona ->   3 xil usul
4 ta pog'ona ->   5 xil usul
5 ta pog'ona ->   8 xil usul
6 ta pog'ona ->  13 xil usul
7 ta pog'ona ->  21 xil usul
8 ta pog'ona ->  34 xil usul

Bu Fibonachchi ketma-ketligi! Mantiq: n-pog'onaga faqat n-1 yoki n-2 dan kelish mumkin.

Masala 2: tangalarni maydalash #

Bizda [1, 5, 10, 25] nominaldagi tangalar bor. N so'mni maydalash uchun eng kam nechta tanga kerak?

Python
def eng_kam_tanga(tangalar, summa):
    """Minimal tangalar soni. O(summa * len(tangalar))."""
    CHEKSIZ = float("inf")
    jadval = [0] + [CHEKSIZ] * summa

    for joriy in range(1, summa + 1):
        for tanga in tangalar:
            if tanga <= joriy and jadval[joriy - tanga] + 1 < jadval[joriy]:
                jadval[joriy] = jadval[joriy - tanga] + 1

    return jadval[summa] if jadval[summa] != CHEKSIZ else -1


def tangalarni_korsat(tangalar, summa):
    """Qaysi tangalar ishlatilganini ham qaytaradi."""
    CHEKSIZ = float("inf")
    jadval = [0] + [CHEKSIZ] * summa
    tanlov = [None] * (summa + 1)

    for joriy in range(1, summa + 1):
        for tanga in tangalar:
            if tanga <= joriy and jadval[joriy - tanga] + 1 < jadval[joriy]:
                jadval[joriy] = jadval[joriy - tanga] + 1
                tanlov[joriy] = tanga

    if jadval[summa] == CHEKSIZ:
        return None

    natija = []
    qolgan = summa
    while qolgan > 0:
        natija.append(tanlov[qolgan])
        qolgan -= tanlov[qolgan]

    return sorted(natija, reverse=True)


tangalar = [1, 5, 10, 25]
for summa in [11, 30, 63]:
    soni = eng_kam_tanga(tangalar, summa)
    tanlangan = tangalarni_korsat(tangalar, summa)
    print(f"{summa:>3} so'm -> {soni} ta tanga: {tanlangan}")
Natija
 11 so'm -> 2 ta tanga: [10, 1]
 30 so'm -> 2 ta tanga: [25, 5]
 63 so'm -> 6 ta tanga: [25, 25, 10, 1, 1, 1]
Ochko'z yondashuv har doim ishlamaydi

"Har safar eng katta tangani ol" degan sodda qoida [1, 5, 10, 25] uchun to'g'ri ishlaydi, lekin [1, 3, 4] va summa 6 uchun:

  • Ochko'z: 4 + 1 + 1 = 3 ta tanga
  • To'g'ri javob: 3 + 3 = 2 ta tanga

Shuning uchun bu masala DP talab qiladi. Ochko'z algoritmlarni keyingi bo'limda ko'ramiz.

Masala 3: ryukzak masalasi #

Ryukzakning ko'tarish quvvati W kg. Har bir buyumning og'irligi va qiymati bor. Eng qimmat yukni qanday tanlash kerak?

Python
def ryukzak(buyumlar, quvvat):
    """0/1 ryukzak masalasi. O(n * W)."""
    n = len(buyumlar)
    jadval = [[0] * (quvvat + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        nomi, ogirlik, qiymat = buyumlar[i - 1]

        for w in range(quvvat + 1):
            if ogirlik > w:
                jadval[i][w] = jadval[i - 1][w]            # sig'maydi
            else:
                olmaslik = jadval[i - 1][w]
                olish = jadval[i - 1][w - ogirlik] + qiymat
                jadval[i][w] = max(olmaslik, olish)

    # Qaysi buyumlar tanlanganini tiklaymiz
    tanlangan = []
    w = quvvat
    for i in range(n, 0, -1):
        if jadval[i][w] != jadval[i - 1][w]:
            nomi, ogirlik, qiymat = buyumlar[i - 1]
            tanlangan.append(buyumlar[i - 1])
            w -= ogirlik

    return jadval[n][quvvat], tanlangan[::-1]


buyumlar = [
    ("Noutbuk",     3, 2000),
    ("Fotoapparat", 2, 1200),
    ("Kitob",       1,  300),
    ("Planshet",    2,  900),
    ("Naushnik",    1,  500),
]
QUVVAT = 5

qiymat, tanlangan = ryukzak(buyumlar, QUVVAT)

print(f"Ryukzak quvvati: {QUVVAT} kg\n")
print(f"{'Buyum':<16}{'Og‘irlik':>10}{'Qiymat':>10}")
print("-" * 36)
for nomi, ogirlik, narx in tanlangan:
    print(f"{nomi:<16}{ogirlik:>9} kg{narx:>10,}")
print("-" * 36)
print(f"{'JAMI':<16}{sum(b[1] for b in tanlangan):>9} kg{qiymat:>10,}")
Natija
Ryukzak quvvati: 5 kg

Buyum             Og'irlik    Qiymat
------------------------------------
Fotoapparat              2     1,200
Planshet                 2       900
Naushnik                 1       500
------------------------------------
JAMI                     5 kg    2,600

Diqqat qiling: eng qimmat buyum (noutbuk, 2000) tanlanmadi - u ko'p joy egallab, umumiy qiymatni pasaytirardi.

Jadval qanday to'ldiriladi? #

Python
def ryukzak_jadvalini_korsat(buyumlar, quvvat):
    n = len(buyumlar)
    jadval = [[0] * (quvvat + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        _, ogirlik, qiymat = buyumlar[i - 1]
        for w in range(quvvat + 1):
            if ogirlik > w:
                jadval[i][w] = jadval[i - 1][w]
            else:
                jadval[i][w] = max(jadval[i - 1][w],
                                   jadval[i - 1][w - ogirlik] + qiymat)

    print("       " + "".join(f"{w:>7}" for w in range(quvvat + 1)))
    print("       " + "  kg".rjust(7 * (quvvat + 1)))
    for i, qator in enumerate(jadval):
        nomi = "bo'sh" if i == 0 else buyumlar[i - 1][0][:6]
        print(f"{nomi:<7}" + "".join(f"{q:>7,}" for q in qator))


ryukzak_jadvalini_korsat(buyumlar, 5)
Natija
             0      1      2      3      4      5
                                              kg
bo'sh        0      0      0      0      0      0
Noutbu       0      0      0  2,000  2,000  2,000
Fotoap       0      0  1,200  2,000  2,000  3,200
Kitob        0    300  1,200  2,000  2,300  3,200
Plansh       0    300  1,200  2,000  2,300  3,200
Naushn       0    500  1,200  1,700  2,500  3,200

Har bir katak: "shu buyumgacha va shu og'irlik chegarasida erishish mumkin bo'lgan maksimal qiymat".

Masala 4: eng uzun umumiy qism ketma-ketlik #

Bu git diff va matn taqqoslash vositalarining asosi:

Python
def eng_uzun_umumiy(birinchi, ikkinchi):
    """Eng uzun umumiy qism ketma-ketlikni topadi. O(n * m)."""
    n, m = len(birinchi), len(ikkinchi)
    jadval = [[0] * (m + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if birinchi[i - 1] == ikkinchi[j - 1]:
                jadval[i][j] = jadval[i - 1][j - 1] + 1
            else:
                jadval[i][j] = max(jadval[i - 1][j], jadval[i][j - 1])

    # Ketma-ketlikni tiklaymiz
    natija = []
    i, j = n, m
    while i > 0 and j > 0:
        if birinchi[i - 1] == ikkinchi[j - 1]:
            natija.append(birinchi[i - 1])
            i -= 1
            j -= 1
        elif jadval[i - 1][j] >= jadval[i][j - 1]:
            i -= 1
        else:
            j -= 1

    return "".join(reversed(natija))


print(eng_uzun_umumiy("dasturlash", "dasturchi"))
print(eng_uzun_umumiy("ALGORITM", "ALGEBRA"))
Natija
dastur
ALGRA

Masala 5: tahrirlash masofasi #

Bir so'zni boshqasiga aylantirish uchun minimal amallar soni. Imlo tekshirgichlar shu algoritmni ishlatadi:

Python
def tahrirlash_masofasi(birinchi, ikkinchi):
    """Levenshtein masofasi: qo'shish, o'chirish, almashtirish. O(n * m)."""
    n, m = len(birinchi), len(ikkinchi)
    jadval = [[0] * (m + 1) for _ in range(n + 1)]

    for i in range(n + 1):
        jadval[i][0] = i                    # hammasini o'chirish
    for j in range(m + 1):
        jadval[0][j] = j                    # hammasini qo'shish

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if birinchi[i - 1] == ikkinchi[j - 1]:
                jadval[i][j] = jadval[i - 1][j - 1]
            else:
                jadval[i][j] = 1 + min(
                    jadval[i - 1][j],        # o'chirish
                    jadval[i][j - 1],        # qo'shish
                    jadval[i - 1][j - 1],    # almashtirish
                )

    return jadval[n][m]


juftliklar = [("kitob", "kitobcha"), ("dastur", "dasturchi"), ("salom", "salam")]
for a, b in juftliklar:
    print(f"{a:<10} -> {b:<12} {tahrirlash_masofasi(a, b)} ta amal")
Natija
kitob      -> kitobcha     3 ta amal
dastur     -> dasturchi    3 ta amal
salom      -> salam        1 ta amal

DP masalasini yechish tartibi #

QadamSavol
1Holat nima? (jadval kataklari nimani anglatadi)
2Rekurrent munosabat qanday? (katak qo'shnilaridan qanday hisoblanadi)
3Bazaviy holat nima? (birinchi qator/ustun)
4Tartib qanday? (jadval qaysi yo'nalishda to'ldiriladi)
5Javob qayerda? (odatda oxirgi katak)
DP ni o'rganishning eng yaxshi usuli

Avval sodda rekursiv yechim yozing - u sekin bo'lsa ham to'g'ri bo'ladi. So'ng unga @lru_cache qo'shing - masala hal bo'ladi. Keyin xohlasangiz uni tabulyatsiyaga o'giring.

Ko'p dasturchi darhol jadval yozishga urinib, chalkashib ketadi. Rekursiyadan boshlash ancha oson yo'l.

Mashhur DP masalalari #

MasalaMurakkablikQayerda ishlatiladi
FibonachchiO(n)O'quv misoli
Tangalarni maydalashO(n·W)Kassa apparatlari
RyukzakO(n·W)Resurslarni taqsimlash
Eng uzun umumiy ketma-ketlikO(n·m)git diff, DNK tahlili
Tahrirlash masofasiO(n·m)Imlo tekshirgich, qidiruv
Eng uzun o'suvchi qismO(n log n)Ma'lumot tahlili
Matritsalarni ko'paytirish tartibiO(n³)Kompilyator optimizatsiyasi
Amaliy topshiriq
  1. Eng uzun o'suvchi qism ketma-ketlik masalasini DP bilan yeching (O(n²) yetarli).
  2. Zinapoya masalasini o'zgartiring: endi 1, 2 yoki 3 pog'ona bosish mumkin.
  3. Ryukzak masalasining "cheksiz" variantini yozing - har bir buyumni ko'p marta olish mumkin.
  4. tahrirlash_masofasi ni o'zgartiring - u qanday amallar bajarilganini ham ko'rsatsin.

Xulosa #

  • DP ikki shart bajarilganda ishlaydi: optimal qism tuzilma va qoplanuvchi qism masalalar.
  • Memoizatsiya - rekursiya + kesh (yuqoridan pastga); yozish oson.
  • Tabulyatsiya - sikl + jadval (pastdan yuqoriga); tezroq va rekursiya cheklovisiz.
  • Ko'pincha butun jadval kerak emas - oxirgi bir-ikki qator yetarli (xotira O(1) gacha tushadi).
  • Yechimni tiklash uchun jadval bo'ylab orqaga qaytish kerak.
  • Sodda rekursiyadan boshlab, keyin kesh qo'shish - DP ni o'zlashtirish yo'li.

Keyingi bo'limda soddaroq, lekin har doim ham ishlamaydigan yondashuvni - ochko'z algoritmlarni ko'ramiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.