Divide and conquer: üç adım
Büyük bir problemi çözmek zorsa onu aynı türden ama daha küçük problemlere ayır. Küçükleri aynı yöntemle çöz (recursion), sonra cevapları combine et. MergeSort'ta bunu gördün: array'i ikiye ayır, iki yarıyı sort et, merge et.
- Divide: problemi daha küçük subproblem'lere ayır.
- Conquer: subproblem'leri recursive olarak çöz. Yeterince küçük olanı (base case) doğrudan çöz.
- Combine: subproblem'lerin cevaplarından asıl cevabı kur.
Running time hep aynı kalıpta bir recurrence ile yazılır: $T(n) = a\,T(n/b) + f(n)$. $a$ = kaç subproblem çözdüğün, $n/b$ = her birinin size'ı, $f(n)$ = divide + combine işi.
Hızı çoğu zaman gerçekte kaç subproblem çözdüğün belirler. MergeSort iki yarıyı da çözer: $n\log n$. Binary search yarılardan birini atar: $\log n$. Karatsuba'da bütün farkı 4 yerine 3 subproblem çözmek yaratır.
MergeSort'ta combine adımı hangisi?
Grade-school multiplication ve ilk divide and conquer denemesi
$1234 \times 5678$'i elle çarparken her digit çiftini çarparsın: $8\times4$, $8\times3$, … Toplam $4 \cdot 4 = 16$ one-digit multiplication. $n$ digit'li iki sayıda $n^2$ çift var, yani grade-school algoritması $O(n^2)$.
Ölçek neden önemli? Notlardaki örnek: biri $n^2$, öbürü $n^{1.6}$ ile büyüyen iki algoritma. İkincisini elle, 10 000 kat yavaş çalıştırsan bile $10000\,n^{1.6} < n^2 \iff n^{0.4} > 10^4 \iff n > 10^{10}$. Bu noktadan sonra yavaş görünen algoritma hep kazanır.
Divide: sayıyı ikiye ayır
$1234 = 12\cdot 100 + 34$ ve $5678 = 56\cdot100 + 78$. Genel hali: $x = a\cdot 10^{n/2} + b$, $y = c\cdot 10^{n/2} + d$. Çarpınca:
$$xy = ac\cdot 10^{n} + (ad + bc)\cdot 10^{n/2} + bd$$Elimizde dört tane $n/2$ digit'li multiplication var: $ac$, $ad$, $bc$, $bd$. Bunları recursive olarak hesapla, sonra shift edip topla (addition $O(n)$).
Recursion tree: level $t$'de $4^t$ subproblem var, her biri $n/2^t$ digit'li. Leaf'ler level $\log_2 n$'de: $4^{\log_2 n} = n^{\log_2 4} = n^2$ tane one-digit multiplication. Hiç kazanç yok: 4 digit'te 16, 8 digit'te 64 multiplication, grade-school yöntemiyle aynı.
"Problemi böldüm, o halde hızlandı" yanlış. Divide adımı tek başına hiçbir şey kazandırmaz: recursion açılınca yine her digit çiftini çarpıyorsun. Kazanç ancak subproblem sayısını azaltırsan gelir.
Dört recursive multiplication yapan divide and conquer algoritmasıyla iki 8 digit'li sayıyı çarparken kaç one-digit multiplication yapılır?
Karatsuba: dört yerine üç multiplication
Formülde $ad$ ile $bc$ ayrı ayrı gerekmiyor, sadece toplamları $ad+bc$ lazım. Bu toplamı tek bir multiplication ile bulabilir miyiz? $(a+b)(c+d)$'yi açınca $ac + ad + bc + bd$ çıkıyor. İçinden $ac$ ve $bd$'yi çıkarırsan geriye tam istediğin kalıyor. $ac$ ile $bd$'yi zaten hesaplıyordun.
Üç recursive multiplication yeter: $ac$, $bd$, $z = (a+b)(c+d)$. Sonra
$$xy = ac\cdot 10^{n} + (z - ac - bd)\cdot 10^{n/2} + bd$$Ek iş birkaç addition ve subtraction, yani $O(n)$.
$a=1,\ b=2,\ c=3,\ d=4$. $ac = 3$, $bd = 8$, $z = (1+2)(3+4) = 21$. $ad + bc = 21 - 3 - 8 = 10$ (kontrol: $1\cdot4 + 2\cdot3 = 10$ ✓). Sonuç: $3\cdot100 + 10\cdot10 + 8 = 408$ ✓. Dört değil, üç one-digit multiplication.
Karatsuba(x, y):
n = max(basamak(x), basamak(y))
if n == 1: return x * y // base case: çarpım tablosu
m = floor(n / 2)
a, b = x div 10^m, x mod 10^m // x = a*10^m + b
c, d = y div 10^m, y mod 10^m
ac = Karatsuba(a, c)
bd = Karatsuba(b, d)
z = Karatsuba(a + b, c + d)
return ac * 10^(2m) + (z - ac - bd) * 10^m + bd
Gerçek bir çalıştırma
Aşağıdaki recursion tree, kod sayfada gerçekten çalıştırılarak üretiliyor. Hazır bir örnek seç ya da en fazla 8 digit'li iki sayı yaz. Her satır bir recursive call: $ac$, $bd$ ya da $z$. Turuncu satır şu anki call. Yandaki panel o call'da sayının $a, b, c, d$ parçalarına nasıl ayrıldığını, üç product'ı ve ortadaki terimin subtraction ile nasıl bulunduğunu gösteriyor; sayaçlar recursive call'ları ve one-digit multiplication'ları sayıyor.
Önce tahmin et: $12345678 \times 87654321$'de grade-school yöntemi 64 one-digit multiplication yapar, ideal Karatsuba sayımı $3^{\log_2 8} = 27$. Gerçek çalıştırma bu ikisinin neresine düşer, kaç recursive call yapar?
$1213 \times 2132$'de 9 multiplication çıkar ($3^2$). Ama ders örneği $1234\times5678$'de $a+b = 46$, $c+d = 134$: toplam bir digit uzuyor ve gerçek çalıştırma 17 multiplication yapıyor, grade-school yönteminin 16'sından fazla. Notlar bunu açıkça söylüyor: "$(a+b)$ $n/2+1$ digit'li olabilir, şimdilik görmezden gelelim". Bu fazlalık sadece constant factor'ü etkiler. Rastgele 64 digit'li sayılarda ortalama yaklaşık 1400 multiplication yapılıyor, grade-school yönteminde 4096.
$x = 23$, $y = 41$ için Karatsuba'nın en üst level'de hesapladığı $z$ kaçtır, $ad+bc$ ne çıkar?
Neden 3 ile 4 arasında bu kadar fark var?
| Level $t$ | Subproblem sayısı | Size | Level'deki iş $\approx 3^t\cdot n/2^t$ |
|---|---|---|---|
| 0 | 1 | 1024 | 1024 |
| 1 | 3 | 512 | 1536 |
| 2 | 9 | 256 | 2304 |
| … | $3^t$ | $n/2^t$ | $n\,(3/2)^t$ |
| 10 | 59 049 | 1 | 59 049 |
Her level'de iş 1,5 katına çıkıyor, yani son level baskın. Toplam yaklaşık $175\,099$, son level'in 3 katı kadar. Dört subproblem olsaydı son level $4^{10} = 1\,048\,576$ olurdu.
Leaf sayısı $3^{\log_2 n} = n^{\log_2 3}$. Üstteki level'lerin işi geometric series'in küçük terimleri, sonucu değiştirmez. Master theorem ile: $a = 3$, $b = 2$, $d = 1$, $a > b^d$ ⇒ $O(n^{\log_b a})$.
İki tree'yi level level kur
Solda her call'u dört subproblem'e bölen tree, sağda Karatsuba'nın üç dallı tree'si. İkisi de 16 digit'li bir multiplication'dan başlıyor ve her level'de size yarıya iniyor. Turuncu node'lar o an eklenen level, yeşiller leaf'ler.
Önce tahmin et: level 4'te, yani leaf'lerde, dört dallı tree'de kaç node olur, Karatsuba'da kaç? Biri öbürünün kaç katı?
n büyüdükçe fark ne kadar açılıyor?
Slider $n$'yi log ölçekte 10'dan $10^9$ digit'e kadar değiştirir; istersen $n$'yi kutuya da yazabilirsin. Hesap sadece leaf sayılarını, yani $n^2$ ve $n^{\log_2 3}$'ü karşılaştırıyor.
Önce tahmin et: saniyede $10^9$ one-digit multiplication yapan bir makine, $n = 10^6$ digit'li iki sayıyı grade-school yöntemiyle kaç dakikada çarpar, Karatsuba ile kaç saniyede?
- 4 yerine 3 multiplication, yani $3/4$ oranı, bir "constant factor" değildir. Bu oran her level'de uygulanıyor, $\log n$ kez çarpılınca exponent değişiyor: $n^2 \to n^{1.585}$.
- Addition'lar bedava değil, ama $O(n)$ sürüyorlar ve son level'deki $n^{1.585}$'in yanında kayboluyorlar.
- Aynı fikir matrix multiplication'da da var: 8 recursive multiplication $\to$ 7 (Strassen), $T(n) = 7T(n/2) + O(n^2) = O(n^{\log_2 7}) \approx O(n^{2.81})$ (Section S1 §4).
S8: Asymptotic efficiency neden sonunda constant factor'lük hız farkını yener, yavaş görünen algoritma hangi noktada kazanır? S9: Karatsuba'nın dört multiplication'dan birini atlamasını sağlayan identity ne? S10: 4'ten 3'e inmek asymptotic running time'ı neden değiştirir, 4 ile ne olurdu?
İpucu 1
S8: $10^4\,n^{1.6}$ ile $n^2$'yi eşitle, $n$'yi çöz. S10: iki recurrence'ın leaf sayısını yaz: $4^{\log_2 n}$ ve $3^{\log_2 n}$.
İpucu 2
$a^{\log_2 n} = n^{\log_2 a}$ identity'sini kullan. $a$ sayısını değiştirmek exponent'i değiştirir.
Örnek cevap
S8: Constant factor sadece bir kez çarpılır, growth rate'in etkisi ise $n$ ile birlikte büyür. $10^4 n^{1.6} < n^2 \iff n > 10^{10}$; bu eşikten sonra $n^{1.6}$'lık algoritma, 10 000 kat yavaş donanımda bile kazanır.
S9: $(a+b)(c+d) - ac - bd = ad + bc$. $ac$ ve $bd$ zaten gerekli, ortadaki terimi tek bir ek multiplication ile buluruz.
S10: 4 subproblem'de $T(n) = 4T(n/2)+O(n)$, leaf'ler $4^{\log_2 n} = n^2$: grade-school yöntemiyle aynı. 3 subproblem'de $3^{\log_2 n} = n^{\log_2 3} \approx n^{1.585}$. Fark her level'de biriktiği için exponent'e yansır.
Bir algoritma $n$ digit'li bir multiplication'ı $t$ tane $n/2$ digit'li multiplication'a bölüyor. Büyük $n$'de grade-school yönteminden ($n^2$) daha az one-digit multiplication hangi $t$ değerlerinde yapılır?
Sınavda tasarım cevabı şablonu
Akdeniz vizelerinde her yıl en az bir "design an algorithm" sorusu var ve ağırlığı büyük (2023-24 S1: 25 puan). Puan adımlara dağıtılıyor, o yüzden cevabı hep aynı beş parçayla yaz.
- Fikir, tek cümle: "Array'de $A[i] = i$ özelliği bir kez bozuluyor, bozulduğu yeri binary search ile bulurum."
- Pseudocode: input, output ve base case dahil. Variable adları anlamlı olsun.
- Neden doğru: attığın yarıda cevabın olamayacağını bir cümleyle söyle (invariant).
- Recurrence: her call'da kaç subproblem var, hangi size'da, ne kadar ek iş yapılıyor.
- Çöz: sonucu yaz ve "worst case" mi "average/expected" mi olduğunu belirt.
| Ne yapıyorsun | Recurrence | Running time |
|---|---|---|
| Yarıyı at, constant iş (binary search) | $T(n/2) + O(1)$ | $O(\log n)$ |
| Yarıyı at, linear iş | $T(n/2) + O(n)$ | $O(n)$ |
| Random pivot ile bir tarafa in | expected: $T(3n/4) + O(n)$ gibi | expected $O(n)$ |
| İki yarıyı da çöz, linear combine | $2T(n/2) + O(n)$ | $O(n\log n)$ |
| Karatsuba | $3T(n/2) + O(n)$ | $O(n^{1.585})$ |
| Dört recursive multiplication | $4T(n/2) + O(n)$ | $O(n^2)$ |
- Sadece pseudocode yazıp açıklamamak. CS161 ödevleri de "pseudocode ve İngilizce açıklama" istiyor.
- Edge case'leri unutmak: cevap array'in en başında ya da hiç boşluk yok.
- İstenen running time'ı aşmak: $O(\log n)$ istenen soruda array'i baştan sona taramak (o $O(n)$).
- "Average" istenen soruda worst case'i söylememek ya da ikisini karıştırmak.
Algoritma her call'da array'i $O(n)$'de partition ediyor ve sadece bir tarafla devam ediyor; o taraf her zaman en fazla $n/2$. Running time?
Contains, A[:n//2] ve A[n//2+1:] üzerinde OR yapıyor. Contains([4,7,1],7) ne döndürür? Tek satırla düzelt ve strong induction step’ini yaz.
İpucu
Çözümü göster
False: ortadaki 7 atlanır. Sağ yarı A[n//2:] olmalı. Hypothesis kısa listelerde doğruluk; n≥2’de iki daha kısa yarı tüm listeyi kapsar. x A’da ancak ve ancak en az bir yarıda olduğundan OR doğrudur. x yokken bütün dallar gezilir; slicing ile T(n)=2T(n/2)+Θ(n)=Θ(n log n) worst case.
Kalıp 1: bir kez değişen özellik üzerinde binary search
Sorted ve elemanları farklı bir array düşün: $A = [1, 2, 3, 5, 7, 8, 10]$. Her index'te $A[i]$ ile $i$'yi karşılaştır:
| $i$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| $A[i]$ | 1 | 2 | 3 | 5 | 7 | 8 | 10 |
| durum | = | = | = | > | > | > | > |
Önce hep "=", sonra hep ">". Özellik bir kez değişiyor ve ilk ">" olan yer ilk eksik sayıyı veriyor. Böyle bir geçiş noktasını bulmak için array'i taramak gerekmez: ortaya bakıp yarısını atarsın.
Elemanlar farklı tam sayılar ve $\ge 1$; array sorted olduğu için $A[i] \ge i$. Eğer $A[i] > i$ ise $A[i+1] \ge A[i] + 1 > i + 1$. Yani ">" bir kez başladı mı sonuna kadar sürer.
FirstMissing(A[1..n]):
lo = 1; hi = n
while lo <= hi:
mid = floor((lo + hi) / 2)
if A[mid] == mid: lo = mid + 1 // 1..mid hepsi var
else: hi = mid - 1 // boşluk mid'de ya da solunda
return lo
Loop invariant: $i < lo$ olan her index'te $A[i] = i$; $i > hi$ olan her index'te $A[i] > i$. Loop bitince $lo = hi + 1$ ve $lo$ ilk ">" index'i. Recurrence: $T(n) = T(n/2) + O(1) = O(\log n)$.
Örnek seç ya da kendi sorted array'ini yaz (en fazla 20 eleman, distinct, $\ge 1$) ve oynat. Yeşil hücreler "kesin =", soluk hücreler "kesin >". Alttaki satır algoritmanın o ana kadar bildiğini, mavi kutu her adımda hangi yarının neden atıldığını gösteriyor.
Önce tahmin et: yazdığın array için cevap kaç çıkar ve kaç iterasyon sürer? 13 elemanlı bir array'de en fazla kaç iterasyon olabilir?
"A medical doctor gives its daily admitted patients distinct appointment numbers starting from 1. That is, the kth coming patient gets an id k-1. However, some patients do not come to their appointments. Assume that m patients get ids and n<m of them come to their appointment everyday. These patients are stored in an n-dimensional array A sorted by their ids. Give an O(logn) time algorithm in pseudocode convention to find the first patient that did not come to his/her appointment. For example, when A=[1, 2, 3, 5, 7, 8, 10], the algorithm must return 4."
İpucu 1
Eksik sayı yoksa $A[i]$ ne olurdu? Bir boşluk $A[i]$ ile $i$ arasındaki ilişkiyi nasıl değiştiriyor?
İpucu 2
$A[mid] = mid$ ise $1..mid$ arasında boşluk olamaz. $A[mid] > mid$ ise boşluk en geç $mid$'de.
İpucu 3
Edge case'ler: $A[1] \ne 1$ (cevap 1) ve hiç boşluk olmaması, yani $A = [1..n]$ (cevap $n+1$; $n < m$ olduğundan bu numara verilmiş ama gelmemiş).
Çözümü göster
Fikir: $A[i] = i$ özelliği önce doğru, ilk boşluktan sonra hep yanlış. Yanlış olduğu ilk index'i binary search ile bulurum; cevap o index'in kendisi.
Pseudocode: yukarıdaki FirstMissing. Recursive yazmak da olur. 2020-21 cevap kâğıtlarından birinde (Altungöz) şu yazılmış ve doğru çalışıyor:
missAppoint(A, first, last):
if first > last: return last + 1
if A[first] != first: return first
middle = floor((first + last) / 2)
if A[middle] == middle: return missAppoint(A, middle + 1, last)
return missAppoint(A, first, middle)
Correctness: Invariant korunur. $A[mid] = mid$ ise array sorted ve elemanlar farklı olduğundan $1..mid$ hepsi array'de, sola bakmak gereksiz. $A[mid] > mid$ ise sağdaki her index de ">", sağa bakmak gereksiz. Bitişte $A[lo-1] = lo-1$ (ya da $lo = 1$) ve $A[lo] \ge lo+1$ (ya da $lo = n+1$). Yani $1..lo-1$ array'de, $lo$ değil: cevap $lo$.
Trace ($A = [1,2,3,5,7,8,10]$): $mid = 4$, $A[4] = 5 > 4$ ⇒ $hi = 3$. $mid = 2$, $A[2] = 2$ ⇒ $lo = 3$. $mid = 3$, $A[3] = 3$ ⇒ $lo = 4$. $lo > hi$, cevap 4 ✓.
Running time: $T(n) = T(n/2) + O(1)$ ⇒ $O(\log n)$ (master theorem, $a = 1, b = 2, d = 0$, $a = b^d$).
"Suppose that you are given a sorted array A of n distinct integers, drawn from 1 to m where n < m. That is, A is a subset of [1,2,….,m]. Give an O(logn) time algorithm to find the smallest non-negative integer that is missing in A. Example A[1,2,3,5,7,8,10] return 4."
İpucu 1
Metin "non-negative" diyor. 0 hiç $A$'da olabilir mi? Örnekteki cevap 4 olduğuna göre soru aslında hangi sayıyı istiyor?
İpucu 2
Bu, yukarıdaki 2020-21 sorusunun hikâyesiz hali. $A[mid]$ ile $mid$'i karşılaştır. İki edge case'i ayrıca düşün: $A[1] \ne 1$ ve hiç boşluk olmaması.
Çözümü göster
Yorum: $A \subseteq \{1,\dots,m\}$ olduğu için 0 hiçbir zaman $A$'da değil. Kelimesi kelimesine okursan cevap hep 0 olur ve soru anlamını yitirir. Örnek 4 dediği için en küçük pozitif eksik sayıyı arıyorum. Cevabın başına bu cümleyi yaz.
Fikir: Hiç eksik olmasaydı $A[i] = i$ olurdu. İlk boşluktan itibaren $A[i] > i$. "$A[i] > i$" olan ilk index'i binary search ile bulurum; cevap o index'in kendisi.
SmallestMissing(A[1..n]):
lo = 1; hi = n
while lo <= hi:
mid = floor((lo + hi) / 2)
if A[mid] == mid: lo = mid + 1 // 1..mid hepsi A'da, cevap sağda
else: hi = mid - 1 // A[mid] > mid: boşluk mid'de ya da solunda
return lo // lo = n+1 ise A = [1..n]; n < m olduğundan n+1 geçerli bir cevap
Neden doğru: Elemanlar farklı, sorted ve $\ge 1$, yani $A[i] \ge i$. Bir kez $A[i] > i$ olunca sonraki her index'te de öyle. Loop bitince $1..lo-1$ index'lerinde $A[i] = i$; $lo$'da ise ya $A[lo] > lo$ ya da $lo = n+1$. İki durumda da $lo$ eksik ve ondan küçük her pozitif sayı array'de.
Trace: $A = [2,4,5]$: $mid = 2$, $A[2] = 4 > 2$ ⇒ $hi = 1$. $mid = 1$, $A[1] = 2 > 1$ ⇒ $hi = 0$. Cevap $lo = 1$. $A = [1,2,3,4,5,6,9]$: $mid = 4$ ⇒ $lo = 5$. $mid = 6$ ⇒ $lo = 7$. $mid = 7$, $A[7] = 9 > 7$ ⇒ $hi = 6$. Cevap 7.
Running time: Her iterasyonda $O(1)$ iş var ve aralık yarıya iniyor: $T(n) = T(n/2) + O(1) = O(\log n)$.
"You are given a sorted array A of n distinct integers, drawn from 1 to m where n<m. That is, A is a subset of [1,2,...,m]. Implement an O(logn) time algorithm (i.e., write a function) to find the smallest non-negative integer that is missing in A given as a parameter. For example, when A is [1, 2, 3, 5, 7, 8, 10], the function must return 4."
İpucu 1
Soru "implement" diyor: pseudocode değil, çalışan bir function. Kullandığın dilde array'ler 0'dan başlıyorsa, boşluksuz bir array'de $A[i]$ kaça eşit olur?
İpucu 2
0-indexed array'de koşul $A[mid] = mid + 1$. Loop bitince elindeki index'i sayıya çevirmeyi unutma.
Çözümü göster
Fikir: 2019-20 S5 ile aynı soru, bu kez çalışan kod isteniyor. Python list'leri 0-indexed olduğu için eksiksiz bir array'de $A[i] = i + 1$. Bu eşitliğin bozulduğu ilk index'i binary search ile bulurum; cevap o index $+1$. ("non-negative" kelimesi için 2019-20 S5'teki yorumu yaz.)
def smallest_missing(A):
lo, hi = 0, len(A) - 1
while lo <= hi:
mid = (lo + hi) // 2
if A[mid] == mid + 1: # A[0..mid] = 1..mid+1, boşluk sağda
lo = mid + 1
else: # A[mid] > mid + 1, boşluk mid'de ya da solunda
hi = mid - 1
return lo + 1 # lo: A[i] != i+1 olan ilk index (yoksa lo = n)
print(smallest_missing([1, 2, 3, 5, 7, 8, 10])) # 4
print(smallest_missing([2, 3, 4])) # 1
print(smallest_missing([1, 2, 3])) # 4
Neden doğru: Loop invariant: $i < lo$ olan her index'te $A[i] = i+1$, $i > hi$ olan her index'te $A[i] > i+1$. Loop bitince $lo = hi + 1$, yani $lo$ eşitliğin bozulduğu ilk index, ya da boşluk yoksa $lo = n$. Her iki durumda $1..lo$ sayıları array'de, $lo+1$ değil.
Running time: Her iterasyonda aralık yarıya iniyor ve $O(1)$ iş yapılıyor: $O(\log n)$. Extra space $O(1)$.
Sık yapılan iki hata: 1-indexed formülü olduğu gibi koda taşıyıp A[mid] == mid yazmak (örnekte 1 döner) ve sonda lo'yu +1 eklemeden döndürmek (örnekte 3 döner).
- 2019-20 metni "smallest non-negative integer" diyor. $A \subseteq \{1..m\}$ olduğundan 0 hiçbir zaman array'de değil; örnekteki cevap 4 olduğuna göre kastedilen en küçük pozitif eksik sayı. Cevabında bu yorumu bir cümleyle yaz.
- 2020-21'in bir kopyasında "k. hasta $k-1$ numarasını alır" yazıyor, öbür kopyada $k$. Örnek ($[1,2,3,5,\dots]$ ⇒ 4) numaraların 1'den başladığını gösteriyor.
- Array sorted değilse bu yöntem çalışmaz, çünkü özellik artık bir kez değişmez. O zaman Kalıp 2'ye geçersin.
$A = [1, 2, 4, 5, 6, 7]$, $lo = 1$, $hi = 6$. İlk adımda $mid = 3$ ve $A[3] = 4$. Ne olur?
Kalıp 1'in başka yüzleri
Sorulacak soru hep aynı: "Ortadaki elemana bakınca cevap hangi yarıda kesin var?" Bunu cevaplayabiliyorsan binary search kurarsın.
Tam sayılardan oluşan sorted $A[1..n]$'de $A[1] \ge 1$ ve $A[n] \le n$ ise bir $i$ için $A[i] = i$. $A[mid] < mid$ ise sol yarıda koşullar hâlâ sağlanıyor ($A[1] \ge 1$, $A[mid-1] \le A[mid] < mid$), sola git. $A[mid] > mid$ ise sağa git. $O(\log n)$.
$m \times n$ 0/1 haritası; her row'da bütün 1'ler (kara) 0'ların (deniz) solunda. En sağdaki kara pikselini bul. (a) $O(m\log n)$, (b) $O(m+n)$.
İpucu 1 (a)
Tek bir row "1 1 1 0 0" tipinde: özellik bir kez değişiyor. Bir row'daki son 1'i kaç adımda bulursun?
İpucu 2 (b)
Şimdiye kadarki en sağ column $j$ olsun. Sonraki row'da $j$'nin solu seni ilgilendirmez; sadece $j+1$'den sağa doğru yürü. $j$ toplamda kaç kez artabilir?
Çözüm taslağı
(a) Her row'da son 1'i binary search ile bul ($O(\log n)$), en büyük column'u tut: $m$ row × $O(\log n)$.
(b) $j = -1$ ile başla (0-indexed). Her row'da while j+1 < n and M[r][j+1] == 1: j = j+1; $j$ arttıysa $(r, j)$'yi kaydet. $j$ en fazla $n$ kez artar, her row'da bir de başarısız kontrol var: $O(m + n)$. Örnek matriste cevap $(1, 3)$.
Yıldız parlaklıkları $A[1..n]$ (farklı değerler). Bir local minimum (komşularından küçük ya da eşit olan bir eleman) bul, her elemana bakmadan. Pseudocode ve running time.
İpucu 1
Ortadaki $A[mid]$'i bir komşusuyla karşılaştır. Komşu daha küçükse o yöne "yokuş aşağı" iniyorsun. O yarıda bir local minimum olduğunu neden garanti edebilirsin?
İpucu 2
Yokuş aşağı yürümeye devam edersen ya bir yerde durursun ya da array'in ucuna çarparsın. İki durumda da elinde ne var? Sonra recurrence'ı yaz.
"A university has two student clubs. The number of students registered to the first club is m and their IDs are stored in an array A (with m elements) whereas the number of students registered to the second club is n and their IDs are stored in an array B (with n elements), where m≤n. A student might be registered to either one of these clubs or both. We want to decide how many students are registered to both clubs. Given two arrays A and B along with their lenghts m and n, write a O(mlogn) algorithm (as a pseudocode) to find the number of elements that are registered to both clubs. For example, when A is [2, 6, 3, 9, 11, 8] and B is [3, 11, 7, 4, 2, 5, 1], the algorithm must return 3 corresponding to the students with IDs 2, 3 and 11. Inside your pseudocode, you are allowed to use functions that are already defined in class videos, slides and book. Also, explain why your running time is O(mlogn) in sufficient detail."
İpucu 1
$A$'nın her ID'si için "bu ID $B$'de var mı?" diye sor. $m$ soru soracaksın ve her biri $O(\log n)$ sürmeli. Hangi arama bu kadar sürer ve $B$'nin nasıl olmasını ister?
İpucu 2
Binary search sorted array ister. $B$'yi bir kez sort etmenin maliyetini ayrıca yaz. Bu maliyet $O(m\log n)$'in içine sığıyor mu? $m = 1$ iken ne olur?
Çözümü göster
Fikir: $B$'yi bir kez sort et. Sonra $A$'nın her ID'sini sorted $B$'de binary search ile ara ve bulunanları say. Bir öğrenci bir kulübe bir kez kaydolduğu için her array'de ID'ler farklı; her eşleşme ayrı bir öğrenci.
CountCommon(A, m, B, n):
MergeSort(B, 1, n) // class'ta tanımlı, worst case O(n log n)
count = 0
for i = 1 to m: // m iterasyon
if BinarySearch(B, 1, n, A[i]) != NIL: // O(log n)
count = count + 1
return count
Trace: Sorted $B = [1,2,3,4,5,7,11]$. 2: bakılanlar 4, 2 ⇒ var. 6: 4, 7, 5 ⇒ yok. 3: 4, 2, 3 ⇒ var. 9: 4, 7, 11 ⇒ yok. 11: 4, 7, 11 ⇒ var. 8: 4, 7, 11 ⇒ yok. Cevap 3 ✓.
Neden $O(m\log n)$ (sorunun istediği açıklama): Loop $m$ kez döner. Her iterasyondaki binary search, $n$ elemanlı aralığı her karşılaştırmada yarıya indirir; en fazla $\lfloor\log_2 n\rfloor + 1$ karşılaştırma yapar, yani recurrence $T(n) = T(n/2) + O(1) = O(\log n)$. Count'u artırmak $O(1)$. Arama kısmının toplamı $m \cdot O(\log n) = O(m\log n)$.
Sort adımını da dürüstçe yaz: $B$ sorted verilmediği için sort ayrıca $O(n\log n)$ tutar; toplam $O(n\log n + m\log n) = O(n\log n)$. Bu ancak $m = \Theta(n)$ iken $O(m\log n)$'e eşit. Unsorted $B$ ile her durumda $O(m\log n)$ zaten mümkün değil: $B$'nin bakmadığın bir elemanı $A$'daki bir ID'ye eşit olabilir, yani her algoritma $B$'nin $n$ elemanını da okumak zorunda ($\Omega(n)$). Oysa $m = 1$ iken $m\log n = \log n$. Cevapta iki satırı ayrı yaz: "$B$ sorted ise $O(m\log n)$; sort dahil $O(n\log n)$."
Sort için MergeSort seç: QuickSort'un worst case'i $O(n^2)$, sadece expected running time'ı $O(n\log n)$.
Daha iyi seçenek: Küçük olan $A$'yı sort et ($O(m\log m)$) ve $B$'nin her elemanını $A$'da ara ($n$ kez $O(\log m)$). Toplam $O((m+n)\log m)$, $m \ge 2$ için. $m \le n$ olduğundan bu hiçbir zaman $O(n\log n)$'den kötü değil, $m$ küçükse çok daha iyi.
Not: Cevaplı kâğıt $B$'yi QuickSort ile sort edip $A$'nın her elemanını binary search ile arıyor ve toplamı $O(m\log n)$ yazıyor; sort adımının maliyeti de eklenmeli.
Ocean View'da $n = m^{10}$ ise hangi algoritma asymptotically daha hızlı?
Kalıp 2: pivot etrafında partition et, tek tarafa in
Array sorted değilse binary search kullanamazsın. Ama QuickSort'un partition adımı şu soruyu sordurur: "Pivot'un solunda kaç eleman olmalıydı, kaç tane var?" Elemanlar farklıysa bu sayım, eksik sayının hangi tarafta olduğunu söyler. Sonra Select'te olduğu gibi sadece o tarafa inersin.
Array'deki her sayı $\ge lo$ ve hepsi farklı. Pivot $p$ seç, $A_< = \{x < p\}$, $A_> = \{x > p\}$. $[lo, p-1]$ aralığında tam $p - lo$ tane yer var.
- $|A_<| = p - lo$ ⇒ $lo..p-1$ hepsi array'de, $p$ de array'de. Eksik en küçük sayı $p$'den büyük: $A_>$ ile, $lo = p+1$ alarak devam.
- $|A_<| < p - lo$ ⇒ $lo..p-1$ içinde bir boşluk var: $A_<$ ile, aynı $lo$ ile devam.
- Array boşsa cevap $lo$.
Animasyonda pivot, tekrarlanabilir olsun diye ilk eleman; gerçek algoritma pivot'u random seçer. Mavi $A_<$, turuncu $A_>$, mor pivot.
"Every patient must get an appointment beforehand to go to the Akdeniz University hospital on a particular day. Appointed patients for each day are given distinct patient numbers starting from 1. Hence, the first appointed patient gets the patient number 1, the second appointed patient gets 2 and so on. Unfortunately, the patients come to their appointments in a totally random order. Also, some patients do not come to their appointments. Assume that m patients got patient numbers on a particular day and n<m of them came to their appointment. These patients' patient numbers are stored in an n-dimensional array A ordered with respect to their arrival times (i.e. not their patient numbers). Give an O(n) average-time complexity algorithm to find the patient with the lowest patient number who did not show up for his/her appointment on that day. For example, let m=20 and n=11 with A=[16,13,9,5,4,13,1,6,7,8,2]. Then, the algorithm must return 3. Hint: Divide-and-conquer."
İpucu 1
Sort etmek $O(n\log n)$, fazla. QuickSort'tan partition'ı al, ama iki tarafa birden inme.
İpucu 2
Pivot'un solundaki eleman sayısını, $1..p-1$ arasındaki numara sayısıyla karşılaştır.
İpucu 3
Expected running time için Select/QuickSelect analizini hatırla: random pivot yarı yarıya probability ile "iyi"dir (ortadaki yarıdan gelir) ve iyi pivot array'i en fazla $3n/4$'e indirir.
Çözümü göster
Fikir: Random pivot ile partition yap. Soldaki eleman sayısı olması gerekene tam eşitse sol taraf eksiksizdir, sağa geç; değilse eksik soldadır. Her adımda tek tarafla devam ettiğim için QuickSelect gibi expected $O(n)$.
SmallestMissing(A, lo): // A: farklı sayılar, hepsi >= lo
if A is empty: return lo
p = A'dan rastgele bir eleman
L = [x in A : x < p]; R = [x in A : x > p] // partition, O(|A|)
if |L| == p - lo: return SmallestMissing(R, p + 1)
else: return SmallestMissing(L, lo)
cevap = SmallestMissing(A, 1)
Correctness: $L$'deki sayılar farklı ve $[lo, p-1]$ içinde. $|L| = p - lo$ ise bu aralığın her sayısı var, $p$ de var; o halde cevap $\ge p+1$ ve cevabı sadece $R$'deki sayılar etkiler. $|L| < p - lo$ ise $[lo, p-1]$'de bir eksik var ve en küçük eksik orada; bu aralık sadece $L$'nin sayılarına bağlı. Boş array'de $lo$ eksiktir. Her call'da array en az 1 küçülür, yani algoritma biter.
Running time: Partition $O(n)$. Random pivot $1/2$ probability ile ortadaki yarıdan gelir, o zaman kalan taraf $\le 3n/4$. Expected olarak 2 denemede bir size $3/4$'e iner: $E[T(n)] \le 2cn + E[T(3n/4)]$ ⇒ $E[T(n)] \le 8cn = O(n)$. Worst case (pivot hep en kötü çıkarsa) $O(n^2)$; soru "average" istediği için bu kabul edilir.
Trace (pivot = ilk eleman, animasyondaki array): $p=16$: $|L| = 10 \ne 15$ ⇒ sol. $p = 13$: $9 \ne 12$ ⇒ sol. $p=9$: $7 \ne 8$. $p=5$: $3 \ne 4$. $p=4$: $2 \ne 3$. $p=1$: $0 = 0$ ⇒ sağ, $lo = 2$. $p=2$: $0 = 0$ ⇒ sağ, $lo = 3$. Array boş: cevap 3 ✓.
- Sayım farklı sayılara dayanıyor. Kaynaktaki örnekte 13 iki kez geçiyor (soru "distinct" diyor, yani bu bir yazım hatası); animasyonda ikinci 13'ü 12 yaptık, cevap yine 3.
- İki tarafa da recursive olarak inersen QuickSort olur: expected $O(n\log n)$. İstenen bu değil.
- Hızlı ama D&C olmayan yol: size'ı $n+1$ olan bir boolean array tut, $x \le n+1$ olanları işaretle, işaretsiz ilk index'i döndür. Worst case $O(n)$, ama $O(n)$ extra space ister. Soru "divide-and-conquer" dediği için ana cevap yukarıdaki olsun, bunu yan not olarak yazabilirsin.
Sorted $D[1..n]$ (farklı tam sayılar) ve $D$'nin $n-1$ elemanını içeren unsorted $C$. $C$'de olmayan sayıyı iyi bir average running time ile bul; adım adım taslak, running time ve space analizi yaz.
İpucu 1
$D$ sorted, yani pivot'u $D$'den seçebilirsin. $D$'nin ortasındaki eleman etrafında $C$'yi partition et. Solda kaç eleman olmalıydı?
İpucu 2
Sol taraftaki eleman sayısını olması gerekenle karşılaştır ve eksiğin hangi yarıda olduğuna karar ver. Sonra her adımda ne kadar iş yaptığını ve problemin ne kadar küçüldüğünü yaz.
$lo = 1$, pivot $p = 6$, $|A_<| = 5$. Ne yaparsın?
Kalıp 3: Select ile dengeli partition
Lecture 4'teki Select (median of medians), unsorted array'de $k$-th smallest elemanı worst case $O(n)$'de bulur. Bunu bir black box gibi kullan: median'ı bul, onun etrafında partition et, subproblem'ler hep yarı yarıya olsun. Örnek: QuickSort'ta pivot olarak Select ile median'ı seçersen worst case $T(n) = 2T(n/2) + O(n) = O(n\log n)$ olur (2019-20 vize S3).
- Select'i constant sayıda çağırmak $O(n)$ tutar: 3 call $= 3\cdot O(n)$.
- Select unsorted array için gerekir; array sorted ise median zaten ortada, $O(1)$.
- "Değere göre yakınlık" gerekiyorsa önce yeni bir array kur (ör. $|x - \text{medyan}|$), sonra Select'i onun üstünde çalıştır.
"Describe an O(n) algorithm that, given a set S of n distinct numbers and a positive integer k<=n, determines the k numbers in S that are closest to the median of S."
İpucu 1
Önce median'ı bul. Hangi algoritma bunu worst case $O(n)$'de yapar?
İpucu 2
"Yakın" değerle ölçülür: her $x$ için $|x - \text{medyan}|$. Bu distance'lar arasında $k$-th smallest değeri bulursan ne elde edersin?
Çözümü göster
Fikir: Median'ı Select ile bul, her sayının median'a distance'ını hesapla, distance'lar arasında $k$-th smallest değeri Select ile bul, ondan küçük ya da ona eşit olanları topla.
KClosest(S, k):
med = Select(S, ceil(n/2)) // O(n)
D = [ |x - med| for x in S ] // O(n)
t = Select(D, k) // k. en küçük uzaklık, O(n)
out = [x in S : |x - med| < t]
S içinde |x - med| == t olanlardan, out k olana kadar ekle
return out // toplam O(n)
Correctness: Distance'ı $t$'den küçük olan her sayı, en yakın $k$ sayının içinde olmak zorunda; $t$ $k$-th smallest distance olduğundan bunlar $< k$ tane. Eşit distance'lı sayılardan ($x = \text{med} \pm t$) $k$'ya tamamlayacak kadar ekleriz. Median'ın kendisi distance 0 ile listede; soru onu saymıyorsa $k+1$ ile çalıştır ve median'ı çıkar.
Trace: $S = \{1, 2, 10, 11, 12\}$, $k = 2$. Median 10, $D = [9, 8, 0, 1, 2]$, $t = \text{Select}(D, 2) = 1$. Distance'ı $< 1$ olan: 10. Distance'ı $= 1$ olandan bir tane: 11. Sonuç $\{10, 11\}$. Median'ı saymıyorsan $k+1 = 3$ ile çalıştır: $t = 2$, $\{10, 11, 12\}$, median'ı çıkar: $\{11, 12\}$.
Running time: üç $O(n)$ adım ve bir tarama: worst case $O(n)$.
Repodaki 2019-20 çözüm notu, sorted düzende median'ın etrafındaki $(n-k)/2..(n+k)/2$ pozisyonlarını alıyor. Bu, değerce en yakın sayıları vermez. Counterexample: $S = \{1, 2, 10, 11, 12\}$, median 10. Değerce en yakın iki sayı 11 ve 12 (distance 1 ve 2), ama rank komşuları 2 ve 11 (2'nin distance'ı 8). Sınavda "closest" kelimesini değer olarak oku, yorumunu bir cümleyle yaz.
Uzunluğu $n$ olan iki array; union'larının median'ı. (1.1) Array'ler unsorted ise; bu neden en iyi running time? (1.2) Array'ler sorted ise daha hızlı olur mu?
İpucu 1 (1.1)
İki array'i arka arkaya koy, Select'i çalıştır. Lower bound için: bir elemana hiç bakmazsan o eleman median olabilir mi?
İpucu 2 (1.2)
Median, $2n$ elemanın $n$-th smallest elemanı. $A[k/2]$ ile $B[k/2]$'yi karşılaştır: küçük olanın ilk $k/2$ elemanı $k$-th smallest olabilir mi?
Çözüm taslağı
(1.1) İki array'i arka arkaya koy, Select ile $n$-th smallest elemanı bul: $O(n)$. Her elemana bakmak zorundasın (bakmadığın eleman median'ı değiştirebilir), yani lower bound $\Omega(n)$; $O(n)$ optimal.
(1.2) $k = n$ ile başla. $A[k/2] \le B[k/2]$ ise $A$'nın ilk $k/2$ elemanının her biri için, ondan küçük en fazla $k-2$ eleman var; hiçbiri $k$-th smallest olamaz. Onları at ve $k \leftarrow k - k/2$ yap. Simetrik durum aynı. Her adımda $k$ yarıya iner: $T(k) = T(k/2) + O(1) = O(\log n)$. (CLRS 4e Alıştırma 9.3-10 aynı soru.)
2019-20 S4'te ikinci Select'i neden $S$ üzerinde değil de distance array'i $D$ üzerinde çalıştırıyoruz?
Kalıp 4: kalan yol üstünde recursion (benzin istasyonları)
"(25 points) You are driving to a faraway city. There is only one route (a direct highway connecting two cities) that you can take, and the total distance is m miles. Your car can only hold enough gas for 300 miles, so you will need to stop for gas. There are n gas stations along the highway between the two cities. The ith gas station has distance x_i to the origin, where the x_i's are stored in a distance array X of length n (the x_i's are sorted). Suppose you want to minimize the number of stops you need to make. Design a divide-and-conquer algorithm that finds out the minimum number of gas stations you can stop at. (You always have a full tank at the origin. Every time you go to a gas station, you will fill up the tank and will be able to go for another 300 miles. The distance between two consecutive gas stations is at most 300). Make your algorithm as fast as possible, and analyze its running time. Your function name must be minStops. (Hint: minStops should take four parameters: The remaining total miles to go, the remaining amount of gas in your car in terms of miles it can go nonstop, the distance array, and the starting index of the remaining stations to be considered for inclusion.)"
İpucu 1
Depodaki yakıt kalan yola yetiyorsa ne olur? Bu base case.
İpucu 2
Yetmiyorsa bir yerde durmak zorundasın. Ulaşabildiğin istasyonlardan en uzağı neden hiçbir zaman kötü bir seçim olmaz?
İpucu 3
O istasyonda depoyu doldurduktan sonra kalan problem aynı türden ama daha küçük: kalan yol $m - X[j]$, yakıt 300, istasyonlar $j+1$'den itibaren.
Çözümü göster
Fikir: Yakıt yetiyorsa dur. Yetmiyorsa ulaşabildiğim en uzak istasyonda dur ve kalan yolu aynı fonksiyonla çöz.
// m (toplam yol) ve n global; şu anki konum = m - r
minStops(r, g, X, s):
if g >= r: return 0 // şehre durmadan ulaşırım
pos = m - r
j = s - 1
while j + 1 <= n and X[j+1] - pos <= g: // ulaşabildiğim en uzak istasyon
j = j + 1
if j < s: return INFINITY // hiçbir istasyona yetmiyor (verilen koşullarda olmaz)
return 1 + minStops(m - X[j], 300, X, j + 1)
cevap = minStops(m, 300, X, 1)
Correctness (greedy stays ahead): Herhangi bir optimal plan ilk durağını ya benim seçtiğim en uzak istasyondan önce yapar ya da tam orada. Onun ilk durağını benimkiyle değiştirirsem plan hâlâ geçerli: ben en az onun kadar ilerideyim ve dolu depoyla çıkıyorum, onun ikinci durağına da yetişirim. Durak sayısı artmadı. Aynı iddia kalan yol için induction ile sürer.
Running time: $j$ hiç geri gitmez. Tüm call'lar boyunca her istasyon en fazla bir kez "ulaşılabilir" diye kontrol edilir, her call bir de başarısız kontrol yapar. Call sayısı $\le n + 1$, toplam $O(n)$. Recurrence olarak: $T(n) = T(n - k) + O(k+1)$, burada $k$ bu adımda geçilen istasyon sayısı ⇒ $O(n)$. Durak sayısı azsa $j$'yi her adımda binary search ile bulmak $O(s\log n)$ verir ($s$ = durak sayısı). Cevabına bunu bir cümle olarak eklemek "as fast as possible" kısmına iyi gelir.
Trace: $m = 1000$, $X = [100, 250, 400, 520, 700, 850]$. Konum 0, yakıt 300 ⇒ ulaşılabilir 100, 250 ⇒ 250'de dur. Konum 250 ⇒ 520'ye kadar ⇒ 520'de dur. Konum 520 ⇒ 700'de dur. Konum 700, kalan 300, yakıt 300 ⇒ bitti. 3 durak, tüm durak kombinasyonlarını deneyen DP ile aynı.
- Her istasyonda "dur ya da durma" diye iki dala ayrılan recursion doğru ama $O(2^n)$. "Make your algorithm as fast as possible" bunu eler.
- Bu recursion problemi ikiye bölmüyor, tek ve daha küçük bir subproblem'e indiriyor (decrease and conquer). Soru buna "divide and conquer" diyor; cevapta subproblem'i ve recurrence'ı açıkça yazman yeterli.
- $X$ sorted olsa da her adımda $X$'i baştan taramak $O(n^2)$ yapar. Taramaya
s'ten başla.
Konum 250, depo dolu (300 mil), sıradaki istasyonlar 400, 520, 700. Algoritma nerede durur?
Kalıp 5: merge sırasında çift sayma
Fiyatlar [5,9,2,1]. Kaç i<j çifti için A[i]>2A[j]? Elle: (5,2), (5,1), (9,2), (9,1); sonuç 4. Bütün çiftlere bakmak Θ(n²). MergeSort gibi bölerek daha hızlı sayabiliriz.
Sol yarı [5,9], sağ yarı [2,1]. Recursive çağrılar bu yarıları sıralı döndürür: L=[5,9], R=[1,2]. Yarıların içi ayrıca sayılır. Burada her iki yarının kendi içinde sayısı 0; geriye cross çiftler kalır.
L’den x=5 için R’deki 1 ve 2 uygundur: 2 çift. x=9 için de aynı iki değer uygundur: 2 daha. R sıralı olduğu için uygun elemanlar başta, bir prefix’tedir. x arttıkça bu prefix daralmaz; j pointer’ı geri dönmez.
def cross_drops(L, R):
j = 0
count = 0
for x in L:
while j < len(R) and x > 2 * R[j]:
j += 1
count += j
return count
n≤1 için (liste,0) döndür. İki yarıyı recursive çöz: (L,cL), (R,cR). Sonuç (merge(L,R), cL+cR+cross_drops(L,R)). Her x bir kez işlenir, j toplam len(R) adım atar; cross ve merge lineer. T(n)=2T(n/2)+Θ(n)=Θ(n log n).
Başta bütün diziyi sıralama. i<j koşulunu, orijinal sol/sağ yarı üyeliği korur. Her çift ya sol içi, ya sağ içi, ya cross’tur; bu üç sınıf ayrık ve tamdır. Recursive doğruluk ve lineer cross sayımı birlikte her çifti tam bir kez sayar.
L=[2,5,9], R=[1,3,8]. x>2y koşulunda cross çift sayısı?
Form A: x>2y; B: y>3x; C: x>y+5; D: y>x+10. A/C uygun prefix’i sayar. B/D’de koşulu sağlamayan prefix’i atlayıp len(R)−j uygun suffix elemanını say. Strict > eşitlikte saymaz.
İpucu
Çözümü göster
B için while R[j]≤3x, D için while R[j]≤x+10 ile j’yi ilerlet; her x için len(R)−j ekle. C için while x>R[j]+5; her x için j ekle. Her formda recursive yapı aynı, cross koşulu farklı.
Dört formun tam pseudocode’u ve yeni girdilerle alıştırma →Son kontrol
Kâğıt kalemle düşün, sonra cevaba bak.
Bir algoritma size'ı $n$ olan problemi, size'ı $n/3$ olan 9 subproblem'e bölüyor; combine $O(n)$. Running time?
Sorted ve farklı pozitif tam sayılardan oluşan $A$'da ilk eksik sayıyı arıyorsun. $A[mid] = mid$ çıktı. Doğru çıkarım hangisi?
Karatsuba'da $(a+b)(c+d)$ multiplication'ı neden işe yarıyor?
Unsorted array'de eksik en küçük sayıyı partition ile ararken random pivot kullanıyorsun. Hangisi doğru?