İçeriğe geç

Menü

Berk Akademi
Yorumlar Google yorumları Özel Ders Başvuru Ücretsiz ön görüşme Ana Sayfa

Dinamik Programlama Nedir? Fibonacci ile Memoization Örneği

dinamik-programlama-nedir-fibonacci-memoization-ornegi
Bu yazıda neler var?
  1. Dinamik programlama nedir ve hangi iki koşula dayanır?
  2. Fibonacci neden aynı alt problemleri tekrar hesaplar?
  3. Memoization ile Fibonacci yukarıdan aşağı nasıl çözülür?
  4. Tabulation ile Fibonacci aşağıdan yukarı nasıl hesaplanır?
  5. Üç Fibonacci yönteminin zaman ve bellek maliyeti nedir?
  6. Memoization hangi hatalarla bozulur ve ne zaman uygundur?
  7. Sık Sorulan Sorular

Dinamik programlama, aynı alt problemlerin sonuçlarını yeniden kullanarak hesaplamayı azaltan bir algoritma tasarım yaklaşımıdır. Fibonacci gibi özyinelemeli problemlerde, daha önce hesaplanan değerleri saklamak gereksiz tekrarları azaltır. Bu saklama işlemi yukarıdan aşağı memoization veya aşağıdan yukarı tabulation yöntemiyle yapılabilir.

Fibonacci örneği yaklaşımın neden gerekli olduğunu açıkça gösterir: saf özyineleme aynı değerleri farklı çağrı dallarında yeniden hesaplar. Dinamik programlamanın temelini anlamak için önce alt problemlerin nasıl oluştuğunu ve hangi özellikleri taşıdığını incelemek gerekir.

Dinamik programlama nedir ve hangi iki koşula dayanır?

Dinamik programlama, büyük bir problemi daha küçük alt problemlere ayırıp bu alt problemlerin sonuçlarını saklayarak aynı hesaplamaları yeniden yapmayı önleyen bir algoritma tasarım yaklaşımıdır. Bir alt problem, asıl problemin çözümünde kullanılan daha küçük ve bağımsız olarak ifade edilebilen problem parçasıdır.

Bu yaklaşım genellikle iki temel koşulla açıklanır: örtüşen alt problemler ve optimal alt yapı. Her problemde bu iki özelliğin aynı biçimde bulunması gerekmez; ancak dinamik programlama kararını verirken bu özellikleri aramak güçlü bir başlangıç sağlar.

Örtüşen alt problemler aynı girdilerin yeniden ortaya çıkmasıdır

Örtüşen alt problemler, çözüm sırasında aynı alt problem girdilerinin birden fazla kez ortaya çıkması anlamına gelir. Örneğin Fibonacci hesabında F(3) değeri farklı çağrı dallarında tekrar istenir. Değer her defasında yeniden hesaplanırsa işlem tekrarı oluşur, daha önce hesaplanan değer saklanırsa aynı sonuç tekrar kullanılabilir.

Dinamik programlama bu tekrarları iki farklı yönde yönetebilir. Memoization, ihtiyaç duyulan alt problemi hesaplandığı anda kaydeden yukarıdan aşağı bir yaklaşımdır. Tabulation ise daha küçük alt problemlerin sonuçlarından başlayarak tabloyu aşağıdan yukarı doldurur.

Optimal alt yapı, büyük çözümün alt çözümlerden kurulabilmesidir

Optimal alt yapı, bir problemin uygun çözümünün, ilgili alt problemlerin uygun çözümlerinden kurulabilmesiyle ilgilidir. Bu ifade özellikle en kısa yol veya en düşük maliyet gibi bir optimizasyon amacı taşıyan problemlerde kullanılır. Büyük problemin iyi bir çözümü, alt parçalar için uygun çözümlerin doğru biçimde birleştirilmesiyle elde edilir.

Fibonacci dizisinde bir optimizasyon amacı bulunmadığı için “optimal” sözcüğü dikkatli yorumlanmalıdır. Burada en iyi seçimi aramak yerine, daha büyük Fibonacci değerlerinin daha küçük Fibonacci değerlerinden kurulması ve bu alt sonuçların tekrar kullanılabilmesi önemlidir. Bu nedenle Fibonacci, dinamik programlamadaki tekrar kullanılabilir alt sonuç yapısını göstermek için uygun ve anlaşılır bir örnektir.

