Dinamik programlama kara büyü değildir
(qsantos.fr)- Advent of Code 2023 Day 12 gibi özel durumu çok olan problemler bile, aynı alt problemi tekrar tekrar çözme yapısı bulunursa dinamik programlama ile ele alınabilir
- Esas fikir, problemi özyinelemeyle parçalara ayırdıktan sonra yinelenen hesaplamaları memoization ile azaltmak ve gerekli değerleri bağımlılık sırasına göre dolduran yinelemeli hesaplamaya taşımaktır
- Fibonacci örneğinde saf özyineleme
f(1)değerini tekrar tekrar değerlendirir; ancak önbellek kullanılırsaf(0)’danf(n)’e kadar yalnızca n + 1 değer değerlendirilir - Levenshtein mesafesi ve Advent of Code Day 12, dize uzunluğu ve kural indeksi gibi durum indekslerini önbellek anahtarı olarak kullanıp özyinelemeli çağrıları dizi doldurmaya dönüştürme sürecini gösterir
- Dinamik programlamayı öğrenmek yalnızca performansı iyileştirmekle kalmaz; algoritmanın ara durumlarını ve bağımlılık ilişkilerini görünür kılar, bellek optimizasyonu fırsatlarını bulmayı da kolaylaştırır
Adı kafa karıştırıcı, ama fikir basit
- “dynamic programming” adı, modern anlamdaki “programlama stili” ya da “dinamik tip” gibi kavramlarla doğrudan ilişkili değildir
- Özünde, problemi daha küçük ve benzer problemlere bölüp sonuçları yeniden kullanan bir algoritma tasarımı yaklaşımıdır
- Tarihsel anlamdaki “programming” üzerinden bakıldığında bu ifadenin anlam kazandığını belirten bir editör notu eklenmiştir
- Başlangıç noktası genellikle problemi daha küçük problemlere ayıran özyinelemeli fonksiyon biçimidir
- Aynı alt problem birden çok kez ortaya çıkıyorsa, hesaplama sonucunu saklayıp yeniden kullanmaya yarayan önbellekleme doğal olarak gerekli hale gelir
Fibonacci ile önbellekleme ve yinelemeli hale getirme
- Fibonacci fonksiyonu
f(n) = f(n - 1) + f(n - 2)olarak tanımlanır; saf özyinelemeli uygulama aynı değerleri tekrar tekrar hesaplar f(1)nihai sonuca gerçekten eklenen bir değer olduğu için,f(n)büyüdükçe saf özyinelemenin değerlendirme sayısı da hızla artar- Sonuçları önbelleğe almak veya memoization kullanmak, önceden hesaplanmış
f(4),f(3),f(2)değerlerini yeniden hesaplama gereğini ortadan kaldırır - Bu yöntemde
f(0)’danf(6)’ya kadar toplam yalnızca 7 değer değerlendirilir; genel olarak bu sayı n + 1 değerlendirmeye düşer - Bir adım daha ileri gidip gerekli değerler
f(0),f(1)’den başlayarak sırayla doldurulursa özyinelemeli çağrılar ortadan kalkarF[2] = F[1] + F[0]F[3] = F[2] + F[1]- Aynı şekilde
F[6] = 8değerine kadar hesaplanır
- Fibonacci’de tüm diziye de gerek yoktur; yalnızca bir önceki değer ve ondan önceki değer olmak üzere iki değeri tutmak yeterlidir
- Bu akış, matematiksel tanımdan başlayıp yinelemeli uygulamaya geçmek için sistematik bir yol gösterir
Düzenleme mesafesi örneğine genişletme
- İki dizenin düzenleme mesafesi, bir dizeyi diğerine dönüştürmek için gereken en az düzenleme sayısıdır
- İzin verilen düzenleme türlerine göre problem değişir
- Yalnızca karakter değiştirmeye izin verilirse Hamming distance
- Ekleme ve silmeye de izin verilirse Levenshtein distance
- Levenshtein mesafesi, iki dize
AveB’nin son karakterleri temel alınarak daha küçük problemlere bölünebilir- Son karakterler aynıysa iki karakter yok sayılır ve kalan dizelerin mesafesi kullanılır
- Son karakterler farklıysa değiştirme, silme ve ekleme arasından en düşük maliyet seçilir
AboşsaB’deki tüm karakterlerin eklenmesi gerekir; bu yüzden maliyetbolurBboşsaA’daki tüm karakterlerin silinmesi gerekir; bu yüzden maliyetaolur
- Bu tanımı doğrudan Python özyinelemesine aktarınca uzun dizelerde ve aralarında çok fark olan dizelerde çok yavaşlar
- Fibonacci çağrı ağacının her seviyesinde kabaca iki dala ayrılırken, bu özyineleme duruma göre üç dala ayrılır
- Python’ın
functools.cachedekoratörü eklendiğinde aynı alt dize kombinasyonlarının hesaplama sonuçları yeniden kullanılabilir - Daha iyi bir uygulama, sürekli yeni dize oluşturmak yerine özgün dizeler
A,Bile alt dize uzunluklarıa,bdeğerlerini geçirir - Son aşamada doğrudan 2 boyutlu bir
cachedizisi oluşturulur vecache[a][b] = levenstein(A[:a], B[:b])olacak şekilde sırayla doldurulur - Yinelemeli sürüm,
avebdeğerlerini 0’dan dize uzunluğuna kadar dolaşır ve önceden doldurulmuş önceki satır ile önceki sütundaki değerlere başvurur
Advent of Code 2023 Day 12’ye uygulama
- Advent of Code 2023 12 Aralık problemi, 1 boyutlu bir nonogram çözme problemidir
- Örnek girdi
.??..??...?##. 1,1,3biçimindedir;?,.veya#olabilir - Brute force yaklaşımı backtracking kullanır; ancak soru işareti sayısı
nise 2^n aday değerlendirilmesi gerektiğinden üstel olarak büyür - Aynı alt problemlerin tekrarlandığı bir yapı ortaya çıkar
..#..??...?##. (1),1,3.#...??...?##. (1),1,3- Önceden işlenmiş baş kısım atıldığında bunlar sırasıyla
.??...?##. 1,3,..??...?##. 1,3gibi neredeyse aynı problemlere dönüşür
- Temel backtracking fonksiyonu
conditionsverulesalır ve olası yerleşim sayısını hesaplar- Kural kalmamışsa kalan koşullarda
#olup olmadığına bakar - Koşul kalmamışsa kural kalıp kalmadığını kontrol eder
- Geçerli karakter
.veya?ise bir hücre ilerleyerek hesaplar - Geçerli karakter
#veya?ise sonraki kural boyutunu ve ayırıcı koşulunu kontrol ettikten sonra sonraki duruma geçer
- Kural kalmamışsa kalan koşullarda
- Python’da yalnızca
@cacheeklemek memoization uygulamak için yeterlidir - Dinamik programlamaya dönüştürmek için dizeleri ve kuralları kesip geçirmek yerine, durum olarak dize ofseti
ive kural ofsetijkullanılır - Ardından
cache[i][j]doğrudan oluşturulur ve indeksler ters sırada doldurularak özyineleme yinelemeli hesaplamayla değiştirilir - Rust uygulama örneği yazıdaki Rust implementation bağlantısında verilmiştir
Önbelleği doğrudan doldurunca görülenler
- Advent of Code Day 12’nin dinamik programlama sürümü, memoization sürümünden daha yavaş görünebilir
- Bu farkın nedeni optimize edilmemiş Python uygulaması olabilir
- Önbelleği doğrudan kurmak, hangi değerlere gerçekten ihtiyaç duyulduğunu daha iyi gösterir
- Day 12 probleminde dinamik programlama sürümü sayesinde yalnızca önceki sütunun gerekli olduğu görülebilir
- Bu nedenle 2 boyutlu dizi, önceki sütunu ve mevcut sütunu temsil eden iki adet 1 boyutlu diziye dönüştürülebilir
Pratik yapılabilecek problemler ve sonuç
- Dinamik programlama önemsiz değildir; ancak çoğu programcı için erişilemez bir teknik de değildir
- Problemi küçük parçalara bölme yöntemini anlamak, birçok durumda yalnızca memoization ile bile saf uygulamaya göre büyük iyileştirme sağlayabilir
- Daha fazla ustalaştıkça bir algoritma ailesini anlayabilir, trade-off’ları daha iyi kavrayabilir ve ek optimizasyonlar bulabilirsiniz
- Pratik için şu problemler önerilir
- Uygulamadan sonra benchmark ve profiling yapmayı unutmamak gerekir
1 yorum
Hacker News yorumları
Yazıda dinamik programlama algoritmasının, özyinelemeyi önbelleğe almanın akıllıca bir yolundan ibaret olduğuna dikkat çekilmesini sevdim. Deneyimime göre özyinelemeli çözümü önce bulmak, dinamik programlama çözümünü bulmak için en iyi başlangıç noktası; bir kez bulunca memoization kolaydır ve büyük hız artışı sağlayabilir.
Hatta bazen aşağıdan yukarıya dinamik programlamadan daha hızlı olabilir, çünkü yalnızca gerçekten gereken çözümleri hesaplar. Kilit nokta şu: çağrı ağacında çok sayıda alt problem olması sorun değil, ama birbirinden farklı alt problem sayısının görece az olması gerekir. Yalnızca bir kez gereken sonucu önbelleğe almanın anlamı yoktur; asıl zorluk, özgün problemi yeterince az sayıda farklı alt probleme ayırabilmektir.
Pratikte kuyruk çağrısı optimizasyonu her zaman uygulanmadığından bunu yapmak mantıklı; ama keşke önce daha sezgisel olan yukarıdan aşağıya özyinelemeli önbellek bakış açısıyla öğrenseydim diye düşünüyorum.
Örneğin LeetCode’daki “Hisse Alıp Satmak İçin En İyi Zaman” serisine bakarsak, https://leetcode.com/problems/best-time-to-buy-and-sell-stoc... gibi bir problemde diziyi doldurma yaklaşımı çok daha doğal değil mi? Bunu özyinelemeyle hiç çözmedim; doğal bir özyinelemeli çözümü var mı onu da pek bilmiyorum.
Yukarıdaki bağlantı III’e ait, ama ilk kez deneyenler için ilk problemden, https://leetcode.com/problems/best-time-to-buy-and-sell-stoc... başlayıp dinamik programlamaya giriş yapmak iyi olur.
“Dinamik programlama” adının kökeni, mucidi Richard Bellman’a dayanıyor. 1950’de RAND’de çok aşamalı karar verme sürecine verilecek bir ad arıyordu; o dönemde Savunma Bakanı Wilson’ın “araştırma” sözcüğünden hastalık derecesinde nefret ettiği, “matematik” sözünden ise daha da çok kaçınmak gerektiği söyleniyor.
Bellman’ın, RAND içinde aslında matematik yaptıkları gerçeğini Wilson’dan ve Hava Kuvvetleri’nden gizleyecek bir ada ihtiyacı vardı. Bu yüzden planlama, karar verme ve düşünmeyle ilgili olsa da “planning” çeşitli nedenlerle iyi bir seçenek değildi; bunun yerine “programming”i seçti ve çok aşamalılık ile zaman içinde değişim kavramını yansıtmak için klasik fizikte kesin bir anlamı olan “dynamic” sözcüğünü ekledi.
“Dynamic”in sıfat olarak olumsuz anlamda kullanılmasının zor olması da hoşuna gitmişti; bir milletvekilinin de karşı çıkmakta zorlanacağı bir ad olduğundan, dynamic programmingi kendi faaliyetlerini kapsayan ad olarak kullandığı söyleniyor.
Kaynak: https://alliance.seas.upenn.edu/~cis520/dynamic/2021/wiki/in...
Bu yazının önce problemi özyinelemeli biçimde ortaya koyup, ardından kademeli olarak caching eklemesini ve en sonunda önbellek boyutunu yalnızca gerektiği kadar küçültmesini beğendim.
Ben çoğu zaman doğrudan dinamik programlama çözümüne gitmeye çalışıp takıldım ya da çalışır hale getirmek için gereksiz yere zorladım. Bundan sonra kendimi adımları sırayla izlemeye zorlamayı düşünüyorum.
Dinamik programlamanın güzel uygulamalarından biri, nükleotid/protein dizilerinin ikili hizalanmasıdır.
https://en.wikipedia.org/wiki/Sequence_alignment
https://en.wikipedia.org/wiki/Needleman%E2%80%93Wunsch_algor...
https://en.wikipedia.org/wiki/Smith%E2%80%93Waterman_algorit...
Çok iyi bir algoritmalar hocamız vardı; UCLA’de okumuştu. Dinamik programlama dersi harikaydı: önce basit çözümü üstel zaman karmaşıklığına sahip bir problemle başlıyor, sonra problemi daha küçük problemlere bölerek karmaşıklığı polinom düzeyine indiriyor, ardından memoization uygulayarak lineere düşürüyordu.
O zaman kullandığı problemlerin neler olduğunu hatırlayabilsem keşke
Hepsi naif çözümü verimsiz olan ve dinamik programlamayla büyük ölçüde iyileşen tipik örnekler
Daha fazla örnek için https://en.wikipedia.org/wiki/Dynamic_programming#Algorithms... sayfasına bakılabilir
“Bu iki ders birlikte alınmalı” gibi özel kısıtlar eklendiğinde, bildiğim kadarıyla sıradan dinamik programlamadan çok daha karmaşık ve ele alınması zor hâle geliyor
Asıl site trafiği kaldıramıyor gibi göründüğü için arşiv bağlantısını bırakıyorum
https://web.archive.org/web/20240114111200/https://qsantos.f...
Dinamik programlama sayesinde yasal Go konumlarının sayısı hesaplanabildi ve bu değer 171 basamaklı bir sayıydı.
Naif yöntem, n×n Go tahtasındaki tüm olası konumlara baktığı için 3^(n^2) zaman alır; dinamik programlama ise fiilen bir boyutu ortadan kaldırarak zaman karmaşıklığını O(n^5 * 5.4^n), alan karmaşıklığını O(n * 5.4^n) düzeyine indirir.
https://tromp.github.io/go/legal.html
https://tromp.github.io/go/gostate.pdf
“Dynamic Programming” adının tuhaf görünmesinin nedeni, buradaki programming’in programlama alanını ifade etmemesi olabilir. Bu durumda anlamı, doğrusal programlamaya benzer şekilde daha çok optimizasyona yakındır.
Dinamik programlama, ayrık zamandaki karar problemlerini; yani kısıtlar altında \sum_t u_t(a_t)’yi maksimize eden en iyi {a_t} sırasını seçme problemini çözme yöntemi olarak görülebilir. Değer fonksiyonu V*’ı V*(t) = max_{a_t}{ u_t(a_t) + V*(t-1) } şeklinde tanımlayarak optimizasyon probleminin boyutunu büyük ölçüde azaltır
“Dinamik programlama” dendiğinde bunu sadece memoization olarak düşünmek yanlış mı? Eksik kalan kısım, memoization kullanabilmek için problemi akıllıca parçalara ayırmak olabilir
Dinamik programlama ise sistematik memoization’a daha yakındır. Giderek daha büyük alt problemleri çözerek tüm problemin çözümüne ulaşırsınız. “Tümevarım algoritması” ifadesi de bir ölçüde uyar; çünkü tipik bir dinamik programlama algoritması aslında matematiksel tümevarım ispatına benzer. Ne yazık ki bu terimin zaten başka anlamları var
Ardından recursive çağrıların ve memoization’ın overhead’i olduğunu görüp tabloyu aşağıdan yukarıya kurarak recursive çağrıları kaldırınca dinamik programlama oluyor
3. adım dinamik programlamanın en karakteristik kısmıdır, ama 2. adımda dursanız da buna dinamik programlama denebileceğini düşünüyorum. Yalnız mümkün olan kadar verimli değildir. Başka bir deyişle memoization cache’lemedir; 3. adım ise “bu cache’i önceden doldurmanın bir yolu var mı?” diye sormaktır
Genel olarak alt problemler çokça örtüşüyorsa ve optimal alt problem, genel optimal çözümün bir parçası olmak zorundaysa dinamik programlama için fırsat vardır. Sadece memoization’ın dinamik programlama olduğunu söylemek, yalnızca hash table’ın soyut veri tipi olduğunu söylemeye benzer
Memoization temelde bir algoritmayı hızlandırma stratejisidir
Bu yıl Advent of Code’u bitirmek keyifliydi. 1. günün, özellikle de 2. bölümün önceki yıllara göre çok daha zor olduğu açıktı ve bunu https://blog.singleton.io/posts/2024-01-02-advent-of-code-20... adresinde de yazdım; ancak mevcut 2022 istatistikleriyle mevcut 2023 istatistiklerini karşılaştırmak tek başına net değil. Çünkü insanların 2022 bulmacalarını çözmek için fazladan bir yılı vardı
14 Ocak 2023 tarihli 2022 istatistiklerini https://web.archive.org/web/20230114172513/https://adventofc... aldığımda fark oldukça büyüktü. 2. bölüm tamamlama istatistiklerini https://blog.singleton.io/static/imgs-aoc23/completion.png çizdiğinizde, 1. gün başlangıç kitlesinin boyutu benzerdi; ancak 2023, 15. güne kadar 2022’den belirgin şekilde daha zor görünüyor
Yine de 5. gün 2. bölümün ne kadar zor olduğuna şaşırdım. Pes etmeden çözdüm ama sanki bariz bir şeyi kaçırıp gereğinden karmaşık çözmüşüm gibi hissetmiştim; aslında biraz zorlayıcı bir problem olduğunu öğrenince rahatladım
Örnek olarak
two1nine,eightwothree,abcone2threexyz,xtwone3four,4nineeightseven2,zoneight234,7pqrstsixteenverilmişti; ancakoneightgibi kritik bir örnek eksikti. Böyle bir örnek olmadan değerleri tam olarak nasıl değiştirmek gerektiğini anlamak zor2022’de ilk birkaç gün boyunca çoğu kişi katılmaya devam etti, birçok günde elde tutma oranı %80’in üzerindeydi ve neredeyse herkes iki bölümü de çözdü. Buna karşılık 2023’te 1. gün 1. bölümü çözenler arasında 2. bölümü de çözenlerin oranı yalnızca %76’ydı; 3. ve 5. günlerde ise çok kişi pes etti
İlginç şekilde son birkaç gün o kadar düşük değil; bu, 2023 Advent of Code’un 2022’ye göre daha yakın tarihli olmasıyla açıklanabilir. Benim yorumum şu: Bu grup, zorluktan bağımsız olarak belli bir noktaya kadar tüm meydan okumaları aşan insanlardan oluşuyor; diğer birçok kişi ise bunun çok fazla zaman aldığını hissedince bırakıyor