9  Problem Oturumu 3

Ders 4-5’in uygulaması: hashing ve doğrusal sıralamanın dört uygulaması — indirgeme, değişmez, radix dönüşümleri, two-finger ve kayan pencere

NotOturum bilgisi

9.1 Bu Problem Oturumu Ne Hakkında?

Üçüncü problem oturumu (Erik Demaine), son iki haftanın konularını uygular: hashing (Ders 5) ve doğrusal-zamanlı sıralama (counting/radix sort, Ders 4-5). Demaine, her problem için izlediği yöntemi açıkça gösterir: kelime problemini biçimsel bir algoritma hedefine çevir, fikirler üret, sonra detayları kontrol et. Bu üç adımlı disiplin, dört problemin de ortak iskeletidir.

“convert the word problem into a concise formal algorithms thing… come up with ideas… check the details.” — Demaine, 0:46

İki anahtar ipucu tüm oturuma damga vurur (Şekil 33.1): bir problem ifadesinde “expected” (beklenen) görürsen rastgelelik söz konusudur ve elindeki tek rastgelelik aracı hashing’dir; “nice polynomial” (sabit üslü polinom, \(u \le n^c\)) görürsen radix sort dene. Dört problem birer “İfade → Yaklaşım → Çözüm → Karmaşıklık” akışıyla işlenir.

“‘expected’ is always a good keyword, because it means randomization is involved… the only form of randomization you will use is essentially hashing.” — Demaine, 6:10

flowchart TD
    R["Problem Oturumu 3<br/>(Ders 4-5 uygulaması)"] --> P1["Problem 1<br/>Hash Tablosundan Dizi"]
    R --> P2["Problem 2<br/>Critter Sort (4 anahtar)"]
    R --> P3["Problem 3<br/>Küp Toplamı"]
    R --> P4["Problem 4<br/>Poker (Sliding Window)"]
    H1["ipucu: 'expected'<br/>(beklenen)"] -.-> HASH["→ hashing kullan"]
    H2["ipucu: 'nice polynomial'<br/>(sabit üslü, u ≤ n^c)"] -.-> RADIX["→ radix sort dene"]
    HASH -.-> P1
    HASH -.-> P3
    RADIX -.-> P2
    RADIX -.-> P3
    RADIX -.-> P4

    classDef root fill:#fef3c7,stroke:#b45309,stroke-width:3px,color:#1f2937
    classDef prob fill:#f3f4f6,stroke:#374151,stroke-width:2px,color:#1f2937
    classDef hint fill:#ffffff,stroke:#d97706,stroke-width:1.5px,color:#1f2937
    classDef tool fill:#fcd34d,stroke:#b45309,stroke-width:2px,color:#1f2937
    class R root
    class P1,P2,P3,P4 prob
    class H1,H2 hint
    class HASH,RADIX tool
Şekil 9.1: Problem Oturumu 3’ün kavram haritası: kök (PS3) dört probleme dallanır ve iki anahtar ipucu düğümü tüm araç seçimini yönlendirir. Problem 1 hash tablosundan dizi kurar (indirgeme + değişmez); Problem 2 dört farklı anahtarı doğrusal sıralar (radix dönüşümleri); Problem 3 küp toplamını çözer (hash vs two-finger); Problem 4 poker ellerini sayar (kayan pencere + frekans tablosu). Soldaki iki ipucu — ‘expected’ bekleneni görünce hashing, ‘nice polynomial’ sabit üslü polinom görünce radix sort — Demaine’in problem ifadesinden araca giden tercüme kuralıdır.

9.2 Problem 1: Hash Tablosundan Dizi

İfade. Bir hash tablosu (set: build \(O(n)\) beklenen, find \(O(1)\) beklenen, insert/delete \(O(1)\) beklenen amortize) black box olarak verilir. Bununla bir dizi (sequence) kur: get/set_at \(O(1)\) beklenen, insert/delete_at \(O(n)\) beklenen, insert/delete first/last \(O(1)\) beklenen amortize.

İpucuYaklaşım — İndirgeme (reduction): diziyi set’e çevir

Bu bir indirgemedir (reduction): sequence problemini, çözümünü zaten bildiğimiz set problemine çeviriyoruz. İlk hamle, zorlukları ayırmaktır. insert_at/delete_at için \(O(n)\) bütçesi var → her seferinde tüm yapıyı yeniden kurmak serbest. Asıl ustalık gerektiren kısım, uçlardaki (\(O(1)\) amortize) işlemlerdir; orada bir indeks hilesi gerekir.

“this is what we call a reduction… we’re reducing the sequence problem to the set problem.” — Demaine, 3:17

