İçeriğe geç
academia.sh

Ders 07 / 10

NP-Tam ve NP-Zor

İndirgeme yoluyla zorluk aktarımının adım sayısıyla ölçülmesi: alt küme toplamı örneğini bölüştürme örneğine çeviren dönüşüm 15 adım tutuyor, kaynağı çözmenin 0,2632'si. Yirmi örnekte indirgeme 300, kaynağı çözme 4321, hedefi çözme 19.847 adım ve 20 örneğin 20'sinde iki yanıt uyuşuyor. Girdi boyu 24'te indirgeme 27 adımda kalırken çözme 16.777.216 adıma çıkıyor. Çevirinin yönünün neyi kanıtlayıp neyi kanıtlamadığı ayrıca yazılıyor.

İçindekiler

Önceki ders tek bir problem ölçtü. Ama zorluk hakkında söylenen cümlelerin çoğu tek bir problem hakkında değildir: “şu problem en az şu kadar zor” biçiminde, iki problemi karşılaştıran bir cümledir. Böyle bir cümle nasıl kurulur ve bedeli nedir.

Araç, M01/K07 İleri Algoritmalar kursunda tanıtılan indirgemedir: bir problemin örneğini, başka bir problemin örneğine çeviren dönüşüm. O kursta indirgeme bir çözüm tekniğiydi — tanıdık bir kalıba çevirip kalıbın yordamını kullanmak. Burada aynı dönüşüm başka bir iş için kullanılır: bir problemin zorluğunu bir başkasına aktarmak. Yordam tekrarlanmaz; sayıya çevrilir.

  • KS20. Kaynak problem alt küme toplamıdır: bir alt kümenin toplamı hedefe eşit mi. Hedef problem bölüştürmedir: sayılar, toplamları eşit iki paya ayrılabilir mi.
  • KS21. İndirgeme, kaynağın bir örneğini hedefin bir örneğine çeviren yordamdır ve yanıtı korumak zorundadır: kaynak “evet” ise hedef de “evet”, kaynak “hayır” ise hedef de “hayır” vermelidir.
  • KS22. İndirgemede bir adım, bir eleman kopyalamasıdır; toplam alma ve eklenen elemanın hesabı için üç adım daha sayılır.
  • KS23. İndirgemenin doğruluğu varsayılmaz, sınanır: 20 örneğin her birinde iki yanıt karşılaştırılır ve uyuşan örnek sayısı yazılır.
  • KS24. Üç adım sayısı ayrı tutulur: indirgeme, kaynağı çözme, hedefi çözme. İkincisi ile üçüncüsü karıştırılırsa indirgemenin ne yaptığı görünmez olur.
  • KS25. İndirgemenin yönü yazılır. Kaynaktan hedefe indirgeme, hedef en az kaynak kadar zordur der; ters yönü söylemez.
  • KS26. Bütçe süpürmesi dört değerde yapılır: 15, 100, 1000, 10.000 adım.
  • KS27. Örnekler ortak tanımın üretecinden, tohum 20260218, 20 örnek, örnek başına 12 sayı. İkinci tohum yoktur.
  • KS28. Hiçbir problemin NP-tam olduğu koşumla gösterilmez. Koşum yalnız tek bir indirgemenin adımını ve doğruluğunu ölçer.

Zorluğu Aktarmak Ne Demek

İndirgemenin okunma biçimi ters sezgiseldir ve yanlış okunması yaygındır. Kaynak problemin bir örneği hedef problemin bir örneğine çevriliyorsa, hedefi çözebilen her yordam kaynağı da çözebilir: önce çevir, sonra hedefi çöz, yanıtı olduğu gibi al. Bunun sonucu şudur — hedef problem için ucuz bir yordam bulunursa, kaynak problem için de ucuz bir yordam olurdu.

Buradan çıkan cümle hedef hakkındadır: hedef, kaynaktan daha kolay olamaz. Çeviri kaynağı ucuzlatmaz; hedefin ucuzlama ihtimalini kaynağa bağlar. Yön karıştırılırsa cümle tersine döner ve hiçbir şey söylemez hale gelir.