Fibonacci neden aynı alt problemleri tekrar hesaplar?

Fibonacci neden aynı alt problemleri tekrar hesaplar?

Doğrudan özyinelemeli Fibonacci kodu, aynı Fibonacci değerlerini farklı çağrı dallarında yeniden hesaplar. Dizinin temel tanımı şöyledir: F(0) = 0, F(1) = 1 ve F(n) = F(n - 1) + F(n - 2).

F(5) hesaplanırken fonksiyon önce F(4) ve F(3) değerlerini ister. F(4) kendi içinde F(3) ve F(2) değerlerini çağırır. Böylece F(3) ile F(2) aynı çağrı ağacının farklı bölümlerinde tekrar görünür:

F(5)
├─ F(4)
│  ├─ F(3)
│  │  ├─ F(2)
│  │  │  ├─ F(1)
│  │  │  └─ F(0)
│  │  └─ F(1)
│  └─ F(2) [tekrar]
│     ├─ F(1)
│     └─ F(0)
└─ F(3) [tekrar]
   ├─ F(2) [tekrar]
   │  ├─ F(1)
   │  └─ F(0)
   └─ F(1)

Aşağıdaki sayaç, fonksiyona her girişte bir kez artırılır. Bu nedenle temel durumlara ulaşan çağrılar da toplam sayıya dâhil edilir:

calls = 0

def fib(n):
    global calls
    calls += 1

    if n <= 1:
        return n

    return fib(n - 1) + fib(n - 2)

result = fib(5)

print("Sonuç:", result)
print("Toplam çağrı:", calls)

Bu kodun çıktısı şöyledir:

Sonuç: 5
Toplam çağrı: 15

F(5) için 15 çağrı yapılmasının nedeni, aynı alt sonuçların önceden saklanmamasıdır. Girdi büyüdükçe çağrı ağacı dallanmaya devam eder ve hesaplama maliyeti hızlı biçimde artma eğilimi gösterir. Memoization, her alt problemi ilk hesaplamadan sonra kaydederek bu tekrarları azaltır; tabulation ise değerleri küçük girdilerden başlayarak sıralı biçimde üretir.

Memoization ile Fibonacci yukarıdan aşağı nasıl çözülür?

Memoization, çözüm sırasında hesaplanan Fibonacci değerlerini bir sözlükte saklayıp sonraki çağrılarda yeniden kullanır ve aynı alt problemlerin tekrar hesaplanmasını azaltır. Bu yaklaşımda fonksiyon, ihtiyaç duyduğu değeri önce önbellekte arar; değer yoksa özyinelemeli olarak hesaplar ve sonucu sözlüğe ekler.

def fibonacci(n, memo=None, stats=None):
    if memo is None:
        memo = {}

    if stats is not None:
        stats["giris"] += 1

    if n in memo:
        return memo[n]

    if stats is not None:
        stats["miss"] += 1

    if n < 2:
        sonuc = n
    else:
        sonuc = fibonacci(n - 1, memo, stats) + fibonacci(n - 2, memo, stats)

    memo[n] = sonuc
    return sonuc


stats = {"giris": 0, "miss": 0}
print(fibonacci(5, stats=stats))
print(stats)

Beklenen çıktı: Fibonacci(5) değeri 5, toplam fonksiyon girişi 10, cache miss sayısı ise 6 olur. Toplam giriş sayısı, önbellekte bulunan değerler için yapılan çağrıları da kapsar. Cache miss sayısı ise gerçekten hesaplanan farklı alt problemlerin sayısını gösterir. Bu örnekte 0 ile 5 arasındaki değerlerin her biri bir kez hesaplanır.

memo=None kullanımı burada önemlidir. Fonksiyon her çağrıldığında doğrudan memo={} yazılsaydı her özyinelemeli çağrı yeni bir sözlük oluşturur ve önceki sonuçlar kaybolurdu. None kontrolü sayesinde sözlük yalnızca ilk çağrıda oluşturulur, sonraki çağrılara aynı nesne aktarılır.

