26 β Backtracking (orqaga qaytish)¶
β¬ οΈ Oldingi: 25 β Dinamik dasturlash (DP) Β· π README Β· Keyingi: 27 β Saralash algoritmlari β‘οΈ
Bu bobda: Backtracking β yechimni qadamba-qadam qurib boradigan, har qadamda variant tanlaydigan va tanlov boshi berk ko'chaga olib kelsa orqaga qaytib boshqasini sinaydigan paradigma. Bu β "aqlli brute force": qaror daraxtini kezamiz, lekin yaroqsiz shoxlarni darhol kesib tashlaymiz (pruning). Umumiy shablonni o'rganamiz, so'ng klassik masalalarni (permutatsiyalar, qism-to'plamlar, kombinatsiyalar, N-vazir, Sudoku, labirint, kombinatsion yig'indi) Python bilan yechamiz. Oxirida pruning, murakkablik va backtracking'ning DP/greedy bilan farqini ko'ramiz.
Halollik / Eslatma: Backtracking eng yomon holatda eksponensial (yoki faktorial) β bu matematik aniq. Pruning amalda ko'p tejaydi, lekin eng yomon nazariy chegarani odatda o'zgartirmaydi. Backtracking to'liq qidiruv bo'lgani uchun barcha yoki optimal yechimni topishni kafolatlaydi. Barcha Python namunalari haqiqatan ishga tushirib tekshirilgan; ko'rsatilgan chiqishlar kod bergan haqiqiy natijalar (N-vazir yechimlari soni, Sudoku va h.k.).
Backtracking g'oyasi¶
Intuitsiya: Labirintda yuribsiz. Chorrahaga kelganda bir yo'lni tanlaysiz. Agar u tupik (berk ko'cha) bo'lib chiqsa β orqaga, oxirgi chorrahaga qaytasiz va boshqa yo'lni sinaysiz. Hech qachon tasodifan yoki butun labirintni xaritasiz aylanib chiqmaysiz β yo'lni qurib borasiz, berk chiqsa bekor qilasiz. Backtracking aynan shu.
Backtracking (orqaga qaytish) β yechimni bo'lakma-bo'lak quradigan strategiya. Har qadamda joriy qisman yechimga bitta yangi bo'lak (nomzod) qo'shamiz. Agar bu bo'lak masalaning cheklovini buzsa yoki yo'l boshi berk ko'chaga olib kelsa β qo'shilgan bo'lakni olib tashlaymiz (undo) va keyingi variantni sinaymiz. Agar qisman yechim to'liq bo'lib qolsa β uni yozamiz.
Bu jarayonni qaror daraxti sifatida tasavvur qilish eng to'g'risi: ildiz β bo'sh yechim; har tugundan chiqayotgan shoxlar β keyingi tanlov variantlari; barglar β to'liq yechimlar (yoki tupiklar).
Diagrammada uchta muhim hodisa ko'rsatilgan:
- Yashil tugunlar β yaroqli qisman yechim; pastga davom etamiz.
- Sariq tupik (TUPIK) β bu shoxdan to'liq yechim chiqmadi; oxirgi tanlovni bekor qilib (
pop) orqaga qaytamiz. - Qizil kesilgan shox (pruning) β tanlov darhol cheklovni buzdi (mas. N-vazirda ikki vazir bir ustunda). Biz bu shoxni umuman qurmaymiz β uning ostidagi butun pastki daraxt tejaladi.
Mana shu uchinchisi β pruning β backtracking'ni oddiy brute force'dan ajratib turadi va uni amaliy qiladi.
Brute force'dan farqi¶
Brute force (22-bob) barcha nomzodni to'liq qurib, keyin tekshiradi. Backtracking esa qisman yechimni erta tekshiradi: agar shu paytdayoq cheklov buzilgan bo'lsa, butun shoxni tashlab yuboradi β to'liq qurishga ham urinmaydi.
BRUTE FORCE: BACKTRACKING:
har bir to'liq nomzodni qur nomzodni bo'lakma-bo'lak qur
-> keyin tekshir -> har bo'lakdan keyin tekshir
-> yaroqsiz bo'lsa: butun shoxni KES
(qolganini qurmaydan tashla)
Misol: N-vazir masalasini sof brute force bilan yechsak β nΓn taxtaga n vazirni barcha joylashtirish usulini ko'rib chiqishimiz kerak bo'lardi (juda katta). Backtracking esa har qatorga bitta vazir qo'yib, darhol "bu vazir oldingilarini urmayaptimi?" deb tekshiradi β urgan bo'lsa, o'sha pozitsiyaga umuman bormaydi. Shu tariqa qidiruv fazosining ulkan qismi yo'q qilinadi.
Eslatma: Backtracking β brute force'ning qism-turi. Ikkalasi ham oxir-oqibatda to'liq qidiruv (barcha yaroqli yechimni topa oladi). Farq β qachon tekshirishda: backtracking iloji boricha erta tekshirib, umidsiz shoxlarni kesadi. "Aqlli brute force" deyilishining sababi shu.
Umumiy shablon (template)¶
Deyarli barcha backtracking masalalari bitta umumiy qolipga tushadi. Uni yaxshilab yodda saqlang β keyin har masala shu qolipning to'ldirilishi bo'lib qoladi:
backtrack(qisman_yechim):
agar qisman_yechim TO'LIQ:
yechimni YOZ (yoki eng yaxshisini yangilab qo'y)
qaytar
har bir nomzod β keyingi_variantlar(qisman_yechim):
agar YAROQLI(qisman_yechim, nomzod): # cheklov tekshiruvi (pruning)
qisman_yechim.qo'sh(nomzod) # 1. TANLA
backtrack(qisman_yechim) # 2. REKURSIV kez
qisman_yechim.olib_tashla(nomzod) # 3. BEKOR QIL (backtrack!)
Uch qadamli "tanla β kez β bekor qil" raqsi β backtracking'ning yuragi. Ayniqsa uchinchi qadam (olib_tashla) muhim: rekursiya qaytib kelgach, biz qilgan o'zgartirishni aynan orqaga qaytaramiz, toki keyingi nomzodni toza holatdan sinashimiz mumkin bo'lsin.
Diqqat:
qo'shvaolib_tashlabir-birining aniq teskarisi bo'lishi shart. Agarqo'shumumiy holatga (mas.ishlatildito'plami, taxta katagi) o'zgartirish kiritsa,olib_tashlaaynan o'sha o'zgartirishni qaytarishi kerak. Aks holda "iflos" holat keyingi shoxlarga oqib o'tadi va javob noto'g'ri chiqadi.
Bobning qolgan qismi shu shablonni olti klassik masalada to'ldiradi.
1-misol. Permutatsiyalar (barcha tartiblar)¶
Masala: [1, 2, 3] ning barcha tartiblarini (permutatsiyalarini) hosil qiling. Bu β eng yaxshi kirish misoli, chunki qaror daraxti juda aniq ko'rinadi.
G'oya: birinchi pozitsiyaga ishlatilmagan har bir elementni qo'yamiz; keyin rekursiv ravishda qolgan pozitsiyalarni to'ldiramiz; qaytib kelgach, qo'ygan elementni "ishlatilmagan" deb belgilab orqaga qaytamiz.
def permutatsiyalar(elementlar):
natija = []
n = len(elementlar)
ishlatildi = [False] * n
joriy = []
def backtrack():
if len(joriy) == n: # TO'LIQ: barcha pozitsiya to'ldi
natija.append(joriy[:]) # nusxasini yoz (joriy keyin o'zgaradi!)
return
for i in range(n):
if ishlatildi[i]:
continue # bu element allaqachon ishlatilgan
ishlatildi[i] = True # 1. TANLA
joriy.append(elementlar[i])
backtrack() # 2. KEZ
joriy.pop() # 3. BEKOR QIL
ishlatildi[i] = False
backtrack()
return natija
p = permutatsiyalar([1, 2, 3])
print(len(p)) # -> 6
print(p) # -> [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
Diagrammada daraxtning shakli ko'rinadi: birinchi pozitsiyaga 3 variant, ikkinchisiga 2 (bittasi ishlatilgan), uchinchisiga 1 β jami 3 Γ 2 Γ 1 = 3! = 6 barg. Har bir barg β bitta to'liq permutatsiya.
Nega to'g'ri ishlaydi? joriy har doim yaroqli prefiks (takrorlanmas tartibning boshi) bo'lib qoladi, chunki ishlatildi to'plami bir elementni ikki marta qo'yishga yo'l qo'ymaydi. Daraxtdagi har bir barg β uzunligi n bo'lgan, takrorsiz tartib β ta'rifi bo'yicha bitta permutatsiya. Daraxt barcha barglarni kezgani uchun barcha permutatsiyalar topiladi.
Bu yerda joriy[:] (nusxa olish) muhim: joriy ro'yxati rekursiya davomida o'zgarib turadi, shuning uchun uning o'sha paytdagi holatini saqlab qolamiz, aks holda natija ichidagi hamma element pirovardida bo'sh ro'yxatga ishora qilib qolardi.
Trassirovka (birinchi to'rt qadam, joriy va ishlatildi):
| Qadam | Amal | joriy | ishlatildi |
|---|---|---|---|
| 1 | 1 qo'shildi |
[1] |
[T, F, F] |
| 2 | 2 qo'shildi |
[1, 2] |
[T, T, F] |
| 3 | 3 qo'shildi β TO'LIQ, yoz |
[1, 2, 3] |
[T, T, T] |
| 4 | 3 olindi, 2 olindi (backtrack) |
[1] |
[T, F, F] |
| 5 | 3 qo'shildi |
[1, 3] |
[T, F, T] |
2-misol. Qism-to'plamlar va kombinatsiyalar¶
Qism-to'plamlar. [1, 2, 3] ning barcha qism-to'plamlarini hosil qiling. Bu yerda shablon biroz boshqacha: har bir tugun (har bir qisman yechim) β o'zi bir to'liq qism-to'plam, shuning uchun barcha tugunlarni yozamiz, faqat barglarni emas.
def qism_toplamlar(elementlar):
natija = []
joriy = []
n = len(elementlar)
def backtrack(boshlanish):
natija.append(joriy[:]) # HAR tugun -> bitta qism-to'plam
for i in range(boshlanish, n): # boshlanish: takrorlanmaslik uchun
joriy.append(elementlar[i]) # TANLA
backtrack(i + 1) # KEZ (i+1: oldingilarni qaytarmaymiz)
joriy.pop() # BEKOR QIL
backtrack(0)
return natija
s = qism_toplamlar([1, 2, 3])
print(len(s)) # -> 8
print(s) # -> [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
boshlanish indeksi β pruning'ning soddagina shakli: [1, 2] va [2, 1] bir xil to'plam bo'lgani uchun biz har doim faqat oldinga (kattaroq indeksga) yuramiz, shu bilan dublikatlarni qurmaymiz ham. n elementli to'plamda 2βΏ = 2Β³ = 8 qism-to'plam.
Kombinatsiyalar. C(n, k) β 1..n dan aynan k ta element tanlash. Faqat bitta cheklov qo'shamiz: uzunlik k ga yetganda yozamiz.
def kombinatsiyalar(n, k):
natija = []
joriy = []
def backtrack(boshlanish):
if len(joriy) == k: # TO'LIQ: k ta tanlandi
natija.append(joriy[:])
return
for son in range(boshlanish, n + 1):
joriy.append(son) # TANLA
backtrack(son + 1) # KEZ
joriy.pop() # BEKOR QIL
backtrack(1)
return natija
c = kombinatsiyalar(4, 2)
print(len(c)) # -> 6
print(c) # -> [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
C(4, 2) = 6 β to'g'ri. E'tibor bering: qism-to'plamlar, kombinatsiyalar va keyingi misollar β hammasi bitta shablonning ozgina o'zgartirilgan ko'rinishi.
Trade-off: Yuqoridagi kodlar
joriy[:]bilan nusxa oladi (ravshanlik uchun). Agar juda ko'p yechim bo'lsa, bu nusxalar xotirani egallaydi. Ko'pincha yechimlarni saqlash o'rniga darhol ishlatib (mas. chop etib yoki sanab) tashlash xotirani tejaydi β backtracking'ning kuchli tomoni: u bir paytda faqat bitta qisman yechimni xotirada saqlaydi, daraxtning chuqurligiO(n).
3-misol. N-vazir (N-queens) β klassik¶
Masala: nΓn shaxmat taxtasiga n ta vazirni bir-birini urmaydigan qilib joylashtiring. Vazir gorizontal, vertikal va ikkala diagonal bo'yicha yuradi.
Asosiy g'oya β kuchli pruning: har qatorga aniq bitta vazir qo'yamiz (chunki ikki vazir bir qatorda bo'lolmaydi). Shunda faqat ustun va diagonallarni tekshirish qoladi. Ikki katak (q1, u1) va (q2, u2) bir diagonalda bo'lishi:
- β diagonal:
q1 β u1 == q2 β u2(qator β ustun bir xil). - β diagonal:
q1 + u1 == q2 + u2(qator + ustun bir xil).
Shu sababli uchta to'plamda band ustun va diagonallarni O(1) da tekshiramiz:
def n_vazir(n):
yechimlar = []
ustun = set()
diag1 = set() # qator - ustun (β)
diag2 = set() # qator + ustun (β)
joylashuv = []
def backtrack(qator):
if qator == n: # TO'LIQ: barcha qatorga vazir qo'yildi
yechimlar.append(joylashuv[:])
return
for u in range(n):
if u in ustun or (qator - u) in diag1 or (qator + u) in diag2:
continue # PRUNING: bu katak hujum ostida
ustun.add(u); diag1.add(qator - u); diag2.add(qator + u) # TANLA
joylashuv.append(u)
backtrack(qator + 1) # KEZ
joylashuv.pop() # BEKOR QIL
ustun.remove(u); diag1.remove(qator - u); diag2.remove(qator + u)
backtrack(0)
return yechimlar
for n in [4, 6, 8]:
print(f"N={n}: {len(n_vazir(n))} ta yechim")
# -> N=4: 2 ta yechim
# -> N=6: 4 ta yechim
# -> N=8: 92 ta yechim
print(n_vazir(4)[0]) # -> [1, 3, 0, 2]
Natija 8 β 92 β bu N-vazirning mashhur to'g'ri javobi (8-vazir masalasining yechimlari soni 92 ta). [1, 3, 0, 2] degani: 0-qatorda vazir 1-ustunda, 1-qatorda 3-ustunda va hokazo.
Diagrammada [1, 3, 0, 2] yechimi va uchta cheklov ko'rsatilgan. Pruningning kuchini his qilish uchun: pruningsiz sof brute force n=8 da 8βΈ = 16 777 216 joylashuvni ko'rardi; backtracking esa atigi 2 057 ta tugunni kezadi β minglab marta kam (buni mashqlarda hisoblaymiz).
Isbot (eskiz): Algoritm har qatorga bitta vazir qo'ygani uchun gorizontal cheklov avtomatik qoniqtiriladi.
ustun,diag1,diag2to'plamlari vertikal va ikkala diagonal cheklovini qo'shishdan oldin tekshiradi, shuning uchun har bir yaroqli qisman joylashuv haqiqatan urishmaydigan.qator == nbarginta urishmaydigan vazir β ta'rifi bo'yicha yechim. Daraxt barcha yaroqli barglarni kezgani uchun barcha yechimlar topiladi.
4-misol. Sudoku yechuvchi¶
Sudoku β backtracking'ning eng tabiiy qo'llanishlaridan. G'oya: birinchi bo'sh katakni topib, unga 1..N dan yaroqli har bir raqamni sinab ko'ramiz; raqamni qo'yib rekursiv davom etamiz; agar boshi berkka tiqilsak β raqamni olib (= 0) keyingisini sinaymiz.
Soddalik uchun 4Γ4 Sudokuni (2Γ2 qutilar, raqamlar 1..4) ko'rsatamiz; 9Γ9 ham aynan shu kod, faqat o'lcham 9 va quti 3Γ3.
def yarokli(plata, q, u, son):
for i in range(4):
if plata[q][i] == son or plata[i][u] == son: # qator yoki ustunda bormi
return False
qb, ub = (q // 2) * 2, (u // 2) * 2 # 2x2 quti boshi
for i in range(qb, qb + 2):
for j in range(ub, ub + 2):
if plata[i][j] == son: # qutida bormi
return False
return True
def sudoku_yech(plata):
for q in range(4):
for u in range(4):
if plata[q][u] == 0: # birinchi bo'sh katak
for son in range(1, 5):
if yarokli(plata, q, u, son):
plata[q][u] = son # TANLA
if sudoku_yech(plata): # KEZ
return True
plata[q][u] = 0 # BEKOR QIL (backtrack)
return False # 1..4 hech biri yaramadi
return True # bo'sh katak yo'q -> yechildi
plata = [
[1, 0, 0, 4],
[0, 0, 0, 0],
[0, 0, 0, 0],
[3, 0, 0, 2],
]
print(sudoku_yech(plata)) # -> True
for r in plata:
print(r)
# -> [1, 2, 3, 4]
# -> [4, 3, 2, 1]
# -> [2, 1, 4, 3]
# -> [3, 4, 1, 2]
E'tibor bering: bo'sh katak qolmasa True qaytadi (yechildi). Ag'ar biror katakda barcha raqam yaroqsiz bo'lsa False qaytadi, va chaqiruvchi avvalgi tanlovini bekor qilib (= 0) boshqa raqamni sinaydi. Aynan shu β orqaga qaytish.
Eslatma: Bu yerda
yaroklifunksiyasi β constraint propagation (cheklov tarqatish)ning sodda shakli: har raqamni qo'yishdan oldin qator/ustun/quti cheklovini tekshirib, yaroqsiz shoxni darhol kesamiz. Murakkabroq Sudoku yechuvchilar bundan ham aqlliroq qoidalar (mas. "eng kam variantli katakdan boshlash") ishlatadi β bu pruningni yanada kuchaytiradi.
5-misol. Labirintdan yo'l topish¶
Masala: 0 β yurish mumkin, 1 β devor bo'lgan gridda yuqori-chap (0,0) dan pastki-o'ng burchakkacha yo'l toping. Backtracking: bir yo'nalishni tanlab yuramiz; tupikka tiqilsak β orqaga qaytib boshqa yo'nalishni sinaymiz.
def labirint_yol(grid):
qatorlar, ustunlar = len(grid), len(grid[0])
yol = []
korilgan = [[False] * ustunlar for _ in range(qatorlar)]
def backtrack(q, u):
if q < 0 or q >= qatorlar or u < 0 or u >= ustunlar:
return False # tashqarida
if grid[q][u] == 1 or korilgan[q][u]:
return False # devor yoki allaqachon ko'rilgan
yol.append((q, u)); korilgan[q][u] = True # TANLA
if (q, u) == (qatorlar - 1, ustunlar - 1):
return True # manzilga yetdik!
for dq, du in [(1, 0), (0, 1), (-1, 0), (0, -1)]: # past, o'ng, yuqori, chap
if backtrack(q + dq, u + du):
return True
yol.pop() # TUPIK -> BEKOR QIL (orqaga qayt)
return False
return yol if backtrack(0, 0) else None
grid = [
[0, 0, 1, 0],
[1, 0, 1, 0],
[0, 0, 0, 0],
[0, 1, 1, 0],
]
print(labirint_yol(grid))
# -> [(0, 0), (0, 1), (1, 1), (2, 1), (2, 2), (2, 3), (3, 3)]
korilgan jadvali β siklga (bir katakka qayta-qayta kirib qolish) qarshi himoya. yol.pop() β tupikdan orqaga qaytishning aniq ifodasi: shu katak yaxshi yo'lga olib bormagani uchun uni yo'ldan olib tashlaymiz.
Bir o'zgartirish bilan bu kod grid'da so'z qidirish masalasini ham yechadi (so'z harflarini qo'shni kataklar bo'ylab tutashtirish): har bargda harfni mos kelishini tekshirib, mos kelmasa shoxni kesamiz, qaytishda katakni tiklab qo'yamiz. Mashqlarda buni ko'ramiz.
6-misol. Kombinatsion yig'indi¶
Masala: musbat sonlar ro'yxati nomzodlar va nishon berilgan; har sondan istalgancha marta ishlatib, yig'indisi nishonga teng bo'lgan barcha kombinatsiyalarni toping. Bu yerda pruning ayniqsa chiroyli ko'rinadi.
def kombinatsion_yigindi(nomzodlar, nishon):
natija = []
nomzodlar = sorted(nomzodlar) # PRUNING uchun saralaymiz
joriy = []
def backtrack(boshlanish, qoldiq):
if qoldiq == 0: # TO'LIQ: nishonga aniq yetdik
natija.append(joriy[:])
return
for i in range(boshlanish, len(nomzodlar)):
if nomzodlar[i] > qoldiq: # PRUNING: saralangan -> qolganlari ham katta
break # bu va undan keyingilarni kesamiz
joriy.append(nomzodlar[i]) # TANLA
backtrack(i, qoldiq - nomzodlar[i]) # i: shu sonni qayta ishlatish mumkin
joriy.pop() # BEKOR QIL
backtrack(0, nishon)
return natija
print(kombinatsion_yigindi([2, 3, 6, 7], 7))
# -> [[2, 2, 3], [7]]
break β bu constraint propagationning yaqqol misoli: ro'yxat saralangani uchun, agar joriy son qoldiqdan katta bo'lsa, undan keyingi sonlar ham albatta kattaroq, demak hammasini birdaniga kesamiz. Bu pruning amalda tugunlar sonini sezilarli kamaytiradi (mashqlarda o'lchaymiz: bu kichik misolda 10 tugun, sodda variantda 28).
Pruning β kesib tashlash¶
Yuqoridagi misollar yashirin bir haqiqatni ko'rsatadi: backtracking samaradorligi to'liq pruning sifatiga bog'liq. Cheklovni qancha erta tekshirsangiz, shuncha katta pastki daraxtni kesib, shuncha ko'p ish tejaysiz.
Pruning'ning umumiy turlari:
- Yaroqlilik cheklovi (feasibility): joriy qisman yechim cheklovni buzsa, davom etmaymiz (N-vazirda ustun/diagonal, Sudokuda qator/ustun/quti).
- Chegara cheklovi (bound): "bu shoxdan eng yaxshi javob ham hozirgi eng yaxshimdan yomon" β butun shoxni tashlaymiz. Bu β branch and bound g'oyasi (optimallashtirish masalalarida).
- Tartib bilan kesish: kombinatsion yig'indidagi
breakkabi β saralanganlikdan foydalanib, bir butun "dum"ni birdaniga kesish.
Anti-pattern: Pruningni butun nomzod qurilgandan keyin tekshirish β bu backtracking emas, oddiy brute force. Backtracking'ning butun foydasi cheklovni iloji boricha erta (har bo'lakdan keyin) tekshirishda. Cheklovni faqat bargda tekshirsangiz, kod to'g'ri ishlaydi-yu, lekin tezligi yo'qoladi.
Trade-off: Kuchliroq pruning kamroq tugun beradi, lekin har tugunda ko'proq tekshirish narxini qo'shadi. Maqsad β bu ikkisining mahsulotini (jami ish) minimallashtirish. Ko'pincha
O(1)da tekshiriladigan oddiy yaroqlilik cheklovi (to'plam orqali, N-vazirdagidek) eng yaxshi muvozanat.
Murakkablik¶
Backtracking β to'liq qidiruv bo'lgani uchun eng yomon holatda murakkablik qaror daraxtining hajmiga teng, ya'ni odatda eksponensial yoki faktorial:
| Masala | Eng yomon holat | Izoh |
|---|---|---|
| Permutatsiyalar | O(n! Β· n) |
n! barg, har birini nusxalash O(n) |
| Qism-to'plamlar | O(2βΏ Β· n) |
2βΏ qism-to'plam |
| N-vazir | O(n!) (yuqori chegara) |
har qatorda β€ n tanlov, pruning bilan amalda ancha kam |
| Sudoku (9Γ9) | eksponensial | amalda pruning bilan tez |
Ikki muhim nuqta:
- Pruning eng yomon nazariy chegarani odatda o'zgartirmaydi, lekin amaliy ishni dramatik kamaytiradi. N-vazirda
8βΈ β 16.8 milliono'rniga atigi ~2057 tugun β bu pruningning amaldagi kuchi, garchi nazariy "yomon holat" baribir eksponensial bo'lsa ham. - Xotira
O(n)(daraxt chuqurligi) β backtracking bir paytda faqat bitta yo'lni (ildizdan joriy tugungacha) xotirada saqlaydi. Bu uning DP'dan (ko'pinchaO(nΒ·m)jadval) ustunligi.
Diqqat: Backtracking eksponensial bo'lgani uchun u faqat kichik kirishlarda yoki kuchli pruning mavjud bo'lganda amaliy.
nkatta bo'lsa va pruning kuchsiz bo'lsa β boshqa paradigma (DP, greedy) yoki yaqinlashtiruvchi (approximation) yechim kerak. Ko'p backtracking masalalari aslida NP-qiyin β ya'ni hech kim ular uchun polinomial aniq algoritm topa olmagan.
Backtracking vs DP vs greedy¶
Uchala paradigma ham qaror ketma-ketligini quradi, lekin maqsad va usul boshqacha:
| Qachon ishlatamiz | Usul | |
|---|---|---|
| Backtracking | Barcha yechimni sanash kerak; yoki cheklovli qoniqtirish (CSP) β N-vazir, Sudoku; yoki to'liq qidiruvdan boshqa yo'l yo'q | Qaror daraxtini kez + pruning; yaroqsiz shoxni bekor qil |
| Dinamik dasturlash | Takrorlanuvchi qism-masalalar bor; bitta optimal qiymat/son kerak | Qism-masalalar javobini eslab qol, qayta hisoblama |
| Greedy | Har qadamdagi mahalliy eng yaxshi tanlov global optimalga olib keladi (isbotlanadigan xossa) | Har qadamda eng yaxshini ol, orqaga qaytma |
Qisqacha farq:
- Greedy β orqaga qaytmaydi: har qadamda bir tanlov qiladi va davom etadi. Tez (odatda
O(n log n)), lekin faqat maxsus masalalarda to'g'ri. - DP β barcha qism-masalalarni hisoblaydi, lekin takrorlanmaydi (memoizatsiya). Bitta optimal qiymatni samarali topadi, ammo barcha yechimni sanash uchun emas.
- Backtracking β barcha yaroqli yo'lni kezadi (orqaga qaytib), pruning bilan yaroqsizlarini tashlaydi. Eng umumiy va eng sekin, lekin barcha/optimal yechimni kafolatlaydi.
Eslatma: Ba'zan masala bir nechta paradigmaga mos keladi. Mas. kichik knapsack'ni backtracking + branch-and-bound bilan ham, DP bilan ham yechish mumkin. Tanlov: bitta optimal qiymat kerakmi (DP) yoki barcha optimal yechimlar kerakmi (backtracking), va
nqanchaligi.
Asosiy g'oyalar (bobni qisqacha)¶
- Backtracking β yechimni qadamba-qadam qurib, har qadamda variant tanlab, tupikka tiqilsa orqaga qaytib (tanlovni bekor qilib) boshqasini sinaydigan paradigma. "Aqlli brute force".
- Shablon:
agar TO'LIQ -> yoz; aks holda har nomzod uchun: agar YAROQLI -> TANLA, KEZ (rekursiya), BEKOR QIL. Uchinchi qadam (bekor qilish) β backtracking'ning belgisi. - Brute force'dan farq: backtracking cheklovni erta (har bo'lakdan keyin) tekshiradi va yaroqsiz shoxni butunlay kesadi (pruning) β to'liq qurishga urinmaydi.
- Klassik masalalar: permutatsiyalar (
n!), qism-to'plamlar (2βΏ), kombinatsiyalar (C(n,k)), N-vazir (8 β 92 yechim), Sudoku, labirint/so'z qidirish, kombinatsion yig'indi β hammasi bitta shablon. - Pruning samaradorlikning kaliti: cheklovni qancha erta tekshirsang, shuncha katta pastki daraxt kesiladi. Tartibdan foydalanib kesish (
break), yaroqlilik va chegara cheklovlari. - Murakkablik: eng yomon holatda eksponensial/faktorial; xotira
O(n)(chuqurlik). Pruning eng yomon chegarani odatda o'zgartirmaydi, lekin amalda dramatik tejaydi. - Backtracking barcha yoki optimal yechimni kafolatlaydi β barcha yechimni sanash kerak bo'lganda yoki cheklovli qoniqtirish (CSP) masalalarida tanlanadi.
Mashqlar¶
Oson¶
1-mashq. [A, B, C] ning permutatsiyalari qaror daraxtini (qog'ozda) chizing. Nechta barg bor? Umuman n element uchun nechta? Formulani ayting.
2-mashq. Backtracking shablonidagi uchta asosiy qadamni (TANLA, KEZ, BEKOR QIL) o'z so'zlaringiz bilan tushuntiring. Nega "BEKOR QIL" qadami zarur? Agar uni unutib qoldirsak nima bo'ladi?
3-mashq. Quyidagilardan qaysi biri pruning (kesib tashlash)ning misoli? (a) N-vazirda yangi vazir oldingisini urganini ko'rib, o'sha ustunni o'tkazib yuborish; (b) barcha permutatsiyalarni qurib bo'lgach, yaroqsizlarini o'chirish; (c) kombinatsion yig'indida saralangan ro'yxatda nomzod > qoldiq bo'lsa break. Har birini "ha/yo'q" deb javob bering va nega.
O'rta¶
4-mashq. [1, 2, 2] (takror element bor!) ning noyob permutatsiyalarini qaytaruvchi funksiya yozing. Oddiy permutatsiya kodi [1,2,2] ni necha marta, sizning kodingiz necha marta qaytaradi? (Maslahat: bir darajada bir xil qiymatni ikki marta tanlamaslik uchun pruning qo'shing.)
5-mashq. Kombinatsion yig'indi kodida break ni continue ga almashtirsangiz, javob to'g'ri qoladimi? Tezlik o'zgaradimi? Nega? (Maslahat: saralanganlik nimani kafolatlaydi.)
6-mashq. Grid'da so'z qidirish funksiyasini yozing: berilgan so'z gridda qo'shni (yuqori/past/chap/o'ng) kataklar orqali tutashib chiqadimi? Bir katakni ikki marta ishlatib bo'lmaydi. Pruning qayerda? Backtrack (tiklash) qadami qayerda?
Qiyin¶
7-mashq. N-vazir kodini o'zgartirib, har bir n (4..8) uchun nechta tugun kezilishini sanang. Buni 8βΈ = 16 777 216 (pruningsiz to'liq qidiruv) bilan solishtiring. Pruning necha barobar tejadi?
8-mashq. 4Γ4 Sudoku yechuvchini ishga tushiring va sudoku_yech rekursiyasi necha marta chaqirilishini sanang. So'ng "eng kam variantli bo'sh katakdan boshlash" (most-constrained-variable) evristikasini qo'shsangiz, bu son qanday o'zgaradi deb o'ylaysiz? (Tahlil; kod ixtiyoriy.)
9-mashq. Kombinatsion yig'indida pruning samarasini o'lchang: bir versiya break (pruning bilan), boshqasi pruningsiz (har shoxga kirib, qoldiq < 0 da to'xtaydigan) β [2, 3, 6, 7], nishon = 7 uchun har biri necha tugun kezadi? Pruning qancha tejaydi?
Yechimlar
1-mashq yechimi¶
Daraxt: ildizdan 3 shox (A, B, C β 1-pozitsiya), har biridan 2 shox (qolgan 2 element), har biridan 1 shox. Barglar: 3 Γ 2 Γ 1 = 6 ta. Ular: ABC, ACB, BAC, BCA, CAB, CBA.
Umuman n element uchun barglar (permutatsiyalar) soni n! β chunki 1-pozitsiyaga n variant, 2-pozitsiyaga nβ1, ..., oxirgisiga 1.
2-mashq yechimi¶
- TANLA: joriy qisman yechimga bitta nomzodni qo'shamiz (va kerakli umumiy holatni β
ishlatildi, taxta katagi β yangilaymiz). - KEZ: shu yangi qisman yechimdan rekursiv davom etamiz (pastki daraxtni o'rganamiz).
- BEKOR QIL: rekursiya qaytgach, qo'shgan nomzodni olib tashlaymiz va o'zgartirilgan holatni qaytaramiz.
"BEKOR QIL" zarur, chunki shundan keyin biz boshqa nomzodni toza boshlang'ich holatdan sinashimiz kerak. Agar uni unutsak, oldingi tanlovning "izi" (mas. ishlatildida True qolib ketgan element, yoki taxtadagi qo'yilgan vazir) keyingi shoxlarga oqib o'tadi β natijada yechimlar noto'g'ri yoki kam chiqadi. Holat "iflos"lanadi.
3-mashq yechimi¶
- (a) Ha β yangi vazir cheklovni buzganini ko'rib, o'sha shoxga kirmaymiz. Bu β yaroqlilik pruning'i.
- (b) Yo'q β bu pruning emas, balki sof brute force: hamma narsani qurib bo'lib, keyin yaroqsizini tashlash. Pruning aynan qurishdan oldin kesishdir.
- (c) Ha β saralanganlikdan foydalanib, joriy va undan keyingi barcha (kattaroq) nomzodlarni birdaniga kesish. Bu β tartib bilan pruning.
4-mashq yechimi¶
Avval saralaymiz, so'ng bir darajada bir xil qiymatni ikki marta tanlamaymiz (yondosh dublikatlarni o'tkazib yuboramiz):
def noyob_permutatsiyalar(elementlar):
elementlar = sorted(elementlar) # dublikatlar yondosh bo'lsin
natija = []
n = len(elementlar)
ishlatildi = [False] * n
joriy = []
def backtrack():
if len(joriy) == n:
natija.append(joriy[:])
return
for i in range(n):
if ishlatildi[i]:
continue
# PRUNING: oldingi bir xil qiymat ishlatilmagan bo'lsa -> o'tkaz
if i > 0 and elementlar[i] == elementlar[i - 1] and not ishlatildi[i - 1]:
continue
ishlatildi[i] = True
joriy.append(elementlar[i])
backtrack()
joriy.pop()
ishlatildi[i] = False
backtrack()
return natija
print(noyob_permutatsiyalar([1, 2, 2])) # -> [[1, 2, 2], [2, 1, 2], [2, 2, 1]]
print(len(noyob_permutatsiyalar([1, 2, 2]))) # -> 3
Oddiy permutatsiya kodi [1,2,2] ni 3! = 6 marta qaytaradi (ikkala 2 ni farqli deb), lekin ulardan faqat 3 tasi noyob. Pruning bilan kod aynan 3 tasini qaytaradi. Pruning sharti: "agar oldingi bir xil qiymat hozir ishlatilmayotgan bo'lsa, joriysini tanlamaymiz" β bu bir darajada bir xil qiymatni faqat bir tartibda ishlatishni ta'minlaydi.
5-mashq yechimi¶
Javob to'g'ri qoladi: continue ham nomzodlar[i] > qoldiq bo'lgan sonni o'tkazib yuboradi, demak yaroqsiz shox baribir qurilmaydi. Lekin tezlik yomonlashadi: ro'yxat saralangani uchun, agar nomzodlar[i] > qoldiq bo'lsa, undan keyingi barcha sonlar ham qoldiqdan katta β ularning hammasi ham o'tkazib yuboriladi. break bularning barchasini birdaniga to'xtatadi (O(1)), continue esa har birini alohida tekshirib o'tadi (bekorga aylanish). Demak break β to'g'riligini saqlab, ortiqcha iteratsiyalarni kesadi. Saralanganlik aynan shu kafolatni beradi.
6-mashq yechimi¶
def soz_bormi(plata, soz):
qatorlar, ustunlar = len(plata), len(plata[0])
def backtrack(q, u, idx):
if idx == len(soz):
return True # butun so'z topildi
if q < 0 or q >= qatorlar or u < 0 or u >= ustunlar:
return False
if plata[q][u] != soz[idx]: # PRUNING: harf mos kelmadi -> kes
return False
belgi = plata[q][u]
plata[q][u] = "#" # TANLA: tashrif belgisi (qayta ishlatmaslik)
topildi = (backtrack(q + 1, u, idx + 1) or backtrack(q - 1, u, idx + 1)
or backtrack(q, u + 1, idx + 1) or backtrack(q, u - 1, idx + 1))
plata[q][u] = belgi # BEKOR QIL: belgini tikla
return topildi
for q in range(qatorlar):
for u in range(ustunlar):
if backtrack(q, u, 0):
return True
return False
plata = [["A", "B", "C", "E"],
["S", "F", "C", "S"],
["A", "D", "E", "E"]]
print(soz_bormi([r[:] for r in plata], "ABCCED")) # -> True
print(soz_bormi([r[:] for r in plata], "ABCB")) # -> False
- Pruning:
plata[q][u] != soz[idx]β harf mos kelmasa, shu shoxni darhol kesamiz (rekursiyaga umuman kirmaymiz). - Backtrack (tiklash):
plata[q][u] = belgiβ rekursiyadan qaytgach katakni asl harfiga tiklaymiz, toki u boshqa yo'lda yana ishlatilishi mumkin bo'lsin. (Vaqtincha"#"qo'yish β bir yo'l ichida bir katakni ikki marta ishlatmaslik uchun.)
7-mashq yechimi¶
def nq_tugunlar(n):
cnt = [0]
ust, d1, d2 = set(), set(), set()
def bt(r):
cnt[0] += 1
if r == n:
return
for u in range(n):
if u in ust or (r - u) in d1 or (r + u) in d2:
continue
ust.add(u); d1.add(r - u); d2.add(r + u)
bt(r + 1)
ust.remove(u); d1.remove(r - u); d2.remove(r + u)
bt(0)
return cnt[0]
for n in range(4, 9):
print(f"N={n}: {nq_tugunlar(n)} tugun")
# -> N=4: 17 tugun
# -> N=5: 54 tugun
# -> N=6: 153 tugun
# -> N=7: 552 tugun
# -> N=8: 2057 tugun
n=8 da backtracking atigi 2057 tugun kezadi, pruningsiz to'liq qidiruv esa 8βΈ = 16 777 216 joylashuvni ko'rardi. Tejash: 16 777 216 / 2057 β 8156 barobar. Mana shu β pruningning amaldagi kuchi: nazariy "yomon holat" eksponensial bo'lsa-da, real ishni minglab marta kamaytiradi.
8-mashq yechimi¶
Berilgan plata uchun sudoku_yech chaqiruvlari sonini sanaymiz:
chaqiruvlar = [0]
def yarokli(plata, q, u, son):
for i in range(4):
if plata[q][i] == son or plata[i][u] == son:
return False
qb, ub = (q // 2) * 2, (u // 2) * 2
for i in range(qb, qb + 2):
for j in range(ub, ub + 2):
if plata[i][j] == son:
return False
return True
def sudoku_yech(plata):
chaqiruvlar[0] += 1
for q in range(4):
for u in range(4):
if plata[q][u] == 0:
for son in range(1, 5):
if yarokli(plata, q, u, son):
plata[q][u] = son
if sudoku_yech(plata):
return True
plata[q][u] = 0
return False
return True
plata = [[1,0,0,4],[0,0,0,0],[0,0,0,0],[3,0,0,2]]
sudoku_yech(plata)
print(chaqiruvlar[0]) # -> 14
Bu kichik misolda 14 ta chaqiruv. Most-constrained-variable evristikasi (eng kam yaroqli variantli bo'sh katakdan boshlash) chaqiruvlar sonini kamaytiradi: eng "qattiq" katakni avval to'ldirib, yaroqsiz shoxlarni daraxtning yuqorisida (kichikroq pastki daraxt bilan) kesamiz. Tartibsiz (birinchi bo'sh katak) tanlovga nisbatan bu odatda tugunlar sonini sezilarli kamaytiradi β chunki tezroq qarama-qarshilikka duch kelib, butun shoxni erta tashlaymiz. Katta (9Γ9 va qiyin) Sudokularda bu farq dramatik bo'ladi.
9-mashq yechimi¶
def pruning_bilan(nomzodlar, nishon):
nomzodlar = sorted(nomzodlar)
tugunlar = [0]
def bt(boshlanish, qoldiq):
tugunlar[0] += 1
if qoldiq == 0:
return
for i in range(boshlanish, len(nomzodlar)):
if nomzodlar[i] > qoldiq: # PRUNING
break
bt(i, qoldiq - nomzodlar[i])
bt(0, nishon)
return tugunlar[0]
def pruningsiz(nomzodlar, nishon):
nomzodlar = sorted(nomzodlar)
tugunlar = [0]
def bt(boshlanish, qoldiq):
tugunlar[0] += 1
if qoldiq <= 0: # faqat manfiyga tushganda to'xtaydi
return
for i in range(boshlanish, len(nomzodlar)):
bt(i, qoldiq - nomzodlar[i])
bt(0, nishon)
return tugunlar[0]
print(pruning_bilan([2, 3, 6, 7], 7)) # -> 10
print(pruningsiz([2, 3, 6, 7], 7)) # -> 28
Pruning bilan 10 tugun, pruningsiz 28 tugun β pruning bu kichik misolda ~2.8 barobar tejadi. nishon katta bo'lsa yoki ko'p nomzod bo'lsa, bu farq tez o'sadi: pruningsiz versiya manfiy qoldiqgacha bemaqsad chuqurlashadi, pruningli versiya esa "umidsiz" shoxlarga umuman kirmaydi. Bu β backtracking samaradorligi to'g'ridan-to'g'ri pruning sifatiga bog'liqligini ko'rsatadi.
β¬ οΈ Oldingi: 25 β Dinamik dasturlash (DP) Β· π README Β· Keyingi: 27 β Saralash algoritmlari β‘οΈ