- Advent of Code 2023 Day 12 जैसे कई special cases वाले problems को भी, अगर वही sub-problems बार-बार हल होने वाला structure मिल जाए, तो dynamic programming से संभाला जा सकता है
- मूल बात यह है कि recursion से problem को तोड़ने के बाद redundant calculations को memoization से घटाया जाए, और ज़रूरी values को dependency order में भरने वाली iterative calculation में बदला जाए
- Fibonacci example में naive recursion
f(1)को बार-बार evaluate करता है, लेकिन cache इस्तेमाल करने परf(0)सेf(n)तक सिर्फ n + 1 values evaluate करनी पड़ती हैं - Levenshtein distance और Advent of Code Day 12 दिखाते हैं कि string length और rule index जैसे state indices को cache key बनाकर recursive calls को array filling में कैसे बदला जाता है
- dynamic programming सीखने से सिर्फ performance improvement ही नहीं, बल्कि algorithm की intermediate states और dependencies भी दिखती हैं, और memory optimization की गुंजाइश ढूंढना आसान हो जाता है
नाम थोड़ा उलझाने वाला है, लेकिन idea सरल है
- “dynamic programming” नाम का आधुनिक “programming style” या “dynamic typing” जैसे अर्थों से सीधा संबंध नहीं है
- इसका मूल मतलब है problem को छोटे, मिलते-जुलते problems में बांटना और उनके results को reuse करने वाली algorithm design approach
- इसमें editorial note जोड़ा गया है कि ऐतिहासिक अर्थ में “programming” को आधार मानें तो यह expression समझ में आता है
- शुरुआत आम तौर पर recursive function जैसी form से होती है, जहां problem को छोटे problems में decompose किया जाता है
- जब वही sub-problems कई बार आते हैं, तो calculation results को store करके फिर से इस्तेमाल करने वाली caching की जरूरत स्वाभाविक रूप से पड़ती है
Fibonacci से caching और iteration में बदलना समझना
- Fibonacci function
f(n) = f(n - 1) + f(n - 2)से define होता है, और naive recursive implementation वही values बार-बार calculate करता है f(1)वह value है जो final result में सचमुच add होती है, इसलिएf(n)बड़ा होने पर naive recursion की evaluation count भी तेजी से बढ़ती है- result को cache या memoize करने पर पहले से calculate किए गए
f(4),f(3),f(2)को फिर calculate करने की जरूरत नहीं रहती - इस approach में
f(0)सेf(6)तक कुल 7 values ही evaluate होती हैं, और सामान्य रूप से यह घटकर n + 1 evaluations रह जाता है - एक कदम आगे बढ़कर,
f(0),f(1)से शुरू करके required values को क्रम से भरें तो recursive calls खत्म हो जाती हैंF[2] = F[1] + F[0]F[3] = F[2] + F[1]- इसी तरह
F[6] = 8तक calculate किया जाता है
- Fibonacci में पूरा array भी जरूरी नहीं; सिर्फ पिछली value और उससे पहले वाली value दो values रखना काफी है
- यह flow mathematical definition से शुरू होकर iterative implementation तक जाने का systematic path दिखाता है
Edit distance example तक विस्तार
- दो strings की edit distance वह minimum number of edits है जो एक string को दूसरी string में बदलने के लिए चाहिए
- किस तरह के edits allow हैं, उसके हिसाब से problem बदल जाती है
- सिर्फ character substitution allow हो तो Hamming distance
- insertion और deletion भी allow हों तो Levenshtein distance
- Levenshtein distance को दो strings
A,Bके आखिरी characters के आधार पर छोटे problems में बांटा जा सकता है- अगर आखिरी characters समान हों, तो दोनों characters को ignore करके बाकी strings की distance इस्तेमाल की जाती है
- अगर आखिरी characters अलग हों, तो substitution, deletion, insertion में से minimum cost चुनी जाती है
- अगर
Aखाली है, तोBके सभी characters insert करने होंगे, इसलिए costbहै - अगर
Bखाली है, तोAके सभी characters delete करने होंगे, इसलिए costaहै
- इस definition को सीधे Python recursion में बदलें तो लंबी strings और अधिक differences वाली strings पर यह बहुत धीमा हो जाता है
- Fibonacci में call tree हर step पर लगभग दो branches में बढ़ता था, जबकि यह recursion situation के अनुसार तीन branches में बढ़ सकता है
- Python का
functools.cacheलगाने पर समान substring combinations के calculation results reuse किए जा सकते हैं - बेहतर implementation नए strings लगातार नहीं बनाती, बल्कि original strings
A,Bऔर substring lengthsa,bही pass करती है - आखिरी step में सीधे 2D
cachearray बनाया जाता है और उसे इस तरह order में भरा जाता है किcache[a][b] = levenstein(A[:a], B[:b])हो - iterative version
aऔरbको 0 से string length तक iterate करता है, और पहले से भरी previous row और previous column की values refer करता है
Advent of Code 2023 Day 12 पर लागू करना
- Advent of Code 2023 के 12 दिसंबर के problem में 1D nonogram solve करना होता है
- example input
.??..??...?##. 1,1,3form में है, और?या तो.या#हो सकता है - brute-force approach backtracking इस्तेमाल करती है, लेकिन question marks
nहों तो 2^n candidates evaluate करने पड़ते हैं, इसलिए यह exponentially बढ़ती है - वही sub-problem repeat होने वाला structure दिखाई देता है
..#..??...?##. (1),1,3.#...??...?##. (1),1,3- पहले से processed prefix को हटा दें तो ये क्रमशः
.??...?##. 1,3,..??...?##. 1,3जैसे लगभग एक जैसे problems बन जाते हैं
- basic backtracking function
conditionsऔरrulesलेकर possible arrangements की संख्या calculate करता है- अगर कोई rule बाकी नहीं है, तो देखता है कि remaining conditions में
#है या नहीं - अगर कोई condition बाकी नहीं है, तो देखता है कि rules बाकी हैं या नहीं
- current character
.या?हो, तो एक position आगे बढ़कर calculate करता है - current character
#या?हो, तो अगले rule size और separator condition की जांच करके next state में जाता है
- अगर कोई rule बाकी नहीं है, तो देखता है कि remaining conditions में
- Python में सिर्फ
@cacheलगाने से memoization apply किया जा सकता है - dynamic programming में बदलने के लिए string और rules को काटकर pass करने के बजाय, string offset
iऔर rule offsetjको state के रूप में इस्तेमाल किया जाता है - इसके बाद
cache[i][j]सीधे बनाया जाता है, और indices को reverse order में भरने के तरीके से recursion को iterative calculation से replace किया जाता है - Rust implementation example लेख के अंदर Rust implementation link में दिया गया है
Cache को सीधे भरने पर क्या दिखता है
- Advent of Code Day 12 का dynamic programming version memoization version से धीमा दिख सकता है
- यह अंतर unoptimized Python implementation की वजह से होने की संभावना है
- cache को सीधे construct करने पर यह ज्यादा अच्छी तरह दिखता है कि कौन-सी values सचमुच जरूरी हैं
- Day 12 problem में dynamic programming version से पुष्टि होती है कि सिर्फ previous column की जरूरत है
- इसलिए 2D array को previous column और current column दिखाने वाले दो 1D arrays में बदला जा सकता है
Practice problems और निष्कर्ष
- dynamic programming मामूली चीज नहीं है, लेकिन ज्यादातर programmers के लिए पहुंच से बाहर technique भी नहीं है
- problem को छोटे problems में बांटने का तरीका समझ लें, तो कई situations में सिर्फ memoization से भी naive implementation की तुलना में बड़ा सुधार मिल सकता है
- और skill आने पर algorithm की एक पूरी family समझ में आती है, trade-offs बेहतर समझ आते हैं, और extra optimizations ढूंढे जा सकते हैं
- practice के लिए ये problems सुझाए गए हैं
- implementation के बाद benchmarking और profiling भूलनी नहीं चाहिए
1 टिप्पणियां
Hacker News की राय
लेख में यह बात अच्छी लगी कि dynamic programming algorithm असल में recursion को cache करने का एक चतुर तरीका भर है। मेरे अनुभव में recursive solution को पहले ढूंढना dynamic programming solution खोजने का सबसे अच्छा starting point है, और एक बार वह मिल जाए तो memoization आसान होता है और बड़ी speed-up दे सकता है
कभी-कभी यह bottom-up dynamic programming से भी तेज़ हो सकता है, क्योंकि यह सिर्फ़ वही solutions calculate करता है जिनकी सच में ज़रूरत होती है। मुख्य बात यह है कि call tree में subproblems बहुत हों तो भी ठीक है, लेकिन अलग-अलग subproblems की संख्या अपेक्षाकृत कम होनी चाहिए। जिस result की ज़रूरत सिर्फ़ एक बार है उसे cache करने का कोई मतलब नहीं, और असली मुश्किल original problem को पर्याप्त रूप से कम संख्या वाले अलग-अलग subproblems में बांटने में होती है
practical तौर पर, tail-call elimination हमेशा लागू नहीं होता, इसलिए वैसा करना सही है, लेकिन काश पहले ज़्यादा intuitive तरीका—top-down recursive cache वाला viewpoint—सिखाया गया होता
उदाहरण के लिए LeetCode की “Best Time to Buy and Sell Stock” series देखें, तो https://leetcode.com/problems/best-time-to-buy-and-sell-stoc... जैसा problem array भरने के तरीके से कहीं ज़्यादा natural लगता है। मैंने इसे recursion से कभी solve नहीं किया, और यह भी ठीक से नहीं जानता कि इसका कोई natural recursive solution है या नहीं
ऊपर वाला link III है, लेकिन पहली बार करने वाले के लिए पहले problem https://leetcode.com/problems/best-time-to-buy-and-sell-stoc... से शुरू करना dynamic programming की अच्छी शुरुआत है
“dynamic programming” नाम की उत्पत्ति इसके आविष्कारक Richard Bellman से हुई। 1950 में RAND में वे multi-stage decision-making process के लिए नाम खोज रहे थे, और कहा जाता है कि उस समय रक्षा मंत्री Wilson को “research” शब्द से लगभग रोगात्मक नफ़रत थी और “mathematics” शब्द से तो और भी बचना पड़ता था
Bellman को RAND के भीतर ऐसा नाम चाहिए था जिससे Wilson और Air Force से यह बात छिपी रहे कि वे असल में mathematics कर रहे हैं। इसलिए, यह planning, decision-making और thinking से जुड़ा था, लेकिन “planning” कई कारणों से ठीक नहीं था, तो उन्होंने “programming” चुना; और multi-stage तथा time-varying concepts को शामिल करने के लिए classical physics में सटीक अर्थ वाले “dynamic” को जोड़ा
उन्हें यह बात भी पसंद आई कि “dynamic” को adjective के रूप में negative sense में इस्तेमाल करना मुश्किल है, और यह ऐसा नाम था जिसका कोई congressman भी आसानी से विरोध नहीं कर सकता था, इसलिए उन्होंने अपनी गतिविधियों को समेटने वाले नाम के रूप में dynamic programming इस्तेमाल किया
स्रोत: https://alliance.seas.upenn.edu/~cis520/dynamic/2021/wiki/in...
इस लेख का तरीका मुझे पसंद आया: पहले problem को recursively expose करना, फिर धीरे-धीरे caching जोड़ना, और अंत में cache size को जितना ज़रूरी हो उतना घटाना
मैं अक्सर सीधे dynamic programming solution पर जाने की कोशिश में अटक जाता था, या उसे काम कराने के लिए ज़बरदस्ती बहुत मेहनत करता था। आगे से मैं खुद को step-by-step stages follow करने के लिए मजबूर करूंगा
dynamic programming का एक शानदार application nucleotide/protein sequences का pairwise alignment है
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...
एक बहुत अच्छे algorithms प्रोफेसर थे, और वे UCLA में पढ़े हुए थे। Dynamic programming की उनकी क्लास शानदार थी: पहले वे ऐसे problem से शुरू करते थे जिसका simple solution exponential time complexity वाला होता था, फिर problem को छोटे problems में बाँटकर complexity को polynomial level तक घटाते थे, और फिर memoization लगाकर उसे linear तक गिरा देते थे
काश याद होता कि उस समय उन्होंने कौन-से problems इस्तेमाल किए थे
ये सभी ऐसे representative examples हैं जहाँ naive solution inefficient होता है और dynamic programming से बहुत बड़ा improvement मिलता है
और उदाहरणों के लिए https://en.wikipedia.org/wiki/Dynamic_programming#Algorithms... देख सकते हैं
जहाँ तक मुझे पता है, “इन दो courses को साथ में लेना होगा” जैसी special constraints जोड़ दी जाएँ तो यह ordinary dynamic programming से कहीं ज्यादा complex और handle करने में मुश्किल हो जाता है
लगता है original site traffic संभाल नहीं पा रही, इसलिए archive link छोड़ रहा हूँ
https://web.archive.org/web/20240114111200/https://qsantos.f...
Dynamic programming की वजह से legal Go board positions की संख्या calculate की जा सकी, और वह value 171 अंकों की संख्या थी
naive तरीके में n×n Go board की सभी possible positions देखने के लिए 3^(n^2) time लगता है, लेकिन dynamic programming effectively एक dimension हटा देता है और time complexity को O(n^5 * 5.4^n), space complexity को O(n * 5.4^n) तक घटा देता है
https://tromp.github.io/go/legal.html
https://tromp.github.io/go/gostate.pdf
“Dynamic Programming” नाम अटपटा लग सकता है, क्योंकि यहाँ programming का मतलब programming field नहीं है। इस case में इसका अर्थ linear programming की तरह optimization के करीब है
Dynamic programming को discrete-time decision problems, यानी constraints के तहत \sum_t u_t(a_t) को maximize करने वाली optimal sequence {a_t} चुनने की method के रूप में देखा जा सकता है। यह value function V* को V*(t) = max_{a_t}{ u_t(a_t) + V*(t-1) } के रूप में define करके optimization problem की dimension को काफी घटा देता है
“डायनेमिक प्रोग्रामिंग” सुनते ही अगर बस memoization समझ लिया जाए, तो क्या यह गलत होगा? शायद छूटा हुआ हिस्सा यह है कि memoization का उपयोग करने के लिए problem को समझदारी से हिस्सों में तोड़ना पड़ता है
Dynamic programming systematic memoization के ज्यादा करीब है। इसमें धीरे-धीरे बड़े subproblems solve करके पूरे problem के solution तक पहुंचते हैं। “inductive algorithm” शब्द भी कुछ हद तक फिट बैठता है, क्योंकि typical dynamic programming algorithm असल में mathematical induction के proof जैसा ही होता है। अफसोस, उस term के पहले से दूसरे meanings हैं
इसके बाद देखते हैं कि recursion और memoization में overhead होता है, और table को bottom-up तरीके से बनाकर recursive calls हटाते हैं, तो dynamic programming बन जाती है
Step 3 dynamic programming का सबसे characteristic हिस्सा है, लेकिन step 2 पर रुकने पर भी इसे dynamic programming कहा जा सकता है, ऐसा मुझे लगता है। बस यह जितना efficient हो सकता है, उतना नहीं होता। दूसरे शब्दों में, memoization caching है, और step 3 यह पूछना है कि क्या उस cache को पहले से भरने का कोई तरीका है
आम तौर पर, अगर subproblems बहुत overlap करते हैं और optimal subproblem पूरे optimal solution का हिस्सा बनना चाहिए, तो dynamic programming का मौका होता है। यह कहना कि सिर्फ memoization ही dynamic programming है, कुछ वैसा है जैसे कहना कि सिर्फ hash table ही abstract data type है
Memoization मूल रूप से algorithm को तेज बनाने की strategy है
इस साल Advent of Code पूरा करना मजेदार रहा। यह साफ था कि Day 1, खासकर Part 2, पिछले सालों की तुलना में कहीं ज्यादा कठिन था, और मैंने इसके बारे में https://blog.singleton.io/posts/2024-01-02-advent-of-code-20... पर भी लिखा, लेकिन सिर्फ मौजूदा 2022 stats और मौजूदा 2023 stats की तुलना करने से बात स्पष्ट नहीं होती। क्योंकि 2022 puzzles को solve करने के लिए लोगों के पास एक साल ज्यादा था
14 जनवरी 2023 के 2022 stats https://web.archive.org/web/20230114172513/https://adventofc... निकालकर देखे, तो अंतर काफी बड़ा था। Part 2 completion stats https://blog.singleton.io/static/imgs-aoc23/completion.png plot करने पर Day 1 की शुरुआती group size मिलती-जुलती थी, लेकिन 2023, Day 15 तक 2022 से साफ तौर पर ज्यादा कठिन दिखता है
Part 1 solve करके Part 2 solve न कर पाने वालों का ratio https://blog.singleton.io/static/imgs-aoc23/ratios.png भी 2023 में कई दिनों पर काफी ज्यादा है, और खासकर Day 5, Day 10, Day 12, और Day 22 Part 2 के कठिन होने की ओर इशारा करता है
हालांकि Day 5 Part 2 कितना कठिन था, यह देखकर हैरानी हुई। मैंने हार माने बिना solve कर लिया, लेकिन लगा कि शायद कोई obvious चीज छूट गई और मैंने बेवजह ज्यादा complex तरीके से solve कर दिया। यह जानकर राहत मिली कि वह वाकई थोड़ा challenging problem था
उदाहरण के तौर पर
two1nine,eightwothree,abcone2threexyz,xtwone3four,4nineeightseven2,zoneight234,7pqrstsixteenदिए गए थे, लेकिनoneightजैसा core example गायब था। ऐसे example के बिना यह ठीक-ठीक पता लगाना मुश्किल है कि values को कैसे replace करना चाहिए2022 में शुरुआती कुछ दिनों तक ज्यादातर लोग लगातार जुड़े रहे, कई दिनों में retention rate 80% से ऊपर था, और लगभग सभी ने दोनों parts solve किए। इसके उलट, 2023 के Day 1 में Part 1 solve करने वालों में से सिर्फ 76% ने Part 2 तक solve किया, और Day 3 व Day 5 पर कई लोगों ने छोड़ दिया
दिलचस्प बात यह है कि आखिरी कुछ दिनों में यह इतना कम नहीं है, जिसे इस बात से समझाया जा सकता है कि 2023 का Advent of Code, 2022 की तुलना में ज्यादा हाल का है। मेरी व्याख्या यह है कि यह group ऐसे लोगों का है जो difficulty की परवाह किए बिना एक हद तक सभी challenges पार कर लेते हैं, और बाकी कई लोग जब महसूस करते हैं कि इसमें बहुत ज्यादा समय लग रहा है, तो छोड़ देते हैं