Daha kısa bir çözüm için functools.lru_cache dekoratörü kullanılabilir. Dekoratör, fonksiyonun belirli argümanlarla ürettiği sonuçları saklar ve aynı argümanla gelen çağrılarda gövdeyi yeniden çalıştırmadan önbellekteki sonucu döndürür. Python’un functools.lru_cache belgelerinde Fibonacci için benzer bir kullanım gösterilir.

from functools import lru_cache

calls = 0

@lru_cache(maxsize=None)
def fibonacci(n):
    global calls
    calls += 1

    if n < 2:
        return n

    return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(5))
print(calls)
print(fibonacci.cache_info())

Bu örnekte sayaç fonksiyon gövdesinin içine yerleştirildiği için yalnızca gerçek hesaplamalar sayılır. fibonacci(5) için sonuç 5, gövdeye giriş sayısı ve cache miss sayısı 6, önbellekten karşılanan çağrı sayısı ise 4 olur.

Tabulation ile Fibonacci aşağıdan yukarı nasıl hesaplanır?

Tabulation ile Fibonacci aşağıdan yukarı nasıl hesaplanır?

Tabulation, en küçük alt problemlerin sonuçlarından başlayıp tabloyu sırayla doldurarak sonuca ulaşır. Fibonacci için önce F(0) ve F(1) bilinir, ardından her yeni değer kendisinden önceki iki değerin toplamıyla hesaplanır.

def fibonacci_table(n):
    if n < 2:
        return n

    tablo = [0] * (n + 1)
    tablo[1] = 1

    for i in range(2, n + 1):
        tablo[i] = tablo[i - 1] + tablo[i - 2]

    return tablo[n]


print(fibonacci_table(5))

Beklenen çıktı: 5. n=0 için doğrudan 0, n=1 için doğrudan 1 döndürülür. Daha büyük bir n değerinde döngü 2'den n'e kadar ilerler ve her adımda yeni bir Fibonacci değeri üretir.

Tablo sürümü, daha sonra bütün ara sonuçlara erişmek gerektiğinde kullanışlıdır. Yalnızca son değeri hesaplamak yeterliyse tablonun tamamını saklamak zorunlu değildir. Çünkü yeni Fibonacci değeri için yalnızca önceki iki değer gerekir.

def fibonacci_two_values(n):
    if n < 2:
        return n

    onceki, mevcut = 0, 1

    for _ in range(2, n + 1):
        onceki, mevcut = mevcut, onceki + mevcut

    return mevcut


print(fibonacci_two_values(5))

Bu sürümde döngü her adımda önceki iki değeri günceller ve yine 5 sonucunu üretir. Tabulation özyinelemeli çağrı kullanmadığı için burada çağrı sayısı yerine döngüde yapılan alt problem hesaplamaları veya toplama adımları karşılaştırılır.

Üç Fibonacci yönteminin zaman ve bellek maliyeti nedir?

Saf özyinelemeli Fibonacci, aynı alt problemleri tekrar tekrar hesapladığı için üstel zaman maliyetine sahiptir; memoization ve tabulation ise her alt problemi doğrusal sayıda hesaplayarak daha düşük zaman maliyeti sunar. Aşağıdaki karşılaştırma, aritmetik işlemlerin sabit maliyetli kabul edildiği klasik analiz çerçevesine dayanır.

Yöntem Zaman karmaşıklığı Ek bellek Temel neden
Saf özyineleme O(2n) üst sınırı, daha sıkı ifadeyle Θ(φn) O(n) özyineleme yığını Aynı alt problemlerin tekrar tekrar hesaplanması
Sözlükle memoization veya lru_cache O(n) O(n), cache ve çağrı yığını Her alt problemin sonucunun saklanıp yeniden kullanılması
Tam tabulation O(n) O(n) tablo belleği Sonuçların aşağıdan yukarı sırayla tabloya yazılması
Yalnızca son iki değeri tutan tabulation O(n) O(1) ek bellek Bir sonraki Fibonacci değeri için yalnızca önceki iki değerin yeterli olması

Buradaki φ sembolü, altın oranı ifade eder ve Fibonacci çağrı ağacının büyüme hızını daha sıkı biçimde anlatmak için kullanılır. O(2n) ifadesi pratik bir üst sınırdır, Θ(φn) ise saf özyinelemeli yaklaşımın büyümesini daha yakından açıklar.

