Bir kasiyer olarak müşteriye 87 kuruş para üstü vereceksin. Ne yaparsın? Muhtemelen önce en büyük madeni parayı seçersin: 50, sonra 25, sonra 10, sonra 1+1. Hiç düşünmeden, her adımda o an için en iyi görüneni alarak çözüme ulaşırsın.
Bu strateji açgözlü (greedy) yaklaşımın tanımıdır: Her adımda yerel olarak en iyi seçimi yap, geri dönüp sorgulamadan devam et. Cazip yanı hızıdır — genelde tek geçiş yeterlidir. Tehlikeli yanı ise şudur: Yerel olarak en iyi seçimler, global olarak en iyi sonucu vermeyebilir.
Bu ders açgözlü algoritmaların iki yüzünü birlikte anlatır. Başarılı oldukları klasik problemleri göreceksin: aktivite seçimi, parçalı sırt çantası, Huffman kodlama, aralık birleştirme. Ama aynı derecede önemli olan diğer yarıyı da öğreneceksin: Açgözlü yaklaşımın ne zaman başarısız olduğunu ve bir açgözlü algoritmanın doğruluğunu nasıl kanıtlayacağını. Çünkü açgözlü bir çözüm yazmak kolaydır; onun doğru olduğundan emin olmak zordur.
Açgözlü seçim özelliği
Bir problemin açgözlü yaklaşımla çözülebilmesi için iki özelliği taşıması gerekir.
Açgözlü seçim özelliği. Yerel olarak en iyi seçim, global olarak en iyi çözümün bir parçası olmalıdır. Yani "şu anda en iyi görüneni al" kararı, ileride pişmanlık yaratmamalıdır.
Optimal alt yapı. Bir seçim yaptıktan sonra kalan problem, aynı türden daha küçük bir problem olmalı ve onun en iyi çözümü, bütünün en iyi çözümüne katkı yapmalıdır.
İkinci özellik dinamik programlamayla paylaşılır. Aradaki fark birincisidir: Dinamik programlama tüm seçenekleri dener ve en iyisini seçer; açgözlü yaklaşım tek bir seçeneği dener ve geri dönmez. Bu yüzden açgözlü algoritmalar çok daha hızlıdır ama yalnızca birinci özellik sağlanıyorsa doğrudur.
Açgözlü: Dinamik programlama:
her adımda tek yol tüm yolları dene, en iyisini sakla
● ●
│ ╱│╲
● ● ● ●
│ ╱│ │ │╲
● ● ●●● ●●
hızlı ama yavaş ama
doğruluk ispatı her zaman doğru
gerektirirAktivite seçimi
Klasik açgözlü problem: Başlangıç ve bitiş saatleri verilen etkinliklerden, birbiriyle çakışmayan en fazla sayıda etkinliği seç.
etkinlikler: [1-4] [3-5] [0-6] [5-7] [3-9] [5-9] [6-10] [8-11] [8-12] [2-14] [12-16]
BİTİŞ saatine göre sırala, sığdıkça al:
[1-4] ✓ → son bitiş: 4
[3-5] ✗ (3 < 4, çakışıyor)
[0-6] ✗
[5-7] ✓ → son bitiş: 7
[3-9] ✗
[8-11] ✓ → son bitiş: 11
[12-16] ✓
Sonuç: 4 etkinlikNeden bitiş saatine göre sıralıyoruz? Çünkü en erken biten etkinlik, geriye en fazla zaman bırakır. Bu sezgi, birazdan göreceğimiz değişim argümanıyla kanıtlanabilir.
package main
import (
"fmt"
"slices"
)
type Activity struct {
Name string
Start, End int
}
// selectByEarliestEnd: DOĞRU açgözlü strateji
func selectByEarliestEnd(activities []Activity) []Activity {
sorted := slices.Clone(activities)
slices.SortFunc(sorted, func(a, b Activity) int {
if a.End != b.End {
return a.End - b.End
}
return a.Start - b.Start
})
var chosen []Activity
lastEnd := -1 << 62
for _, a := range sorted {
if a.Start >= lastEnd {
chosen = append(chosen, a)
lastEnd = a.End
}
}
return chosen
}
// selectByEarliestStart: YANLIŞ strateji — uzun bir etkinlik her şeyi engeller
func selectByEarliestStart(activities []Activity) []Activity {
sorted := slices.Clone(activities)
slices.SortFunc(sorted, func(a, b Activity) int { return a.Start - b.Start })
var chosen []Activity
lastEnd := -1 << 62
for _, a := range sorted {
if a.Start >= lastEnd {
chosen = append(chosen, a)
lastEnd = a.End
}
}
return chosen
}
// selectByShortest: YANLIŞ strateji — kısa bir etkinlik iki uzunu engelleyebilir
func selectByShortest(activities []Activity) []Activity {
sorted := slices.Clone(activities)
slices.SortFunc(sorted, func(a, b Activity) int {
return (a.End - a.Start) - (b.End - b.Start)
})
var chosen []Activity
for _, a := range sorted {
fits := true
for _, c := range chosen {
if a.Start < c.End && c.Start < a.End {
fits = false
break
}
}
if fits {
chosen = append(chosen, a)
}
}
slices.SortFunc(chosen, func(a, b Activity) int { return a.Start - b.Start })
return chosen
}
func names(as []Activity) []string {
out := make([]string, len(as))
for i, a := range as {
out[i] = fmt.Sprintf("%s[%d-%d]", a.Name, a.Start, a.End)
}
return out
}
func main() {
activities := []Activity{
{"A", 1, 4}, {"B", 3, 5}, {"C", 0, 6}, {"D", 5, 7}, {"E", 3, 9},
{"F", 5, 9}, {"G", 6, 10}, {"H", 8, 11}, {"I", 8, 12}, {"J", 2, 14}, {"K", 12, 16},
}
best := selectByEarliestEnd(activities)
fmt.Println("en erken biten (DOĞRU):", names(best))
fmt.Println(" seçilen sayı:", len(best))
fmt.Println()
byStart := selectByEarliestStart(activities)
fmt.Println("en erken başlayan (yanlış):", names(byStart))
fmt.Println(" seçilen sayı:", len(byStart))
fmt.Println()
byShort := selectByShortest(activities)
fmt.Println("en kısa süreli (yanlış):", names(byShort))
fmt.Println(" seçilen sayı:", len(byShort))
// En kısa stratejisinin açıkça başarısız olduğu örnek
fmt.Println()
counter := []Activity{
{"uzun1", 0, 10}, {"kısa", 9, 11}, {"uzun2", 10, 20},
}
fmt.Println("karşı örnek:", names(counter))
fmt.Println(" en kısa ile:", names(selectByShortest(counter)), "→", len(selectByShortest(counter)), "etkinlik")
fmt.Println(" en erken biten ile:", names(selectByEarliestEnd(counter)), "→", len(selectByEarliestEnd(counter)), "etkinlik")
}en erken biten (DOĞRU): [A[1-4] D[5-7] H[8-11] K[12-16]] seçilen sayı: 4 en erken başlayan (yanlış): [C[0-6] G[6-10] K[12-16]] seçilen sayı: 3 en kısa süreli (yanlış): [B[3-5] D[5-7] H[8-11] K[12-16]] seçilen sayı: 4 karşı örnek: [uzun1[0-10] kısa[9-11] uzun2[10-20]] en kısa ile: [kısa[9-11]] → 1 etkinlik en erken biten ile: [uzun1[0-10] uzun2[10-20]] → 2 etkinlik
Son karşı örnek çok öğreticidir: "En kısa etkinliği seç" stratejisi makul görünür ama kısa bir etkinlik, iki uzun etkinliğin tam ortasına denk gelip her ikisini de engelleyebilir. Açgözlü algoritmalarda stratejinin kendisi problemin çözümüdür; yanlış strateji seçmek yanlış cevap verir.
Açgözlü ne zaman başarısız olur?
En bilinen karşı örnek bozuk para problemidir: Belirli madeni para değerleriyle bir tutarı en az sayıda parayla ödemek.
Para değerleri: 1, 15, 25 Ödenecek: 30
AÇGÖZLÜ: en büyükten başla
25 al → kalan 5
5 için 25 ve 15 sığmaz → 1×5
Sonuç: 25 + 1+1+1+1+1 = 6 para
EN İYİ: 15 + 15 = 2 para
Açgözlü 3 kat kötü sonuç verdi!package main
import (
"fmt"
"slices"
)
// greedyCoins: en büyükten başlar — HER PARA SİSTEMİNDE DOĞRU DEĞİL
func greedyCoins(coins []int, amount int) ([]int, bool) {
sorted := slices.Clone(coins)
slices.Sort(sorted)
slices.Reverse(sorted)
var used []int
for _, c := range sorted {
for amount >= c {
used = append(used, c)
amount -= c
}
}
return used, amount == 0
}
// optimalCoins: dinamik programlama — her zaman en iyi sonucu verir
func optimalCoins(coins []int, amount int) ([]int, bool) {
const inf = 1 << 30
best := make([]int, amount+1)
choice := make([]int, amount+1)
for i := 1; i <= amount; i++ {
best[i] = inf
}
for i := 1; i <= amount; i++ {
for _, c := range coins {
if c <= i && best[i-c]+1 < best[i] {
best[i] = best[i-c] + 1
choice[i] = c
}
}
}
if best[amount] >= inf {
return nil, false
}
var used []int
for amount > 0 {
used = append(used, choice[amount])
amount -= choice[amount]
}
slices.Sort(used)
slices.Reverse(used)
return used, true
}
func main() {
systems := []struct {
name string
coins []int
amount int
}{
{"Türk lirası kuruşları", []int{1, 5, 10, 25, 50}, 87},
{"kanonik olmayan sistem", []int{1, 15, 25}, 30},
{"kanonik olmayan 2", []int{1, 3, 4}, 6},
{"eksik sistem", []int{3, 5}, 7},
}
for _, s := range systems {
g, gok := greedyCoins(s.coins, s.amount)
o, ook := optimalCoins(s.coins, s.amount)
fmt.Printf("%s — paralar %v, tutar %d\n", s.name, s.coins, s.amount)
if !gok {
fmt.Println(" açgözlü: çözüm bulamadı")
} else {
fmt.Printf(" açgözlü: %v → %d para\n", g, len(g))
}
if !ook {
fmt.Println(" en iyi: çözüm yok")
} else {
fmt.Printf(" en iyi: %v → %d para\n", o, len(o))
}
if gok && ook {
fmt.Printf(" açgözlü optimal mi: %t\n", len(g) == len(o))
}
fmt.Println()
}
fmt.Println("Sonuç: açgözlü yaklaşım yalnızca 'kanonik' para sistemlerinde doğrudur.")
fmt.Println("Gerçek para birimleri kanoniktir (bilinçli tasarım); rastgele sistemler değil.")
}Türk lirası kuruşları — paralar [1 5 10 25 50], tutar 87 açgözlü: [50 25 10 1 1] → 5 para en iyi: [50 25 10 1 1] → 5 para açgözlü optimal mi: true kanonik olmayan sistem — paralar [1 15 25], tutar 30 açgözlü: [25 1 1 1 1 1] → 6 para en iyi: [15 15] → 2 para açgözlü optimal mi: false kanonik olmayan 2 — paralar [1 3 4], tutar 6 açgözlü: [4 1 1] → 3 para en iyi: [3 3] → 2 para açgözlü optimal mi: false eksik sistem — paralar [3 5], tutar 7 açgözlü: çözüm bulamadı en iyi: çözüm yok Sonuç: açgözlü yaklaşım yalnızca 'kanonik' para sistemlerinde doğrudur. Gerçek para birimleri kanoniktir (bilinçli tasarım); rastgele sistemler değil.
Üçüncü örnekte açgözlü yaklaşımın bir çözüm bulamadığına dikkat et: 3 ve 5 kuruşla 7 ödemeye çalışırken önce 5 alır, kalan 2'yi ödeyemez ve tıkanır — oysa 7 bu sistemde hiç ödenemez zaten. Ama {1,3,4} ile 6 örneğinde açgözlü 4+1+1 = 3 para kullanırken, en iyi çözüm 3+3 = 2 paradır.
Bu, açgözlü algoritmaların en tehlikeli yanıdır: Sessizce yanlış cevap verirler. Hata mesajı yoktur, çökme yoktur; yalnızca optimal olmayan bir sonuç. Bu yüzden doğruluğu kanıtlamak isteğe bağlı değildir.
Parçalı sırt çantası
Kapasitesi sınırlı bir çantaya, değeri ve ağırlığı bilinen eşyaları koyacaksın. Eşyaları bölebiliyorsan açgözlü yaklaşım optimaldir: Birim ağırlık başına değeri en yüksek olandan başla.
package main
import (
"fmt"
"slices"
)
type Item struct {
Name string
Value float64
Weight float64
}
type Taken struct {
Item Item
Fraction float64
}
// fractionalKnapsack: açgözlü OPTIMAL — eşyalar bölünebilir
func fractionalKnapsack(items []Item, capacity float64) ([]Taken, float64) {
sorted := slices.Clone(items)
// Birim değere göre azalan sırala
slices.SortFunc(sorted, func(a, b Item) int {
ra, rb := a.Value/a.Weight, b.Value/b.Weight
switch {
case ra > rb:
return -1
case ra < rb:
return 1
}
return 0
})
var taken []Taken
total := 0.0
remaining := capacity
for _, it := range sorted {
if remaining <= 0 {
break
}
if it.Weight <= remaining {
taken = append(taken, Taken{it, 1.0})
total += it.Value
remaining -= it.Weight
continue
}
// Kalan kapasiteye sığan kısmı al
fraction := remaining / it.Weight
taken = append(taken, Taken{it, fraction})
total += it.Value * fraction
remaining = 0
}
return taken, total
}
// integralGreedy: BÖLÜNEMEZ eşyalarda açgözlü — optimal DEĞİL
func integralGreedy(items []Item, capacity float64) ([]Item, float64) {
sorted := slices.Clone(items)
slices.SortFunc(sorted, func(a, b Item) int {
ra, rb := a.Value/a.Weight, b.Value/b.Weight
switch {
case ra > rb:
return -1
case ra < rb:
return 1
}
return 0
})
var taken []Item
total, remaining := 0.0, capacity
for _, it := range sorted {
if it.Weight <= remaining {
taken = append(taken, it)
total += it.Value
remaining -= it.Weight
}
}
return taken, total
}
// integralOptimal: kaba kuvvetle tüm alt kümeleri dene
func integralOptimal(items []Item, capacity float64) ([]Item, float64) {
n := len(items)
bestValue := 0.0
var bestSet []Item
for mask := range 1 << n {
weight, value := 0.0, 0.0
var set []Item
for i := range n {
if mask&(1<<i) != 0 {
weight += items[i].Weight
value += items[i].Value
set = append(set, items[i])
}
}
if weight <= capacity && value > bestValue {
bestValue, bestSet = value, set
}
}
return bestSet, bestValue
}
func main() {
items := []Item{
{"altın", 60, 10},
{"gümüş", 100, 20},
{"bronz", 120, 30},
}
const capacity = 50
fmt.Println("eşyalar (değer/ağırlık oranı):")
for _, it := range items {
fmt.Printf(" %-8s değer=%-6.0f ağırlık=%-4.0f oran=%.2f\n",
it.Name, it.Value, it.Weight, it.Value/it.Weight)
}
fmt.Println("kapasite:", capacity)
taken, total := fractionalKnapsack(items, capacity)
fmt.Println()
fmt.Println("PARÇALI sırt çantası (açgözlü optimal):")
for _, t := range taken {
fmt.Printf(" %-8s %.0f%% alındı → değer %.1f\n",
t.Item.Name, t.Fraction*100, t.Item.Value*t.Fraction)
}
fmt.Printf(" toplam değer: %.1f\n", total)
// Bölünemez durumda açgözlü başarısız olabilir
fmt.Println()
indivisible := []Item{
{"A", 60, 10}, {"B", 100, 20}, {"C", 120, 30},
}
g, gv := integralGreedy(indivisible, capacity)
o, ov := integralOptimal(indivisible, capacity)
fmt.Println("BÖLÜNEMEZ sırt çantası:")
fmt.Printf(" açgözlü: %v → %.0f\n", names(g), gv)
fmt.Printf(" en iyi: %v → %.0f\n", names(o), ov)
fmt.Println(" açgözlü optimal mi:", gv == ov)
// Açgözlünün başarısız olduğu örnek
fmt.Println()
tricky := []Item{
{"X", 10, 5}, {"Y", 11, 6}, {"Z", 11, 6},
}
g2, gv2 := integralGreedy(tricky, 11)
o2, ov2 := integralOptimal(tricky, 11)
fmt.Println("zorlu örnek (kapasite 11):")
fmt.Printf(" açgözlü: %v → %.0f\n", names(g2), gv2)
fmt.Printf(" en iyi: %v → %.0f\n", names(o2), ov2)
fmt.Println(" açgözlü optimal mi:", gv2 == ov2)
}
func names(items []Item) []string {
out := make([]string, len(items))
for i, it := range items {
out[i] = it.Name
}
return out
}eşyalar (değer/ağırlık oranı): altın değer=60 ağırlık=10 oran=6.00 gümüş değer=100 ağırlık=20 oran=5.00 bronz değer=120 ağırlık=30 oran=4.00 kapasite: 50 PARÇALI sırt çantası (açgözlü optimal): altın 100% alındı → değer 60.0 gümüş 100% alındı → değer 100.0 bronz 67% alındı → değer 80.0 toplam değer: 240.0 BÖLÜNEMEZ sırt çantası: açgözlü: [A B] → 160 en iyi: [B C] → 220 açgözlü optimal mi: false zorlu örnek (kapasite 11): açgözlü: [X Y] → 21 en iyi: [X Y] → 21 açgözlü optimal mi: true
Aradaki fark tek bir kelimeden geliyor: bölünebilirlik. Eşyaları parçalayabiliyorsan, kalan kapasiteyi her zaman en değerli malzemeyle doldurabilirsin ve açgözlü seçim asla pişmanlık yaratmaz. Bölünemiyorsa, bir eşyayı almak kapasitenin bir kısmını boşa harcayabilir ve bu, ileride daha iyi bir kombinasyonu engelleyebilir.
Bölünemez sırt çantası problemi, dinamik programlama gerektirir ve onu Dinamik Programlama II dersinde göreceksin.
Huffman kodlama
Bir metni en az bitle temsil etmek istiyorsun. Her karaktere sabit uzunlukta kod vermek yerine, sık geçen karakterlere kısa, seyrek geçenlere uzun kod verirsen toplam boyut küçülür. Huffman kodlaması bu fikri optimal biçimde uygular ve açgözlü bir strateji kullanır: Her adımda en az frekanslı iki düğümü birleştir.
package main
import (
"container/heap"
"fmt"
"maps"
"slices"
"strings"
)
type node struct {
char rune
freq int
left, right *node
}
func (n *node) isLeaf() bool { return n.left == nil && n.right == nil }
type nodeHeap []*node
func (h nodeHeap) Len() int { return len(h) }
func (h nodeHeap) Less(i, j int) bool {
if h[i].freq != h[j].freq {
return h[i].freq < h[j].freq
}
return h[i].char < h[j].char // eşitlikte deterministik sıra
}
func (h nodeHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *nodeHeap) Push(x any) { *h = append(*h, x.(*node)) }
func (h *nodeHeap) Pop() any { old := *h; n := old[len(old)-1]; *h = old[:len(old)-1]; return n }
// buildHuffman: her adımda en az frekanslı iki düğümü birleştirir
func buildHuffman(freq map[rune]int) *node {
h := &nodeHeap{}
for _, r := range slices.Sorted(maps.Keys(freq)) {
*h = append(*h, &node{char: r, freq: freq[r]})
}
heap.Init(h)
if h.Len() == 1 { // tek karakter özel durumu
only := heap.Pop(h).(*node)
return &node{freq: only.freq, left: only}
}
for h.Len() > 1 {
a := heap.Pop(h).(*node)
b := heap.Pop(h).(*node)
merged := &node{
freq: a.freq + b.freq,
left: a,
right: b,
char: min(a.char, b.char), // deterministik sıralama için
}
heap.Push(h, merged)
}
return heap.Pop(h).(*node)
}
func buildCodes(root *node) map[rune]string {
codes := map[rune]string{}
var walk func(*node, string)
walk = func(n *node, prefix string) {
if n == nil {
return
}
if n.isLeaf() {
if prefix == "" {
prefix = "0"
}
codes[n.char] = prefix
return
}
walk(n.left, prefix+"0")
walk(n.right, prefix+"1")
}
walk(root, "")
return codes
}
func main() {
text := "merhaba dünya merhaba go"
freq := map[rune]int{}
for _, r := range text {
freq[r]++
}
root := buildHuffman(freq)
codes := buildCodes(root)
fmt.Printf("metin: %q (%d karakter)\n\n", text, len([]rune(text)))
fmt.Printf("%-6s %8s %10s %8s\n", "karakter", "frekans", "kod", "bit")
totalBits := 0
for _, r := range slices.Sorted(maps.Keys(freq)) {
code := codes[r]
bits := freq[r] * len(code)
totalBits += bits
label := string(r)
if r == ' ' {
label = "' '"
}
fmt.Printf("%-6s %8d %10s %8d\n", label, freq[r], code, bits)
}
runeCount := len([]rune(text))
fixedBits := runeCount * 8 // her karaktere 8 bit
fmt.Println()
fmt.Println("sabit uzunluklu kodlama (8 bit/karakter):", fixedBits, "bit")
fmt.Println("Huffman kodlaması:", totalBits, "bit")
fmt.Printf("sıkıştırma oranı: %%%.1f\n", 100*(1-float64(totalBits)/float64(fixedBits)))
// Kodlama ve çözme
var encoded strings.Builder
for _, r := range text {
encoded.WriteString(codes[r])
}
var decoded strings.Builder
cur := root
for _, bit := range encoded.String() {
if bit == '0' {
cur = cur.left
} else {
cur = cur.right
}
if cur.isLeaf() {
decoded.WriteRune(cur.char)
cur = root
}
}
fmt.Println()
fmt.Println("kodlanmış uzunluk:", encoded.Len(), "bit")
fmt.Printf("çözülen metin: %q\n", decoded.String())
fmt.Println("özgün metinle aynı mı:", decoded.String() == text)
}metin: "merhaba dünya merhaba go" (24 karakter) karakter frekans kod bit ' ' 3 100 9 a 5 01 10 b 2 1010 8 d 1 10110 5 e 2 1100 8 g 1 10111 5 h 2 1101 8 m 2 1110 8 n 1 11110 5 o 1 11111 5 r 2 000 6 y 1 0010 4 ü 1 0011 4 sabit uzunluklu kodlama (8 bit/karakter): 192 bit Huffman kodlaması: 85 bit sıkıştırma oranı: %55.7 kodlanmış uzunluk: 85 bit çözülen metin: "merhaba dünya merhaba go" özgün metinle aynı mı: true
Huffman kodlamasının açgözlü seçimi şudur: En az frekanslı iki düğümü birleştir. Sezgi, en seyrek karakterlerin ağaçta en derine inmesi ve dolayısıyla en uzun kodu almasıdır. Bu strateji kanıtlanabilir biçimde optimaldir — hiçbir ön eksiz kod, Huffman'ın ürettiğinden daha az bit kullanamaz.
Kodların ön eksiz (prefix-free) olması kritik bir özelliktir: Hiçbir kod, başka bir kodun ön eki değildir. Bu sayede kodlanmış akış, ayırıcı karakter olmadan tek anlamlı biçimde çözülebilir. Ağaçta yalnızca yaprakların karakter tutması bunu otomatik olarak garanti eder.
Aralık birleştirme
Çakışan aralıkları birleştirmek, günlük hayatta çok sık karşına çıkan bir problemdir: takvim uygunluğu, kaynak tahsisi, IP aralıkları, metin işaretlemeleri.
package main
import (
"fmt"
"slices"
)
type Interval struct {
Start, End int
}
// merge: çakışan aralıkları birleştirir — O(n log n)
func merge(intervals []Interval) []Interval {
if len(intervals) == 0 {
return nil
}
sorted := slices.Clone(intervals)
slices.SortFunc(sorted, func(a, b Interval) int {
if a.Start != b.Start {
return a.Start - b.Start
}
return a.End - b.End
})
out := []Interval{sorted[0]}
for _, cur := range sorted[1:] {
last := &out[len(out)-1]
if cur.Start <= last.End { // çakışıyor veya değiyor
last.End = max(last.End, cur.End)
continue
}
out = append(out, cur)
}
return out
}
// insert: sıralı ve birleşik bir listeye yeni aralık ekler
func insert(intervals []Interval, newOne Interval) []Interval {
var out []Interval
i := 0
for i < len(intervals) && intervals[i].End < newOne.Start {
out = append(out, intervals[i])
i++
}
for i < len(intervals) && intervals[i].Start <= newOne.End {
newOne.Start = min(newOne.Start, intervals[i].Start)
newOne.End = max(newOne.End, intervals[i].End)
i++
}
out = append(out, newOne)
return append(out, intervals[i:]...)
}
// minRooms: aynı anda en fazla kaç aralık çakışıyor (kaç oda gerekir)
func minRooms(intervals []Interval) int {
type event struct {
time int
delta int
}
var events []event
for _, iv := range intervals {
events = append(events, event{iv.Start, 1}, event{iv.End, -1})
}
slices.SortFunc(events, func(a, b event) int {
if a.time != b.time {
return a.time - b.time
}
return a.delta - b.delta // aynı anda biten önce sayılır
})
current, best := 0, 0
for _, e := range events {
current += e.delta
best = max(best, current)
}
return best
}
func main() {
intervals := []Interval{{1, 3}, {2, 6}, {8, 10}, {15, 18}, {17, 20}}
fmt.Println("aralıklar:", intervals)
fmt.Println("birleştirilmiş:", merge(intervals))
fmt.Println()
merged := merge(intervals)
fmt.Println("mevcut:", merged)
fmt.Println("[4,9] eklendi:", insert(merged, Interval{4, 9}))
fmt.Println("[25,30] eklendi:", insert(merged, Interval{25, 30}))
fmt.Println("[0,25] eklendi:", insert(merged, Interval{0, 25}))
fmt.Println()
meetings := []Interval{{9, 10}, {9, 12}, {11, 13}, {9, 11}, {14, 15}}
fmt.Println("toplantılar:", meetings)
fmt.Println("gereken oda sayısı:", minRooms(meetings))
fmt.Println()
cases := [][]Interval{
{},
{{1, 5}},
{{1, 2}, {3, 4}, {5, 6}},
{{1, 10}, {2, 3}, {4, 5}},
{{1, 4}, {4, 5}},
}
for _, c := range cases {
fmt.Printf(" %-32v → %v\n", c, merge(c))
}
}aralıklar: [{1 3} {2 6} {8 10} {15 18} {17 20}]
birleştirilmiş: [{1 6} {8 10} {15 20}]
mevcut: [{1 6} {8 10} {15 20}]
[4,9] eklendi: [{1 10} {15 20}]
[25,30] eklendi: [{1 6} {8 10} {15 20} {25 30}]
[0,25] eklendi: [{0 25}]
toplantılar: [{9 10} {9 12} {11 13} {9 11} {14 15}]
gereken oda sayısı: 3
[] → []
[{1 5 }] → [{1 5}]
[{1 2 } {3 4 } {5 6 }] → [{1 2} {3 4} {5 6}]
[{1 10 } {2 3 } {4 5 }] → [{1 10}]
[{1 4 } {4 5 }] → [{1 5}]minRooms fonksiyonundaki teknik ayrıca not edilmeye değer: Aralıkları başlangıç ve bitiş olaylarına ayırıp zamana göre sıralamak, "aynı anda kaç şey aktif" sorusunu tek geçişte yanıtlar. Bu süpürme çizgisi (sweep line) tekniği, geometrik ve zamansal problemlerin genel çözüm kalıbıdır.
Değişim argümanıyla doğruluk ispatı
Açgözlü bir algoritmanın doğruluğunu kanıtlamanın en yaygın yöntemi değişim argümanıdır (exchange argument). Mantık şöyle işler:
1. Optimal bir çözüm O olduğunu varsay.
2. Açgözlü çözüm G ile O'nun ilk farklılaştığı noktayı bul.
3. O'daki seçimi, G'nin seçimiyle DEĞİŞTİR.
4. Bu değişimin çözümü BOZMADIĞINI ve KÖTÜLEŞTİRMEDİĞİNİ göster.
5. Böylece G kadar iyi bir optimal çözüm elde edilir.
6. Adım adım tekrarlayarak G'nin de optimal olduğu sonucuna varılır.Aktivite seçimi için uygulayalım:
İDDİA: En erken biten etkinliği seçmek optimaldir.
İSPAT:
O optimal çözüm olsun, etkinlikleri bitiş sırasına göre o₁, o₂, ...
G açgözlü çözüm olsun, g₁ = en erken biten etkinlik.
g₁ ile o₁ aynıysa sorun yok, ikinci etkinliğe geç.
Farklıysa: g₁ en erken biten olduğundan bitiş(g₁) ≤ bitiş(o₁).
O'da o₁'i g₁ ile değiştir:
- g₁, o₁'den erken bittiği için o₂ ile çakışmaz ✓
- Etkinlik SAYISI değişmedi ✓
→ Yeni çözüm de optimal ve g₁ içeriyor
Aynı argümanı kalan etkinlikler için tekrarla.
Sonuç: G optimaldir. ∎Bu argümanın özü şudur: Açgözlü seçim, optimal bir çözümü bozmadan onun içine yerleştirilebiliyorsa, açgözlü strateji doğrudur. Bozuk para probleminde bu argüman kurulamaz — 25 kuruşu almak, bazı sistemlerde optimal çözümü gerçekten bozar.
package main
import (
"fmt"
"slices"
)
type Job struct {
Name string
Deadline int
Profit int
}
// scheduleGreedy: kâra göre azalan sırala, her işi mümkün olan EN SON slota koy
func scheduleGreedy(jobs []Job) ([]string, int) {
sorted := slices.Clone(jobs)
slices.SortFunc(sorted, func(a, b Job) int {
if a.Profit != b.Profit {
return b.Profit - a.Profit
}
return a.Deadline - b.Deadline
})
maxDeadline := 0
for _, j := range jobs {
maxDeadline = max(maxDeadline, j.Deadline)
}
slots := make([]string, maxDeadline+1)
total := 0
for _, j := range sorted {
// En son boş slotu bul: erken slotları başka işlere sakla
for t := min(j.Deadline, maxDeadline); t >= 1; t-- {
if slots[t] == "" {
slots[t] = j.Name
total += j.Profit
break
}
}
}
var schedule []string
for t := 1; t <= maxDeadline; t++ {
if slots[t] != "" {
schedule = append(schedule, fmt.Sprintf("t%d:%s", t, slots[t]))
}
}
return schedule, total
}
// bruteForce: tüm permütasyonları dene (küçük girdiler için)
func bruteForce(jobs []Job) int {
n := len(jobs)
best := 0
var try func(mask int)
try = func(mask int) {
// Seçilen işleri son tarihe göre sırala ve geçerli mi kontrol et
var chosen []Job
for i := range n {
if mask&(1<<i) != 0 {
chosen = append(chosen, jobs[i])
}
}
slices.SortFunc(chosen, func(a, b Job) int { return a.Deadline - b.Deadline })
profit := 0
for i, j := range chosen {
if i+1 > j.Deadline {
return // geçersiz: bu iş zamanında bitmez
}
profit += j.Profit
}
best = max(best, profit)
}
for mask := range 1 << n {
try(mask)
}
return best
}
func main() {
jobs := []Job{
{"A", 2, 100},
{"B", 1, 19},
{"C", 2, 27},
{"D", 1, 25},
{"E", 3, 15},
}
fmt.Println("işler (son tarih, kâr):")
for _, j := range jobs {
fmt.Printf(" %s: son tarih=%d kâr=%d\n", j.Name, j.Deadline, j.Profit)
}
schedule, total := scheduleGreedy(jobs)
optimal := bruteForce(jobs)
fmt.Println()
fmt.Println("açgözlü program:", schedule)
fmt.Println("açgözlü kâr:", total)
fmt.Println("kaba kuvvet en iyi kâr:", optimal)
fmt.Println("açgözlü optimal mi:", total == optimal)
fmt.Println()
fmt.Println("Bu problemde açgözlü strateji DOĞRUDUR ve değişim argümanıyla kanıtlanabilir:")
fmt.Println("En kârlı işi mümkün olan en son slota koymak, erken slotları")
fmt.Println("daha sıkı son tarihli işlere bırakır ve hiçbir kayba yol açmaz.")
}işler (son tarih, kâr): A: son tarih=2 kâr=100 B: son tarih=1 kâr=19 C: son tarih=2 kâr=27 D: son tarih=1 kâr=25 E: son tarih=3 kâr=15 açgözlü program: [t1:C t2:A t3:E] açgözlü kâr: 142 kaba kuvvet en iyi kâr: 142 açgözlü optimal mi: true Bu problemde açgözlü strateji DOĞRUDUR ve değişim argümanıyla kanıtlanabilir: En kârlı işi mümkün olan en son slota koymak, erken slotları daha sıkı son tarihli işlere bırakır ve hiçbir kayba yol açmaz.
Açgözlü mü, dinamik programlama mı?
Bir problemle karşılaştığında hangi yaklaşımı seçeceğini belirlemek, deneyimle gelişen bir beceridir. Aşağıdaki sorular süreci kısaltır.
Açgözlü bir strateji aklına geliyor mu? Önce dene. En büyüğü al, en küçüğü al, en erken biteni al, en yüksek oranlıyı al. Aday stratejiler genelde birkaç tanedir ve hepsini düşünmek dakikalar sürer.
Karşı örnek bulabiliyor musun? Bu adım en kritik olanıdır ve atlanmaya en müsait. Stratejinin başarısız olduğu küçük bir örnek bulmak, çoğu zaman doğruluğunu kanıtlamaya çalışmaktan kolaydır. Küçük girdilerle kaba kuvvet çözümü yazıp açgözlü sonuçla karşılaştırmak, karşı örnek aramanın en pratik yoludur — bu dersteki örneklerde tam olarak bunu yaptık.
Değişim argümanı kurulabiliyor mu? Açgözlü seçimi optimal bir çözümün içine yerleştirmek, o çözümü bozmuyorsa strateji doğrudur. Bu argümanı kurmaya çalışmak, aynı zamanda neden çalıştığını anlamanı sağlar.
Geri dönüp pişman olma ihtiyacı var mı? Bir seçimin ileride kötü sonuç doğurabileceği durumlarda açgözlü yaklaşım yetmez. "Bu eşyayı alsam kapasite boşa gider mi?" sorusu akla geliyorsa dinamik programlamaya geçmelisin.
Problem boyutu küçük mü? Öyleyse kaba kuvvet ya da dinamik programlama zaten yeterlidir ve doğruluk kaygısı ortadan kalkar. Açgözlü yaklaşımın değeri, büyük girdilerde hız kazandırmasıdır.
Pratik bir gözlem: Açgözlü algoritmaların doğru olduğu problemler nispeten azdır ama çok sık karşımıza çıkarlar — çizelgeleme, sıkıştırma, ağ kurma, kaynak tahsisi. Doğru olmadığı durumlarda ise genelde yaklaşık bir çözüm verirler ve bazı problemlerde bu, kanıtlanabilir sınırlar içinde kalır. Optimal çözümü bulmak üstel süre gerektiriyorsa, hızlı ve yeterince iyi bir açgözlü çözüm gerçek hayatta tercih edilir.
Sık yapılan hatalar
- Açgözlü stratejiyi kanıtlamadan kabul etmek. Sessizce optimal olmayan sonuç üretir; hata mesajı almazsın.
- Yanlış sıralama ölçütü seçmek. Aktivite seçiminde bitiş yerine başlangıç veya süreye göre sıralamak yanlış cevap verir.
- Bozuk para problemini her sistemde açgözlü çözmek. Yalnızca kanonik sistemlerde doğrudur.
- Bölünemez sırt çantasında oran temelli açgözlü kullanmak. Bölünebilirlik olmadan strateji çöker.
- Karşı örnek aramamak. Küçük girdilerde kaba kuvvetle karşılaştırmak, hataları anında ortaya çıkarır.
- Huffman'da eşitlik durumunu belirsiz bırakmak. Aynı frekanslı düğümler için deterministik bir sıra tanımlamazsan çıktı çalıştırmalar arasında değişir.
- Aralık problemlerinde sınırların dâhil olup olmadığını netleştirmemek.
[1,4]ile[4,5]çakışıyor mu? Kararını baştan ver.
Alıştırmalar
Bir istasyona gelen trenlerin varış ve kalkış saatleri verilmiş. Hiçbir trenin beklemesine gerek kalmaması için en az kaç platform gerektiğini hesapla.
İpucu
Varış ve kalkışları ayrı olaylar olarak düşünüp zamana göre sırala. Varışta sayacı artır, kalkışta azalt; gördüğün en yüksek değer cevaptır.
Çözümü göster
package main
import (
"fmt"
"slices"
)
type Train struct {
Name string
Arrival, Depart int // dakika cinsinden
}
// minPlatforms: süpürme çizgisi yaklaşımı — O(n log n)
func minPlatforms(trains []Train) (int, int) {
type event struct {
time int
delta int
}
var events []event
for _, t := range trains {
events = append(events, event{t.Arrival, +1}, event{t.Depart, -1})
}
// Aynı anda kalkış ve varış varsa kalkış önce işlenir: platform serbest kalır
slices.SortFunc(events, func(a, b event) int {
if a.time != b.time {
return a.time - b.time
}
return a.delta - b.delta
})
current, best, peakTime := 0, 0, 0
for _, e := range events {
current += e.delta
if current > best {
best, peakTime = current, e.time
}
}
return best, peakTime
}
func format(minutes int) string {
return fmt.Sprintf("%02d:%02d", minutes/60, minutes%60)
}
func main() {
trains := []Train{
{"T1", 9*60 + 0, 9*60 + 30},
{"T2", 9*60 + 15, 10*60 + 0},
{"T3", 9*60 + 45, 10*60 + 15},
{"T4", 10*60 + 0, 10*60 + 30},
{"T5", 11*60 + 0, 11*60 + 30},
}
fmt.Println("tren çizelgesi:")
for _, t := range trains {
fmt.Printf(" %s: %s → %s\n", t.Name, format(t.Arrival), format(t.Depart))
}
platforms, peak := minPlatforms(trains)
fmt.Println()
fmt.Println("gereken platform sayısı:", platforms)
fmt.Println("en yoğun an:", format(peak))
fmt.Println()
cases := [][]Train{
{},
{{"A", 0, 10}},
{{"A", 0, 10}, {"B", 10, 20}}, // sıra sıra: 1 platform yeter
{{"A", 0, 10}, {"B", 5, 15}, {"C", 8, 12}},
}
for _, c := range cases {
p, _ := minPlatforms(c)
fmt.Printf(" %d tren → %d platform\n", len(c), p)
}
}tren çizelgesi: T1: 09:00 → 09:30 T2: 09:15 → 10:00 T3: 09:45 → 10:15 T4: 10:00 → 10:30 T5: 11:00 → 11:30 gereken platform sayısı: 2 en yoğun an: 09:15 0 tren → 0 platform 1 tren → 1 platform 2 tren → 1 platform 3 tren → 3 platform
Sıralama karşılaştırmasındaki a.delta - b.delta satırı önemli bir karar içerir: Bir tren tam olarak diğerinin kalktığı anda geliyorsa, aynı platformu kullanabilir mi? Kalkışı önce işleyerek "evet" demiş oluyoruz. Gerçek bir sistemde bu, temizlik ve hazırlık süresine bağlıdır — modelin bu detayı açıkça belirtmesi gerekir.
Süpürme çizgisi tekniği, "aynı anda kaç şey aktif" sorusunun genel çözümüdür ve takvim çakışmalarından ağ bağlantı sayımına kadar geniş bir alanda kullanılır.
Bozuk para problemi için, açgözlü yaklaşımın başarısız olduğu para sistemlerini otomatik olarak bulan bir program yaz. Verilen para değerleri için açgözlü ve optimal çözümleri karşılaştırıp ilk farkı bildir.
İpucu
Küçük tutarları sırayla dene; açgözlü sonuç ile dinamik programlama sonucu ilk kez farklılaştığında karşı örneği bulmuş olursun.
Çözümü göster
package main
import (
"fmt"
"slices"
)
func greedyCount(coins []int, amount int) (int, bool) {
sorted := slices.Clone(coins)
slices.Sort(sorted)
slices.Reverse(sorted)
count := 0
for _, c := range sorted {
for amount >= c {
amount -= c
count++
}
}
return count, amount == 0
}
func optimalCount(coins []int, amount int) (int, bool) {
const inf = 1 << 30
best := make([]int, amount+1)
for i := 1; i <= amount; i++ {
best[i] = inf
for _, c := range coins {
if c <= i && best[i-c]+1 < best[i] {
best[i] = best[i-c] + 1
}
}
}
if best[amount] >= inf {
return 0, false
}
return best[amount], true
}
// findCounterexample: açgözlünün başarısız olduğu en küçük tutarı bulur
func findCounterexample(coins []int, limit int) (amount, greedy, optimal int, found bool) {
for a := 1; a <= limit; a++ {
g, gok := greedyCount(coins, a)
o, ook := optimalCount(coins, a)
if !ook {
continue // bu tutar hiç ödenemiyor
}
if !gok || g > o {
return a, g, o, true
}
}
return 0, 0, 0, false
}
func main() {
systems := [][]int{
{1, 5, 10, 25, 50}, // Türk/ABD kuruşları: kanonik
{1, 2, 5, 10, 20, 50}, // Euro benzeri: kanonik
{1, 15, 25}, // kanonik değil
{1, 3, 4}, // kanonik değil
{1, 5, 8}, // kanonik değil
{1, 2, 4, 8, 16}, // ikinin kuvvetleri: kanonik
{1, 7, 10}, // kanonik değil
}
fmt.Printf("%-24s %10s %12s %10s %10s\n", "para sistemi", "kanonik", "karşı örnek", "açgözlü", "en iyi")
for _, coins := range systems {
amount, g, o, found := findCounterexample(coins, 200)
label := fmt.Sprint(coins)
if !found {
fmt.Printf("%-24s %10s %12s %10s %10s\n", label, "EVET", "-", "-", "-")
continue
}
fmt.Printf("%-24s %10s %12d %10d %10d\n", label, "hayır", amount, g, o)
}
fmt.Println()
fmt.Println("Gerçek para birimleri kanonik olacak şekilde TASARLANMIŞTIR:")
fmt.Println("kasiyerlerin açgözlü davranması yeterli olsun diye.")
fmt.Println()
// Ayrıntılı bir karşı örnek
coins := []int{1, 15, 25}
amount := 30
g, _ := greedyCount(coins, amount)
o, _ := optimalCount(coins, amount)
fmt.Printf("%v ile %d ödemek:\n", coins, amount)
fmt.Printf(" açgözlü: 25 + 1×5 = %d para\n", g)
fmt.Printf(" en iyi: 15 + 15 = %d para\n", o)
}para sistemi kanonik karşı örnek açgözlü en iyi [1 5 10 25 50] EVET - - - [1 2 5 10 20 50] EVET - - - [1 15 25] hayır 30 6 2 [1 3 4] hayır 6 3 2 [1 5 8] hayır 10 3 2 [1 2 4 8 16] EVET - - - [1 7 10] hayır 14 5 2 Gerçek para birimleri kanonik olacak şekilde TASARLANMIŞTIR: kasiyerlerin açgözlü davranması yeterli olsun diye. [1 15 25] ile 30 ödemek: açgözlü: 25 + 1×5 = 6 para en iyi: 15 + 15 = 2 para
Bu yaklaşım, açgözlü algoritma yazarken benimsenmesi gereken alışkanlığı gösteriyor: Stratejiyi otomatik olarak sına. Küçük girdilerde kaba kuvvet ya da dinamik programlama çözümü yazıp sonuçları karşılaştırmak, karşı örnek varsa onu saniyeler içinde bulur.
Bir para sisteminin kanonik olup olmadığını kontrol etmek için tüm tutarları denemek gerekmez; kanoniklik testi için sınırlı bir aralığı kontrol etmenin yeterli olduğu kanıtlanmıştır. Yine de pratik amaçlar için makul bir üst sınıra kadar denemek en kolay yöntemdir.
Bir yolculukta, deposu belirli bir mesafe yetecek bir araçla ilerliyorsun. Yol üzerindeki istasyonların konumları verilmiş. En az sayıda durakla hedefe ulaşmak için hangi istasyonlarda duracağını bul ve stratejinin doğruluğunu tartış.
İpucu
Açgözlü strateji: Menzil içindeki en uzak istasyona kadar git. Bu, "en uzağa git" sezgisinin doğru olduğu bir problemdir; değişim argümanıyla kanıtlanabilir.
Çözümü göster
package main
import (
"fmt"
"slices"
)
// minStops: en az durakla hedefe ulaşmak
func minStops(stations []int, destination, tankRange int) ([]int, bool) {
all := slices.Clone(stations)
slices.Sort(all)
var stops []int
position := 0
i := 0
for position+tankRange < destination {
// Menzil içindeki EN UZAK istasyonu bul
farthest := -1
for i < len(all) && all[i] <= position+tankRange {
farthest = all[i]
i++
}
if farthest == -1 || farthest <= position {
return nil, false // ulaşılamaz
}
stops = append(stops, farthest)
position = farthest
}
return stops, true
}
// bruteForceMinStops: tüm alt kümeleri dener (doğrulama için)
func bruteForceMinStops(stations []int, destination, tankRange int) (int, bool) {
all := slices.Clone(stations)
slices.Sort(all)
n := len(all)
best := -1
for mask := range 1 << n {
var chosen []int
for i := range n {
if mask&(1<<i) != 0 {
chosen = append(chosen, all[i])
}
}
// Geçerli mi kontrol et
position := 0
valid := true
for _, s := range chosen {
if s-position > tankRange {
valid = false
break
}
position = s
}
if valid && destination-position > tankRange {
valid = false
}
if valid && (best == -1 || len(chosen) < best) {
best = len(chosen)
}
}
if best == -1 {
return 0, false
}
return best, true
}
func main() {
scenarios := []struct {
name string
stations []int
destination int
tankRange int
}{
{"normal yolculuk", []int{100, 200, 300, 400, 500, 600}, 700, 250},
{"sık istasyon", []int{50, 100, 150, 200}, 250, 100},
{"uzun boşluk", []int{100, 500}, 600, 200},
{"istasyon gerekmiyor", []int{100, 200}, 150, 300},
{"tek istasyon yeterli", []int{150}, 300, 200},
}
for _, s := range scenarios {
stops, ok := minStops(s.stations, s.destination, s.tankRange)
brute, bok := bruteForceMinStops(s.stations, s.destination, s.tankRange)
fmt.Printf("%s (menzil %d km, hedef %d km)\n", s.name, s.tankRange, s.destination)
fmt.Println(" istasyonlar:", s.stations)
if !ok {
fmt.Println(" açgözlü: hedefe ULAŞILAMAZ")
} else {
fmt.Printf(" açgözlü duraklar: %v → %d durak\n", stops, len(stops))
}
if !bok {
fmt.Println(" kaba kuvvet: ulaşılamaz")
} else {
fmt.Println(" kaba kuvvet en az durak:", brute)
}
if ok && bok {
fmt.Println(" açgözlü optimal mi:", len(stops) == brute)
}
fmt.Println()
}
fmt.Println("DOĞRULUK İSPATI (değişim argümanı):")
fmt.Println("Optimal çözümün ilk durağı s* olsun, açgözlünün ilk durağı g olsun.")
fmt.Println("g, menzil içindeki en uzak istasyon olduğu için s* ≤ g.")
fmt.Println("Optimal çözümde s* yerine g kullanılırsa:")
fmt.Println(" - g'den sonra ulaşılabilecek her yer s*'den de ulaşılabilirdi (g daha ileride)")
fmt.Println(" - durak SAYISI değişmedi")
fmt.Println("→ g içeren bir optimal çözüm vardır; tümevarımla açgözlü optimaldir.")
}normal yolculuk (menzil 250 km, hedef 700 km) istasyonlar: [100 200 300 400 500 600] açgözlü duraklar: [200 400 600] → 3 durak kaba kuvvet en az durak: 3 açgözlü optimal mi: true sık istasyon (menzil 100 km, hedef 250 km) istasyonlar: [50 100 150 200] açgözlü duraklar: [100 200] → 2 durak kaba kuvvet en az durak: 2 açgözlü optimal mi: true uzun boşluk (menzil 200 km, hedef 600 km) istasyonlar: [100 500] açgözlü: hedefe ULAŞILAMAZ kaba kuvvet: ulaşılamaz istasyon gerekmiyor (menzil 300 km, hedef 150 km) istasyonlar: [100 200] açgözlü duraklar: [] → 0 durak kaba kuvvet en az durak: 0 açgözlü optimal mi: true tek istasyon yeterli (menzil 200 km, hedef 300 km) istasyonlar: [150] açgözlü duraklar: [150] → 1 durak kaba kuvvet en az durak: 1 açgözlü optimal mi: true DOĞRULUK İSPATI (değişim argümanı): Optimal çözümün ilk durağı s* olsun, açgözlünün ilk durağı g olsun. g, menzil içindeki en uzak istasyon olduğu için s* ≤ g. Optimal çözümde s* yerine g kullanılırsa: - g'den sonra ulaşılabilecek her yer s*'den de ulaşılabilirdi (g daha ileride) - durak SAYISI değişmedi → g içeren bir optimal çözüm vardır; tümevarımla açgözlü optimaldir.
Bu problem, açgözlü yaklaşımın doğru olduğu bir durumu gösteriyor ve sezgi güçlüdür: Menzil içinde daha yakın bir istasyonda durmanın hiçbir avantajı yoktur, çünkü daha uzak istasyondan ulaşılabilecek her yer daha yakın olandan da ulaşılabilir. Bu, değişim argümanının doğrudan uygulanabildiği tipik bir yapıdır.
Problemin bir varyantı — "en az yakıt maliyetiyle git", istasyonlarda fiyatlar farklı olduğunda — açgözlü ile çözülemez ve dinamik programlama ya da öncelik kuyruğu tabanlı bir yaklaşım gerektirir. Küçük bir kural değişikliğinin problemin sınıfını değiştirmesi, açgözlü algoritmaların ne kadar kırılgan olduğunu gösteren iyi bir örnektir.
Kısa sınav
Açgözlü bir algoritmanın doğru olması için hangi özellik gereklidir?
Aktivite seçimi probleminde hangi ölçüte göre sıralama doğru sonucu verir?
{1, 15, 25} para sistemiyle 30 ödemek için açgözlü yaklaşım kaç para kullanır?
Sırt çantası probleminde açgözlü yaklaşım hangi durumda optimaldir?
Huffman kodlamasının açgözlü seçimi nedir?
Değişim argümanı (exchange argument) neyi kanıtlar?
Özet
- Açgözlü algoritmalar her adımda yerel olarak en iyi seçimi yapar ve geri dönmez; bu yüzden hızlıdırlar.
- Doğru olabilmeleri için açgözlü seçim özelliği ve optimal alt yapı gerekir.
- Aktivite seçiminde doğru ölçüt bitiş saatidir; başlangıç veya süreye göre sıralamak yanlış sonuç verir.
- Bozuk para problemi, açgözlü yaklaşımın klasik karşı örneğidir: yalnızca kanonik sistemlerde doğrudur.
- Parçalı sırt çantasında açgözlü optimaldir; bölünemez sürümde değildir.
- Huffman kodlaması en az frekanslı iki düğümü birleştirir ve ön eksiz kodlar arasında optimal sonuç verir.
- Aralık birleştirme ve süpürme çizgisi teknikleri, zamansal problemlerin genel çözüm kalıbıdır.
- Değişim argümanı, açgözlü doğruluğunu kanıtlamanın standart yöntemidir.
- Bir açgözlü strateji yazdığında, küçük girdilerde kaba kuvvetle karşılaştırarak karşı örnek ara; bulamazsan kanıtlamaya çalış.