Çözüm. Anahtar fikir: her öğeye anahtar = sıradaki indeksi ver, böylece set’te saklanabilir.

  • get_at(i): find(first + i).value. set_at(i, x): find(first + i) ile nesneyi bul, value’sunu x yap. \(O(1)\) beklenen.
  • insert/delete_at(i): iter ile öğeleri bir diziye çıkar, kaydır, build ile yeniden kur. \(O(n)\) — bütçe içinde.
  • insert/delete_last: anahtar = first + length (veya first + length − 1). \(O(1)\).
  • insert/delete_first: sorun — indeks 0’a eklemek tüm anahtarları kaydırır (\(O(n)\)). Çözüm: bir first değişkeni tut (en küçük anahtar); başa eklerken first’ü azalt (negatif anahtarlara izin ver). İndeks i artık first + i anahtarına eşlenir.

Aşağıdaki özet, motorun (HashSequence) çekirdek mantığını gösterir: first değişkeni ve insert_first’ün first’ü azaltışı, insert_at’in iter + build ile yeniden kurması.

class HashSequence:                              # diziyi black-box set'e indirger
    def __init__(self, iterable=()):
        self._d = {}                             # set: anahtar -> değer
        self._first = 0                          # en küçük anahtar (negatif olabilir)
        self.build(iterable)

    def get_at(self, i):
        return self._d[self._first + i]          # find(first+i) — O(1) beklenen

    def insert_first(self, x):
        self._first -= 1                         # başa ekle: first AZALIR (negatif anahtar)
        self._d[self._first] = x                 # O(1) — hiçbir şey kaymaz

    def insert_last(self, x):
        self._d[self._first + len(self._d)] = x  # anahtar = first + length — O(1)

    def insert_at(self, i, x):
        items = self.to_list()                   # iter ile çıkar — O(n)
        items.insert(i, x)
        self.build(items)                        # yeniden kur — O(n), bütçe içinde

Bu, bir değişmez (invariant) doğurur: anahtarlar her zaman first ile first + length − 1 arasındadır. Demaine, değişmez yazmanın veri yapısının neden doğru olduğunu gösterdiğini vurgular; doğruluk her işlemin değişmezi koruduğunu göstererek tümevarımla ispatlanır. Şekil 9.2 bu değişmezin build → insert_first → insert_last → delete_first boyunca nasıl korunduğunu adım adım izler.

“This is what we call an invariant. Useful to write these things down so you can understand… why is your data structure correct?” — Demaine, 26:23

Karmaşıklık. get/set_at \(O(1)\) beklenen, insert/delete_at \(O(n)\) beklenen, uç işlemleri \(O(1)\) beklenen amortize. (Negatif anahtarlar, çift-katlama “folding” hilesiyle non-negatife eşlenebilir.)

Şekil 9.2: Hash tablosundan Sequence İNDİRGEMESİ + değişmez (invariant) izi — Problem 1. Anahtar ekseni −2..4 (7 hücre); indeks i → anahtar (first + i) eşlemesi. Satır 1 build [x,y,z]: anahtarlar 0..2, first=0. Satır 2 insert_first(w): w başa eklenir, first −1’e iner ve −1 anahtarı doğar (amber) — hiçbir şey kaymadan O(1). Satır 3 insert_last(v): v sona eklenir, anahtar 3 = first+length doğar (amber). Satır 4 delete_first: en küçük anahtar (−1) atılır, first 0’a döner. Her satırın altındaki amber köşeli parantez değişmezi gösterir: anahtarlar her işlemden sonra TAM OLARAK first..first+n−1 aralığını doldurur — boşluk yok, taşma yok. Bu, veri yapısının doğruluğunu tümevarımla garanti eder (Demaine 26:23).

9.3 Problem 2: Critter Sort

İfade. n nesneyi dört farklı anahtar türüne göre sırala; her biri için en hızlı doğru algoritmayı bul. (a) tam sayılar \(-n..n\); (b) 26-harfli, uzunluğu \(\le 10 \cdot \log n\) olan string’ler; (c) tam sayılar \(0..n^2\); (d) rasyoneller \(w/f\), \(0 < w < f < n^2\).

İpucuYaklaşım — Her Anahtarı radix sort’un Kabul Ettiği Tamsayıya Dönüştür

Hepsinde radix sort denenir, ama her biri bir dönüşüm ister: radix sort yalnızca \(0..u-1\) aralığında tamsayı anahtar alır ve doğrusal kalması için \(u \le n^c\) (sabit üslü) olmalıdır. İş, her anahtar türünü bu kalıba sokan doğru dönüşümü bulmaktır — negatifi kaydır, string’i tek sayıya çevir, rasyoneli çapraz çarp veya ölçekle.