Bu karşılaştırmada tam sayı işlemlerinin sabit maliyetli olduğu varsayılır. Büyük Fibonacci sayılarında sayıların basamak veya bit uzunluğu arttığı için toplama işlemlerinin kendi maliyeti de hesaba katılabilir. Bu nedenle tablo, algoritmaların alt problem hesaplama davranışını karşılaştırmak için kullanılan temel modeldir.

Memoization hangi hatalarla bozulur ve ne zaman uygundur?

Memoization’ın doğru çalışması için cache’in ilgili çağrılar arasında korunması ve bir alt problemin aynı girdilerle yeniden oluştuğunun tanınması gerekir. Cache her çağrıda sıfırlanırsa daha önce hesaplanan sonuçlar kaybolur ve algoritma tekrar hesaplama yapmaya başlar.

Memo sözlüğünü fonksiyon içinde yeniden oluşturmak

Aşağıdaki hatalı örnekte memo sözlüğü fonksiyonun her çağrısında yeniden oluşturulur. Böylece fibonacci(n - 1) ve fibonacci(n - 2) çağrılarının her biri kendi boş sözlüğüyle çalışır.

def fibonacci(n):
    memo = {}

    if n in memo:
        return memo[n]

    if n < 2:
        return n

    memo[n] = fibonacci(n - 1) + fibonacci(n - 2)
    return memo[n]

Düzeltilmiş yaklaşımda sözlük dışarıdan taşınır. İlk çağrıda memo değeri None ise yeni bir sözlük oluşturulur, sonraki özyinelemeli çağrılar aynı sözlüğü kullanır.

def fibonacci(n, memo=None):
    if memo is None:
        memo = {}

    if n in memo:
        return memo[n]

    if n < 2:
        return n

    memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
    return memo[n]

Değiştirilebilir varsayılan argüman kullanmak

memo={} biçimindeki değiştirilebilir varsayılan argüman, fonksiyon her çağrıldığında yeni bir sözlük oluşturmaz. Aynı sözlük, birbirinden bağımsız üst seviye çağrılar arasında da korunabildiği için beklenmedik durum taşınabilir.

def fibonacci(n, memo={}):
    if n in memo:
        return memo[n]

    if n < 2:
        return n

    memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
    return memo[n]

Bu nedenle memo=None kullanıp sözlüğü fonksiyonun içinde yalnızca gerektiğinde oluşturmak daha güvenlidir. Aynı yaklaşım, liste veya küme gibi değiştirilebilir diğer varsayılan argümanlar için de geçerlidir.

Her özyinelemeli problem memoization için uygun değildir

Özyinelemeli bir çözümün bulunması, problemin otomatik olarak dinamik programlamaya uygun olduğu anlamına gelmez. Memoization’dan yararlanabilmek için alt problemlerin belirli koşulları karşılaması gerekir.

  1. Çözüm ağacında aynı girdilerle oluşan örtüşen alt problemler bulunmalıdır.
  2. Bir alt problemin sonucu, aynı girdilerle karşılaşıldığında yeniden kullanılabilmelidir.
  3. Problemin daha büyük çözümünü alt sonuçlardan kuran uygun bir geçiş bağıntısı bulunmalıdır.
  4. Durum, sonucu etkileyen girdileri sınırlı ve açık bir biçimde temsil edebilmelidir.

Bu koşullar yoksa cache eklemek yalnızca bellek kullanımını artırabilir. Örneğin her çağrıda farklı ve daha önce karşılaşılmamış bir durum oluşuyorsa saklanan sonuçlar yeterince tekrar kullanılmaz.

Fibonacci’de memoization ile tabulation arasında seçim yaparken özyinelemeli anlatımın okunabilirliği, çağrı yığınının kullanacağı bellek ve yalnızca gerekli sonuçların saklanıp saklanamayacağı birlikte değerlendirilir. Özyinelemeli çözümün yapısını korumak istiyorsan memoization anlaşılır bir seçenek olabilir. Döngüyle ilerlemek ve çağrı yığını kullanmamak istiyorsan tabulation, özellikle yalnızca son iki değeri saklayan biçimiyle daha az ek bellek kullanabilir.

Sık Sorulan Sorular

