Bölüm 6: Dinamik Programlama
Giriş
Öğrenme Çıktıları
Bir problemde örtüşen alt problemleri ve optimal alt yapıyı fark etmek; dinamik programlamanın ne zaman uygun olduğuna karar vermek.
Alt problemi (durumu) tanımlamak, özyineleme bağıntısını ve taban durumlarını yazmak, tablo doldurma sırasını belirlemek.
Fibonacci, 0/1 ve sınırsız sırt çantası, alt küme toplamı, LIS, en uzun ortak alt dizgi ve LCS tablolarını elle doldurmak ve Java ile yazmak.
Memoization ile tablolamayı karşılaştırmak; O(n · W), O(m · n), O(n²), O(n log n) karmaşıklıklarını gerekçelendirmek; tablodan çözümü geri izlemek.
Temel Kavramlar
Dinamik Programlama (Dynamic Programming) Nedir?
- Bazen karmaşık problemleri çözmek için akıllı yöntemlere ihtiyaç olur; dinamik programlama bunlardan biridir.
- Büyük problemi küçük, tekrarlayan alt problemlere böler ve adım adım çözer.
- Her alt problemin çözümü bir tabloya yazılır; aynı alt problemle yeniden karşılaşıldığında çözüm tablodan okunur, böylece zaman kazanılır.
- Özellikle optimizasyon problemlerinde (en büyük değer, en kısa yol, en az işlem) sıkça kullanılır.
DP = özyineleme + hatırlama. Her dinamik programlama problemi düz özyineleme ile de çözülebilir; DP, aynı işi iki kez yapmamayı sağlar.
Ad, Richard Bellman'dan (1950'ler) gelir; "programlama" burada kod yazmak değil, tablo ile planlama anlamındadır.
Temel Kavramlar
DP İçin İki Temel Özellik
Özyinelemeli çözüm aynı alt problemi tekrar tekrar çağırır. Örnek: fib(5) hesaplanırken fib(3) iki kez, fib(2) üç kez hesaplanır.
Alt problemler birbirinden bağımsızsa (birleştirme sıralaması gibi) tablo işe yaramaz; bu böl ve fethettir.
Problemin optimal çözümü, alt problemlerin optimal çözümlerinden kurulabilir.
Bellman'ın optimallik ilkesi: optimal bir çözümün içerdiği her alt çözüm de kendi alt problemi için optimaldir.
V(i) = max(a[i], V(i−1) + a[i]). Değer, bir önceki alt problemin optimal değerinden hesaplanır.Temel Kavramlar
Bir DP Çözümü Tasarlamanın Adımları
- Optimal çözümün yapısını anla: hangi alt problemler çözülmeli?
- Durumu ve bağıntıyı yaz: çözümü daha küçük alt problemler cinsinden ifade et (özyineleme bağıntısı + taban durumları).
- Hesaplama sırasını belirle: bir hücre hesaplanırken ihtiyaç duyduğu hücreler hazır olmalı.
- Tablonun boyutunu belirle: kaç boyut, her boyut kaç değer? (ör. (n+1) × (W+1))
- Tabloyu doldur: aşağıdan yukarı (tablolama) ya da yukarıdan aşağı (memoization).
- Çözümü geri izle: yalnızca değeri değil, seçilen elemanları da tablodan geriye doğru okuyarak bul.
Toplam süre ≈ (durum sayısı) × (bir durumun hesaplanma süresi). Bu kural bölümdeki tüm karmaşıklıkları açıklar.
Temel Kavramlar
Memoization ve Tablolama (Tabulation)
- Özyinelemeli çözüm aynen yazılır; sonuçlar bir önbellekte (dizi / HashMap) saklanır.
- Çağrıdan önce tabloya bakılır: varsa döndürülür, yoksa hesaplanıp yazılır.
- Yalnızca gerçekten ihtiyaç duyulan alt problemler çözülür.
- Derin özyinelemede yığın taşması (StackOverflowError) riski.
- En küçük alt problemden başlanır, tablo döngülerle sırayla doldurulur.
- Özyineleme yok, çağrı maliyeti yok; genellikle daha hızlıdır.
- Doldurma sırası önceden doğru belirlenmelidir.
- Yalnızca önceki satır gerekiyorsa bellek azaltılabilir (ör. O(W)).
Temel Kavramlar
DP, Böl ve Fethet ve Açgözlü Yaklaşım
| Böl ve Fethet | Dinamik Programlama | Açgözlü (Greedy) | |
|---|---|---|---|
| Alt problemler | bağımsız | örtüşen | her adımda tek seçim |
| Seçim | yok, böl ve birleştir | tüm seçenekler denenir, en iyisi tabloya | o anki en iyi seçilir, geri dönülmez |
| Optimallik | — | optimal alt yapı varsa garanti | yalnızca açgözlü seçim özelliği varsa |
| Örnek | birleştirme sıralaması, ikili arama | 0/1 sırt çantası, LCS, Bellman-Ford | kesirli sırt çantası, Dijkstra, Kruskal |
| Bellek | özyineleme yığını | tablo (çoğu zaman O(n · W) ya da O(m · n)) | genellikle O(1) ek bellek |
Temel Kavramlar
Dinamik Programlama Uygulamaları
Fibonacci
Fibonacci: Düz Özyineleme
Fibonacci dizisi: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …
F(0) = 0, F(1) = 1F(n) = F(n−1) + F(n−2), n ≥ 2static long fib(int n) { if (n <= 1) return n; // taban durumları return fib(n - 1) + fib(n - 2); }
- Tanımın birebir kopyası; doğru ama çok yavaş.
- Çağrı sayısı:
T(n) = T(n−1) + T(n−2) + 1→ Θ(φⁿ), φ ≈ 1,618 (altın oran). - fib(40) yaklaşık 331 milyon çağrı yapar; fib(50) ise dakikalar sürer.
- Neden? Aynı alt problemler (fib(3), fib(2), …) tekrar tekrar hesaplanır: örtüşen alt problemler.
Fibonacci
fib(5) Özyineleme Ağacı: Çağrı Sırası
Turuncu: çalışan çağrı · mavi: yığında bekleyen çağrılar · yeşil: bitmiş çağrı (değeriyle).
fib(5) çağrıldı Yığın: fib(5)
fib(4) çağrıldı Yığın: fib(5) → fib(4)
fib(3) çağrıldı Yığın: fib(5) → fib(4) → fib(3)
fib(2) çağrıldı Yığın: fib(5) → fib(4) → fib(3) → fib(2)
fib(1) taban durumu: hemen 1 döner. Yığın: fib(5) → fib(4) → fib(3) → fib(2) → fib(1)
fib(0) taban durumu: hemen 0 döner. Yığın: fib(5) → fib(4) → fib(3) → fib(2) → fib(0)
fib(1) taban durumu: hemen 1 döner. Yığın: fib(5) → fib(4) → fib(3) → fib(1)
fib(2) çağrıldı — 2. kez hesaplanıyor! Yığın: fib(5) → fib(4) → fib(2)
fib(1) taban durumu: hemen 1 döner. Yığın: fib(5) → fib(4) → fib(2) → fib(1)
fib(0) taban durumu: hemen 0 döner. Yığın: fib(5) → fib(4) → fib(2) → fib(0)
fib(3) çağrıldı — 2. kez hesaplanıyor! Yığın: fib(5) → fib(3)
fib(2) çağrıldı — 3. kez hesaplanıyor! Yığın: fib(5) → fib(3) → fib(2)
fib(1) taban durumu: hemen 1 döner. Yığın: fib(5) → fib(3) → fib(2) → fib(1)
fib(0) taban durumu: hemen 0 döner. Yığın: fib(5) → fib(3) → fib(2) → fib(0)
fib(1) taban durumu: hemen 1 döner. Yığın: fib(5) → fib(3) → fib(1)
Sonuç fib(5) = 5, toplam 15 çağrı. Kırmızı kutular tekrar: fib(3) 2 kez, fib(2) 3 kez, fib(1) 5 kez hesaplandı.
Fibonacci
Tekrarlanan Hesapların Bedeli
| n | F(n) | düz özyineleme çağrısı | memoization çağrısı |
|---|---|---|---|
| 5 | 5 | 15 | 9 |
| 10 | 55 | 177 | 19 |
| 20 | 6.765 | 21.891 | 39 |
| 30 | 832.040 | 2.692.537 | 59 |
| 40 | 102.334.155 | 331.160.281 | 79 |
- Düz özyinelemede çağrı sayısı 2·F(n+1) − 1: Fibonacci sayılarının kendisi kadar hızlı (üstel) büyür.
- Oysa yalnızca n+1 farklı alt problem vardır: fib(0), fib(1), …, fib(n).
- Her birini bir kez çözüp saklarsak çağrı sayısı 2n − 1'e iner: üstelden doğrusala.
Fibonacci
Memoization: fib(5) Adım Adım
fib(5): tabloda yok; önce fib(4) çağrılır.
fib(4): tabloda yok; önce fib(3) çağrılır.
fib(3): tabloda yok; önce fib(2) çağrılır.
fib(2): tabloda yok; önce fib(1) çağrılır.
fib(1): taban durumu, 1 döner.
fib(1) döndü (a = 1); şimdi fib(0) çağrılır.
fib(0): taban durumu, 0 döner.
bellek[2] = 1 + 0 = 1 tabloya yazılır ve döndürülür.
fib(2) döndü (a = 1); şimdi fib(1) çağrılır.
fib(1): taban durumu, 1 döner.
bellek[3] = 1 + 1 = 2 tabloya yazılır ve döndürülür.
fib(3) döndü (a = 2); şimdi fib(2) çağrılır.
fib(2): bellek[2] = 1 tabloda hazır; yeniden hesaplanmaz.
bellek[4] = 2 + 1 = 3 tabloya yazılır ve döndürülür.
fib(4) döndü (a = 3); şimdi fib(3) çağrılır.
fib(3): bellek[3] = 2 tabloda hazır; yeniden hesaplanmaz.
bellek[5] = 3 + 2 = 5 tabloya yazılır ve döndürülür.
static long[] bellek = new long[50]; static long fib(int n) { if (n <= 1) return n; // taban if (bellek[n] != 0) return bellek[n]; long a = fib(n - 1); long b = fib(n - 2); bellek[n] = a + b; return bellek[n]; }
Fibonacci
Tablolama: fibonacci(9) Adım Adım
fib dizisi n + 1 = 10 elemanla oluşturulur (Java'da başlangıçta hepsi 0).
Taban durumları yazılır: fib[0] = 0, fib[1] = 1. Boş hücreler henüz hesaplanmadı.
i = 2: fib[2] = fib[1] + fib[0] = 1 + 0 = 1
i = 3: fib[3] = fib[2] + fib[1] = 1 + 1 = 2
i = 4: fib[4] = fib[3] + fib[2] = 2 + 1 = 3
i = 5: fib[5] = fib[4] + fib[3] = 3 + 2 = 5
i = 6: fib[6] = fib[5] + fib[4] = 5 + 3 = 8
i = 7: fib[7] = fib[6] + fib[5] = 8 + 5 = 13
i = 8: fib[8] = fib[7] + fib[6] = 13 + 8 = 21
i = 9: fib[9] = fib[8] + fib[7] = 21 + 13 = 34
Döngü biter; fib[9] = 34 döndürülür. Her hücre bir kez hesaplandı: O(n) zaman, O(n) bellek.
static int fibonacci(int n) { int[] fib = new int[n + 1]; fib[0] = 0; // taban durumları fib[1] = 1; for (int i = 2; i <= n; i++) fib[i] = fib[i - 1] + fib[i - 2]; return fib[n]; }
Fibonacci
Fibonacci: O(1) Bellek ve Karşılaştırma
static long fib(int n) { if (n <= 1) return n; long once = 0, simdi = 1; for (int i = 2; i <= n; i++) { long yeni = once + simdi; once = simdi; simdi = yeni; } return simdi; }
| Yöntem | Zaman | Ek bellek |
|---|---|---|
| Düz özyineleme | Θ(φⁿ) | O(n) yığın |
| Memoization | O(n) | O(n) tablo + yığın |
| Tablolama | O(n) | O(n) tablo |
| İki değişken | O(n) | O(1) |
0/1 Sırt Çantası
0/1 Sırt Çantası (0/1 Knapsack) Problemi
- n eşya: her eşyanın bir değeri vᵢ ve bir ağırlığı (hacmi) wᵢ var.
- Çantanın kapasitesi W.
- Her eşya ya tamamen alınır (1) ya da hiç alınmaz (0); bölünemez.
- Amaç: toplam ağırlık ≤ W olacak şekilde toplam değeri en büyük yapmak.
Cevap
12 kg'lık eşya dışındaki dördü: 2 + 2 + 1 + 10 = 15 ₺, toplam 8 kg. 12 kg'lık eşyayı alan her seçim en fazla 4 + 2 + 2 = 8 ₺ eder.0/1 Sırt Çantası
Neden Kaba Kuvvet ya da Açgözlü Değil?
Her eşya için 2 seçenek → 2ⁿ alt küme. n = 50 için 10¹⁵'ten fazla küme: pratikte imkânsız.
W = 50; eşyalar (60 ₺, 10 kg), (100 ₺, 20 kg), (120 ₺, 30 kg). Oranlar 6, 5, 4 → açgözlü ilk ikisini alır: 160 ₺. Optimal: ikinci + üçüncü = 220 ₺.
0/1 Sırt Çantası
0/1 Sırt Çantası: Durum ve Bağıntı
Durum: K[i][c] = ilk i eşya ve c kapasite ile elde edilebilecek en büyük değer.
wᵢ > c ise: K[i][c] = K[i−1][c]
aksi hâlde: K[i][c] = max(K[i−1][c], vᵢ + K[i−1][c − wᵢ])
- Almazsam: kapasite aynı kalır, önceki eşyalarla en iyisi.
- Alırsam: vᵢ kazanırım, kalan c − wᵢ kapasiteyi önceki eşyalarla en iyi şekilde doldururum.
- Cevap: K[n][W]. Tablo (n+1) × (W+1), satır satır doldurulur.
0/1 Sırt Çantası
0/1 Sırt Çantası: Tabloyu Adım Adım Doldur (W = 6)
Tablo (n+1) × (W+1) = 5 × 7. Satır 0 (eşya yok) ve sütun 0 (kapasite yok) 0'dır.
i=1, c=1: w=4 > 1, eşya sığmaz → üstteki değer kopyalanır: K[1][1] = 0
i=1, c=2: w=4 > 2, eşya sığmaz → üstteki değer kopyalanır: K[1][2] = 0
i=1, c=3: w=4 > 3, eşya sığmaz → üstteki değer kopyalanır: K[1][3] = 0
i=1, c=4: max(alma = 0, al = 5 + K[0][0] = 5) = 5 (al)
i=1, c=5: max(alma = 0, al = 5 + K[0][1] = 5) = 5 (al)
i=1, c=6: max(alma = 0, al = 5 + K[0][2] = 5) = 5 (al)
i=2, c=1: w=3 > 1, eşya sığmaz → üstteki değer kopyalanır: K[2][1] = 0
i=2, c=2: w=3 > 2, eşya sığmaz → üstteki değer kopyalanır: K[2][2] = 0
i=2, c=3: max(alma = 0, al = 4 + K[1][0] = 4) = 4 (al)
i=2, c=4: max(alma = 5, al = 4 + K[1][1] = 4) = 5 (alma)
i=2, c=5: max(alma = 5, al = 4 + K[1][2] = 4) = 5 (alma)
i=2, c=6: max(alma = 5, al = 4 + K[1][3] = 4) = 5 (alma)
i=3, c=1: w=2 > 1, eşya sığmaz → üstteki değer kopyalanır: K[3][1] = 0
i=3, c=2: max(alma = 0, al = 3 + K[2][0] = 3) = 3 (al)
i=3, c=3: max(alma = 4, al = 3 + K[2][1] = 3) = 4 (alma)
i=3, c=4: max(alma = 5, al = 3 + K[2][2] = 3) = 5 (alma)
i=3, c=5: max(alma = 5, al = 3 + K[2][3] = 7) = 7 (al)
i=3, c=6: max(alma = 5, al = 3 + K[2][4] = 8) = 8 (al)
i=4, c=1: max(alma = 0, al = 2 + K[3][0] = 2) = 2 (al)
i=4, c=2: max(alma = 3, al = 2 + K[3][1] = 2) = 3 (alma)
i=4, c=3: max(alma = 4, al = 2 + K[3][2] = 5) = 5 (al)
i=4, c=4: max(alma = 5, al = 2 + K[3][3] = 6) = 6 (al)
i=4, c=5: max(alma = 7, al = 2 + K[3][4] = 7) = 7 (alma)
i=4, c=6: max(alma = 8, al = 2 + K[3][5] = 9) = 9 (al)
Tablo doldu. Cevap K[4][6] = 9. Toplam 4 × 6 = 24 hücre, her biri O(1).
int sirtCantasi(int[] v, int[] w, int W) { int n = v.length; int[][] K = new int[n + 1][W + 1]; for (int i = 1; i <= n; i++) for (int c = 1; c <= W; c++) { int vi = v[i - 1], wi = w[i - 1]; if (wi > c) // sığmaz K[i][c] = K[i - 1][c]; else // alma / al K[i][c] = Math.max(K[i - 1][c], vi + K[i - 1][c - wi]); } return K[n][W]; }
0/1 Sırt Çantası
Geri İzleme: Hangi Eşyalar Alındı?
Kural: K[i][c] ≠ K[i−1][c] ise eşya i alınmıştır (değer ancak onu alarak arttı) → c ← c − wᵢ; eşitse alınmamıştır.
Geri izleme K[4][6] = 9 hücresinden başlar. Hangi eşyalar alındı?
K[4][6] = 9 ≠ K[3][6] = 8 → eşya 4 alındı (v=2, w=1); c = 6 − 1 = 5
K[3][5] = 7 ≠ K[2][5] = 5 → eşya 3 alındı (v=3, w=2); c = 5 − 2 = 3
K[2][3] = 4 ≠ K[1][3] = 0 → eşya 2 alındı (v=4, w=3); c = 3 − 3 = 0
K[1][0] = K[0][0] = 0 → eşya 1 alınmadı; c değişmez.
Seçilen eşyalar: 2, 3, 4 → değer 4 + 3 + 2 = 9, ağırlık 3 + 2 + 1 = 6 = W.
0/1 Sırt Çantası
0/1 Sırt Çantası: Karmaşıklık ve Bellek
- Zaman: O(n · W), bellek: O(n · W) — (n+1)(W+1) hücre, her biri O(1).
- Bu süre sözde-polinomdur (pseudo-polynomial): W'nin değerine bağlıdır, bit uzunluğuna değil. W = 10⁹ ise tablo kurulamaz.
- 0/1 sırt çantası NP-zordur; DP yalnızca W küçükken pratiktir.
- Bellek azaltma (sağdaki kod): satır i yalnızca satır i−1'i kullanır → tek boyutlu dizi, O(W). Kapasite büyükten küçüğe gezilmeli; yoksa aynı eşya iki kez alınır.
- Tek boyutlu sürümde geri izleme için ayrıca
boolean[][] dahiltablosu tutulur.
int[] dp = new int[W + 1]; for (int i = 0; i < n; i++) for (int c = W; c >= w[i]; c--) dp[c] = Math.max(dp[c], v[i] + dp[c - w[i]]); return dp[W];
Sınırsız Sırt Çantası
Sınırsız Sırt Çantası (Unbounded Knapsack)
- Her eşyadan istenildiği kadar (sınırsız sayıda) alınabilir; gerisi 0/1 ile aynıdır.
- Durum: dp[i][c] = 0..i eşya türleri ve c kapasite ile en büyük kâr.
Satır indisi 0'dan başlar (dp[−1][c] = 0 kabul edilir).
Sınırsız Sırt Çantası
Sınırsız Sırt Çantası: Tabloyu Adım Adım Doldur
Kapasite 0 sütunu 0'dır: hiçbir eşya sığmaz.
i=0, c=1: yalnızca eşya 0 var, ağırlığı 1 sığar → 15 + dp[0][0] = 15.
i=0, c=2..8: dp[0][c] = 15 + dp[0][c−1] → eşya 0'dan c tane: 30, 45, …, 120. Aynı satır tekrar kullanıldı!
i=1, c=1: ağırlık 3 > 1, sığmaz → dp[1][1] = dp[0][1] = 15
i=1, c=2: ağırlık 3 > 2, sığmaz → dp[1][2] = dp[0][2] = 30
i=1, c=3: max(dp[0][3] = 45, 50 + dp[1][0] = 50) = 50
i=1, c=4: max(dp[0][4] = 60, 50 + dp[1][1] = 65) = 65
i=1, c=5: max(dp[0][5] = 75, 50 + dp[1][2] = 80) = 80
i=1, c=6: max(dp[0][6] = 90, 50 + dp[1][3] = 100) = 100
i=1, c=7: max(dp[0][7] = 105, 50 + dp[1][4] = 115) = 115
i=1, c=8: max(dp[0][8] = 120, 50 + dp[1][5] = 130) = 130
i=2, c=1: ağırlık 4 > 1, sığmaz → dp[2][1] = dp[1][1] = 15
i=2, c=2: ağırlık 4 > 2, sığmaz → dp[2][2] = dp[1][2] = 30
i=2, c=3: ağırlık 4 > 3, sığmaz → dp[2][3] = dp[1][3] = 50
i=2, c=4: max(dp[1][4] = 65, 60 + dp[2][0] = 60) = 65
i=2, c=5: max(dp[1][5] = 80, 60 + dp[2][1] = 75) = 80
i=2, c=6: max(dp[1][6] = 100, 60 + dp[2][2] = 90) = 100
i=2, c=7: max(dp[1][7] = 115, 60 + dp[2][3] = 110) = 115
i=2, c=8: max(dp[1][8] = 130, 60 + dp[2][4] = 125) = 130
i=3, c=1: ağırlık 5 > 1, sığmaz → dp[3][1] = dp[2][1] = 15
i=3, c=2: ağırlık 5 > 2, sığmaz → dp[3][2] = dp[2][2] = 30
i=3, c=3: ağırlık 5 > 3, sığmaz → dp[3][3] = dp[2][3] = 50
i=3, c=4: ağırlık 5 > 4, sığmaz → dp[3][4] = dp[2][4] = 65
i=3, c=5: max(dp[2][5] = 80, 90 + dp[3][0] = 90) = 90
i=3, c=6: max(dp[2][6] = 100, 90 + dp[3][1] = 105) = 105
i=3, c=7: max(dp[2][7] = 115, 90 + dp[3][2] = 120) = 120
i=3, c=8: max(dp[2][8] = 130, 90 + dp[3][3] = 140) = 140
Cevap dp[3][8] = 140: 90 + dp[3][3] = 90 + 50 → bir tane eşya 3 (90, 5 kg) + bir tane eşya 1 (50, 3 kg).
Sınırsız Sırt Çantası
Sınırsız Sırt Çantası: Tek Boyutlu Çözüm
static int sinirsizSirtCantasi(int[] w, int[] v, int W) { int[] dp = new int[W + 1]; // dp[c]: c kapasiteyle en iyi for (int c = 1; c <= W; c++) for (int j = 0; j < w.length; j++) if (w[j] <= c) dp[c] = Math.max(dp[c], dp[c - w[j]] + v[j]); return dp[W]; }
- Satırlar birleştirilebilir: dp[c] = tüm eşyalar için max(dp[c − wⱼ] + vⱼ).
- Örnek için dp = [0, 15, 30, 50, 65, 90, 105, 120, 140] — tablonun son satırı.
- Zaman O(n · W), bellek O(W).
Alt Küme Toplamı
Alt Küme Toplamı (Subset Sum)
- Verilen tam sayılardan, toplamı tam olarak hedef değere eşit olan bir alt küme var mı? (Karar problemi: evet / hayır.)
- Durum: dp[i][j] = ilk i elemanla toplam j elde edilebilir mi? (boolean)
dp[i][j] = dp[i−1][j] veya ( aᵢ ≤ j ve dp[i−1][j − aᵢ] )
Sırt çantasının "değer = ağırlık" özel durumudur: max yerine mantıksal veya kullanılır.
Alt Küme Toplamı
Alt Küme Toplamı: Tablo (a = 10, 0, 5, 8, 6, 2, 4; hedef 15)
Sütun 0 hep ✓ (boş küme toplamı 0). Satır 0'da (eleman yok) diğer toplamlar elde edilemez (·).
i=1, a=10: satır i−1 kopyalanır, ayrıca dp[i−1][j−10] ✓ olan her j için ✓ → yeni ulaşılan toplamlar: 10.
i=2, a=0: satır i−1 kopyalanır, ayrıca dp[i−1][j−0] ✓ olan her j için ✓ → yeni toplam yok (0 eklemek hiçbir toplamı değiştirmez).
i=3, a=5: satır i−1 kopyalanır, ayrıca dp[i−1][j−5] ✓ olan her j için ✓ → yeni ulaşılan toplamlar: 5, 15.
i=4, a=8: satır i−1 kopyalanır, ayrıca dp[i−1][j−8] ✓ olan her j için ✓ → yeni ulaşılan toplamlar: 8, 13.
i=5, a=6: satır i−1 kopyalanır, ayrıca dp[i−1][j−6] ✓ olan her j için ✓ → yeni ulaşılan toplamlar: 6, 11, 14.
i=6, a=2: satır i−1 kopyalanır, ayrıca dp[i−1][j−2] ✓ olan her j için ✓ → yeni ulaşılan toplamlar: 2, 7, 12.
i=7, a=4: satır i−1 kopyalanır, ayrıca dp[i−1][j−4] ✓ olan her j için ✓ → yeni ulaşılan toplamlar: 4, 9.
dp[7][15] = dp[6][15] = ✓ → 4 olmadan da toplam 15 mümkün; seçilmez, yukarı çık.
dp[6][15] = dp[5][15] = ✓ → 2 olmadan da toplam 15 mümkün; seçilmez, yukarı çık.
dp[5][15] = dp[4][15] = ✓ → 6 olmadan da toplam 15 mümkün; seçilmez, yukarı çık.
dp[4][15] = dp[3][15] = ✓ → 8 olmadan da toplam 15 mümkün; seçilmez, yukarı çık.
dp[3][15] ✓ ama dp[2][15] · → 5 seçildi; j = 15 − 5 = 10
dp[2][10] = dp[1][10] = ✓ → 0 olmadan da toplam 10 mümkün; seçilmez, yukarı çık.
dp[1][10] ✓ ama dp[0][10] · → 10 seçildi; j = 10 − 10 = 0
Geri izleme {5, 10} alt kümesini bulur: 10 + 5 = 15. {5, 8, 2} ve {5, 6, 4} de geçerlidir; tablo yalnızca birini verir.
Alt Küme Toplamı
Alt Küme Toplamı: Kod ve Analiz
boolean altKume(int[] a, int hedef) { int n = a.length; boolean[][] dp = new boolean[n + 1][hedef + 1]; for (int i = 0; i <= n; i++) dp[i][0] = true; // boş küme for (int i = 1; i <= n; i++) { int x = a[i - 1]; for (int j = 1; j <= hedef; j++) dp[i][j] = dp[i - 1][j] // x alınmaz || (x <= j && dp[i - 1][j - x]); } return dp[n][hedef]; }
- Zaman ve bellek: O(n · hedef) — yine sözde-polinom.
- Problem NP-tamdır; hedef büyükse tablo kurulamaz.
- Bu örnekte 6 farklı çözüm var: {10, 5}, {5, 8, 2}, {5, 6, 4} ve bunların 0 eklenmiş hâlleri.
- Tek boyutlu sürüm: j büyükten küçüğe gezilir (0/1 gibi).
- Uygulama: eşit bölme problemi — toplamı S olan diziyi iki eşit parçaya bölmek = hedef S/2 ile alt küme toplamı.
En Uzun Artan Alt Dizi
En Uzun Artan Alt Dizi (Longest Increasing Subsequence)
- Alt dizi (subsequence): elemanlar sırası korunarak, atlanarak seçilebilir. Alt dizgi / alt dizi parçası (substring, subarray): elemanlar bitişik olmalıdır.
- LIS: elemanları kesin artan en uzun alt dizi.
Bu dizinin birden çok LIS'i vardır (ör. 0, 4, 6, 9, 13, 15); uzunluk her zaman tektir.
En Uzun Artan Alt Dizi
LIS: O(n²) Dinamik Programlama
Başlangıç: her eleman tek başına uzunluk 1'lik artan alt dizidir.
i=0: a[0]=0, öncesinde eleman yok → dp[0] = 1.
i=1: a[1]=8; küçük öncekiler (mavi) arasında en büyük dp, j=0 (dp=1) → dp[1] = 2
i=2: a[2]=4; küçük öncekiler (mavi) arasında en büyük dp, j=0 (dp=1) → dp[2] = 2
i=3: a[3]=12; küçük öncekiler (mavi) arasında en büyük dp, j=1 (dp=2) → dp[3] = 3
i=4: a[4]=2; küçük öncekiler (mavi) arasında en büyük dp, j=0 (dp=1) → dp[4] = 2
i=5: a[5]=10; küçük öncekiler (mavi) arasında en büyük dp, j=1 (dp=2) → dp[5] = 3
i=6: a[6]=6; küçük öncekiler (mavi) arasında en büyük dp, j=2 (dp=2) → dp[6] = 3
i=7: a[7]=14; küçük öncekiler (mavi) arasında en büyük dp, j=3 (dp=3) → dp[7] = 4
i=8: a[8]=1; küçük öncekiler (mavi) arasında en büyük dp, j=0 (dp=1) → dp[8] = 2
i=9: a[9]=9; küçük öncekiler (mavi) arasında en büyük dp, j=6 (dp=3) → dp[9] = 4
i=10: a[10]=5; küçük öncekiler (mavi) arasında en büyük dp, j=2 (dp=2) → dp[10] = 3
i=11: a[11]=13; küçük öncekiler (mavi) arasında en büyük dp, j=9 (dp=4) → dp[11] = 5
i=12: a[12]=3; küçük öncekiler (mavi) arasında en büyük dp, j=4 (dp=2) → dp[12] = 3
i=13: a[13]=11; küçük öncekiler (mavi) arasında en büyük dp, j=9 (dp=4) → dp[13] = 5
i=14: a[14]=7; küçük öncekiler (mavi) arasında en büyük dp, j=6 (dp=3) → dp[14] = 4
i=15: a[15]=15; küçük öncekiler (mavi) arasında en büyük dp, j=11 (dp=5) → dp[15] = 6
En büyük dp = 6 (i=15). önceki indisleri izlenir: 15 → 11 → 9 → 6 → 2 → 0 ⇒ 0, 4, 6, 9, 13, 15 (eşitlikte ilk j seçilir; 0, 2, 6, 9, 11, 15 de 6 uzunlukta bir LIS'tir).
Turuncu: işlenen eleman · mavi: a[j] < a[i] olan adaylar · yeşil: en iyi önceki eleman. Toplam O(n²) karşılaştırma.
En Uzun Artan Alt Dizi
LIS: Aktif Listeler Yöntemi
Her uzunluk için sonu en küçük olan artan liste tutulur; küçük son, gelecekte uzamaya daha açıktır.
Liste yok: [0] ile ilk aktif liste başlatılır.
8 tüm sonlardan büyük: en uzun liste kopyalanıp 8 eklenir (uzunluk 2).
4, sonu 0 olan listeyi uzatır → [0, 4]; aynı uzunluktaki [0, 8] artık gereksiz (sonu daha büyük).
12 tüm sonlardan büyük: en uzun liste kopyalanıp 12 eklenir (uzunluk 3).
2, sonu 0 olan listeyi uzatır → [0, 2]; aynı uzunluktaki [0, 4] artık gereksiz (sonu daha büyük).
10, sonu 2 olan listeyi uzatır → [0, 2, 10]; aynı uzunluktaki [0, 4, 12] artık gereksiz (sonu daha büyük).
6, sonu 2 olan listeyi uzatır → [0, 2, 6]; aynı uzunluktaki [0, 2, 10] artık gereksiz (sonu daha büyük).
14 tüm sonlardan büyük: en uzun liste kopyalanıp 14 eklenir (uzunluk 4).
1, sonu 0 olan listeyi uzatır → [0, 1]; aynı uzunluktaki [0, 2] artık gereksiz (sonu daha büyük).
9, sonu 6 olan listeyi uzatır → [0, 2, 6, 9]; aynı uzunluktaki [0, 2, 6, 14] artık gereksiz (sonu daha büyük).
5, sonu 1 olan listeyi uzatır → [0, 1, 5]; aynı uzunluktaki [0, 2, 6] artık gereksiz (sonu daha büyük).
13 tüm sonlardan büyük: en uzun liste kopyalanıp 13 eklenir (uzunluk 5).
3, sonu 1 olan listeyi uzatır → [0, 1, 3]; aynı uzunluktaki [0, 1, 5] artık gereksiz (sonu daha büyük).
11, sonu 9 olan listeyi uzatır → [0, 2, 6, 9, 11]; aynı uzunluktaki [0, 2, 6, 9, 13] artık gereksiz (sonu daha büyük).
7, sonu 3 olan listeyi uzatır → [0, 1, 3, 7]; aynı uzunluktaki [0, 2, 6, 9] artık gereksiz (sonu daha büyük).
15 tüm sonlardan büyük: en uzun liste kopyalanıp 15 eklenir (uzunluk 6).
Dizi bitti: en uzun aktif liste 0, 2, 6, 9, 11, 15 → LIS uzunluğu 6.
En Uzun Artan Alt Dizi
LIS: O(n²) Kod
static int lis(int[] a) { int n = a.length, enUzun = 0; int[] dp = new int[n]; for (int i = 0; i < n; i++) { dp[i] = 1; // tek başına for (int j = 0; j < i; j++) if (a[j] < a[i] && dp[j] + 1 > dp[i]) dp[i] = dp[j] + 1; // uzat enUzun = Math.max(enUzun, dp[i]); } return enUzun; }
- İç döngü, a[i]'den küçük her önceki elemanın dizisini uzatmayı dener.
- Zaman O(n²), bellek O(n).
- Diziyi kurmak için
onceki[i] = jsaklanır ya da sondan başa dp değerleri azalarak taranır. - Cevap dp[n−1] değil, max dp[i]'dir: LIS herhangi bir yerde bitebilir.
En Uzun Artan Alt Dizi
LIS: O(n log n) — Sonlar Dizisi ve İkili Arama
static int lisHizli(int[] a) { int[] son = new int[a.length]; int uz = 0; // aktif liste sayısı for (int x : a) { int alt = 0, ust = uz; // son[alt] >= x ilk yer while (alt < ust) { int orta = (alt + ust) / 2; if (son[orta] < x) alt = orta + 1; else ust = orta; } son[alt] = x; // yerine koy / uzat if (alt == uz) uz++; } return uz; }
- Aktif listelerin yalnızca sonları saklanır; son[] her zaman artan sıradadır, yer ikili aramayla bulunur → O(n log n).
- Örnek dizide son = [0, 1, 3, 7, 11, 15]: uzunluk doğru (6) ama bu dizi bir LIS değildir (7, 11'den sonra gelir).
- Diziyi kurmak için her elemanın önceki indisi ayrıca tutulur.
En Uzun Ortak Alt Dizgi
En Uzun Ortak Alt Dizgi (Longest Common Substring)
- İki dizginin bitişik karakterlerden oluşan en uzun ortak parçası.
- Örnek:
"abdca"ve"cbda"→ ortak alt dizgiler "a", "b", "d", "c", "bd" → cevap "bd", uzunluk 2. - Durum: L[i][j] = Y'nin ilk i ve X'in ilk j karakterinin tam sonunda biten en uzun ortak parçanın uzunluğu.
aksi hâlde L[i][j] = 0 (bitişiklik bozulur, sayaç sıfırlanır)
Cevap = tablodaki en büyük değer (son hücre değil!)
Kaba kuvvet: X'in O(m²) alt dizgisini Y içinde aramak → O(m² · n). DP: O(m · n) zaman.
En Uzun Ortak Alt Dizgi
Ortak Alt Dizgi: Tabloyu Adım Adım Doldur
Sütunlar X = "abdca", satırlar Y = "cbda". Turuncu: hesaplanan hücre · mavi: kullanılan çapraz hücre.
Satır 0 ve sütun 0 sıfırdır: boş dizgiyle ortak parça yoktur.
'c' ≠ 'a' → L[1][1] = 0 · en iyi: 0
'c' ≠ 'b' → L[1][2] = 0 · en iyi: 0
'c' ≠ 'd' → L[1][3] = 0 · en iyi: 0
Y[0]='c' = X[3]='c' → L[1][4] = L[0][3] + 1 = 1 · en iyi: 1
'c' ≠ 'a' → L[1][5] = 0 · en iyi: 1
'b' ≠ 'a' → L[2][1] = 0 · en iyi: 1
Y[1]='b' = X[1]='b' → L[2][2] = L[1][1] + 1 = 1 · en iyi: 1
'b' ≠ 'd' → L[2][3] = 0 · en iyi: 1
'b' ≠ 'c' → L[2][4] = 0 · en iyi: 1
'b' ≠ 'a' → L[2][5] = 0 · en iyi: 1
'd' ≠ 'a' → L[3][1] = 0 · en iyi: 1
'd' ≠ 'b' → L[3][2] = 0 · en iyi: 1
Y[2]='d' = X[2]='d' → L[3][3] = L[2][2] + 1 = 2 · en iyi: 2
'd' ≠ 'c' → L[3][4] = 0 · en iyi: 2
'd' ≠ 'a' → L[3][5] = 0 · en iyi: 2
Y[3]='a' = X[0]='a' → L[4][1] = L[3][0] + 1 = 1 · en iyi: 2
'a' ≠ 'b' → L[4][2] = 0 · en iyi: 2
'a' ≠ 'd' → L[4][3] = 0 · en iyi: 2
'a' ≠ 'c' → L[4][4] = 0 · en iyi: 2
Y[3]='a' = X[4]='a' → L[4][5] = L[3][4] + 1 = 1 · en iyi: 2
En büyük değer L[3][3] = 2; çapraz geriye 2 adım: X[1..2] = "bd". Son hücre L[4][5] = 1 cevap değildir.
En Uzun Ortak Alt Dizi
En Uzun Ortak Alt Dizi (Longest Common Subsequence)
- İki dizinin, sırası korunarak (atlamalı) seçilebilen en uzun ortak alt dizisi.
- Örnek:
"abdca"ve"cbda"→ "bda", uzunluk 3 (ortak alt dizgi yalnızca 2 idi). - Kullanım:
diff/ git karşılaştırması, DNA dizilerinin benzerliği, intihal tespiti.
aksi hâlde L[i][j] = max( L[i−1][j] , L[i][j−1] ) (birini atla)
Cevap = L[n][m] (son hücre)
| Eşleşme | Eşleşmeme | Cevap | |
|---|---|---|---|
| Ortak alt dizgi | çapraz + 1 | 0 | tablonun maksimumu |
| Ortak alt dizi (LCS) | çapraz + 1 | max(üst, sol) | son hücre |
En Uzun Ortak Alt Dizi
LCS: Kodu Adım Adım İzle
L dizisi (n+1) × (m+1) = 5 × 6; Java'da başlangıçta tüm hücreler 0.
'c' ≠ 'a' → max(üst 0, sol 0) = 0
'c' ≠ 'b' → max(üst 0, sol 0) = 0
'c' ≠ 'd' → max(üst 0, sol 0) = 0
'c' = 'c' → çapraz + 1: L[1][4] = 0 + 1 = 1
'c' ≠ 'a' → max(üst 0, sol 1) = 1
'b' ≠ 'a' → max(üst 0, sol 0) = 0
'b' = 'b' → çapraz + 1: L[2][2] = 0 + 1 = 1
'b' ≠ 'd' → max(üst 0, sol 1) = 1
'b' ≠ 'c' → max(üst 1, sol 1) = 1
'b' ≠ 'a' → max(üst 1, sol 1) = 1
'd' ≠ 'a' → max(üst 0, sol 0) = 0
'd' ≠ 'b' → max(üst 1, sol 0) = 1
'd' = 'd' → çapraz + 1: L[3][3] = 1 + 1 = 2
'd' ≠ 'c' → max(üst 1, sol 2) = 2
'd' ≠ 'a' → max(üst 1, sol 2) = 2
'a' = 'a' → çapraz + 1: L[4][1] = 0 + 1 = 1
'a' ≠ 'b' → max(üst 1, sol 1) = 1
'a' ≠ 'd' → max(üst 2, sol 1) = 2
'a' ≠ 'c' → max(üst 2, sol 2) = 2
'a' = 'a' → çapraz + 1: L[4][5] = 2 + 1 = 3
Tablo doldu: L[4][5] = 3. 20 hücre, her biri O(1) → O(m · n).
static int lcs(String x, String y) { int m = x.length(), n = y.length(); int[][] L = new int[n + 1][m + 1]; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (y.charAt(i - 1) == x.charAt(j - 1)) L[i][j] = L[i - 1][j - 1] + 1; else L[i][j] = Math.max(L[i - 1][j], L[i][j - 1]); return L[n][m]; }
En Uzun Ortak Alt Dizi
LCS: Diziyi Geri İzleyerek Bulma
Geri izleme son hücreden, L[4][5] = 3'ten başlar; bulunan karakterler sondan başa toplanır.
'a' = 'a' → karakter LCS'ye eklenir ("a"), çapraz geçilir.
'd' ≠ 'c'; sol (2) > üst (1) → sola gidilir.
'd' = 'd' → karakter LCS'ye eklenir ("da"), çapraz geçilir.
'b' = 'b' → karakter LCS'ye eklenir ("bda"), çapraz geçilir.
'c' ≠ 'a'; üst (0) ≥ sol (0) → yukarı çıkılır.
Sınıra ulaşıldı. Yeşil hücreler eşleşmeler: LCS = "bda", uzunluk 3. Geri izleme O(m + n).
En Uzun Ortak Alt Dizi
Deneyin: LCS ve Ortak Alt Dizgi Tablosu
X en fazla 9, Y en fazla 7 karakter. Yeşil hücreler geri izleme yolunu gösterir. Düzenleme uzaklığı: ekleme, silme ve değiştirme 1 birim.
Diğer Problemler
Diğer Klasik DP Problemleri
| Problem | Durum ve bağıntı | Süre |
|---|---|---|
| Düzenleme uzaklığı (edit distance) | D[i][j] = min(D[i−1][j] + 1, D[i][j−1] + 1, D[i−1][j−1] + [aᵢ ≠ bⱼ]); D[i][0] = i, D[0][j] = j | O(m · n) |
| Para üstü (coin change) | en az para: P[x] = 1 + min P[x − c] (c ≤ x); P[0] = 0 | O(k · X) |
| En büyük toplamlı alt dizi | V(i) = max(a[i], V(i−1) + a[i]) — Kadane; cevap max V(i) | O(n) |
| Matris zinciri çarpımı | M[i][j] = min { M[i][k] + M[k+1][j] + pᵢ₋₁ · pₖ · pⱼ : i ≤ k < j } | O(n³) |
| Gezgin satıcı (Held-Karp) | dp[S][i] = S kümesini gezip i'de biten en kısa yol; dp[S][i] = min dp[S − i][j] + d(j, i) | O(n² · 2ⁿ) |
| Bellman-Ford | dₖ(v) = en fazla k kenarlı en kısa yol; dₖ(v) = min(dₖ₋₁(v), dₖ₋₁(u) + w(u, v)) | O(V · E) |
| Optimal ikili arama ağacı | e[i][j] = min { e[i][r−1] + e[r+1][j] : i ≤ r ≤ j } + w(i, j) | O(n³) |
Özet
Özet
| Problem | Durum / bağıntı | Zaman · bellek |
|---|---|---|
| Fibonacci | F(n) = F(n−1) + F(n−2); memoization ya da tablo | O(n) · O(1) |
| 0/1 sırt çantası | K[i][c] = max(K[i−1][c], vᵢ + K[i−1][c−wᵢ]) | O(nW) · O(W) |
| Sınırsız sırt çantası | dp[i][c] = max(dp[i−1][c], pᵢ + dp[i][c−wᵢ]) | O(nW) · O(W) |
| Alt küme toplamı | dp[i][j] = dp[i−1][j] ∨ dp[i−1][j−aᵢ] | O(n · hedef) |
| LIS | dp[i] = 1 + max dp[j] (j < i, a[j] < a[i]); ya da aktif listeler | O(n²) / O(n log n) |
| Ortak alt dizgi | eşitse çapraz + 1, değilse 0; cevap tablonun maksimumu | O(mn) |
| LCS | eşitse çapraz + 1, değilse max(üst, sol); cevap son hücre | O(mn) |
Özet
Sık Yapılan Hatalar
v[i − 1]'i kullanır. v[i] yazmak son eşyada taşma verir.Özet
Örnek Problemler
- 0/1 sırt çantası: W = 5; eşyalar (v, w) = (3, 2), (4, 3), (5, 4), (6, 5). En büyük değer?
- LCS("ABCBDAB", "BDCABA") uzunluğu kaçtır? Bir LCS yazın.
- Paralar {1, 3, 4}, tutar 6: en az kaç para? Açgözlü kaç para kullanır?
- [10, 22, 9, 33, 21, 50, 41, 60] dizisinin LIS uzunluğu ve bir LIS?
- "kitten" → "sitting" düzenleme uzaklığı?
- Düz özyinelemeli fib(6) kaç çağrı yapar? Memoization ile kaç?
Cevapları göster
- 7: eşya 1 + eşya 2 (w = 2 + 3 = 5, v = 3 + 4).
- 4; ör. "BCBA" (ya da "BDAB", "BCAB").
- DP: 2 para (3 + 3). Açgözlü: 4 + 1 + 1 = 3 para.
- 5: 10, 22, 33, 50, 60 (ya da 10, 22, 33, 41, 60).
- 3: k→s değiştir, e→i değiştir, sona g ekle.
- 2 · F(7) − 1 = 2 · 13 − 1 = 25 çağrı; memoization ile 2 · 6 − 1 = 11.
Özet
Alıştırmalar
- 0/1 sırt çantasını memoization (yukarıdan aşağı) ile yazın.
- Paralar {1, 2, 5} ile 5 tutarı kaç farklı şekilde ödenebilir?
- "ababc" ve "babca" için en uzun ortak alt dizgiyi bulun.
- {3, 34, 4, 12, 5, 2} kümesinde toplamı 9 ve 30 olan alt küme var mı?
- [−2, 1, −3, 4, −1, 2, 1, −5, 4] için en büyük toplamlı alt dizi (Kadane)?
- Ağırlık {2, 3, …, 8}, değer {3, 5, 8, 6, 10, 11, 15}, W = 10: 0/1 ve sınırsız sonuçlar?
Cevaplar / ipuçları
Integer[][] bellek;f(i, c): i = n ise 0; aksi hâlde max(f(i+1, c), v[i] + f(i+1, c − w[i])), sonucu kaydet.- 4: 5, 2+2+1, 2+1+1+1, 1·5 (dış döngü paralar üzerinde:
d[x] += d[x − c]). - "babc", uzunluk 4.
- 9: evet (4 + 5). 30: hayır (34 hariç toplam 26 < 30).
- 6: [4, −1, 2, 1].
- 0/1: 18 (ör. w 4 + 6 → 8 + 10). Sınırsız: 19 (ör. w 4 + 4 + 2 → 8 + 8 + 3).
Özet
Kaynaklar
- Cormen, Leiserson, Rivest ve Stein, Introduction to Algorithms, 4. baskı, MIT Press, 2022 (Bölüm 14: Dynamic Programming).
- Kleinberg ve Tardos, Algorithm Design, Pearson, 2005 (Bölüm 6).
- Dasgupta, Papadimitriou ve Vazirani, Algorithms, McGraw-Hill, 2006 (Bölüm 6).
- R. Bellman, Dynamic Programming, Princeton University Press, 1957.