Nedir dinamik programlama ?
Özyineleme, not, vb. Arasında ne fark vardır?
Üzerinde wikipedia makalesini okudum , ama hala gerçekten anlamıyorum.
Nedir dinamik programlama ?
Özyineleme, not, vb. Arasında ne fark vardır?
Üzerinde wikipedia makalesini okudum , ama hala gerçekten anlamıyorum.
Yanıtlar:
Dinamik programlama, gelecekteki bir sorunu çözmeyi kolaylaştırmak için geçmiş bilgisini kullandığınız zamandır.
Bunun iyi bir örneği n = 1,000,002 için Fibonacci dizisini çözmektir.
Bu çok uzun bir süreç olacak, ama n = 1,000,000 ve n = 1,000,001 için sonuçları verirsem? Aniden sorun daha kolay yönetilebilir hale geldi.
Dinamik programlama, string düzenleme problemi gibi string problemlerinde çok kullanılır. Sorunun bir alt kümesini / gruplarını çözersiniz ve sonra bu bilgileri daha zor orijinal sorunu çözmek için kullanırsınız.
Dinamik programlama ile, sonuçlarınızı genellikle bir tür tabloda saklarsınız. Bir sorunun cevabına ihtiyacınız olduğunda, tabloya bakın ve bunun ne olduğunu zaten bilip bilmediğinize bakın. Değilse, tablonuzdaki verileri kendinize cevaba doğru bir atlama taşı vermek için kullanırsınız.
Cormen Algorithms kitabının dinamik programlama hakkında harika bir bölümü var. VE Google Kitaplar'da ücretsiz! Buradan kontrol edin .
Dinamik programlama, özyinelemeli algoritmada aynı alt sorunun birden çok kez hesaplanmasını önlemek için kullanılan bir tekniktir.
Let Fibonacci sayılarının basit örneği inceleyelim: n bularak inci tanımlanan Fibonacci sayı
F n = F n-1 + F n-2 ve F 0 = 0, F 1 = 1
Bunu yapmanın bariz yolu özyinelemelidir:
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
Özyineleme çok sayıda gereksiz hesaplama yapar, çünkü belirli bir Fibonacci sayısı birden çok kez hesaplanır. Bunu iyileştirmenin kolay bir yolu, sonuçları önbelleğe almaktır:
cache = {}
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
if n in cache:
return cache[n]
cache[n] = fibonacci(n - 1) + fibonacci(n - 2)
return cache[n]
Bunu yapmanın daha iyi bir yolu, sonuçları doğru sırayla değerlendirerek özyineleme işleminden hep birlikte kurtulmaktır:
cache = {}
def fibonacci(n):
cache[0] = 0
cache[1] = 1
for i in range(2, n + 1):
cache[i] = cache[i - 1] + cache[i - 2]
return cache[n]
Sabit alanı bile kullanabilir ve yol boyunca sadece gerekli kısmi sonuçları saklayabiliriz:
def fibonacci(n):
fi_minus_2 = 0
fi_minus_1 = 1
for i in range(2, n + 1):
fi = fi_minus_1 + fi_minus_2
fi_minus_1, fi_minus_2 = fi, fi_minus_1
return fi
Dinamik programlama nasıl uygulanır?
Dinamik programlama genellikle dizeler, ağaçlar veya tamsayı dizileri gibi doğal olarak soldan sağa bir sıraya sahip problemler için çalışır. Saf özyinelemeli algoritma aynı alt sorunu birden çok kez hesaplamazsa, dinamik programlama yardımcı olmaz.
Mantığı anlamanıza yardımcı olacak bir sorun koleksiyonu yaptım: https://github.com/tristanguigue/dynamic-programing
if n in cache
yukarıdan aşağıya örnekte olduğu gibi veya bir şey eksik mi?
Memoization, bir işlev çağrısının önceki sonuçlarını kaydettiğiniz zamandır (gerçek bir işlev, aynı girdiler verildiğinde her zaman aynı şeyi döndürür). Sonuçlar saklanmadan önce algoritmik karmaşıklık için fark yaratmaz.
Özyineleme, genellikle daha küçük bir veri kümesiyle kendini çağıran bir işlevin yöntemidir. Yinelemeli işlevlerin çoğu benzer yinelemeli işlevlere dönüştürülebildiğinden, bu algoritmik karmaşıklık için de bir fark yaratmaz.
Dinamik programlama, daha kolay çözülebilen alt problemleri çözme ve bunun cevabını oluşturma sürecidir. Çoğu DP algoritması, bir Açgözlü algoritması (varsa) ve bir üstel (tüm olasılıkları numaralandırın ve en iyisini bulun) algoritması arasındaki çalışma sürelerinde olacaktır.
Algoritmanızın çalışma süresini kısaltan bir optimizasyonu.
Açgözlü Algoritma genellikle saf denir , aynı veri kümesi üzerinde birden çok kez çalışabileceğinden, Dinamik Programlama, nihai çözümü oluşturmaya yardımcı olması için saklanması gereken kısmi sonuçların daha derin bir şekilde anlaşılmasıyla bu tuzaktan kaçınır.
Basit bir örnek, bir ağaca veya grafiğe yalnızca çözüme katkıda bulunacak düğümlerden geçerek veya şimdiye kadar bulduğunuz çözümleri bir tabloya yerleştirerek aynı düğümleri tekrar tekrar gezmekten kaçınmaktır.
UVA'nın çevrimiçi hakimlerinden dinamik programlama için uygun bir sorun örneği: Edit Steps Ladder.
Programlama Zorlukları kitabından alınan bu sorunun analizinin önemli bir kısmını hızlı bir şekilde bilgilendireceğim, bir göz atmanızı öneririm.
Bu soruna iyi bakın, bize iki dizenin ne kadar uzak olduğunu söyleyen bir maliyet fonksiyonu tanımlarsak, iki tür üç doğal değişiklik türünü göz önünde bulundururuz:
Değiştirme - "s" deseninden tek bir karakteri "t" metninde "çekim" i "nokta" olarak değiştirmek gibi farklı bir karakterle değiştirin.
Ekleme - "t" metniyle eşleşmesine yardımcı olması için, "ago" metnini "agog" olarak değiştirmek gibi "s" desenine tek bir karakter ekleyin.
Silme - "t" metniyle eşleşmesine yardımcı olması için "s" harfinden tek bir karakteri silerek "saat" i "bizim" olarak değiştirme.
Bu işlemlerin her birini bir adım maliyet olarak ayarladığımızda, iki dize arasındaki düzenleme mesafesini tanımlarız. Peki bunu nasıl hesaplıyoruz?
Dizideki son karakterin eşleştirilmesi, ikame edilmesi, eklenmesi veya silinmesi gerektiği gözlemini kullanarak özyinelemeli bir algoritma tanımlayabiliriz. Son düzenleme işlemindeki karakterlerin kesilmesi, bir çift işlemin bir çift daha küçük dize bırakmasını sağlar. İ ve j sırasıyla ve t'nin ilgili önekinin son karakteri olsun. son işlemden sonra, bir eşleşme / değiştirme, ekleme veya silme işleminden sonra dizeye karşılık gelen üç çift daha kısa dize vardır. Üç çift daha küçük dizeyi düzenleme maliyetini biliyorsak, hangi seçeneğin en iyi çözüme yol açacağına karar verebilir ve bu seçeneği buna göre seçebiliriz. Bu maliyeti, özyineleme olan harika şeyle öğrenebiliriz:
#define MATCH 0 /* enumerated type symbol for match */ #define INSERT 1 /* enumerated type symbol for insert */ #define DELETE 2 /* enumerated type symbol for delete */ int string_compare(char *s, char *t, int i, int j) { int k; /* counter */ int opt[3]; /* cost of the three options */ int lowest_cost; /* lowest cost */ if (i == 0) return(j * indel(’ ’)); if (j == 0) return(i * indel(’ ’)); opt[MATCH] = string_compare(s,t,i-1,j-1) + match(s[i],t[j]); opt[INSERT] = string_compare(s,t,i,j-1) + indel(t[j]); opt[DELETE] = string_compare(s,t,i-1,j) + indel(s[i]); lowest_cost = opt[MATCH]; for (k=INSERT; k<=DELETE; k++) if (opt[k] < lowest_cost) lowest_cost = opt[k]; return( lowest_cost ); }
Bu algoritma doğrudur, ancak imkansız bir şekilde yavaştır.
Bilgisayarımızda çalıştığımızda, iki 11 karakterli dizeyi karşılaştırmak birkaç saniye alır ve hesaplama hiçbir zaman daha uzun bir şeye asla inmez.
Algoritma neden bu kadar yavaş? Üstel zaman alır çünkü değerleri tekrar tekrar hesaplar. Dizideki her pozisyonda, özyineleme üç şekilde dallanır, yani en azından 3 ^ n oranında büyür - aslında, daha da hızlıdır, çünkü çağrıların çoğu her ikisini de değil, iki endeksten sadece birini azaltır.
Peki algoritmayı nasıl pratik hale getirebiliriz? Önemli gözlem, bu özyinelemeli çağrıların çoğunun daha önce hesaplanmış olan şeyleri hesaplamasıdır. Nasıl bilebiliriz? Sadece | s | · | T | olası benzersiz özyinelemeli çağrılar, çünkü özyinelemeli çağrıların parametreleri olarak işlev görecek çok farklı (i, j) çiftleri vardır.
Bu (i, j) çiftlerinin her birinin değerlerini bir tabloda depolayarak, bunları yeniden hesaplamaktan kaçınabilir ve sadece gerektiğinde ararız.
Tablo iki boyutlu bir matris m olup burada | s | · | t | hücreleri, bu alt problemin en uygun çözümünün maliyetini ve bu konuma nasıl geldiğimizi açıklayan bir üst işaretçiyi içerir:
typedef struct { int cost; /* cost of reaching this cell */ int parent; /* parent cell */ } cell; cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */
Dinamik programlama sürümü özyinelemeli sürümden üç farklılığa sahiptir.
İlk olarak, ara değerlerini yinelemeli çağrılar yerine tablo aramasını kullanarak alır.
** İkinci olarak, ** her hücrenin üst alanını güncelleyerek düzenleme sırasını daha sonra yeniden oluşturmamızı sağlayacaktır.
** Üçüncüsü, Üçüncüsü,
cell()
sadece m [| s |] [| t |] .cost döndürmek yerine daha genel bir hedef fonksiyonu kullanılarak gerçekleştirilmiştir. Bu, bu rutini daha geniş bir sorun sınıfına uygulamamızı sağlayacaktır.
Burada, en uygun kısmi sonuçları toplamak için gerekenlerin çok özel bir analizi, çözümü “dinamik” yapan şeydir.
İşte aynı soruna alternatif, tam bir çözüm. Ayrıca, yürütülmesi farklı olsa da "dinamik" bir programdır. UVA'nın çevrimiçi hakemine göndererek çözümün ne kadar verimli olduğunu kontrol etmenizi öneririm. Böyle ağır bir sorunun nasıl bu kadar verimli bir şekilde ele alındığını şaşırtıcı buluyorum.
Dinamik programlamanın temel bitleri "örtüşen alt problemler" ve "optimal alt yapı" dır. Bir problemin bu özellikleri, optimal bir çözümün, alt problemlerine en uygun çözümlerden oluştuğu anlamına gelir. Örneğin, en kısa yol problemleri en uygun alt yapıyı gösterir. A'dan C'ye en kısa yol, A'dan bazı B düğümüne en kısa yoldur, ardından bu düğümden B'ye C en kısa yoldur.
Daha ayrıntılı olarak, en kısa yol problemini çözmek için:
Aşağıdan yukarıya çalıştığımızdan, bunları kullanma zamanı geldiğinde, alt problemlere onları hatırlayarak çözümlerimiz var.
Unutmayın, dinamik programlama problemleri hem örtüşen alt problemlere hem de optimal altyapıya sahip olmalıdır. Fibonacci dizisinin oluşturulması dinamik bir programlama problemi değildir; üst üste binen alt problemlere sahip olduğu için, ancak optimal bir altyapıya sahip olmadığı için (not optimizasyon problemi olmadığı için) hatırlatma kullanır.
Dinamik program
Tanım
Dinamik programlama (DP), çakışan alt problemlerle ilgili problemleri çözmek için kullanılan genel bir algoritma tasarım tekniğidir. Bu teknik 1950'lerde Amerikalı matematikçi “Richard Bellman” tarafından icat edildi.
Anahtar fikir
Anahtar fikir, yeniden hesaplamayı önlemek için çakışan daha küçük alt sorunların cevaplarını kaydetmektir.
Dinamik Programlama Özellikleri
Ayrıca Dinamik Programlama (belirli tür problemler için güçlü bir algoritma) konusunda çok yeniyim
En basit ifadeyle, dinamik programlamayı önceki bilgileri kullanarak özyinelemeli bir yaklaşım olarak düşünün
Önceki bilgi burada en önemli şeydir, Zaten sahip olduğunuz alt sorunların çözümünü takip edin.
Bunu düşünün, Wikipedia'dan dp için en temel örnek
Fibonacci dizisini bulma
function fib(n) // naive implementation
if n <=1 return n
return fib(n − 1) + fib(n − 2)
İşlev çağrısını say n = 5 ile parçalayalım
fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
Özellikle, fib (2) sıfırdan üç kez hesaplandı. Daha büyük örneklerde, fib veya alt problemlerin daha birçok değeri yeniden hesaplanarak üstel bir zaman algoritmasına yol açar.
Şimdi, zaten bir veri yapısında öğrendim değerini saklayarak denemek sağlayan bir say Haritası
var m := map(0 → 0, 1 → 1)
function fib(n)
if key n is not in map m
m[n] := fib(n − 1) + fib(n − 2)
return m[n]
Burada, eğer zaten yoksa, alt sorunların çözümünü haritaya kaydediyoruz. Daha önce hesaplamış olduğumuz değerleri kaydetme tekniği Memoization olarak adlandırılmıştır.
Sonunda, bir sorun için, önce durumları bulmaya çalışın (olası alt problemler ve önceki alt problemin çözümünü başkalarına da kullanabilmeniz için daha iyi özyineleme yaklaşımını düşünmeye çalışın).
Dinamik programlama, çakışan alt problemlerle ilgili problemleri çözmek için bir tekniktir. Dinamik bir programlama algoritması her alt problemi sadece bir kez çözer ve ardından cevabını bir tabloya (dizi) kaydeder. Alt sorunla her karşılaşıldığında cevabı yeniden hesaplama işinden kaçınmak. Dinamik programlamanın altında yatan fikir şudur: Aynı şeyleri, genellikle alt sorunların bilinen sonuçlarının bir tablosunu tutarak iki kez hesaplamaktan kaçının.
Dinamik bir programlama algoritmasının geliştirilmesindeki yedi adım aşağıdaki gibidir:
6. Convert the memoized recursive algorithm into iterative algorithm
zorunlu adım? Bu son şeklinin özyinelemesiz olduğu anlamına mı gelir?
kısaca özyineleme notlama ve Dinamik programlama arasındaki fark
Adından da anlaşılacağı gibi dinamik programlama, bir sonraki yeni çözümü dinamik olarak oluşturmak için önceki hesaplanan değeri kullanıyor
Dinamik programlama nasıl uygulanır: Çözümünüz en uygun altyapıya ve çakışan alt soruna dayanıyorsa, bu durumda daha önce hesaplanan değeri kullanmak yararlı olacaktır, böylece yeniden hesaplamanız gerekmez. Aşağıdan yukarıya yaklaşımdır. Bu durumda fib (n) 'yi hesaplamanız gerektiğini varsayalım, tek yapmanız gereken önceki hesaplanmış fib (n-1) ve fib (n-2) değerlerini eklemektir.
Özyineleme: Temelde, sorunu kolayca çözmek için daha küçük bir bölüme ayırırsınız, ancak daha önce diğer özyineleme çağrısında hesaplanmış aynı değere sahipsek, yeniden hesaplamayı önlemez.
Memoization: Eski hesaplanan özyineleme değerinin tabloya kaydedilmesi, önceki bir çağrı tarafından zaten hesaplanmışsa yeniden hesaplamayı önleyecek memoizasyon olarak bilinir, böylece herhangi bir değer bir kez hesaplanır. Hesaplamadan önce, bu değerin önceden hesaplanıp hesaplanmadığını kontrol ediyoruz, eğer daha önce hesaplanmışsa, aynı şeyi yeniden hesaplama yerine tablodan döndürüyoruz. Ayrıca yukarıdan aşağıya bir yaklaşımdır
İşte basit bir piton kod örneği Recursive
, Top-down
,Bottom-up
Fibonacci serisi için yaklaşım:
def fib_recursive(n):
if n == 1 or n == 2:
return 1
else:
return fib_recursive(n-1) + fib_recursive(n-2)
print(fib_recursive(40))
def fib_memoize_or_top_down(n, mem):
if mem[n] is not 0:
return mem[n]
else:
mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
return mem[n]
n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))
def fib_bottom_up(n):
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
if n == 1 or n == 2:
return 1
for i in range(3, n+1):
mem[i] = mem[i-1] + mem[i-2]
return mem[n]
print(fib_bottom_up(40))