go
Algoritmalar dersleri
Algoritmalar/Tasarım Paradigmaları

Dinamik Programlama II: Klasik Problemler

LCS, düzenleme mesafesi, sırt çantası ve en uzun artan alt dizi.

Ders 11 / 1840 dkİleri
Bu derste öğreneceklerin
  • En uzun ortak alt dizi (LCS)
  • Düzenleme mesafesi (Levenshtein)
  • 0/1 sırt çantası
  • En uzun artan alt dizi (LIS): O(n²) ve O(n log n)
  • Izgara yolları
  • Çözümü geri oluşturma
  • DP problemlerini tanıma

Ö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  ← cevap

Aşağıdaki görselleştirmede LCS tablosunun nasıl dolduğunu ve hangi hücrelerin cevaba katkı yaptığını izleyebilirsin:

main.go
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")
}
Çıktı
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 ortak
ZamanO(m × n)AlanO(m × n) / O(min(m,n)) optimize

Geri 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ştir
main.go
package 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)
	}
}
Çıktı
"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: 5
ZamanO(m × n)AlanO(m × n) / O(n) optimize

Aş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.
main.go
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ı")
}
Çıktı
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ı
ZamanO(n × kapasite)AlanO(n × kapasite) / O(kapasite) optimize

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.

main.go
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)
	}
}
Çıktı
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
ZamanO(n²) / O(n log n)AlanO(n)

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.

main.go
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)
	}
}
Çıktı
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

Alıştırma·Palindrom alt dizi
Kolay

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
main.go
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))
}
Çıktı
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
ZamanO(n²)AlanO(n²)

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.

Alıştırma·Kelime bölme
Orta

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
main.go
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))
}
Çıktı
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
ZamanO(n × maxLen)AlanO(n)

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.

Alıştırma·Matris zinciri çarpımı
Zor

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
main.go
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.")
}
Çıktı
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.
ZamanO(n³)AlanO(n²)

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

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.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.