Çözüm. Dört şık:

  • (a) \(-n..n\): Negatifleri “folding” ile araya serpmek sırayı bozar (Demaine’in özellikle uyardığı tuzak). Doğru hamle: her anahtara n ekle\(0..2n\). Radix sort, \(u = 2n+1\)doğrusal.
  • (b) String’ler: Her harfi bir basamağa eşle, sonra tüm string’i base-n tek sayıya çevir (en önemli harf en yüksek basamak; kısa string’leri sonda doldur). String uzunluğu \(10 \cdot \log n\) olsa da, base-n radix sort her counting-sort geçişinde \(\log n\) harfi birden işler → yalnızca 10 geçiş (\(u = n^{10}\)) → doğrusal. (Harf-harf base-26 tuple sort olsaydı \(\log n\) geçiş gerekir = \(n \log n\), yavaş.)

“Tuple sort is the thing… sort by the last thing, then sort by the previous thing… You can also think of this as radix sorting on a number written in base 26.” — Demaine, 33:53

  • (c) \(0..n^2\): Doğrudan radix sort (iki counting-sort geçişi, \(u = n^2\)) → doğrusal.
  • (d) Rasyoneller \(w/f\): İki yol. (1) Merge sort + çapraz çarpım: \(w_i/f_i\) ile \(w_j/f_j\)’yi kıyaslamak için bölme yerine \(w_i \cdot f_j\) ile \(w_j \cdot f_i\)’yi karşılaştır (paydalar pozitif olduğundan işaret korunur) → \(O(n \log n)\). (2) Doğrusal: İki farklı oran en az \(1/n^4\) uzaktadır (\(|w_i f_j - w_j f_i| \ge 1\), payda \(< n^4\)); her oranı \(n^4\) ile ölçekleyip tabana yuvarla (\(w_i \cdot n^4 \mathbin{//} f_i\)) → farklı oranlar farklı tamsayılara gider; \(w_i < f_i\) olduğundan anahtarlar \(\lfloor w_i \cdot n^4 / f_i \rfloor < n^4\), yani \(u \le n^4\) (bölmeden önceki ara çarpım \(w_i \cdot n^4\) bile \(< n^6\) — her iki sınır da sabit-üslü), radix sort → doğrusal.

“Merge sort is always a good answer. It’s not the best answer.” — Demaine, 40:43

İpucuBuilder Notu — pad değeri harf kodlarından KÜÇÜK olmalı

Şık (b)’de Notion harfleri \(0..25\)’e eşlemeyi önerir, ama kısa string’leri sonda pad ile doldururken dikkat gerekir: pad = 0 ve 'a' = 0 olsaydı ab ile aba aynı sayıya giderdi (ab + pad = ab + a), leksikografik sıra bozulurdu. Motor bu yüzden harfleri \(1..26\) kodlar ve pad = 0 kullanır (taban 27) — pad tüm harflerden küçük kaldığından ab < aba korunur. İfade düzeyinde bağlayıcı olan, leksikografik doğru sıralamadır; kodlama detayı ona hizmet eder.

Karmaşıklık. (a), (b), (c) doğrusal; (d) merge sort ile \(O(n \log n)\) veya ölçekleme + radix sort ile doğrusal.

9.4 Problem 3: Küp Toplamı

İfade. n tam sayı (küp kenar uzunlukları) verilir; toplamı h olan iki sayı bul. (a) tam çözüm, doğrusal beklenen zaman. (b) tam çift yoksa h’ye en yakın küçük(-eşit) çifti, doğrusal en-kötü-durum zamanında bul.

İpucuYaklaşım — ‘expected’ → hash, ‘en-kötü-durum’ → radix + two-finger

İki şık iki ipucuyla ayrışır. (a) “beklenen” geçer → randomizasyon → hashing. (b) “en-kötü-durum” geçer → hash yasak (worst-case garantisi yok); ama \(h = 600 \cdot n^6\) sabit bir polinomdur → radix sort ipucu, ardından sıralı dizide two-finger ile çift arama.

Çözüm.

(a) Hash ile: Tüm sayıları bir hash tablosuna koy (build \(O(n)\) beklenen). Sonra her \(s_i\) için find(h − s_i) çağır — n çağrı, her biri \(O(1)\) beklenen.

“Subtract h from si and see whether that exists.” — Demaine, 1:00:38

(b) Two-finger ile: Önce h’den büyük tüm sayıları at (asla çözümde olamazlar, sayılar negatif değil). Kalanlar \(\le h\) olduğundan radix sort’la sırala (doğrusal). Sonra iki parmak: i = 0 (en küçük), j = son (en büyük). \(s_i + s_j > h\) ise \(j{-}{-}\) (toplam çok büyük); \(\le h\) ise aday listesine kaydet ve \(i{+}{+}\) (daha büyük toplam dene). Her parmak diziyi yalnızca bir kez kateder.

“The two-finger algorithm… This is the big idea. You’re doing it all the time in this class.” — Demaine, 1:10:34

Şekil 9.3 bu izi \([21, 5, 13, 8, 2, 34]\) girdisi ve \(h = 30\) üzerinde gösterir: 34 > 30 olduğundan elenir, kalanlar sıralı \([2, 5, 8, 13, 21]\) olur. İki parmak 4 adımda ilerler; en iyi aday \((29, 8, 21)\), yani \(8 + 21 = 29 \le 30\) — h’yi geçmeyen en büyük çift. Doğruluk bir değişmezle ispatlanır: her adımda, j’nin sağındaki ve i’nin solundaki tüm çiftler ya çok büyüktür ya da görülen en iyi adaydan küçük-eşittir.

Karmaşıklık. (a) doğrusal beklenen (hash); (b) doğrusal en-kötü-durum (radix sort + two-finger). İkili-arama yaklaşımı \(O(n \log n)\) verirdi.

Şekil 9.3: İki parmak (two-finger) küp toplamı izi — Problem 3b İMZA figürü. Ham girdi \([21, 5, 13, 8, 2, 34]\), hedef \(h = 30\). 34 > h olduğundan elenir (üzeri çizili); kalanlar radix sort ile sıralı \([2, 5, 8, 13, 21]\) olur. Sol parmak i (amber) en küçükten, sağ parmak j (koyu amber) en büyükten başlar. Adım 1 (\(2+21=23 \le 30\)): aday, i++. Adım 2 (\(5+21=26 \le 30\)): aday, i++. Adım 3 (\(8+21=29 \le 30\)): EN İYİ aday, i++. Adım 4 (\(13+21=34 > 30\)): toplam fazla, j–. Sonuç best = (29, 8, 21) — h’yi geçmeyen en büyük çift. Her parmak diziyi yalnız bir kez katettiği için toplam O(n) (ikili-arama O(n log n) yerine).

9.5 Problem 4: Poker — Kayan Pencere ve Frekans Tablosu

İfade. n kartlık bir deste (her kartta 26 harften biri). Bir “el” = i. konumdan başlayıp döngüsel k kart al, sonra sırala. (a) İki indeks i, j için ellerin eşit olup olmadığını sabit zamanda söyleyen bir yapı kur. (b) En sık görülen eli bul.

İpucuYaklaşım — Sıralı el = frekans tablosu; pencereyi O(1)’de kaydır

El sıralandığından, hangi harfin nerede olduğu değil, her harften kaç tane olduğu önemlidir. 26 harf olduğundan her el bir frekans tablosuyla (26 sayı) temsil edilir. Bu tablo, base-\((n+1)\) yazılmış 26 basamaklı bir sayıdır; \(u = (n+1)^{26}\) sabit üslü bir polinomdur → radix sort uygulanabilir. Ardışık eller bir kart farkıyla aktığından, tablo her seferinde sıfırdan değil kayan pencere ile \(O(1)\)’de güncellenir.

Çözüm.

(a) Kayan pencere (sliding window): İlk k kartın frekans tablosunu hesapla (26 sayım). Pencereyi bir kaydır: çıkan kartın sayacını azalt, giren kartın sayacını artır → \(O(1)\)’de bir sonraki elin tablosu. Her i için tabloyu kopyala (26 sayı, sabit). İki eli karşılaştırmak = 26 sayıyı karşılaştırmak → \(O(1)\).

“It’s called a sliding window technique, where you compute it for the first k guys. And then you remove this item and add this item.” — Demaine, 1:24:21

Aşağıdaki özet, motorun (hand_tables) kayan pencere çekirdeğini gösterir: ilk el \(O(k)\), her sonraki el çıkan/giren tek kartla \(O(1)\).

def hand_tables(deck, k):                        # her i için döngüsel k-kartlık el
    n = len(deck)
    freq = [0] * 26                              # 26 harf → frekans tablosu
    for t in range(k):
        freq[deck[t]] += 1                       # ilk el — O(k)
    tables = [tuple(freq)]
    for i in range(1, n):
        freq[deck[i - 1]] -= 1                   # çıkan kart: −1  (kayan pencere)
        freq[deck[(i + k - 1) % n]] += 1         # giren kart: +1  (döngüsel)
        tables.append(tuple(freq))               # bir sonraki el — O(1)
    return tables

(b) En sık el: Tüm frekans tablolarını (her biri \((n+1)^{26}\) aralığında bir sayı) radix sort ile sırala (doğrusal, sabit üslü polinom). Tek bir taramayla eşit ardışık grupları say → en sık (ve istenirse leksikografik en iyi) eli bul.

Şekil 9.4 bu izi deste \(a\,b\,a\,c\,b\,a\) (\(n = 6\), \(k = 3\), döngüsel) üzerinde gösterir: el0 → el1 geçişinde çıkan a (−1), giren c (+1). Eller arasında el0, el4 ve el5 aynı tabloya (\(a{:}2, b{:}1\)) sahiptir; bu üç tekrar onu en sık el (3 kez) yapar.

Karmaşıklık. (a) yapı kurma \(O(n)\) (kayan pencere), karşılaştırma \(O(1)\); (b) radix sort + tarama → \(O(n)\).

Şekil 9.4: Kayan pencere (sliding window) + frekans tablosu izi — Problem 4. Deste \(a\,b\,a\,c\,b\,a\) (\(n=6\), \(k=3\), DÖNGÜSEL); soluk şerit sağda döngüsel sarma kopyasıdır. Her el için yalnız a/b/c sütunları gösterilen mini frekans tablosu (26 sayıdan ilgili üçü) sağda yer alır. el0 → el1 geçişinde çıkan kart a (−1, soluk daire) ve giren kart c (+1, amber daire) tabloyu O(1)’de günceller — sıfırdan saymaya gerek yok. el0, el4 ve el5 aynı tabloya (\(a{:}2, b{:}1\)) sahiptir (amber çerçeveli) → en sık el (3 kez). İki eli karşılaştırmak = 26 sayıyı karşılaştırmak = O(1) (sabit alfabe).

9.6 Ne Öğrendik?

ÖnemliAltı Taşınabilir Araç

Bu oturum, Ders 4-5’in teorisini dört somut problemde uyguladı ve altı taşınabilir araç kazandırdı:

  1. Reduction (indirgeme): bir problemi (sequence), çözümünü zaten bildiğimiz başka bir probleme (set/hash) çevirmek.
  2. Invariant (değişmez): veri yapısının doğruluğunu tümevarımla garanti eden, her işlemde korunan koşul (first..first+length−1).
  3. “Expected → hashing, polynomial → radix sort”: problem ifadesindeki kelime ipuçlarını araca çevirmek.
  4. Radix sort dönüşümleri: negatife n ekle, string’i base-n sayıya çevir, rasyoneli çapraz çarp veya ölçekle-yuvarla.
  5. Two-finger: sıralı dizide, monoton ilerleyen iki parmakla \(O(n)\) çift arama (ikili-aramanın \(n \log n\)’ini geçer).
  6. Sliding window + frequency table: sabit alfabe (26) → her pencereyi \(O(1)\)’de güncelle; sıralı eli 26 sayıyla temsil et.

9.7 Sonraki

UyarıDers 6 (L6) — İkili Ağaçlar, Bölüm 1

Sırada Ders 6 (L6): İkili Ağaçlar — Bölüm 1 var — Erik Demaine ile, sıralı diziyi (O(n) ekleme) ve hash tablosunu (worst-case O(n)) aşan dengeli bir yapıya — ikili ağaçlara — geçiyoruz: tüm işlemler worst-case O(log n). Bu oturumdaki değişmez (invariant) ve two-finger sezgileri, ağaç işlemlerinin doğruluğunu kurarken doğrudan işine yarayacak.

İpucuBuilder Notu — reduction = adapter pattern; sliding window = stream processing

İki araç, yazılım mühendisliğinde doğrudan karşılık bulur. Reduction, bir API’yi başka bir API üzerine kurmaktır: tıpkı bir adapter pattern’in mevcut bir arayüzü (set) beklenen bir arayüze (sequence) sarması gibi — yeni mantık değil, mevcut yeteneğin yeniden paketlenmesi. Sliding window ise stream processing’in kalbidir: sonsuz/uzun bir akışta sabit boyutlu pencerenin özetini her adımda sıfırdan değil artımlı (çıkan −, giren +) güncellemek. CNN konvolüsyon kernel’i de sabit bir pencereyi görüntü üzerinde gezdirir — ama paylaşılan yalnızca pencere geometrisidir: konvolüsyon her konumda dot-product’ı sıfırdan hesaplar; bu dersin yük taşıyan özelliği olan O(1) artımlı güncelleme ise stream/windowed-aggregation tarafına özgüdür.