17-bo‘lim
Dinamik dasturlash
Optimal qism tuzilma va qoplanuvchi qism masalalar, memoizatsiya va tabulyatsiya, ryukzak va tanga masalalari.
Ushbu bo‘lim mundarijasi
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:
| Shart | Ma'nosi |
|---|---|
| Optimal qism tuzilma | Katta masalaning yechimi kichiklarining yechimidan quriladi |
| Qoplanuvchi qism masalalar | Bir 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.
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 #
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,}")
fib_memo 12,586,269,025
fib_jadval 12,586,269,025
fib_optimal 12,586,269,025
| Yondashuv | Vaqt | Xotira | Afzalligi |
|---|---|---|---|
| Memoizatsiya | O(n) | O(n) | Yozish oson, faqat kerakli qismlar hisoblanadi |
| Tabulyatsiya | O(n) | O(n) | Rekursiya cheklovi yo'q, tezroq |
| Optimallashtirilgan | O(n) | O(1) | Eng kam xotira |
Masala 1: zinapoya #
Zinapoyada
nta pog'ona bor. Har safar 1 yoki 2 ta pog'ona bosib chiqishingiz mumkin. Yuqoriga chiqishning necha xil usuli bor?
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")
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.Nso'mni maydalash uchun eng kam nechta tanga kerak?
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}")
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]
"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
Wkg. Har bir buyumning og'irligi va qiymati bor. Eng qimmat yukni qanday tanlash kerak?
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,}")
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? #
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)
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:
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"))
dastur
ALGRA
Masala 5: tahrirlash masofasi #
Bir so'zni boshqasiga aylantirish uchun minimal amallar soni. Imlo tekshirgichlar shu algoritmni ishlatadi:
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")
kitob -> kitobcha 3 ta amal
dastur -> dasturchi 3 ta amal
salom -> salam 1 ta amal
DP masalasini yechish tartibi #
| Qadam | Savol |
|---|---|
| 1 | Holat nima? (jadval kataklari nimani anglatadi) |
| 2 | Rekurrent munosabat qanday? (katak qo'shnilaridan qanday hisoblanadi) |
| 3 | Bazaviy holat nima? (birinchi qator/ustun) |
| 4 | Tartib qanday? (jadval qaysi yo'nalishda to'ldiriladi) |
| 5 | Javob qayerda? (odatda oxirgi katak) |
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 #
| Masala | Murakkablik | Qayerda ishlatiladi |
|---|---|---|
| Fibonachchi | O(n) | O'quv misoli |
| Tangalarni maydalash | O(n·W) | Kassa apparatlari |
| Ryukzak | O(n·W) | Resurslarni taqsimlash |
| Eng uzun umumiy ketma-ketlik | O(n·m) | git diff, DNK tahlili |
| Tahrirlash masofasi | O(n·m) | Imlo tekshirgich, qidiruv |
| Eng uzun o'suvchi qism | O(n log n) | Ma'lumot tahlili |
| Matritsalarni ko'paytirish tartibi | O(n³) | Kompilyator optimizatsiyasi |
- Eng uzun o'suvchi qism ketma-ketlik masalasini DP bilan yeching (O(n²) yetarli).
- Zinapoya masalasini o'zgartiring: endi 1, 2 yoki 3 pog'ona bosish mumkin.
- Ryukzak masalasining "cheksiz" variantini yozing - har bir buyumni ko'p marta olish mumkin.
tahrirlash_masofasini 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.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
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.