Memoization ile tabulation arasındaki temel fark nedir?

Memoization, özyinelemeli çözüm sırasında ihtiyaç duyulan alt problemlerin sonuçlarını saklayan yukarıdan aşağı bir yaklaşımdır. Tabulation ise başlangıç durumlarından ilerleyerek alt problemleri aşağıdan yukarı sırayla hesaplar ve genellikle döngülerle uygulanır.

Fibonacci hesabında neden yalnızca son iki değeri saklamak yeterlidir?

Fibonacci dizisinde yeni değer, kendisinden önce gelen iki değerin toplamıyla hesaplanır. Bu nedenle daha eski değerler sonraki adımlarda doğrudan kullanılmaz ve tablo yerine iki değişken tutularak O(1) ek bellekle ilerlenebilir.

Memo sözlüğü neden her özyinelemeli çağrıda yeniden oluşturulmamalıdır?

Memo sözlüğü her çağrıda yeniden oluşturulursa önceki çağrıların sonuçları silinir ve aynı alt problemler tekrar hesaplanır. Sözlüğün çağrılar arasında paylaşılması, hesaplanan sonuçların sonraki çağrılarda kullanılmasını sağlar.

Her özyinelemeli problem dinamik programlama ile çözülebilir mi?

Her özyinelemeli problem dinamik programlamaya uygun değildir. Örtüşen alt problemlerin, yeniden kullanılabilir sonuçların, geçiş bağıntısının ve sınırlı bir durum temsilinin bulunması gerekir.

Fibonacci örneği, dinamik programlamada doğru durum tanımının ve uygun saklama stratejisinin zaman ile bellek maliyetini nasıl değiştirdiğini açıkça gösterir.

Bu içerik aradığın cevabı verdi mi?
Yanıtın, hangi yazıları geliştirmemiz gerektiğini anlamamıza yardımcı olur.
Bu içeriğin üretilmesinde yapay zeka araçlarından destek alınmıştır.

Bu konudan sonra ne okuyabilirsin?

Tüm yazılar

İlgili Eğitimler

Berk Keskin, yazılım geliştirici ve eğitmen
Yazar

Berk Keskin Kimdir?

Yazılıma 12 yaşında başladı; İzmir Ekonomi Üniversitesi'ni bölüm birincisi ve yüksek şeref öğrencisi olarak tamamladı. Bugün yalnızca eğitim vermekle kalmıyor, sektörde aktif olarak yazılım projeleri geliştiriyor ve gerçek dünya deneyimini birebir derslerine taşıyor. Ezberden uzak, mühendislik zihniyetini merkeze alan sürdürülebilir öğrenme sistemleri tasarlayarak sorgulayan, üreten ve problem çözebilen yeni nesil yazılımcılar yetiştiriyor.

Sektörel Deneyim & Projeler

  • Ticarify Entegrasyon Yazılım logosu CEO Ticarify Entegrasyon YazılımPazaryerleri ve e-ticaret sitelerine otomatik e-fatura kesimi, sipariş ve kargo takibi hizmetleri sunan e-Dönüşüm platformunun API mimarisini ve yazılım ekibini yönetmektedir.
  • Benim Düğünüm logosu CEO Benim DüğünümDijital etkinlik ve anı paylaşım platformu.
  • Siberdizayn logosu Yazılım Ekibi Lideri SiberdizaynYüksek anlık oyuncu trafiğine sahip oyun kontrol panelleri ve sunucu altyapıları geliştiren yazılım ekibine liderlik etmektedir.
  • MEDYOGRAFYA 360° Dijital Çözümler logosu Dijital Strateji Lideri MEDYOGRAFYA 360° Dijital ÇözümlerŞirketlerin dijital çözümlerde uzun vadede nasıl ilerlemesi gerektiği ve dijital dönüşüm süreçlerinin yönetilmesine destek olmaktadır.
  • İzmir Ekonomi Üniversitesi logosu Danışma Kurulu Üyesi İzmir Ekonomi ÜniversitesiMezun olduğu üniversitesinde, Bilgisayar Programcılığı bölümünün akademik müfredatını güncel sektör ihtiyaçlarına göre şekillendirmek adına Danışma Kurulu'nda görev almaktadır.
WhatsApp Hemen Ara