Ders 05 / 23
Geri İzleme
Budamanın doğruluğu koruyan tek kısaltma olduğu ve ölçütü bozulduğunda ne olduğu: yedi vezirlik tahtada budamalı arama 552, budamasız arama 960.800 düğüm geziyor, ama komşu sütunu da kesen aşırı budama 82 düğümde bitip çözümlerin tamamını kaybediyor.
İçindekiler
Dinamik programlama alt problem uzayını tümüyle dolaşıp saklıyordu ve bunu yapabiliyordu, çünkü uzay tabloya sığıyordu. Bazı problemlerde uzay tabloya sığmaz: sekiz vezirlik bir tahtada 16.777.216 yerleşim vardır ve bunların yalnız 92’si çözümdür. Böyle bir uzayda tek yol, büyük bölümünün çözüm içermediğini kanıtlayıp kesmektir.
Kalıbın adı geri izleme (backtracking): çözüm parça parça kurulur, kurulan parça bir kısıtı çiğnediği anda o dal bırakılır ve bir önceki karara dönülür. Kesme işlemine budama (pruning) denir. Budama, bu kursta görülen kısaltmalar arasında özel bir yerdedir — ilk dersteki eleme gibi doğruluğu korur, ama yalnız ölçütü doğruysa. Bu ders o “yalnız”ı sayar.
- TY38. Ölçülen problem: tahtada birbirini tehdit etmeyen vezir yerleşimlerinin sayısı.
- TY39. Bir adım, arama ağacındaki bir düğümdür; yapraklar da düğüm sayılır.
- TY40. Kâhin budamasız aramadır: bütün yerleşimleri üretir ve geçerliliği sonda sınar. Hiçbir dalı kesmediği için hiçbir çözümü kaçıramaz.
- TY41. Kalıbın ön koşulu: budama ölçütü yalnız kesin olarak çözümsüz dalları kesmelidir.
- TY42. Üç ölçüt ölçülür — doğru, aşırı (çözüm içeren dalı da keser), eksik (çözümsüz dalı kesmez).
- TY43. Dağarcık burada tahtadır: altı vezirlik tahtada üç kare yasak, 40 tahta.
- TY44. Ayrılan girdi, kâhin ile kalıbın çözüm sayısının farklı olduğu tahtadır.
- TY45. Sekiz vezirlik tahtada budamasız aramanın tarayacağı 16.777.216 yerleşim hesaplanır, koşturulmaz; ilk dersin adım bütçesi bunu gerektirir.
- TY46. Her ölçüm 20260219 tohumlu ikinci dağarcıkta da koşturulur.
Kısmi Çözüm Ağacı
Geri izleme, çözümü bir dizi karar olarak görür. Her karar bir düğüm üretir, her düğümün çocukları o karardan sonraki seçeneklerdir. Bu yapıya kısmi çözüm ağacı denir ve yaprakları tam çözüm adaylarıdır.
Budamasız arama bu ağacın tamamını gezer ve geçerlilik sınamasını yalnız yapraklarda yapar. Budamalı arama sınamayı her düğümde yapar: kısmi çözüm zaten bir kısıtı çiğniyorsa, o düğümün altındaki bütün yapraklar da çiğneyecektir ve dal kesilebilir. Kesmenin meşruluğu tam olarak bu çıkarıma dayanır — alt ağaçta çözüm olmadığı kanıtlanmış olmalıdır.
Kanıt zayıfladığında iki yönde hata yapılır. Ölçüt gereğinden fazla keserse çözüm içeren dallar da gider ve yanıt eksik çıkar. Gereğinden az keserse çözümsüz dallar gezilmeye devam eder; bu tek başına yanlış yanıt vermez, ama yaprakta yapılan sınama da zayıfsa geçersiz yerleşimler çözüm sayılır.
İki hata biçiminin çıktıdan ayırt edilme olanağı yoktur. Eksik yerleşim listesi de fazla yerleşim listesi de bir liste olarak dönmeyi sürdürür; hangisinin doğru olduğunu söyleyecek tek şey, dalları hiç kesmeyen kâhinin ürettiği sayıdır.
Budamanın Kestiği Uzay
# Ortak tanimin geri izleme olcumu: ayni arama, budamali ve budamasiz. class Sayac: def __init__(self): self.adim = 0 def say(self, n=1): self.adim += n def vezir(n, budama=True): """Budamasiz surum butun yerlesimleri dener; budamali surum catismada durur.""" s = Sayac() cozum = [] def gez(satir, yer): s.say() if satir == n: cozum.append(tuple(yer)) return for sutun in range(n): if budama and any(sutun == y or abs(sutun - y) == satir - i for i, y in enumerate(yer)): continue yer.append(sutun) gez(satir + 1, yer) yer.pop() gez(0, []) if not budama: cozum = [c for c in cozum if all(c[i] != c[j] and abs(c[i] - c[j]) != j - i for i in range(n) for j in range(i + 1, n))] return {"dugum": s.adim, "cozum": len(cozum)} for n in (5, 6, 7): b, k = vezir(n, True), vezir(n, False) print(f"n={n} budamali dugum {b['dugum']:5d} cozum {b['cozum']:3d}" f" | budamasiz dugum {k['dugum']:7d} cozum {k['cozum']:3d}" f" | oran {k['dugum'] / b['dugum']:7.1f}") b8 = vezir(8, True) print(f"n=8 budamali dugum {b8['dugum']:5d} cozum {b8['cozum']:3d}" f" | budamasiz {8 ** 8} yerlesim taranirdi (oran {8 ** 8 / b8['dugum']:.1f})")
n=5 budamali dugum 54 cozum 10 | budamasiz dugum 3906 cozum 10 | oran 72.3 n=6 budamali dugum 153 cozum 4 | budamasiz dugum 55987 cozum 4 | oran 365.9 n=7 budamali dugum 552 cozum 40 | budamasiz dugum 960800 cozum 40 | oran 1740.6 n=8 budamali dugum 2057 cozum 92 | budamasiz 16777216 yerlesim taranirdi (oran 8156.2)
Çözüm sütunları üç satırda da aynı: 10, 4, 40. Budama hiçbir çözümü kaçırmıyor, yani ayrılan girdi sıfırdır. Düğüm sütunları ise uçurumu gösteriyor: yedi vezirlik tahtada 552’ye karşı 960.800 düğüm, oran 1740,6. Sekiz vezirlikte budamalı arama 2057 düğümde bitiyor; budamasız arama 16.777.216 yerleşim tarardı ve oran 8156,2 olurdu.
Oran ile büyüyor: 72,3 sonra 365,9 sonra 1740,6 sonra 8156,2 — her adımda yaklaşık beş kat. Budamanın kazancı ölçek arttıkça artıyor. Ama ikinci bir okuma bunu dengeler: budamalı düğüm sayısının kendisi de 54, 153, 552, 2057 diye gidiyor, yani her adımda yaklaşık 3,7 kat. Budama üstel uzayı kesiyor ama üstelliği yok etmiyor. İki katı büyük bir tahta, budamalı aramada da erişilmez kalır; budama erteler, çözmez.
Kesmenin Hangi Kısıttan Geldiği
Vezir probleminin iki kısıtı vardır — aynı sütun ve aynı köşegen — ve budama ikisini birden kullanıyordu. Kısıtlar ayrı ayrı da uygulanabilir; her biri kesin olarak çözümsüz dalları kestiği için üçü de doğruluğu korur.
# Onceki bloklarin uzerine: Sayac oradan gelir. def parcali_budama(n, olcut): """olcut: yok | sutun | kosegen | ikisi. Hepsinde yaprakta tam dogrulama var.""" s = Sayac() cozum = 0 def gez(satir, yer): nonlocal cozum s.say() if satir == n: if all(yer[i] != yer[j] and abs(yer[i] - yer[j]) != j - i for i in range(n) for j in range(i + 1, n)): cozum += 1 return for sutun in range(n): if olcut in ("sutun", "ikisi") and sutun in yer: continue if olcut in ("kosegen", "ikisi") and any( abs(sutun - y) == satir - i for i, y in enumerate(yer)): continue yer.append(sutun) gez(satir + 1, yer) yer.pop() gez(0, []) return {"dugum": s.adim, "cozum": cozum} print("n=7 olcut dugum cozum") for olcut in ("yok", "sutun", "kosegen", "ikisi"): r = parcali_budama(7, olcut) print(f" {olcut:9s} {r['dugum']:7d} {r['cozum']:7d}")
n=7 olcut dugum cozum
yok 960800 40
sutun 13700 40
kosegen 10736 40
ikisi 552 40
Çözüm sütunu dört satırda da 40; dördü de doğru. Düğüm sütunu ise budamanın gücünün neye bağlı olduğunu gösteriyor. Yalnız sütun kısıtı 960.800 düğümü 13.700’e indiriyor — 70,1 kat. Yalnız köşegen kısıtı 10.736’ya indiriyor — 89,5 kat. İkisi birlikte 552, yani 1740,6 kat. Birleşik kesme, iki tekil kesmenin toplamından çok daha fazlasını kazandırıyor; çünkü bir kısıtın bıraktığı dalların çoğunu öteki kısıt kesiyor.
Buradan çıkan tasarım kuralı ölçülmüştür: bir kısıt ne kadar erken kanıtlanabilirse o kadar çok keser. Kısıtları yaprakta sınamak bir doğrulamadır, düğümde sınamak bir budamadır; aradaki fark bu problemde 960.800 ile 552 arasındaki farktır.
Budama Ölçütü Yanlış Olduğunda
Aynı arama, üç farklı ölçütle koşturulur. İkisi ön koşulu bozar.
# Onceki blogun uzerine: Sayac ve vezir oradan gelir. def vezir_kipli(n, kip): """kip: dogru | asiri (komsu sutun degerini de keser) | eksik (yalniz onceki satir)""" s = Sayac() cozum = [] def catisma(sutun, satir, yer): if kip == "eksik": i = satir - 1 return i >= 0 and (sutun == yer[i] or abs(sutun - yer[i]) == 1) temel = any(sutun == y or abs(sutun - y) == satir - i for i, y in enumerate(yer)) if kip == "asiri": # "vezirler yan yana sutunda olmasin" return temel or any(abs(sutun - y) == 1 for y in yer) return temel def gez(satir, yer): s.say() if satir == n: cozum.append(tuple(yer)) return for sutun in range(n): if catisma(sutun, satir, yer): continue yer.append(sutun) gez(satir + 1, yer) yer.pop() gez(0, []) return {"dugum": s.adim, "cozum": len(cozum)} print(" n kahin cozum dogru budama asiri budama eksik budama") ayrilan = {"dogru": 0, "asiri": 0, "eksik": 0} for n in (5, 6, 7): kahin = vezir(n, False) satir = f"{n:2d} {kahin['cozum']:12d}" for kip in ("dogru", "asiri", "eksik"): r = vezir_kipli(n, kip) if r["cozum"] != kahin["cozum"]: ayrilan[kip] += 1 satir += f" {r['cozum']:4d} ({r['dugum']:5d})" print(satir) print("kahinden ayrilan tahta sayisi (3 uzerinden):", ayrilan)
n kahin cozum dogru budama asiri budama eksik budama
5 10 10 ( 54) 0 ( 20) 184 ( 306)
6 4 4 ( 153) 0 ( 39) 2642 ( 3747)
7 40 40 ( 552) 0 ( 82) 45514 (59196)
kahinden ayrilan tahta sayisi (3 uzerinden): {'dogru': 0, 'asiri': 3, 'eksik': 3}
Üç sayı yan yana ve bu kez ikisi de kötü. Doğru budama üç tahtada da kâhinin çözüm sayısını veriyor: 10, 4, 40. Aşırı budama üçünde de sıfır çözüm buluyor — “vezirler yan yana sütunlarda durmasın” kuralı makul görünür, ama kesin olarak çözümsüz olmayan dalları kestiği için geriye hiçbir şey bırakmıyor. Eksik budama yalnız bir önceki satıra baktığı için uzak satırlardaki köşegen çatışmalarını göremiyor ve yedi vezirlik tahtada 40 yerine 45.514 yerleşimi çözüm sayıyor.
Düğüm sütunları burada tuzaktır. Aşırı budama yedi vezirlikte 82 düğümde bitiyor; doğru budamanın 552 düğümüne göre 6,7 kat hızlı. Bir ölçüm yalnız düğüm sayarsa aşırı budama en iyi yordam görünür. Eksik budama ise 59.196 düğüm gezerek en yavaşı; yani yanlış olmak her zaman hızlı değildir. Yanlışlığın iki biçimi vardır ve düğüm sayısı ikisini de ele vermez; ele veren tek şey kâhinin çözüm sayısıdır.
Yasaklı Karelerde Dağarcık
Tek bir tahta ailesi bir dağarcık değildir. Ölçüm, altı vezirlik tahtaya üçer yasak kare eklenerek kırk ayrı örneğe taşınır ve iki tohumla koşturulur.
# Onceki bloklarin uzerine: Sayac ve vezir_kipli oradan gelir. def uretec(tohum): d = tohum def sonraki(n): nonlocal d d = (d * 1103515245 + 12345) % 2147483648 return d % n return sonraki N = 6 YASAK_SAYISI = 3 def tahta_dagarcigi(tohum, adet=40): """Her tahtada 3 kare yasak. Yasak kareler tohumdan gelir.""" r = uretec(tohum) return [{"no": i + 1, "yasak": {(r(N), r(N)) for _ in range(YASAK_SAYISI)}} for i in range(adet)] def yasakli_arama(n, yasak, kip, s): """kip: kahin (budamasiz, sonda dogrula) | dogru | asiri""" cozum = [] def gecerli(c): if any((i, c[i]) in yasak for i in range(n)): return False return all(c[i] != c[j] and abs(c[i] - c[j]) != j - i for i in range(n) for j in range(i + 1, n)) def gez(satir, yer): s.say() if satir == n: if kip != "kahin" or gecerli(tuple(yer)): cozum.append(tuple(yer)) return for sutun in range(n): if kip != "kahin": if (satir, sutun) in yasak: continue if any(sutun == y or abs(sutun - y) == satir - i for i, y in enumerate(yer)): continue if kip == "asiri" and any(abs(sutun - y) == 1 for y in yer): continue yer.append(sutun) gez(satir + 1, yer) yer.pop() gez(0, []) return len(cozum) for ad, tohum in (("birinci dagarcik (20260218)", 20260218), ("ikinci dagarcik (20260219)", 20260219)): tahtalar = tahta_dagarcigi(tohum) sh, sd, sa = Sayac(), Sayac(), Sayac() ay_dogru, ay_asiri, kayip = 0, 0, 0 for t in tahtalar: h = yasakli_arama(N, t["yasak"], "kahin", sh) d = yasakli_arama(N, t["yasak"], "dogru", sd) a = yasakli_arama(N, t["yasak"], "asiri", sa) ay_dogru += h != d ay_asiri += h != a kayip += h - a print(ad) print(" kahin dugum", sh.adim, "| dogru budama dugum", sd.adim, "| oran", round(sh.adim / sd.adim, 2)) print(" ayrilan girdi: dogru budama", ay_dogru, "/ 40 | asiri budama", ay_asiri, "/ 40 | kaybolan cozum", kayip)
birinci dagarcik (20260218) kahin dugum 2239480 | dogru budama dugum 4736 | oran 472.86 ayrilan girdi: dogru budama 0 / 40 | asiri budama 40 / 40 | kaybolan cozum 92 ikinci dagarcik (20260219) kahin dugum 2239480 | dogru budama dugum 4452 | oran 503.03 ayrilan girdi: dogru budama 0 / 40 | asiri budama 40 / 40 | kaybolan cozum 90
Kâhinin düğüm sayısı iki dağarcıkta birebir aynı: 2.239.480. Bu beklenen sonuçtur — budamasız arama yasak karelere bakmadan bütün yerleşimleri üretir, yani düğüm sayısı tahtadan bağımsızdır. Doğru budamanın düğümü 4736 ile 4452 arasında değişiyor, çünkü yasak kareler dalları erkenden kesiyor; oran 472,86 ve 503,03.
Ayrılan girdi sayıları iki dağarcıkta da aynı yönü gösteriyor: doğru budama 0/40, aşırı budama 40/40. Kaybolan çözüm sayısı 92 ile 90; aradaki iki çözümlük fark yasak karelerin yerleşiminden gelir ve ölçünün anlamlı saydığı eşiğin altındadır. Okuma iki dağarcıkta da değişmiyor: doğru budama kırk tahtanın kırkında kâhinle aynı, aşırı budama kırkında da farklı.
Özet
- Geri izleme, kısmi çözüm ağacında bir dalın altında çözüm olmadığı kanıtlandığında o dalı keser; kesmenin meşruluğu bu kanıta bağlıdır.
- Doğru budama yedi vezirlik tahtada 552, budamasız arama 960.800 düğüm geziyor (oran 1740,6) ve çözüm sayıları aynı kalıyor: 10, 4, 40.
- Oran ile yaklaşık beş kat büyüyor (72,3 · 365,9 · 1740,6 · 8156,2), ama budamalı düğüm sayısı da 3,7 kat büyüyor: budama üstelliği kesmez, erteler.
- Aşırı budama yedi vezirlikte 82 düğümde bitip sıfır çözüm buluyor; yalnız düğüm sayan bir ölçümde en iyi yordam görünür.
- Eksik budama 59.196 düğüm gezip 40 yerine 45.514 yerleşimi çözüm sayıyor; yanlışlık her zaman hızlı değildir.
- Kırk yasaklı tahtada doğru budama 0/40, aşırı budama 40/40 ayrılıyor; ikinci dağarcıkta aynı sonuç, kaybolan çözüm 92 yerine 90.
Sonraki Adım
Şimdiye kadarki bütün yordamlar belirlenimciydi: aynı girdi her zaman aynı adımları ve aynı yanıtı veriyordu. Son tasarım yaklaşımı bu güvenceyi gevşetir ve karşılığında bir şey ister — bazen adım sayısının, bazen de yanıtın kendisinin rastgele olmasını. Sonraki ders iki tür rastgeleleştirmeyi aynı dağarcıkta ölçer ve tek bir soruyu yanıtlar: rastgele bir yordamın kaç girdide yanıldığı, tohum değiştiğinde değişiyor mu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.