İki sınıf adı bu okumanın üzerine kurulur. Bir problem, NP sınıfındaki her problem ona indirgenebiliyorsa NP-zordur. Bir problem hem NP-zor hem de NP sınıfındaysa, yani kendi “evet” yanıtının kısa bir sertifikası varsa, NP-tamdır. Fark tek bir koşuldadır: NP-zor bir problem NP sınıfında olmak zorunda değildir, hatta bir karar problemi olmak zorunda bile değildir.

Bir İndirgemenin Ölçülmesi

Aşağıdaki dönüşüm şunu yapar: sayıların toplamı TT, hedef hh olsun; listeye tek bir yeni eleman eklenir. Bu eleman, toplamın iki eşit paya bölünebilmesini ancak kaynakta hh’ye toplanan bir alt küme varsa mümkün kılacak biçimde seçilir. Dönüşümün yanıtı koruyup korumadığı varsayılmaz; her örnekte sınanır.

TOHUM = 20260218


def ornekler(tohum=TOHUM, n=12, sayi=20):
    d = tohum
    kume = []
    for _ in range(sayi):
        sayilar = []
        for _ in range(n):
            d = (d * 1103515245 + 12345) % 2147483648
            sayilar.append(d % 97 + 3)
        d = (d * 1103515245 + 12345) % 2147483648
        kume.append({"sayilar": sayilar, "hedef": sum(sayilar) // 3 + d % 7})
    return kume


def alt_kume_hakem(sayilar, hedef):
    """Kaynak problem: bir alt kumenin toplami hedefe esit mi."""
    adim, n = 0, len(sayilar)
    for maske in range(1 << n):
        adim += 1
        if sum(sayilar[i] for i in range(n) if maske >> i & 1) == hedef:
            return True, adim
    return False, adim


def indirge(sayilar, hedef):
    """Alt kume toplami ornegini iki esit paya bolusturme ornegine cevirir.
    Bir adim = bir eleman kopyalamasi; uc adim da toplam ve ek eleman icin."""
    toplam = sum(sayilar)
    yeni = list(sayilar) + [2 * hedef - toplam] if 2 * hedef >= toplam else \
        list(sayilar) + [toplam - 2 * hedef]
    return yeni, len(sayilar) + 3


def bolusturme_hakem(sayilar):
    """Hedef problem: sayilar iki esit toplamli paya bolunebilir mi."""
    toplam = sum(sayilar)
    if toplam % 2:
        return False, 1
    var, adim = alt_kume_hakem(sayilar, toplam // 2)
    return var, adim + 1


ORN = ornekler()
uyusan, ia, ka, ha = 0, 0, 0, 0
for o in ORN:
    var1, a1 = alt_kume_hakem(o["sayilar"], o["hedef"])
    yeni, ad = indirge(o["sayilar"], o["hedef"])
    var2, a2 = bolusturme_hakem(yeni)
    uyusan += var1 == var2
    ia, ka, ha = ia + ad, ka + a1, ha + a2
print("20 ornek , n=12 | yaniti uyusan:", uyusan, "/ 20")
print("  indirgeme:", ia, "adim | kaynagi cozme:", ka,
      "adim | hedefi cozme:", ha, "adim")
print("  indirgemenin kaynagi cozmeye orani:", round(ia / ka, 4))
print()
o = ORN[0]
yeni, ad = indirge(o["sayilar"], o["hedef"])
var1, a1 = alt_kume_hakem(o["sayilar"], o["hedef"])
var2, a2 = bolusturme_hakem(yeni)
print("ilk ornek | indirgeme", ad, "adim | kaynak yanit", var1, f"({a1} adim)",
      "| hedef yanit", var2, f"({a2} adim)")
print("  oran:", round(ad / a1, 4))
print()
print("butce  indirgenen  kaynagi cozulen  hedefi cozulen")
for b in (15, 100, 1000, 10000):
    i1 = sum(1 for x in ORN if indirge(x["sayilar"], x["hedef"])[1] <= b)
    k1 = sum(1 for x in ORN if alt_kume_hakem(x["sayilar"], x["hedef"])[1] <= b)
    h1 = sum(1 for x in ORN
             if bolusturme_hakem(indirge(x["sayilar"], x["hedef"])[0])[1] <= b)
    print(f"{b:5d}  {i1:10d}  {k1:15d}  {h1:14d}")
20 ornek , n=12 | yaniti uyusan: 20 / 20
  indirgeme: 300 adim | kaynagi cozme: 4321 adim | hedefi cozme: 19847 adim
  indirgemenin kaynagi cozmeye orani: 0.0694

ilk ornek | indirgeme 15 adim | kaynak yanit True (57 adim) | hedef yanit True (288 adim)
  oran: 0.2632

butce  indirgenen  kaynagi cozulen  hedefi cozulen
   15          20                0               0
  100          20                7               0
 1000          20               19              12
10000          20               20              20

İndirgeme Ucuz, Çözme Değil

İlk örnek üç sayıyı yan yana koyuyor: indirgeme 15 adım, kaynağı çözme 57 adım, hedefi çözme 288 adım. İndirgeme, kaynağı çözmenin 0,2632‘si kadar. Yirmi örneğin toplamında oran daha da düşüyor: 300’e karşı 4321, yani 0,0694.

İkinci sayı çifti daha öğreticidir. Hedefi çözmek 19.847 adım tutuyor, kaynağı çözmenin neredeyse beş katı. Çeviri hiçbir şeyi ucuzlatmadı; tersine, listeye bir eleman eklediği için hedef örnek kaynaktan daha pahalı hale geldi. İndirgemenin işi ucuzlatmak değildir. İşi, iki problemin zorluğu arasında bir bağ kurmaktır, ve bu bağ kurulurken hesap ucuza gelir.

Doğruluk sütunu bu bağın ön koşuludur: 20 örneğin 20’sinde iki yanıt uyuşuyor. Uyuşmasaydı indirgeme geçersiz olurdu ve adım sayısının hiçbir anlamı kalmazdı. Yirmi örnekte uyuşması ise dönüşümün her girdide doğru olduğunu kanıtlamaz; dönüşümün doğruluğu, eklenen elemanın seçiminden gelen bir gerekçedir ve ölçüm o gerekçeyi yalnız sınar.

Bütçe süpürmesi üç sütunda üç ayrı davranış gösteriyor. İndirgeme sütunu bütçe 15’te dolu ve 10.000’de hâlâ 20; bütçeyi büyütmek hiçbir şey eklemiyor. Kaynağı çözme 0’dan 7’ye, 19’a, 20’ye çıkıyor. Hedefi çözme daha geç açılıyor: 1000’de yalnız 12, 10.000’de 20. Aynı bütçe, çevrilmiş örnek için daha az yetiyor.

Girdi Boyu Büyüdüğünde Ne Kalıyor

Tek bir girdi büyüklüğünde alınan 0,2632 oranı, indirgemenin ucuzluğunu tam olarak anlatmaz. Asıl soru, girdi büyüklüğü büyüdüğünde iki tarafın birbirine göre ne yaptığıdır.

print(" n  indirgeme  kaynagi cozme  hedefi cozme  indirgeme/cozme")
for n in (8, 12, 16, 20, 24):
    print(f"{n:2d}  {n + 3:9d}  {1 << n:13d}  {1 << (n + 1):12d}"
          f"  {(n + 3) / (1 << n):15.6f}")
 n  indirgeme  kaynagi cozme  hedefi cozme  indirgeme/cozme
 8         11            256           512         0.042969
12         15           4096          8192         0.003662
16         19          65536        131072         0.000290
20         23        1048576       2097152         0.000022
24         27       16777216      33554432         0.000002

İndirgeme sütunu 11, 15, 19, 23, 27: girdi büyüklüğüyle birer birer, düz. Çözme sütunları her satırda ikiye katlanıyor. Oran 0,042969’dan 0,000002’ye iniyor. Girdi boyu üç katına çıkarken indirgemenin payı yirmi bin kat küçülüyor.

Bu tablonun taşıdığı sonuç şudur: indirgemenin bedeli, aktardığı zorluğun yanında yok sayılabilir hale geliyor. Bir problemin zor olduğu biliniyorsa ve o problem ucuz bir dönüşümle başka bir probleme çevrilebiliyorsa, ikinci problem hakkında söylenecek söz birinciden gelir. Zorluk taşınabilir bir şeydir ve taşıma masrafı düşüktür.

Yanlış Bir Çevirim Nasıl Görünür

İndirgemenin ucuzluğu tek başına bir erdem değildir. Daha ucuz ama yanıtı korumayan bir çevirim her zaman kurulabilir, ve ucuzluğuna bakarak ayırt edilemez. Aşağıdaki blok doğru çevirimin yanına, hedefi düpedüz atan bir çevirimi koyar. Üçüncü ölçüm ters yöndedir: bölüştürme örneği alt küme toplamı örneğine çevrilir.

TOHUM = 20260218


def ornekler(tohum=TOHUM, n=12, sayi=20):
    d = tohum
    kume = []
    for _ in range(sayi):
        sayilar = []
        for _ in range(n):
            d = (d * 1103515245 + 12345) % 2147483648
            sayilar.append(d % 97 + 3)
        d = (d * 1103515245 + 12345) % 2147483648
        kume.append({"sayilar": sayilar, "hedef": sum(sayilar) // 3 + d % 7})
    return kume


def alt_kume_hakem(sayilar, hedef):
    adim, n = 0, len(sayilar)
    for maske in range(1 << n):
        adim += 1
        if sum(sayilar[i] for i in range(n) if maske >> i & 1) == hedef:
            return True, adim
    return False, adim


def bolusturme_hakem(sayilar):
    toplam = sum(sayilar)
    if toplam % 2:
        return False, 1
    var, adim = alt_kume_hakem(sayilar, toplam // 2)
    return var, adim + 1


def dogru_indirge(sayilar, hedef):
    """Yaniti koruyan cevirim: tek bir denklestirici eleman eklenir."""
    toplam = sum(sayilar)
    ek = 2 * hedef - toplam if 2 * hedef >= toplam else toplam - 2 * hedef
    return list(sayilar) + [ek], len(sayilar) + 3


def naif_indirge(sayilar, hedef):
    """Hedefi atan cevirim: liste oldugu gibi bolusturmeye verilir."""
    return list(sayilar), len(sayilar)


def ters_indirge(sayilar):
    """Bolusturme ornegini alt kume toplami ornegine cevirir."""
    toplam = sum(sayilar)
    return list(sayilar), toplam // 2, len(sayilar) + 1


print("cevirim        uyusan  ayrilan  cevirim adimi")
for ad, f in (("dogru", dogru_indirge), ("naif ", naif_indirge)):
    uyusan = ayrilan = ca = 0
    for o in ornekler():
        k, _ = alt_kume_hakem(o["sayilar"], o["hedef"])
        yeni, adim = f(o["sayilar"], o["hedef"])
        h, _ = bolusturme_hakem(yeni)
        ca += adim
        if k == h:
            uyusan += 1
        else:
            ayrilan += 1
    print(f"{ad:13s}  {uyusan:6d}  {ayrilan:7d}  {ca:13d}")
print()
uyusan, ta, ha = 0, 0, 0
for o in ornekler():
    yeni, _ = dogru_indirge(o["sayilar"], o["hedef"])
    h, a1 = bolusturme_hakem(yeni)
    s, hedef2, adim = ters_indirge(yeni)
    k, a2 = alt_kume_hakem(s, hedef2)
    uyusan += h == k
    ta, ha = ta + adim, ha + a2
print("ters yon (bolusturme -> alt kume toplami)")
print("  uyusan:", uyusan, "/ 20 | cevirim adimi:", ta,
      "| hedefi cozme adimi:", ha)
cevirim        uyusan  ayrilan  cevirim adimi
dogru              20        0            300
naif                8       12            240

ters yon (bolusturme -> alt kume toplami)
  uyusan: 20 / 20 | cevirim adimi: 280 | hedefi cozme adimi: 19827

Naif çevirim 240 adım harcıyor, doğrusundan 60 adım ucuz. Ama 20 örneğin 12’sinde yanlış yanıt veriyor. Yanıtı korumadığı için indirgeme değildir, ve hiçbir zorluk aktarmaz. Sekiz örnekte doğru yanıt vermesi de bir savunma sayılmaz: bir dönüşüm ya her örnekte yanıtı korur ya da geçersizdir. İndirgemede iş, adım sayısında değil, doğruluk gerekçesindedir.

Naif çevirimin nerede kırıldığı da okunabilir. Bölüştürme sorusu her zaman toplamın yarısını sorar; bu örneklerdeki hedef ise toplamın yaklaşık üçte biridir. Hedefi atmak, soruyu değiştirmek demektir, ve değişen soru başka bir yanıt verir. Doğru çevirimin eklediği tek eleman tam olarak bu farkı kapatmak içindir: eklenen değer, yarıyı hedefin bulunduğu yere taşır.

Ters yön daha kısa: bölüştürme örneği, hedefi toplamın yarısı olan bir alt küme toplamı örneğine 280 adımda çevriliyor ve 20 örneğin 20’sinde yanıt korunuyor. İki yön birlikte okunduğunda ortaya çıkan sonuç şudur: bu iki problem birbirine karşılıklı olarak indirgeniyor, yani birinin ucuz bir yordamı ötekini de ucuzlatır. Zorluk açısından bu iki problem ayrılamaz.

Koşumun Söylemediği

Bu derste ölçülen hiçbir sayı, bölüştürme probleminin NP-tam olduğunu göstermez. Gösterilen tek şey, bir problemin ona indirgendiğidir. NP-zorluk tanımı, NP sınıfındaki bütün problemlerin indirgenebilmesini ister; bu, sonlu bir koşumun kapsayamayacağı bir niceleyicidir. Bu iki problemin NP-tam olduğu kuramın sonucudur ve buraya kuramın sonucu olarak girer; yukarıdaki tabloların hiçbiri onu kanıtlamaz.

Ölçümün gerçekten kanıtladığı şey daha küçük ve daha somuttur: bu dönüşüm 20 örneğin 20’sinde yanıtı korudu, 15 adım tuttu, ve girdi büyüklüğü büyüdükçe payı düştü. Bir raporda yazılabilecek cümle budur. “Bölüştürme NP-tamdır” cümlesi ise ölçüme değil, kaynağa dayanır ve kaynağı belirtilmeden yazılmaz.

Ters yön de açıkça söylenmeli. Kaynaktan hedefe kurulan bir indirgeme, hedefin en az kaynak kadar zor olduğunu destekler. Hedefin en fazla ne kadar zor olduğu hakkında hiçbir şey söylemez, ve kaynağın kolay olduğunu hiç söylemez. Bir yönde kurulmuş bir bağı iki yönde okumak, bu konudaki en sık hatadır.

Özet

  • İndirgeme, bir problemin örneğini başka bir problemin örneğine çeviren ve yanıtı koruyan dönüşümdür; yön, hedefin kaynaktan daha kolay olamayacağını söyler.
  • İlk örnekte indirgeme 15, kaynağı çözme 57, hedefi çözme 288 adım; oran 0,2632. Yirmi örnekte 300’e karşı 4321 ve 19.847.
  • Çeviri hiçbir şeyi ucuzlatmıyor: hedef örnek bir eleman daha taşıdığı için kaynaktan beş kat pahalıya çözülüyor.
  • Yirmi örneğin 20’sinde iki yanıt uyuşuyor; bu bir sınamadır, dönüşümün her girdide doğru olduğunun kanıtı değildir.
  • Girdi boyu 8’den 24’e çıkarken indirgeme 11’den 27 adıma, çözme 256’dan 16.777.216 adıma gidiyor ve oran 0,000002’ye iniyor.
  • NP-zor, NP sınıfındaki her problemin indirgenebildiği problemdir; NP-tam ise buna ek olarak NP sınıfında olandır. Hiçbiri sonlu bir koşumla gösterilemez.

Sonraki Adım

Buraya kadar ölçülen her şey evet yönündeydi: sertifika bir “evet”i doğruluyordu, indirgeme bir “evet”i taşıyordu. Sonraki ders eksik yönü ölçer. Bir yanıt “hayır” olduğunda ne gösterilebilir, ve gösterilecek şey aynı uzunlukta mı. Yirmi örnekte “evet” sertifikası 13 adımda kapanırken “hayır” kanıtının kaç adım istediği, ve bazı “hayır” yanıtlarının neden ucuza kapandığı sayılacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat