Önceki derste dinamik programlamanın mekaniğini öğrendin: durum tanımlama, geçiş yazma, memoization ve tabulation. Bu derste o mekaniği, klasik problemler üzerinde uygulayacaksın.
Bu problemleri tek tek ezberlemek amaç değil. Amaç, aralarındaki ortak kalıpları görmek. İki metin arasındaki benzerliği ölçen problem, iki dizi arasında karar veren tüm problemlerle aynı yapıya sahiptir. Sırt çantası problemi, "al veya alma" seçimi içeren her problemin iskeletidir. Izgara yolları, iki boyutlu DP'nin en sade hâlidir. Bu kalıpları tanıdığında, daha önce hiç görmediğin bir problemi de tanıdığın bir biçime indirgeyebilirsin.
Her problemde yalnızca optimal değeri değil, optimal çözümün kendisini de geri oluşturmayı göreceksin. Gerçek uygulamalarda "düzenleme mesafesi 3" bilgisi tek başına yetmez; hangi üç değişikliğin yapılacağını bilmek gerekir.
En uzun ortak alt dizi (LCS)
İki metnin ortak olan, ardışık olmak zorunda olmayan en uzun alt dizisi. Sürüm kontrol sistemlerinin fark (diff) hesaplamasının temelidir.
"AGGTAB" ve "GXTXAYB" → LCS = "GTAB" (uzunluk 4)
DURUM: dp[i][j] = ilk metnin i karakteri ile ikinci metnin j karakteri
arasındaki LCS uzunluğu
GEÇİŞ: karakterler eşitse: dp[i][j] = dp[i-1][j-1] + 1
eşit değilse: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
TEMEL DURUM: dp[0][j] = dp[i][0] = 0
"" G X T X A Y B
"" 0 0 0 0 0 0 0 0
A 0 0 0 0 0 1 1 1
G 0 1 1 1 1 1 1 1
G 0 1 1 1 1 1 1 1
T 0 1 1 2 2 2 2 2
A 0 1 1 2 2 3 3 3
B 0 1 1 2 2 3 3 4 ← cevapAşağıdaki görselleştirmede LCS tablosunun nasıl dolduğunu ve hangi hücrelerin cevaba katkı yaptığını izleyebilirsin:
package main
import (
"fmt"
"strings"
)
// lcs: uzunluk + tablo + alt dizinin kendisi
func lcs(a, b string) (int, string, [][]int) {
ra, rb := []rune(a), []rune(b)
m, n := len(ra), len(rb)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if ra[i-1] == rb[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}
}
}
// Geri oluşturma: tablodan sağ alttan sol üste yürü
var sb []rune
i, j := m, n
for i > 0 && j > 0 {
switch {
case ra[i-1] == rb[j-1]:
sb = append(sb, ra[i-1])
i--
j--
case dp[i-1][j] >= dp[i][j-1]:
i--
default:
j--
}
}
// Ters çevir
for l, r := 0, len(sb)-1; l < r; l, r = l+1, r-1 {
sb[l], sb[r] = sb[r], sb[l]
}
return dp[m][n], string(sb), dp
}
// lcsOptimized: yalnızca uzunluk — O(min(m,n)) bellek
func lcsOptimized(a, b string) int {
ra, rb := []rune(a), []rune(b)
if len(ra) < len(rb) {
ra, rb = rb, ra // kısa olanı sütun yap
}
prev := make([]int, len(rb)+1)
cur := make([]int, len(rb)+1)
for i := 1; i <= len(ra); i++ {
for j := 1; j <= len(rb); j++ {
if ra[i-1] == rb[j-1] {
cur[j] = prev[j-1] + 1
} else {
cur[j] = max(prev[j], cur[j-1])
}
}
prev, cur = cur, prev
}
return prev[len(rb)]
}
func printTable(a, b string, dp [][]int) {
ra, rb := []rune(a), []rune(b)
fmt.Print(" ''")
for _, r := range rb {
fmt.Printf("%4c", r)
}
fmt.Println()
for i := range dp {
if i == 0 {
fmt.Print(" '' ")
} else {
fmt.Printf(" %2c ", ra[i-1])
}
for _, v := range dp[i] {
fmt.Printf("%4d", v)
}
fmt.Println()
}
}
func main() {
a, b := "AGGTAB", "GXTXAYB"
length, seq, dp := lcs(a, b)
fmt.Printf("metinler: %q ve %q\n", a, b)
printTable(a, b, dp)
fmt.Println()
fmt.Println("LCS uzunluğu:", length)
fmt.Printf("LCS: %q\n", seq)
fmt.Println("optimize sürüm aynı mı:", lcsOptimized(a, b) == length)
fmt.Println()
cases := [][2]string{
{"merhaba", "marhaba"},
{"abc", "abc"},
{"abc", "xyz"},
{"", "abc"},
{"kitap", "kitaplık"},
{"çilek", "çiçek"},
}
fmt.Printf("%-14s %-14s %8s %-12s\n", "metin 1", "metin 2", "uzunluk", "LCS")
for _, c := range cases {
l, s, _ := lcs(c[0], c[1])
fmt.Printf("%-14q %-14q %8d %-12q\n", c[0], c[1], l, s)
}
// Benzerlik oranı: diff araçlarının kullandığı ölçüt
fmt.Println()
x, y := "dinamik programlama", "dinamik program"
l, _, _ := lcs(x, y)
maxLen := max(len([]rune(x)), len([]rune(y)))
fmt.Printf("benzerlik: %q ↔ %q → %%%.1f\n", x, y, 100*float64(l)/float64(maxLen))
fmt.Println("ortak kısım:", strings.Count(x, "")-1, "karakterden", l, "tanesi ortak")
}metinler: "AGGTAB" ve "GXTXAYB"
'' G X T X A Y B
'' 0 0 0 0 0 0 0 0
A 0 0 0 0 0 1 1 1
G 0 1 1 1 1 1 1 1
G 0 1 1 1 1 1 1 1
T 0 1 1 2 2 2 2 2
A 0 1 1 2 2 3 3 3
B 0 1 1 2 2 3 3 4
LCS uzunluğu: 4
LCS: "GTAB"
optimize sürüm aynı mı: true
metin 1 metin 2 uzunluk LCS
"merhaba" "marhaba" 6 "mrhaba"
"abc" "abc" 3 "abc"
"abc" "xyz" 0 ""
"" "abc" 0 ""
"kitap" "kitaplık" 5 "kitap"
"çilek" "çiçek" 4 "çiek"
benzerlik: "dinamik programlama" ↔ "dinamik program" → %78.9
ortak kısım: 19 karakterden 15 tanesi ortakGeri oluşturma kısmı dikkatli incelenmeye değer: Tabloda sağ alttan başlayıp sol üste yürürsün. Karakterler eşitse çapraz git ve karakteri kaydet; değilse hangi komşunun değeri büyükse o yöne git. Bu, "hangi kararların optimal değeri ürettiğini" geriye doğru okumaktır ve neredeyse tüm DP problemlerinde aynı biçimde çalışır.
Düzenleme mesafesi (Levenshtein)
Bir metni diğerine çevirmek için gereken en az işlem sayısı. İzin verilen işlemler: ekleme, silme ve değiştirme. Yazım denetimi, DNA dizilimi karşılaştırma ve arama motorlarının "şunu mu demek istediniz" özelliğinin temelidir.
"kitten" → "sitting" = 3 işlem
kitten → sitten (k→s değiştir)
sitten → sittin (e→i değiştir)
sittin → sitting (g ekle)
GEÇİŞ: karakterler eşitse: dp[i][j] = dp[i-1][j-1] (işlem yok)
eşit değilse: dp[i][j] = 1 + min(
dp[i-1][j], ← sil
dp[i][j-1], ← ekle
dp[i-1][j-1]) ← değiştirpackage main
import "fmt"
type operation struct {
kind string
from, to rune
pos int
}
// editDistance: mesafe + tablo + işlem listesi
func editDistance(a, b string) (int, []operation, [][]int) {
ra, rb := []rune(a), []rune(b)
m, n := len(ra), len(rb)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
dp[i][0] = i // ilk sütun: hepsini sil
}
for j := range dp[0] {
dp[0][j] = j // ilk satır: hepsini ekle
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if ra[i-1] == rb[j-1] {
dp[i][j] = dp[i-1][j-1]
continue
}
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
}
}
// İşlemleri geri oluştur
var ops []operation
i, j := m, n
for i > 0 || j > 0 {
switch {
case i > 0 && j > 0 && ra[i-1] == rb[j-1]:
i--
j--
case i > 0 && j > 0 && dp[i][j] == dp[i-1][j-1]+1:
ops = append(ops, operation{"değiştir", ra[i-1], rb[j-1], i - 1})
i--
j--
case j > 0 && (i == 0 || dp[i][j] == dp[i][j-1]+1):
ops = append(ops, operation{"ekle", 0, rb[j-1], i})
j--
default:
ops = append(ops, operation{"sil", ra[i-1], 0, i - 1})
i--
}
}
// Ters çevir
for l, r := 0, len(ops)-1; l < r; l, r = l+1, r-1 {
ops[l], ops[r] = ops[r], ops[l]
}
return dp[m][n], ops, dp
}
// editDistanceOptimized: O(n) bellek
func editDistanceOptimized(a, b string) int {
ra, rb := []rune(a), []rune(b)
prev := make([]int, len(rb)+1)
for j := range prev {
prev[j] = j
}
for i := 1; i <= len(ra); i++ {
cur := make([]int, len(rb)+1)
cur[0] = i
for j := 1; j <= len(rb); j++ {
if ra[i-1] == rb[j-1] {
cur[j] = prev[j-1]
} else {
cur[j] = 1 + min(prev[j], cur[j-1], prev[j-1])
}
}
prev = cur
}
return prev[len(rb)]
}
func main() {
a, b := "kitten", "sitting"
dist, ops, dp := editDistance(a, b)
fmt.Printf("%q → %q: %d işlem\n\n", a, b, dist)
for _, op := range ops {
switch op.kind {
case "değiştir":
fmt.Printf(" konum %d: %q → %q (değiştir)\n", op.pos, op.from, op.to)
case "ekle":
fmt.Printf(" konum %d: %q ekle\n", op.pos, op.to)
case "sil":
fmt.Printf(" konum %d: %q sil\n", op.pos, op.from)
}
}
fmt.Println()
fmt.Print(" ")
fmt.Print(" ''")
for _, r := range []rune(b) {
fmt.Printf("%4c", r)
}
fmt.Println()
for i := range dp {
if i == 0 {
fmt.Print(" '' ")
} else {
fmt.Printf(" %2c ", []rune(a)[i-1])
}
for _, v := range dp[i] {
fmt.Printf("%4d", v)
}
fmt.Println()
}
fmt.Println()
cases := [][2]string{
{"merhaba", "merhba"},
{"aynı", "aynı"},
{"", "abc"},
{"abc", ""},
{"kitap", "kitaplık"},
{"çiçek", "çilek"},
{"istanbul", "İstanbul"},
}
fmt.Printf("%-14s %-14s %10s %8s\n", "metin 1", "metin 2", "mesafe", "optimize")
for _, c := range cases {
d, _, _ := editDistance(c[0], c[1])
fmt.Printf("%-14q %-14q %10d %8d\n", c[0], c[1], d, editDistanceOptimized(c[0], c[1]))
}
fmt.Println()
fmt.Println("Yazım denetimi: bir kelimeye en yakın sözlük kelimelerini bulur.")
word := "progrmlama"
dictionary := []string{"programlama", "program", "problem", "prova", "pragma"}
fmt.Printf("girdi: %q\n", word)
for _, d := range dictionary {
dist, _, _ := editDistance(word, d)
fmt.Printf(" %-14q mesafe: %d\n", d, dist)
}
}"kitten" → "sitting": 3 işlem
konum 0: 'k' → 's' (değiştir)
konum 4: 'e' → 'i' (değiştir)
konum 6: 'g' ekle
'' s i t t i n g
'' 0 1 2 3 4 5 6 7
k 1 1 2 3 4 5 6 7
i 2 2 1 2 3 4 5 6
t 3 3 2 1 2 3 4 5
t 4 4 3 2 1 2 3 4
e 5 5 4 3 2 2 3 4
n 6 6 5 4 3 3 2 3
metin 1 metin 2 mesafe optimize
"merhaba" "merhba" 1 1
"aynı" "aynı" 0 0
"" "abc" 3 3
"abc" "" 3 3
"kitap" "kitaplık" 3 3
"çiçek" "çilek" 1 1
"istanbul" "İstanbul" 1 1
Yazım denetimi: bir kelimeye en yakın sözlük kelimelerini bulur.
girdi: "progrmlama"
"programlama" mesafe: 1
"program" mesafe: 3
"problem" mesafe: 5
"prova" mesafe: 6
"pragma" mesafe: 5Aşağıdaki görselleştirmede düzenleme mesafesi tablosunu inceleyebilirsin:
Temel durumların anlamı düzenleme mesafesinde çok net görünür: dp[i][0] = i, "ilk metnin i karakterini boş metne çevirmek için i silme gerekir" demektir. Temel durumları doğru kurmak, DP problemlerinin en kritik adımıdır ve genelde problemin en küçük hâlini düşünmekle bulunur.
0/1 sırt çantası
Açgözlü dersinde bölünemez sırt çantasının açgözlü yaklaşımla çözülemediğini görmüştün. İşte doğru çözümü.
DURUM: dp[i][w] = ilk i eşya ile kapasite w kullanılarak elde edilebilen
en yüksek değer
GEÇİŞ: dp[i][w] = max(dp[i-1][w], ← eşyayı ALMA
dp[i-1][w - ağırlık[i]] + değer[i]) ← eşyayı AL
(ikinci seçenek yalnızca ağırlık ≤ w ise geçerli)
Adı 0/1: her eşya ya alınır (1) ya alınmaz (0), bölünemez.package main
import (
"fmt"
"slices"
)
type Item struct {
Name string
Weight int
Value int
}
// knapsack: 2D tablo + seçilen eşyalar
func knapsack(items []Item, capacity int) (int, []Item, [][]int) {
n := len(items)
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, capacity+1)
}
for i := 1; i <= n; i++ {
for w := range capacity + 1 {
dp[i][w] = dp[i-1][w] // almama
if items[i-1].Weight <= w {
withItem := dp[i-1][w-items[i-1].Weight] + items[i-1].Value
dp[i][w] = max(dp[i][w], withItem)
}
}
}
// Geri oluşturma
var chosen []Item
w := capacity
for i := n; i > 0; i-- {
if dp[i][w] != dp[i-1][w] { // bu eşya alınmış
chosen = append(chosen, items[i-1])
w -= items[i-1].Weight
}
}
slices.Reverse(chosen)
return dp[n][capacity], chosen, dp
}
// knapsackOptimized: tek satır — O(capacity) bellek
func knapsackOptimized(items []Item, capacity int) int {
dp := make([]int, capacity+1)
for _, item := range items {
// TERSTEN: her eşya en fazla bir kez kullanılsın
for w := capacity; w >= item.Weight; w-- {
dp[w] = max(dp[w], dp[w-item.Weight]+item.Value)
}
}
return dp[capacity]
}
// unboundedKnapsack: her eşya SINIRSIZ kullanılabilir — ileri gezilir
func unboundedKnapsack(items []Item, capacity int) int {
dp := make([]int, capacity+1)
for _, item := range items {
for w := item.Weight; w <= capacity; w++ { // İLERİ
dp[w] = max(dp[w], dp[w-item.Weight]+item.Value)
}
}
return dp[capacity]
}
func main() {
items := []Item{
{"altın külçe", 10, 60},
{"gümüş takı", 20, 100},
{"bronz heykel", 30, 120},
}
const capacity = 50
value, chosen, dp := knapsack(items, capacity)
fmt.Println("eşyalar:")
for _, it := range items {
fmt.Printf(" %-16s ağırlık=%-4d değer=%-4d oran=%.1f\n",
it.Name, it.Weight, it.Value, float64(it.Value)/float64(it.Weight))
}
fmt.Println("kapasite:", capacity)
fmt.Println()
fmt.Println("en yüksek değer:", value)
fmt.Println("seçilen eşyalar:")
totalWeight := 0
for _, it := range chosen {
fmt.Printf(" %s (%d kg, %d değer)\n", it.Name, it.Weight, it.Value)
totalWeight += it.Weight
}
fmt.Println("toplam ağırlık:", totalWeight, "/", capacity)
// DP tablosunun bir kısmını göster
fmt.Println()
fmt.Println("DP tablosu (kapasite 0, 10, 20, 30, 40, 50):")
fmt.Printf("%-18s", "eşya")
for _, w := range []int{0, 10, 20, 30, 40, 50} {
fmt.Printf("%6d", w)
}
fmt.Println()
for i := range dp {
label := "(hiçbiri)"
if i > 0 {
label = items[i-1].Name
}
fmt.Printf("%-18s", label)
for _, w := range []int{0, 10, 20, 30, 40, 50} {
fmt.Printf("%6d", dp[i][w])
}
fmt.Println()
}
fmt.Println()
fmt.Println("optimize sürüm:", knapsackOptimized(items, capacity))
fmt.Println("sınırsız sürüm:", unboundedKnapsack(items, capacity),
"← aynı eşya tekrar alınabilir")
fmt.Println()
fmt.Println("Tek satırlık farka dikkat: döngü yönü")
fmt.Println(" ters (w--): her eşya bir kez → 0/1 sırt çantası")
fmt.Println(" ileri (w++): eşya tekrar tekrar → sınırsız sırt çantası")
}eşyalar: altın külçe ağırlık=10 değer=60 oran=6.0 gümüş takı ağırlık=20 değer=100 oran=5.0 bronz heykel ağırlık=30 değer=120 oran=4.0 kapasite: 50 en yüksek değer: 220 seçilen eşyalar: gümüş takı (20 kg, 100 değer) bronz heykel (30 kg, 120 değer) toplam ağırlık: 50 / 50 DP tablosu (kapasite 0, 10, 20, 30, 40, 50): eşya 0 10 20 30 40 50 (hiçbiri) 0 0 0 0 0 0 altın külçe 0 60 60 60 60 60 gümüş takı 0 60 100 160 160 160 bronz heykel 0 60 100 160 180 220 optimize sürüm: 220 sınırsız sürüm: 300 ← aynı eşya tekrar alınabilir Tek satırlık farka dikkat: döngü yönü ters (w--): her eşya bir kez → 0/1 sırt çantası ileri (w++): eşya tekrar tekrar → sınırsız sırt çantası
Aşağıdaki görselleştirmede sırt çantası tablosunun nasıl dolduğunu izleyebilirsin:
Sınırsız sürümle 0/1 sürüm arasındaki tek farkın döngü yönü olması, DP'nin en zarif ayrıntılarından biridir. Ters yönde gezdiğinde dp[w-weight] hâlâ önceki yinelemenin (yani bu eşyayı hiç kullanmamış hâlin) değerini taşır; ileri gezdiğinde ise aynı yinelemede güncellenmiş değeri okur ve eşya tekrar kullanılmış olur.
En uzun artan alt dizi
Bir dizideki, ardışık olmak zorunda olmayan en uzun artan alt dizi. İki farklı çözümü vardır ve ikisi de öğreticidir.
package main
import (
"fmt"
"slices"
)
// lisDP: O(n²) — alt diziyi de verir
func lisDP(data []int) (int, []int) {
n := len(data)
if n == 0 {
return 0, nil
}
dp := make([]int, n) // dp[i] = i ile BİTEN en uzun artan alt dizi
prev := make([]int, n)
for i := range dp {
dp[i], prev[i] = 1, -1
}
bestLen, bestEnd := 1, 0
for i := 1; i < n; i++ {
for j := range i {
if data[j] < data[i] && dp[j]+1 > dp[i] {
dp[i], prev[i] = dp[j]+1, j
}
}
if dp[i] > bestLen {
bestLen, bestEnd = dp[i], i
}
}
var seq []int
for i := bestEnd; i != -1; i = prev[i] {
seq = append(seq, data[i])
}
slices.Reverse(seq)
return bestLen, seq
}
// lisBinary: O(n log n) — ikili arama ile
func lisBinary(data []int) (int, []int) {
// tails[k] = uzunluğu k+1 olan artan alt dizilerin en küçük olası son elemanı
var tails []int
prev := make([]int, len(data))
tailIdx := []int{} // tails'teki her konumun data içindeki indeksi
for i, v := range data {
pos, found := slices.BinarySearch(tails, v)
if found {
continue
}
prev[i] = -1
if pos > 0 {
prev[i] = tailIdx[pos-1]
}
if pos == len(tails) {
tails = append(tails, v)
tailIdx = append(tailIdx, i)
continue
}
tails[pos] = v
tailIdx[pos] = i
}
if len(tails) == 0 {
return 0, nil
}
var seq []int
for i := tailIdx[len(tailIdx)-1]; i != -1; i = prev[i] {
seq = append(seq, data[i])
}
slices.Reverse(seq)
return len(tails), seq
}
func main() {
cases := [][]int{
{10, 9, 2, 5, 3, 7, 101, 18},
{1, 2, 3, 4, 5},
{5, 4, 3, 2, 1},
{2, 2, 2},
{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5},
{},
}
fmt.Printf("%-38s %8s %8s %8s\n", "dizi", "O(n²)", "O(nlogn)", "aynı")
for _, c := range cases {
a, _ := lisDP(c)
b, _ := lisBinary(c)
fmt.Printf("%-38v %8d %8d %8t\n", c, a, b, a == b)
}
fmt.Println()
data := []int{10, 9, 2, 5, 3, 7, 101, 18}
l1, s1 := lisDP(data)
l2, s2 := lisBinary(data)
fmt.Println("dizi:", data)
fmt.Printf("O(n²) çözüm: uzunluk=%d alt dizi=%v\n", l1, s1)
fmt.Printf("O(n log n): uzunluk=%d alt dizi=%v\n", l2, s2)
fmt.Println("ikisi de geçerli artan alt dizi (farklı olabilirler)")
fmt.Println()
fmt.Println("Uygulama: en az kaç eleman silinerek dizi artan yapılır?")
for _, c := range [][]int{{3, 1, 4, 1, 5}, {5, 4, 3}, {1, 2, 3}} {
l, _ := lisBinary(c)
fmt.Printf(" %v → %d eleman silinmeli\n", c, len(c)-l)
}
}dizi O(n²) O(nlogn) aynı [10 9 2 5 3 7 101 18 ] 4 4 true [1 2 3 4 5 ] 5 5 true [5 4 3 2 1 ] 1 1 true [2 2 2 ] 1 1 true [3 1 4 1 5 9 2 6 5 3 5 ] 4 4 true [] 0 0 true dizi: [10 9 2 5 3 7 101 18] O(n²) çözüm: uzunluk=4 alt dizi=[2 5 7 101] O(n log n): uzunluk=4 alt dizi=[2 3 7 18] ikisi de geçerli artan alt dizi (farklı olabilirler) Uygulama: en az kaç eleman silinerek dizi artan yapılır? [3 1 4 1 5] → 2 eleman silinmeli [5 4 3] → 2 eleman silinmeli [1 2 3] → 0 eleman silinmeli
tails dizisinin anlamı ilk bakışta kafa karıştırıcıdır: Bu dizi bir artan alt dizi değildir; her konumda "o uzunlukta bir alt dizinin sahip olabileceği en küçük son eleman"ı tutar. Son elemanı küçük tutmak, ileride daha fazla elemanı eklemeye izin verir — açgözlü bir sezgi. Uzunluk doğru çıkar ama alt dizinin kendisini almak için ek kayıt tutmak gerekir; kodda prev ve tailIdx bunu yapar.
Izgara yolları
İki boyutlu DP'nin en sade örneği ve pek çok problemin temeli.
package main
import "fmt"
// uniquePaths: sol üstten sağ alta kaç yol var (sağa ve aşağı hareket)
func uniquePaths(rows, cols int) ([][]int, int) {
dp := make([][]int, rows)
for i := range dp {
dp[i] = make([]int, cols)
dp[i][0] = 1
}
for j := range dp[0] {
dp[0][j] = 1
}
for i := 1; i < rows; i++ {
for j := 1; j < cols; j++ {
dp[i][j] = dp[i-1][j] + dp[i][j-1]
}
}
return dp, dp[rows-1][cols-1]
}
// minPathSum: en az maliyetli yolu bul + yolu geri oluştur
func minPathSum(grid [][]int) (int, [][2]int) {
rows, cols := len(grid), len(grid[0])
dp := make([][]int, rows)
for i := range dp {
dp[i] = make([]int, cols)
}
dp[0][0] = grid[0][0]
for i := 1; i < rows; i++ {
dp[i][0] = dp[i-1][0] + grid[i][0]
}
for j := 1; j < cols; j++ {
dp[0][j] = dp[0][j-1] + grid[0][j]
}
for i := 1; i < rows; i++ {
for j := 1; j < cols; j++ {
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
}
}
// Yolu geriye doğru kur
var path [][2]int
i, j := rows-1, cols-1
for i > 0 || j > 0 {
path = append(path, [2]int{i, j})
switch {
case i == 0:
j--
case j == 0:
i--
case dp[i-1][j] <= dp[i][j-1]:
i--
default:
j--
}
}
path = append(path, [2]int{0, 0})
for l, r := 0, len(path)-1; l < r; l, r = l+1, r-1 {
path[l], path[r] = path[r], path[l]
}
return dp[rows-1][cols-1], path
}
func main() {
dp, paths := uniquePaths(3, 4)
fmt.Println("3×4 ızgarada yol sayısı:", paths)
for _, row := range dp {
fmt.Println(" ", row)
}
fmt.Println()
grid := [][]int{
{1, 3, 1, 8},
{1, 5, 1, 2},
{4, 2, 1, 3},
}
fmt.Println("maliyet ızgarası:")
for _, row := range grid {
fmt.Println(" ", row)
}
cost, path := minPathSum(grid)
fmt.Println("en az maliyet:", cost)
fmt.Print("yol: ")
for i, p := range path {
if i > 0 {
fmt.Print(" → ")
}
fmt.Printf("(%d,%d)", p[0], p[1])
}
fmt.Println()
total := 0
for _, p := range path {
total += grid[p[0]][p[1]]
}
fmt.Println("yol üzerindeki değerler toplamı:", total, "| doğrulama:", total == cost)
fmt.Println()
fmt.Printf("%-12s %14s\n", "ızgara", "yol sayısı")
for _, d := range [][2]int{{1, 1}, {2, 2}, {3, 3}, {5, 5}, {10, 10}} {
_, p := uniquePaths(d[0], d[1])
fmt.Printf("%-12s %14d\n", fmt.Sprintf("%d×%d", d[0], d[1]), p)
}
}3×4 ızgarada yol sayısı: 10 [1 1 1 1] [1 2 3 4] [1 3 6 10] maliyet ızgarası: [1 3 1 8] [1 5 1 2] [4 2 1 3] en az maliyet: 10 yol: (0,0) → (0,1) → (0,2) → (1,2) → (2,2) → (2,3) yol üzerindeki değerler toplamı: 10 | doğrulama: true ızgara yol sayısı 1×1 1 2×2 2 3×3 6 5×5 70 10×10 48620
DP problemlerini tanımak
Bu derste gördüğün problemler, DP'nin ana kalıplarını temsil ediyor. Yeni bir problemle karşılaştığında, onu tanıdığın bir kalıba indirgemeye çalışmak en verimli yoldur.
İki dizi/metin karşılaştırması. Durum genelde dp[i][j] biçiminde iki boyutludur: birinci dizinin i, ikincinin j elemanı. Ortak alt dizi, düzenleme mesafesi, dizi hizalama ve benzerlik ölçütleri bu kalıba girer. Geçişte tipik olarak üç seçenek vardır: iki elemanı eşleştir, birinciyi atla, ikinciyi atla.
Al veya alma. Durum dp[i][kaynak] biçimindedir: i. elemana kadar geldik, elimizde şu kadar kaynak var. Sırt çantası, alt küme toplamı, ev soyguncusu ve bölme problemleri bu kalıptadır. Geçişte her eleman için iki dal açılır.
Doğrusal dizi üzerinde karar. Durum tek boyutludur: dp[i]. Merdiven çıkma, en uzun artan alt dizi, maksimum alt dizi ve bozuk para bu kalıba girer. Geçiş, dp[i]'nin daha küçük indekslere bağlı olduğu bir bağıntıdır.
Izgara üzerinde hareket. Durum konumdur: dp[i][j]. Yol sayma, en az maliyetli yol, engelli ızgaralar. Geçiş, hangi yönlerden gelinebileceğine bağlıdır.
Aralık üzerinde DP. Durum bir aralıktır: dp[i][j] = i'den j'ye kadarki alt problem. Matris çarpım sırası, taş birleştirme ve palindrom bölme problemleri buradadır. Bu kalıpta genelde aralığı ikiye bölen bir nokta üzerinde döngü kurulur.
Pratik bir ipucu: Problemi okurken "her adımda ne karar veriyorum?" ve "o kararı vermek için ne bilmem gerekiyor?" sorularını sor. İkinci sorunun cevabı, durumun ta kendisidir. Üç şey bilmen gerekiyorsa durum üç boyutludur — ve bu, tablonun büyüklüğünü dolayısıyla karmaşıklığı belirler.
Son olarak, karmaşıklığı durumdan okumayı öğren: Durum sayısı × her durum için yapılan iş = toplam karmaşıklık. dp[i][j] tablosunda i ve j her biri n'e kadar gidiyorsa ve her hücre sabit sürede hesaplanıyorsa karmaşıklık O(n²)'dir. Her hücre için bir döngü daha kuruluyorsa O(n³) olur. Bu hesap, çözümün kısıtlar altında yeterince hızlı olup olmadığını yazmadan önce söyler.
Bu problemler nerede kullanılıyor?
Klasik DP problemleri sınav sorusu gibi görünse de, hepsi gerçek sistemlerin içinde çalışıyor. Nerede karşına çıktıklarını bilmek, hem motivasyon hem de doğru aracı seçme becerisi kazandırır.
Ortak alt dizi ve sürüm karşılaştırma. Bir dosyanın iki sürümü arasındaki farkı gösteren araçlar, tam olarak bu algoritmayı kullanır: Ortak satırları bulup değişenleri işaretlemek. Gerçek uygulamalar performans için ek hileler kullanır — dosyaların başındaki ve sonundaki aynı kısımları önceden kırpmak, satırları karma değerleriyle karşılaştırmak — ama çekirdek yine ortak alt dizi problemidir.
Düzenleme mesafesi ve metin düzeltme. Arama motorlarının yazım önerileri, veritabanı kayıtlarının eşleştirilmesi, biyoinformatikte DNA dizilimi hizalama ve otomatik çeviri değerlendirmesi bu ölçüte dayanır. Farklı işlemlere farklı maliyetler atamak (klavyede yakın harfler daha ucuz olabilir) algoritmayı değiştirmez, yalnızca geçiş formülünü zenginleştirir.
Sırt çantası ve kaynak tahsisi. Sınırlı bütçeyle en yüksek getiriyi sağlayacak yatırım kümesini seçmek, sınırlı sunucu kapasitesine en değerli işleri yerleştirmek, sınırlı bant genişliğine paketleri sığdırmak — hepsi aynı problemdir. Gerçek sistemlerde boyutlar genelde birden fazladır (hem bellek hem işlemci kısıtı gibi) ve bu, durumu bir boyut daha genişletir.
En uzun artan alt dizi ve dizi analizi. Bir zaman serisinin en uzun yükselen eğilimi, kart oyunlarında en uzun sıralı dizi, uçuş bağlantılarında en uzun ileri gidiş — aynı kalıp. Ayrıca bir dizinin "ne kadar sıralı" olduğunun ölçütü olarak da kullanılır.
Izgara yolları ve yol planlama. Robot navigasyonu, oyunlarda harita üzerinde hareket maliyeti, görüntü işlemede en düşük enerjili dikey şerit bulma (akıllı yeniden boyutlandırma) bu kalıba girer.
Ortak ders şudur: DP, "seçimlerin birikerek bir sonuç ürettiği" her problemde işe yarar. Problemi doğru soyutlayabildiğinde, çözüm zaten yazılmış olur.
Sık yapılan hatalar
- Durumu yetersiz tanımlamak. İki dizi karşılaştırırken tek boyutlu durum kullanmak çalışmaz.
- Temel durumları eksik doldurmak. Izgara problemlerinde ilk satır ve ilk sütun ayrıca ele alınmalıdır.
- Geri oluşturmayı unutmak. Çoğu gerçek problem optimal değeri değil optimal çözümü ister.
- Döngü yönünü karıştırmak. 0/1 sırt çantasında ters, sınırsızda ileri gezilir.
- Rune ve bayt karışıklığı. Türkçe metinlerde
len(s)bayt sayısını verir;[]rune(s)kullan. - Alan optimizasyonunu erken yapmak. Önce doğru çalışan 2D çözümü yaz, sonra sıkıştır.
- Karmaşıklığı durumdan hesaplamamak. Durum sayısı × hücre başına iş, çözümün yeterli olup olmadığını baştan söyler.
Alıştırmalar
Bir metindeki en uzun palindrom alt diziyi (ardışık olmak zorunda değil) bul. LCS'nin zarif bir uygulamasıdır: Metni kendi tersiyle karşılaştır.
İpucu
Bir metnin en uzun palindrom alt dizisi, o metin ile tersinin LCS'sine eşittir. Neden? Çünkü palindrom, tersten okunduğunda da aynı olan dizidir.
Çözümü göster
package main
import "fmt"
func reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// lcs: uzunluk ve alt dizi
func lcs(a, b string) (int, string) {
ra, rb := []rune(a), []rune(b)
m, n := len(ra), len(rb)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if ra[i-1] == rb[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}
}
}
var seq []rune
i, j := m, n
for i > 0 && j > 0 {
switch {
case ra[i-1] == rb[j-1]:
seq = append(seq, ra[i-1])
i--
j--
case dp[i-1][j] >= dp[i][j-1]:
i--
default:
j--
}
}
for l, r := 0, len(seq)-1; l < r; l, r = l+1, r-1 {
seq[l], seq[r] = seq[r], seq[l]
}
return dp[m][n], string(seq)
}
// longestPalindromicSubsequence: metin ile tersinin LCS'si
func longestPalindromicSubsequence(s string) (int, string) {
return lcs(s, reverse(s))
}
func isPalindrome(s string) bool {
return s == reverse(s)
}
func main() {
cases := []string{
"bbbab",
"cbbd",
"karak",
"abcdef",
"kayak",
"a",
"",
"ıslıkçı",
}
fmt.Printf("%-14s %8s %-14s %10s\n", "metin", "uzunluk", "palindrom", "doğrula")
for _, s := range cases {
length, seq := longestPalindromicSubsequence(s)
fmt.Printf("%-14q %8d %-14q %10t\n", s, length, seq, isPalindrome(seq))
}
fmt.Println()
fmt.Println("Neden çalışıyor?")
fmt.Println("Palindrom = tersten okunduğunda aynı olan dizi.")
fmt.Println("Metnin kendisiyle tersi arasındaki ortak alt dizi,")
fmt.Println("her iki yönde de aynı sırayla görünür → palindromdur.")
fmt.Println()
s := "karak"
fmt.Printf("örnek: %q ve tersi %q\n", s, reverse(s))
l, seq := lcs(s, reverse(s))
fmt.Printf("LCS: %q (uzunluk %d) → palindrom mu: %t\n", seq, l, isPalindrome(seq))
}metin uzunluk palindrom doğrula "bbbab" 4 "bbbb" true "cbbd" 2 "bb" true "karak" 5 "karak" true "abcdef" 1 "a" true "kayak" 5 "kayak" true "a" 1 "a" true "" 0 "" true "ıslıkçı" 3 "ısı" true Neden çalışıyor? Palindrom = tersten okunduğunda aynı olan dizi. Metnin kendisiyle tersi arasındaki ortak alt dizi, her iki yönde de aynı sırayla görünür → palindromdur. örnek: "karak" ve tersi "karak" LCS: "karak" (uzunluk 5) → palindrom mu: true
Bu çözümün güzelliği, yeni bir algoritma yazmadan var olan bir aracı yeniden kullanmasıdır. Problemi tanıdığın bir biçime indirgemek, algoritma tasarımının en güçlü tekniklerinden biridir: "Bu problem aslında şu problem" diyebilmek, sıfırdan çözüm üretmekten hem hızlı hem güvenlidir.
Not: En uzun palindrom alt dizi (subsequence) ile en uzun palindrom alt metin (substring) farklı problemlerdir. İkincisi ardışıklık gerektirir ve farklı bir DP ya da genişletme yaklaşımıyla çözülür.
Bir metnin, verilen sözlükteki kelimelere bölünüp bölünemeyeceğini belirle. Bölünebiliyorsa bölünmüş hâlini de göster. Birden fazla bölme varsa birini döndürmek yeterli.
İpucu
Durum: dp[i] = metnin ilk i karakteri sözlük kelimelerine bölünebilir mi. Geçiş: dp[i] doğrudur eğer bir j < i varsa ki dp[j] doğru ve s[j:i] sözlükte.
Çözümü göster
package main
import (
"fmt"
"strings"
)
// wordBreak: bölünebilir mi + bölünmüş hâli
func wordBreak(s string, dictionary []string) (bool, []string) {
words := make(map[string]bool, len(dictionary))
maxLen := 0
for _, w := range dictionary {
words[w] = true
maxLen = max(maxLen, len([]rune(w)))
}
r := []rune(s)
n := len(r)
dp := make([]bool, n+1)
choice := make([]int, n+1) // choice[i] = son kelimenin başlangıcı
dp[0] = true
for i := range choice {
choice[i] = -1
}
for i := 1; i <= n; i++ {
for j := max(0, i-maxLen); j < i; j++ {
if dp[j] && words[string(r[j:i])] {
dp[i] = true
choice[i] = j
break
}
}
}
if !dp[n] {
return false, nil
}
// Geri oluşturma
var parts []string
for i := n; i > 0; i = choice[i] {
parts = append([]string{string(r[choice[i]:i])}, parts...)
}
return true, parts
}
// countWordBreaks: kaç farklı şekilde bölünebilir
func countWordBreaks(s string, dictionary []string) int {
words := make(map[string]bool, len(dictionary))
maxLen := 0
for _, w := range dictionary {
words[w] = true
maxLen = max(maxLen, len([]rune(w)))
}
r := []rune(s)
n := len(r)
dp := make([]int, n+1)
dp[0] = 1
for i := 1; i <= n; i++ {
for j := max(0, i-maxLen); j < i; j++ {
if words[string(r[j:i])] {
dp[i] += dp[j]
}
}
}
return dp[n]
}
func main() {
dictionary := []string{"kitap", "lık", "raf", "ı", "ev", "de", "kalem", "lik"}
cases := []string{
"kitaplık",
"kitaplıkraf",
"evde",
"kalemlik",
"kitapraf",
"telefon",
"",
}
fmt.Println("sözlük:", dictionary)
fmt.Println()
fmt.Printf("%-16s %12s %-30s %10s\n", "metin", "bölünebilir", "bölünmüş hâli", "yol sayısı")
for _, s := range cases {
ok, parts := wordBreak(s, dictionary)
count := countWordBreaks(s, dictionary)
display := "-"
if ok {
display = strings.Join(parts, " + ")
}
fmt.Printf("%-16q %12t %-30s %10d\n", s, ok, display, count)
}
fmt.Println()
// Birden fazla bölme olan örnek
dict2 := []string{"a", "aa", "aaa"}
s2 := "aaaa"
ok, parts := wordBreak(s2, dict2)
fmt.Printf("sözlük %v ile %q:\n", dict2, s2)
fmt.Println(" bölünebilir:", ok, "→", parts)
fmt.Println(" toplam bölme sayısı:", countWordBreaks(s2, dict2))
}sözlük: [kitap lık raf ı ev de kalem lik] metin bölünebilir bölünmüş hâli yol sayısı "kitaplık" true kitap + lık 1 "kitaplıkraf" true kitap + lık + raf 1 "evde" true ev + de 1 "kalemlik" true kalem + lik 1 "kitapraf" true kitap + raf 1 "telefon" false - 0 "" true 1 sözlük [a aa aaa] ile "aaaa": bölünebilir: true → [a aaa] toplam bölme sayısı: 7
maxLen ile döngüyü sınırlamak küçük ama önemli bir iyileştirmedir: Sözlükteki en uzun kelimeden daha uzun bir parçayı kontrol etmenin anlamı yoktur. Bu, naif O(n²) iç döngüyü O(n × maxLen)'e indirir ve uzun metinlerde belirgin fark yaratır.
Bu problem, metin ayrıştırma ve doğal dil işlemede gerçekten kullanılır: Boşluk kullanmayan dillerde (Çince, Japonca) kelime sınırlarını bulmak tam olarak bu problemdir. Gerçek sistemler ek olarak olasılık ağırlıkları kullanır ve "en olası bölme"yi arar — yine bir DP problemi.
Bir matris zincirini çarpmanın en az sayıda temel çarpma gerektiren parantezlemesini bul. Bu, aralık DP'sinin klasik örneğidir. Optimal parantezlemeyi de göster.
İpucu
Durum: dp[i][j] = i'den j'ye kadarki matrisleri çarpmanın en az maliyeti. Geçiş: Zinciri her mümkün k noktasından ikiye böl, iki parçanın maliyeti + birleştirme maliyetini hesapla, en küçüğünü seç.
Çözümü göster
package main
import (
"fmt"
"strings"
)
// matrixChainOrder: dims[i-1] × dims[i] boyutlu matrisler için en az çarpma
func matrixChainOrder(dims []int) (int, string, [][]int) {
n := len(dims) - 1 // matris sayısı
if n <= 0 {
return 0, "", nil
}
// dp[i][j] = i..j matrislerini çarpmanın en az maliyeti (1 tabanlı)
dp := make([][]int, n+1)
split := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, n+1)
split[i] = make([]int, n+1)
}
// length = zincir uzunluğu; küçükten büyüğe doldur
for length := 2; length <= n; length++ {
for i := 1; i+length-1 <= n; i++ {
j := i + length - 1
dp[i][j] = 1 << 60
for k := i; k < j; k++ {
cost := dp[i][k] + dp[k+1][j] + dims[i-1]*dims[k]*dims[j]
if cost < dp[i][j] {
dp[i][j] = cost
split[i][j] = k
}
}
}
}
// Parantezlemeyi geri oluştur
var build func(i, j int) string
build = func(i, j int) string {
if i == j {
return fmt.Sprintf("M%d", i)
}
k := split[i][j]
return "(" + build(i, k) + build(k+1, j) + ")"
}
return dp[1][n], build(1, n), dp
}
// naiveCost: sırayla soldan sağa çarpmanın maliyeti
func naiveCost(dims []int) int {
n := len(dims) - 1
if n <= 1 {
return 0
}
total := 0
rows := dims[0]
for i := 1; i <= n; i++ {
total += rows * dims[i-1] * dims[i]
_ = rows
rows = dims[0]
}
// Soldan sağa: ((M1 M2) M3) ...
total = 0
curRows, curCols := dims[0], dims[1]
for i := 2; i <= n; i++ {
total += curRows * curCols * dims[i]
curCols = dims[i]
}
return total
}
func main() {
// Matris boyutları: M1 = 10×30, M2 = 30×5, M3 = 5×60
scenarios := []struct {
name string
dims []int
}{
{"3 matris", []int{10, 30, 5, 60}},
{"4 matris", []int{40, 20, 30, 10, 30}},
{"5 matris", []int{5, 10, 3, 12, 5, 50}},
{"2 matris", []int{10, 20, 30}},
}
for _, s := range scenarios {
cost, parens, _ := matrixChainOrder(s.dims)
naive := naiveCost(s.dims)
fmt.Printf("%s — boyutlar: ", s.name)
for i := 1; i < len(s.dims); i++ {
if i > 1 {
fmt.Print(", ")
}
fmt.Printf("M%d=%d×%d", i, s.dims[i-1], s.dims[i])
}
fmt.Println()
fmt.Println(" optimal parantezleme:", parens)
fmt.Println(" en az çarpma:", cost)
fmt.Println(" soldan sağa çarpma:", naive)
if naive > 0 {
fmt.Printf(" kazanç: %.2f kat\n", float64(naive)/float64(cost))
}
fmt.Println()
}
// DP tablosunu göster
dims := []int{10, 30, 5, 60}
_, _, dp := matrixChainOrder(dims)
n := len(dims) - 1
fmt.Println("DP tablosu (3 matris için):")
fmt.Print(" ")
for j := 1; j <= n; j++ {
fmt.Printf("%8d", j)
}
fmt.Println()
for i := 1; i <= n; i++ {
fmt.Printf(" %2d ", i)
for j := 1; j <= n; j++ {
if j < i {
fmt.Printf("%8s", "-")
continue
}
fmt.Printf("%8d", dp[i][j])
}
fmt.Println()
}
fmt.Println()
fmt.Println("Neden parantezleme önemli?")
fmt.Println("(A×B)×C ile A×(B×C) aynı sonucu verir ama")
fmt.Println("yapılan temel çarpma sayısı çok farklı olabilir.")
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Aralık DP kalıbı: dp[i][j] için i..j arasındaki tüm")
fmt.Println("bölme noktalarını dene — O(n³) karmaşıklık buradan gelir.")
}3 matris — boyutlar: M1=10×30, M2=30×5, M3=5×60
optimal parantezleme: ((M1M2)M3)
en az çarpma: 4500
soldan sağa çarpma: 4500
kazanç: 1.00 kat
4 matris — boyutlar: M1=40×20, M2=20×30, M3=30×10, M4=10×30
optimal parantezleme: ((M1(M2M3))M4)
en az çarpma: 26000
soldan sağa çarpma: 48000
kazanç: 1.85 kat
5 matris — boyutlar: M1=5×10, M2=10×3, M3=3×12, M4=12×5, M5=5×50
optimal parantezleme: (((M1M2)(M3M4))M5)
en az çarpma: 1655
soldan sağa çarpma: 1880
kazanç: 1.14 kat
2 matris — boyutlar: M1=10×20, M2=20×30
optimal parantezleme: (M1M2)
en az çarpma: 6000
soldan sağa çarpma: 6000
kazanç: 1.00 kat
DP tablosu (3 matris için):
1 2 3
1 0 1500 4500
2 - 0 9000
3 - - 0
Neden parantezleme önemli?
(A×B)×C ile A×(B×C) aynı sonucu verir ama
yapılan temel çarpma sayısı çok farklı olabilir.
--------------------------------------------------
Aralık DP kalıbı: dp[i][j] için i..j arasındaki tüm
bölme noktalarını dene — O(n³) karmaşıklık buradan gelir.Matris zinciri çarpımı, aralık DP'sinin ders kitabı örneğidir ve önceki kalıplardan farklı bir yapı gösterir: Tabloyu satır satır değil, aralık uzunluğuna göre doldurursun. Küçük aralıkların cevabı büyük aralıkların hesabında kullanıldığı için doldurma sırası bu şekilde olmak zorundadır.
Karmaşıklığın O(n³) olmasının sebebi durum sayısından değil, hücre başına yapılan işten gelir: O(n²) durum var ve her durum için O(n) bölme noktası denenir. Bu, karmaşıklığı durumdan hesaplama alışkanlığının neden önemli olduğunu gösteren iyi bir örnektir.
Aynı kalıp pek çok problemde karşına çıkar: bir diziyi en az maliyetle birleştirme, palindrom bölme, optimal ikili arama ağacı kurma, ve balon patlatma tipi oyun problemleri. "Aralığı bir noktadan ikiye böl" cümlesini gördüğünde bu kalıbı düşün.
Kısa sınav
İki metin arasındaki LCS probleminde durum nasıl tanımlanır?
Düzenleme mesafesinde dp[i][0] = i temel durumu ne anlama gelir?
0/1 sırt çantasının tek satırlık uygulamasında döngü neden ters yönde gezilir?
En uzun artan alt dizinin O(n log n) çözümünde tutulan tails dizisi nedir?
DP'de optimal çözümün kendisini (değeri değil) elde etmek için ne yapılır?
Matris zinciri çarpımının karmaşıklığı neden O(n³)'tür?
Özet
- LCS iki dizinin ortak alt dizisini bulur; geçişte karakterler eşitse çapraz, değilse iki komşunun büyüğü alınır.
- Düzenleme mesafesi ekleme, silme ve değiştirme işlemleriyle bir metni diğerine çevirmenin en az maliyetini verir.
- 0/1 sırt çantasında her eleman için "al veya alma" kararı verilir; tek satırlık uygulamada ters yönde gezilir.
- Sınırsız sırt çantasıyla 0/1 arasındaki tek fark döngü yönüdür.
- En uzun artan alt dizi O(n²) DP ile ya da ikili aramayla O(n log n) sürede çözülür.
- Izgara problemleri iki boyutlu DP'nin en sade örneğidir; ilk satır ve sütun temel durumdur.
- Geri oluşturma için karar kayıtları tutulur ve sondan geriye doğru yürünür.
- DP kalıpları: iki dizi karşılaştırma, al/alma, doğrusal karar, ızgara hareketi, aralık bölme.
- Karmaşıklık = durum sayısı × hücre başına yapılan iş; bu hesap çözümün yeterli olup olmadığını baştan söyler.