गतिशील प्रोग्रामिंग क्या है ?
यह पुनरावृत्ति, संस्मरण आदि से कैसे भिन्न है?
मैंने इस पर विकिपीडिया लेख पढ़ा है, लेकिन मैं अभी भी इसे वास्तव में नहीं समझता हूं।
गतिशील प्रोग्रामिंग क्या है ?
यह पुनरावृत्ति, संस्मरण आदि से कैसे भिन्न है?
मैंने इस पर विकिपीडिया लेख पढ़ा है, लेकिन मैं अभी भी इसे वास्तव में नहीं समझता हूं।
जवाबों:
डायनेमिक प्रोग्रामिंग तब होती है जब आप भविष्य की समस्या को हल करने के लिए पिछले ज्ञान का उपयोग करते हैं।
एक अच्छा उदाहरण n = 1,000,002 के लिए फाइबोनैचि अनुक्रम को हल कर रहा है।
यह एक बहुत लंबी प्रक्रिया होगी, लेकिन क्या होगा अगर मैं आपको n = 1,000,000 और n = 1,000,001 के लिए परिणाम दूं? अचानक समस्या बस अधिक प्रबंधनीय हो गई।
स्ट्रिंग प्रोग्रामिंग में डायनामिक प्रोग्रामिंग का बहुत उपयोग किया जाता है, जैसे कि स्ट्रिंग एडिट की समस्या। आप समस्या का एक सबसेट (ओं) को हल करते हैं और फिर उस जानकारी का उपयोग अधिक कठिन मूल समस्या को हल करने के लिए करते हैं।
गतिशील प्रोग्रामिंग के साथ, आप अपने परिणामों को किसी प्रकार की तालिका में आम तौर पर संग्रहीत करते हैं। जब आपको किसी समस्या के उत्तर की आवश्यकता होती है, तो आप तालिका का संदर्भ लेते हैं और देखते हैं कि क्या आप पहले से ही जानते हैं कि यह क्या है। यदि नहीं, तो आप अपने आप को उत्तर की ओर एक कदम रखने के लिए अपनी तालिका में डेटा का उपयोग करते हैं।
कॉर्मेन एल्गोरिदम पुस्तक में गतिशील प्रोग्रामिंग के बारे में एक महान अध्याय है। और यह Google Books पर मुफ़्त है! इसे यहां देखें।
डायनामिक प्रोग्रामिंग एक ऐसी तकनीक है जिसका उपयोग एक पुनरावर्ती एल्गोरिथ्म में एक ही उपप्रक्रम में कई बार कंप्यूटिंग से बचने के लिए किया जाता है।
आइए, फाइबोनैचि संख्याओं का सरल उदाहरण लेते हैं: द्वारा परिभाषित n वें फाइबोनैचि संख्याओं का पता लगाना
F n = F n-1 + F n-2 और F 0 = 0, F 1 = 1
ऐसा करने का स्पष्ट तरीका पुनरावर्ती है:
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
पुनरावृत्ति अनावश्यक गणनाओं का एक बहुत करता है क्योंकि किसी दिए गए फाइबोनैचि संख्या की कई बार गणना की जाएगी। इसे सुधारने का एक आसान तरीका परिणामों को कैश करना है:
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]
ऐसा करने का एक बेहतर तरीका यह है कि सही क्रम में परिणामों का मूल्यांकन करके सभी को एक साथ पुन: प्राप्त किया जाए:
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]
हम भी निरंतर स्थान का उपयोग कर सकते हैं और रास्ते में केवल आवश्यक आंशिक परिणामों को संग्रहीत कर सकते हैं:
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
गतिशील प्रोग्रामिंग कैसे लागू करें?
डायनेमिक प्रोग्रामिंग आम तौर पर उन समस्याओं के लिए काम करती है जिनके पास अंतर्निहित, पेड़ या पूर्णांक अनुक्रमों जैसे दाहिने क्रम में बाएं से होते हैं। यदि भोले पुनरावर्ती एल्गोरिथ्म कई बार एक ही सबप्रोब्लेम की गणना नहीं करते हैं, तो गतिशील प्रोग्रामिंग मदद नहीं करेगा।
मैंने तर्क समझने में मदद करने के लिए समस्याओं का एक संग्रह बनाया: https://github.com/tristanguigue/dynamic-programing
if n in cache
जैसा कि ऊपर नीचे उदाहरण के साथ या क्या मैं कुछ याद कर रहा हूं।
संस्मरण वह है जब आप एक फ़ंक्शन कॉल के पिछले परिणामों को संग्रहीत करते हैं (एक वास्तविक फ़ंक्शन हमेशा एक ही चीज़ देता है, उसी इनपुट को दिया जाता है)। परिणामों को संग्रहीत करने से पहले यह एल्गोरिथम जटिलता के लिए कोई अंतर नहीं करता है।
रिकर्सियन एक फ़ंक्शन की विधि है, जो आमतौर पर छोटे डेटासेट के साथ होती है। चूंकि अधिकांश पुनरावर्ती कार्यों को समान पुनरावृत्ति कार्यों में परिवर्तित किया जा सकता है, इसलिए यह एल्गोरिथम जटिलता के लिए कोई अंतर नहीं करता है।
डायनेमिक प्रोग्रामिंग आसान-से-हल करने वाली उप-समस्याओं को हल करने और उस से उत्तर बनाने की प्रक्रिया है। अधिकांश डीपी एल्गोरिदम एक लालची एल्गोरिथ्म (यदि मौजूद है) और एक घातांक (सभी संभावनाओं को मानने और सबसे अच्छा एक खोजने के बीच) के चल रहे समय में होगा।
यह आपके एल्गोरिथ्म का एक अनुकूलन है जो चल रहे समय में कटौती करता है।
जबकि एक लालची एल्गोरिथ्म को आमतौर पर भोला कहा जाता है , क्योंकि यह डेटा के एक ही सेट पर कई बार चल सकता है, डायनेमिक प्रोग्रामिंग इस नुकसान को आंशिक परिणामों की गहरी समझ के माध्यम से बचाती है जिसे अंतिम समाधान बनाने में मदद करने के लिए संग्रहीत किया जाना चाहिए।
एक सरल उदाहरण एक पेड़ या ग्राफ को केवल नोड्स के माध्यम से ट्रेस करना है जो समाधान के साथ योगदान देगा, या एक टेबल में समाधानों को डाल देगा जो आपने अब तक पाया है आप एक ही नोड को बार-बार ट्रेस करने से बच सकते हैं।
यह एक समस्या का एक उदाहरण है जो UVA के ऑनलाइन जज से डायनामिक प्रोग्रामिंग के लिए अनुकूल है: स्टेप्स स्टेप लैडर।
मैं इस समस्या के विश्लेषण के महत्वपूर्ण हिस्से की त्वरित ब्रीफिंग करने जा रहा हूं, जो कि प्रोग्रामिंग प्रोग्रामिंग चुनौतियों से लिया गया है, मेरा सुझाव है कि आप इसकी जांच करें।
उस समस्या पर एक अच्छी नज़र डालें, अगर हम एक लागत समारोह को परिभाषित करते हैं जो हमें बताता है कि दो तार कितने दूर हैं, तो हम दो प्राकृतिक परिवर्तनों के तीन प्रकारों पर विचार करते हैं:
प्रतिस्थापन - पाठ "t" में एक एकल वर्ण को पैटर्न "s" से भिन्न वर्ण में बदलें, जैसे "शॉट" को "स्पॉट" में बदलना।
निवेशन - टेक्स्ट "t" से मेल खाने के लिए पैटर्न "s" में एक एकल वर्ण डालें, जैसे कि "पहले" को "agog" में बदलना।
विलोपन - पाठ "t" से मेल खाने में पैटर्न "s" से एकल वर्ण को हटाएं, जैसे "घंटे" को "हमारे" में बदलना।
जब हम एक-एक कदम उठाने के लिए इस प्रत्येक ऑपरेशन को सेट करते हैं तो हम दो तारों के बीच की दूरी को परिभाषित करते हैं। तो हम इसकी गणना कैसे करते हैं?
हम एक पुनरावर्ती एल्गोरिथ्म को इस अवलोकन का उपयोग करके परिभाषित कर सकते हैं कि स्ट्रिंग में अंतिम वर्ण या तो मिलान, प्रतिस्थापित, सम्मिलित या हटाया जाना चाहिए। अंतिम संपादन ऑपरेशन में पात्रों को काटकर एक जोड़ी ऑपरेशन छोड़ देता है एक जोड़ी छोटे तारों को छोड़ देता है। आइए और जम्मू, क्रमशः और टी के प्रासंगिक उपसर्ग के अंतिम चरित्र हैं। पिछले ऑपरेशन के बाद मैच / प्रतिस्थापन, सम्मिलन या विलोपन के बाद स्ट्रिंग के अनुरूप तीन जोड़े छोटे तार होते हैं। यदि हम तीन तारों के छोटे तारों को संपादित करने की लागत जानते हैं, तो हम यह तय कर सकते हैं कि कौन सा विकल्प सबसे अच्छा समाधान की ओर जाता है और तदनुसार उस विकल्प को चुनें। हम इस लागत को जान सकते हैं, भयानक चीज़ के माध्यम से जो पुनरावृत्ति है:
#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 ); }
यह एल्गोरिथ्म सही है, लेकिन असंभव भी धीमा है।
हमारे कंप्यूटर पर चल रहा है, दो 11-वर्ण स्ट्रिंग्स की तुलना करने के लिए कई सेकंड लगते हैं, और गणना कभी भी-कभी भी भूमि में गायब हो जाती है।
एल्गोरिथ्म इतना धीमा क्यों है? यह घातीय समय लेता है क्योंकि यह बार-बार और फिर से मूल्यों को फिर से प्रदर्शित करता है। स्ट्रिंग में हर स्थिति में, पुनरावर्तन शाखाएं तीन तरीके से होती हैं, जिसका अर्थ है कि यह कम से कम 3 ^ n - की दर से बढ़ता है, वास्तव में, यहां तक कि ज्यादातर कॉल केवल दो सूचकांकों में से एक को कम करते हैं, न कि दोनों को।
तो हम एल्गोरिदम को व्यावहारिक कैसे बना सकते हैं? महत्वपूर्ण अवलोकन यह है कि इनमें से अधिकांश पुनरावर्ती कॉल उन चीजों की गणना कर रहे हैं जिनकी गणना पहले ही की जा चुकी है।हम कैसे जानते हैं? खैर, वहाँ केवल हो सकता है | · | टी | संभव अद्वितीय पुनरावर्ती कॉल, चूंकि केवल वही हैं जो पुनरावर्ती कॉल के मापदंडों के रूप में सेवा करने के लिए कई अलग-अलग (i, j) जोड़े हैं।
इन (i, j) जोड़ियों में से प्रत्येक के लिए मानों को एक तालिका में संग्रहीत करके, हम उन्हें पुन: प्रकाशित करने से रोक सकते हैं और उन्हें आवश्यकतानुसार देख सकते हैं।
तालिका एक दो आयामी मैट्रिक्स मीटर है जहां प्रत्येक | s | · | t | कोशिकाओं में इस उपप्रकार के इष्टतम समाधान की लागत होती है, साथ ही माता-पिता सूचक बताते हैं कि हमें इस स्थान पर कैसे मिला:
typedef struct { int cost; /* cost of reaching this cell */ int parent; /* parent cell */ } cell; cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */
गतिशील प्रोग्रामिंग संस्करण में पुनरावर्ती संस्करण से तीन अंतर हैं।
प्रथम, यह पुनरावर्ती कॉल के बजाय टेबल लुकअप का उपयोग करके अपने मध्यवर्ती मूल्यों को प्राप्त करता है।
** दूसरा, ** यह प्रत्येक सेल के मूल क्षेत्र को अद्यतन करता है, जो हमें बाद में संपादन अनुक्रम को फिर से बनाने में सक्षम करेगा।
** तीसरा, ** तीसरा, यह
cell()
केवल एम वापस करने के बजाय अधिक सामान्य लक्ष्य फ़ंक्शन का उपयोग करने के लिए यंत्रीकृत है [| s |] | tost .cost यह हमें इस दिनचर्या को समस्याओं के व्यापक वर्ग में लागू करने में सक्षम करेगा।
यहां, सबसे इष्टतम आंशिक परिणामों को इकट्ठा करने के लिए जो कुछ भी होता है उसका एक विशेष विश्लेषण, वह है जो समाधान को "गतिशील" बनाता है।
यहां एक ही समस्या का एक वैकल्पिक, पूर्ण समाधान है। यह एक "गतिशील" भी है, हालांकि इसका निष्पादन अलग है। मेरा सुझाव है कि आप इसे यूवीए के ऑनलाइन जज को सौंपकर समाधान कितना कुशल है, इसकी जांच करें। मुझे आश्चर्यजनक लगता है कि इतनी भारी समस्या को इतनी कुशलता से कैसे निपटाया गया।
गतिशील प्रोग्रामिंग के प्रमुख बिट्स "ओवरलैपिंग उप-समस्याएं" और "इष्टतम उपस्ट्रक्चर" हैं। एक समस्या के इन गुणों का मतलब है कि एक इष्टतम समाधान इसकी उप-समस्याओं के इष्टतम समाधान से बना है। उदाहरण के लिए, सबसे छोटी पथ की समस्याएं इष्टतम उप-संरचना का प्रदर्शन करती हैं। A से C तक का सबसे छोटा रास्ता A से कुछ नोड B का सबसे छोटा रास्ता है और उसके बाद उस नोड B से C तक का सबसे छोटा रास्ता है।
अधिक से अधिक विवरण में, एक छोटी-सी समस्या को हल करने के लिए:
क्योंकि हम नीचे-ऊपर काम कर रहे हैं, हमारे पास पहले से ही उप-समस्याओं के समाधान हैं जब उन्हें याद करने के लिए, उनका उपयोग करने का समय आता है।
याद रखें, डायनेमिक प्रोग्रामिंग समस्याओं में ओवरलैपिंग उप-समस्याएं और इष्टतम सबस्ट्रक्चर दोनों होने चाहिए। फाइबोनैचि अनुक्रम उत्पन्न करना एक गतिशील प्रोग्रामिंग समस्या नहीं है; यह संस्मरण का उपयोग करता है, क्योंकि इसमें उप-समस्याओं का अतिव्यापी होता है, लेकिन इसमें इष्टतम सबस्ट्रक्चर नहीं होता है (क्योंकि कोई अनुकूलन समस्या नहीं है)।
गतिशील प्रोग्रामिंग
परिभाषा
ओवरलैपिंग उप-समस्याओं के साथ समस्याओं को हल करने के लिए डायनामिक प्रोग्रामिंग (डीपी) एक सामान्य एल्गोरिदम डिजाइन तकनीक है। इस तकनीक का आविष्कार अमेरिकी गणितज्ञ "रिचर्ड बेलमैन" ने 1950 के दशक में किया था।
कुंजी विचार
प्रमुख विचार पुनर्संयोजन से बचने के लिए छोटी उप-समस्याओं के अतिव्यापी जवाबों को बचाने के लिए है।
गतिशील प्रोग्रामिंग गुण
मैं डायनेमिक प्रोग्रामिंग (विशेष प्रकार की समस्याओं के लिए एक शक्तिशाली एल्गोरिदम) के लिए बहुत नया हूं
सबसे सरल शब्दों में, बस पिछले ज्ञान का उपयोग करने के साथ एक पुनरावर्ती दृष्टिकोण के रूप में गतिशील प्रोग्रामिंग को सोचें
पिछला ज्ञान यह है कि यहां सबसे ज्यादा क्या मायने रखता है, आपके पास पहले से मौजूद उप-समस्याओं के समाधान का ध्यान रखें।
इस पर विचार करें, विकिपीडिया से dp के लिए सबसे बुनियादी उदाहरण
रिट्रेसमेंट अनुक्रम ढूँढना
function fib(n) // naive implementation
if n <=1 return n
return fib(n − 1) + fib(n − 2)
फ़ंक्शन कॉल को n = 5 के साथ तोड़ दें
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))
विशेष रूप से, फ़ाइबर (2) की गणना खरोंच से तीन बार की गई थी। बड़े उदाहरणों में, कई और अधिक मूल्य के फ़िब, या उप-समस्याएं, पुनर्गणना की जाती हैं, जो एक घातीय समय एल्गोरिथ्म के लिए अग्रणी हैं।
अब, हम डेटा-संरचना में पहले से पाए गए मान को एक मानचित्र कहते हैं, इसे संग्रहीत करने का प्रयास करते हैं
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]
यहां हम मानचित्र में उप-समस्याओं के समाधान को सहेज रहे हैं, अगर हमारे पास यह पहले से नहीं है। मूल्यों को सहेजने की यह तकनीक जिसे हमने पहले से ही गणना की थी, को मेमोइज़ेशन कहा जाता है।
अंत में, एक समस्या के लिए, पहले राज्यों को खोजने का प्रयास करें (संभव उप-समस्याओं और बेहतर पुनरावर्तन दृष्टिकोण के बारे में सोचने की कोशिश करें ताकि आप पिछली उप-समस्या के समाधान का उपयोग आगे कर सकें)।
डायनेमिक प्रोग्रामिंग अतिव्यापी समस्याओं के साथ समस्याओं को हल करने के लिए एक तकनीक है। एक गतिशील प्रोग्रामिंग एल्गोरिथ्म बस एक बार हर उप समस्या को हल करता है और फिर एक तालिका (सरणी) में इसका उत्तर बचाता है। हर बार उप-समस्या का सामना करने पर उत्तर की पुन: गणना के काम से बचना। गतिशील प्रोग्रामिंग का अंतर्निहित विचार यह है: आमतौर पर उप समस्याओं के ज्ञात परिणामों की तालिका रखते हुए, एक ही सामान की दो बार गणना करने से बचें।
गतिशील प्रोग्रामिंग एल्गोरिदम के विकास में सात चरण निम्नानुसार हैं:
6. Convert the memoized recursive algorithm into iterative algorithm
एक अनिवार्य कदम? इसका मतलब यह होगा कि इसका अंतिम रूप गैर-पुनरावर्ती है?
संक्षेप में पुनरावृत्ति ज्ञापन और गतिशील प्रोग्रामिंग के बीच अंतर
डायनामिक प्रोग्रामिंग जैसा कि नाम से पता चलता है कि पिछले गणना मूल्य का उपयोग गतिशील रूप से अगले नए समाधान का निर्माण करने के लिए किया गया है
डायनेमिक प्रोग्रामिंग कहां लागू करें: यदि आप समाधान इष्टतम सबस्ट्रक्चर और ओवरलैपिंग उप समस्या पर आधारित हैं, तो उस स्थिति में पहले की गणना की गई वैल्यू का उपयोग करना उपयोगी होगा, इसलिए आपको इसे रीकंप्यूट करने की आवश्यकता नहीं है। यह नीचे की ओर दृष्टिकोण है। मान लीजिए कि आपको फ़ाइब (n) की गणना करने की आवश्यकता है, उस स्थिति में आपको फ़ाइब (n-1) और फ़ाइबर (n-2) के पिछले परिकलित मान को जोड़ना होगा
पुनरावर्तन: मूल रूप से आप इसे आसानी से हल करने के लिए छोटे हिस्से में समस्या को विभाजित करते हैं, लेकिन इसे ध्यान में रखें कि अगर हम एक ही मूल्य को अन्य पुनरावर्तन कॉल में पहले से गणना नहीं करते हैं तो पुन: गणना से बचें।
संस्मरण: मूल रूप से तालिका में पुरानी गणना की गई पुनरावर्तन मूल्य को संग्रहीत करना संस्मरण के रूप में जाना जाता है जो पुन: गणना से बचता है यदि इसकी पहले से ही कुछ पिछली कॉल द्वारा गणना की जाती है तो किसी भी मूल्य की गणना एक बार की जाएगी। इसलिए गणना करने से पहले हम जांचते हैं कि क्या इस मूल्य की गणना पहले ही की जा चुकी है या नहीं अगर पहले से ही गणना की गई है तो हम पुनर्मिलन के बजाय तालिका से समान लौटाते हैं। यह टॉप अप एप्रोच भी है
यहाँ का एक सरल अजगर कोड उदाहरण है Recursive
, Top-down
, Bottom-up
फाइबोनैचि श्रृंखला के लिए दृष्टिकोण:
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))