go
Algoritmalar dersleri
Algoritmalar/Tasarım Paradigmaları

Açgözlü (Greedy) Algoritmalar

Yerel en iyi seçimler, doğruluk ispatı ve klasik açgözlü problemler.

Ders 8 / 1830 dkOrta
Bu derste öğreneceklerin
  • Açgözlü seçim özelliği
  • Aktivite seçimi / aralık planlama
  • Açgözlü ne zaman başarısız olur: bozuk para problemi
  • Parçalı sırt çantası
  • Huffman kodlama
  • Aralık birleştirme
  • Değişim argümanı (exchange argument) ile ispat

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
   gerektirir

Aktivite 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 etkinlik

Neden 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.

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

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

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

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.

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

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.

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

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.

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

Alıştırma·Platform sayısı
Kolay

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

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.

Alıştırma·Açgözlü karşı örnek avcısı
Orta

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

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.

Alıştırma·Benzin istasyonu problemi
Zor

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

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

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