बॉटम-अप और टॉप-डाउन में क्या अंतर है?


177

नीचे से ऊपर दृष्टिकोण (गतिशील प्रोग्रामिंग करने के लिए) पहले "छोटे" subproblems को देख में होते हैं, और फिर छोटे समस्याओं का हल का उपयोग कर बड़ा subproblems का समाधान।

ऊपर से नीचे एक "प्राकृतिक तरीके से" और जाँच में समस्या को हल करने से पहले अगर आप subproblem का हल गणना की है में होते हैं।

मैं थोड़ा उलझन में हूँ। इन दोनों के बीच क्या अंतर है?


जवाबों:


247

Rev4: उपयोगकर्ता Sammaron द्वारा एक बहुत ही स्पष्ट टिप्पणी ने उल्लेख किया है कि, शायद, यह उत्तर पहले ऊपर-नीचे और नीचे-ऊपर उलझा हुआ है। हालांकि मूल रूप से यह उत्तर (रेव 3) और अन्य उत्तरों में कहा गया है कि "बॉटम-अप मेमोइज़ेशन है" ("उपप्रोब्लेम्स मानें"), यह प्रतिलोम हो सकता है (अर्थात "टॉप-डाउन" "उपप्रोब्लेम्स और" हो सकता है) नीचे-ऊपर "" उपप्रकारों की रचना हो सकती है ")। पहले, मैंने संस्मरण पर पढ़ा है कि गतिशील प्रोग्रामिंग के एक उपप्रकार के विपरीत एक अलग तरह की गतिशील प्रोग्रामिंग है। मैं उस दृष्टिकोण को उद्धृत कर रहा था, इसके लिए सदस्यता नहीं लेने के बावजूद। मैंने इस उत्तर को शब्दावली के अज्ञेय के रूप में फिर से लिखा है जब तक कि साहित्य में उचित संदर्भ नहीं मिल सकते। मैंने इस उत्तर को एक समुदाय विकी में भी बदल दिया है। कृपया शैक्षणिक स्रोतों को प्राथमिकता दें। संदर्भ की सूची:} {साहित्य: }

संक्षिप्त

डायनेमिक प्रोग्रामिंग एक तरह से आपकी गणनाओं को क्रमबद्ध करने के बारे में है जो डुप्लिकेट कार्य को पुन: करने से बचा जाता है। आपको एक मुख्य समस्या है (आपके उपप्रकारों के पेड़ की जड़), और उपप्रोलेज (उपप्रकार)। उपप्रोब्लेम्स आमतौर पर दोहराए जाते हैं और ओवरलैप होते हैं

उदाहरण के लिए, Fibonnaci के अपने पसंदीदा उदाहरण पर विचार करें। यदि हम एक भोली पुनरावर्ती कॉल करते हैं, तो यह उपप्रकारों का पूर्ण वृक्ष है:

TOP of the tree
fib(4)
 fib(3)...................... + fib(2)
  fib(2)......... + fib(1)       fib(1)........... + fib(0)
   fib(1) + fib(0)   fib(1)       fib(1)              fib(0)
    fib(1)   fib(0)
BOTTOM of the tree

(कुछ अन्य दुर्लभ समस्याओं में, यह पेड़ कुछ शाखाओं में अनंत हो सकता है, गैर-समाप्ति का प्रतिनिधित्व करता है, और इस प्रकार पेड़ का तल असीम रूप से बड़ा हो सकता है। इसके अलावा, कुछ समस्याओं में आप यह नहीं जान सकते हैं कि पूरा पेड़ आगे से कैसा दिखता है। समय। इस प्रकार, आपको यह तय करने के लिए एक रणनीति / एल्गोरिथ्म की आवश्यकता हो सकती है कि किस उपप्रोब्लेम्स को प्रकट करना है।)


संस्मरण, सारणीकरण

गतिशील प्रोग्रामिंग की कम से कम दो मुख्य तकनीकें हैं जो परस्पर अनन्य नहीं हैं:

  • संस्मरण - यह एक लाईसेज़-फैयर दृष्टिकोण है: आप मानते हैं कि आप पहले से ही सभी उपप्रोमास की गणना कर चुके हैं और आपको पता नहीं है कि इष्टतम मूल्यांकन क्रम क्या है। आमतौर पर, आप रूट से एक पुनरावर्ती कॉल (या कुछ पुनरावृत्त समतुल्य) का प्रदर्शन करेंगे, और या तो आशा करते हैं कि आप इष्टतम मूल्यांकन आदेश के करीब पहुंच जाएंगे, या एक प्रमाण प्राप्त करेंगे कि आप इष्टतम मूल्यांकन क्रम पर पहुंचने में मदद करेंगे। आप यह सुनिश्चित करेंगे कि पुनरावर्ती कॉल कभी भी एक उपप्रोग्राम को पुन: प्रदर्शित नहीं करता है क्योंकि आप परिणामों को कैश करते हैं, और इस प्रकार डुप्लिकेट उप-पेड़ पुन: विवादित नहीं होते हैं।

    • उदाहरण: यदि आप फाइबोनैचि अनुक्रम की गणना कर रहे हैं fib(100), तो आप इसे कॉल करेंगे, और यह कॉल करेगा fib(100)=fib(99)+fib(98), जो कॉल करेगा fib(99)=fib(98)+fib(97), ... आदि ..., जो कॉल करेगा fib(2)=fib(1)+fib(0)=1+0=1। तब यह अंततः हल हो जाएगा fib(3)=fib(2)+fib(1), लेकिन इसे पुनर्गणना करने की आवश्यकता नहीं है fib(2), क्योंकि हमने इसे कैश किया था।
    • यह पेड़ के शीर्ष पर शुरू होता है और पत्तियों से उपप्रकारों का मूल्यांकन करता है / जड़ की ओर वापस ऊपर की ओर बढ़ता है।
  • सारणीकरण - आप एक "टेबल-फिलिंग" एल्गोरिथम के रूप में गतिशील प्रोग्रामिंग के बारे में भी सोच सकते हैं (हालांकि आमतौर पर बहुआयामी, इस 'तालिका' में बहुत दुर्लभ मामलों में गैर-यूक्लिडियन ज्यामिति हो सकती है)। यह संस्मरण की तरह है, लेकिन अधिक सक्रिय है, और इसमें एक अतिरिक्त चरण शामिल है: आपको समय से पहले, सटीक क्रम चुनना होगा जिसमें आप अपनी गणना करेंगे। इसका मतलब यह नहीं होना चाहिए कि आदेश स्थिर होना चाहिए, लेकिन यह कि आपके पास संस्मरण की तुलना में अधिक लचीलापन है।

    • उदाहरण: आप फिबोनैकी प्रदर्शन कर रहे हैं, तो आप इस क्रम में संख्या की गणना करने के लिए चुन सकता है: fib(2), fib(3), fib(4)... हर मूल्य कैशिंग, ताकि आपको अगली वाले और अधिक आसानी से गणना कर सकता है। आप इसे एक टेबल भरने (कैशिंग का दूसरा रूप) के रूप में भी सोच सकते हैं।
    • मैं व्यक्तिगत रूप से 'सारणीकरण' शब्द को बहुत अधिक नहीं सुनता, लेकिन यह एक बहुत ही अच्छा शब्द है। कुछ लोग इसे "गतिशील प्रोग्रामिंग" मानते हैं।
    • एल्गोरिथ्म को चलाने से पहले, प्रोग्रामर पूरे पेड़ पर विचार करता है, फिर एक विशेष क्रम में रूट के लिए उपप्रोम्बल्स का मूल्यांकन करने के लिए एक एल्गोरिथ्म लिखता है, आम तौर पर एक तालिका में भर जाता है।
    • * फुटनोट: कभी-कभी 'टेबल' ग्रिड जैसी कनेक्टिविटी के साथ आयताकार तालिका नहीं होती है। बल्कि, इसमें एक अधिक जटिल संरचना हो सकती है, जैसे कि एक पेड़, या समस्या डोमेन के लिए विशिष्ट संरचना (उदाहरण के लिए नक्शे पर उड़ान दूरी के भीतर शहर), या यहां तक ​​कि एक ट्रेली आरेख, जो ग्रिड की तरह है, जिसमें नहीं है एक ऊपर-नीचे-दाएं कनेक्टिविटी संरचना, आदि। उदाहरण के लिए, user3290797 ने एक पेड़ में अधिकतम स्वतंत्र सेट खोजने का एक गतिशील प्रोग्रामिंग उदाहरण जोड़ा , जो एक पेड़ में रिक्त स्थान को भरने से मेल खाता है।

(यह "सामान्य प्रोग्रामिंग" प्रतिमान में सबसे सामान्य है, मैं कहूंगा कि प्रोग्रामर पूरे पेड़ को मानता है, फिरएक एल्गोरिथ्म लिखता है जो उपप्रोमा का मूल्यांकन करने के लिए एक रणनीति को लागू करता है जो कि आप जो भी गुण चाहते हैं उसका अनुकूलन कर सकते हैं (आमतौर पर समय-जटिलता और अंतरिक्ष-जटिलता का संयोजन)। आपकी रणनीति कहीं न कहीं से शुरू होनी चाहिए, कुछ विशेष उपप्रोग्राम के साथ, और शायद उन मूल्यांकन के परिणामों के आधार पर खुद को अनुकूलित कर सकते हैं। "डायनेमिक प्रोग्रामिंग" के सामान्य अर्थ में, आप इन उपप्रकारों को कैश करने का प्रयास कर सकते हैं, और अधिक सामान्यतः, सूक्ष्म अंतर के साथ उपप्रोलेमीम को फिर से भेजने से बचने का प्रयास करें, शायद विभिन्न डेटा संरचनाओं में ग्राफ़ का मामला है। बहुत बार, ये डेटा संरचनाएं एरे या टेबलों की तरह उनके मूल में होती हैं। यदि हमें उनकी आवश्यकता नहीं है, तो उपप्रोलेम्स के समाधानों को फेंक दिया जा सकता है।

[पहले, इस जवाब ने टॉप-डाउन बनाम बॉटम-अप शब्दावली के बारे में एक बयान दिया; मेमोइज़ेशन और टेबुलेशन नामक स्पष्ट रूप से दो मुख्य दृष्टिकोण हैं जो उन शर्तों के साथ पूर्वाग्रह में हो सकते हैं (हालांकि पूरी तरह से नहीं)। ज्यादातर लोग जो सामान्य शब्द का उपयोग करते हैं वह अभी भी "डायनेमिक प्रोग्रामिंग" है और कुछ लोग "डायनामिक प्रोग्रामिंग" के उस विशेष उपप्रकार को संदर्भित करने के लिए "मेमोइज़ेशन" कहते हैं। यह उत्तर यह कहने की गिरावट करता है कि कौन सा टॉप-डाउन और बॉटम-अप है जब तक कि समुदाय अकादमिक पेपर में उचित संदर्भ नहीं पा सकता है। अंततः, शब्दावली के बजाय अंतर को समझना महत्वपूर्ण है।]


फायदा और नुकसान

कोडिंग में आसानी

मेमोइज़ेशन को कोड करना बहुत आसान है (आप आम तौर पर "मेमोइज़र" एनोटेशन या रैपर फ़ंक्शन लिख सकते हैं जो स्वचालित रूप से आपके लिए करता है), और आपके दृष्टिकोण की पहली पंक्ति होनी चाहिए। सारणीकरण का नकारात्मक पक्ष यह है कि आपको एक आदेश के साथ आना होगा।

* (यह वास्तव में केवल आसान है यदि आप फ़ंक्शन को स्वयं लिख रहे हैं, और / या एक अशुद्ध / गैर-कार्यात्मक प्रोग्रामिंग भाषा में कोडिंग कर रहे हैं ... उदाहरण के लिए यदि कोई पहले से ही एक fibफ़ंक्शन लिखता है , तो यह आवश्यक रूप से स्वयं को पुनरावर्ती कॉल करता है, और आप उन पुनरावर्ती कॉलों को सुनिश्चित करने के बिना फ़ंक्शन को याद नहीं कर सकते हैं जो आपके नए ज्ञापन फ़ंक्शन को कॉल करते हैं (और मूल असम्बद्ध फ़ंक्शन को नहीं))

Recursiveness

ध्यान दें कि ऊपर-नीचे और नीचे-ऊपर दोनों को पुनरावृत्ति या पुनरावृत्ति तालिका-भरने के साथ लागू किया जा सकता है, हालांकि यह स्वाभाविक नहीं हो सकता है।

व्यावहारिक चिंता

संस्मरण के साथ, यदि पेड़ बहुत गहरा है (जैसे fib(10^6)), तो आप स्टैक स्पेस से बाहर निकल जाएंगे, क्योंकि प्रत्येक विलंबित गणना को स्टैक पर रखा जाना चाहिए, और आपके पास उनमें से 10 ^ 6 होंगे।

optimality

या तो दृष्टिकोण समय-इष्टतम नहीं हो सकता है यदि आप जो आदेश देते हैं (या कोशिश करें) उपप्रोब्लेम्स पर जाएं, वह इष्टतम नहीं है, खासकर अगर सबप्रोब्लेम की गणना करने का एक से अधिक तरीका है (सामान्य रूप से कैशिंग इसका समाधान करेगा, लेकिन यह सैद्धांतिक रूप से संभव है) कैशिंग कुछ विदेशी मामलों में नहीं)। संस्मरण आमतौर पर आपके समय-जटिलता को आपके अंतरिक्ष-परिसर में जोड़ देगा (जैसे सारणीकरण के साथ आपके पास गणनाओं को फेंकने के लिए अधिक स्वतंत्रता है, जैसे कि फ़िब के साथ सारणीकरण का उपयोग करने से आपको O (1) स्थान का उपयोग करने की अनुमति मिलती है, लेकिन Fo के साथ संस्मरण O (N) का उपयोग करता है स्टैक स्पेस)।

उन्नत अनुकूलन

यदि आप भी बहुत जटिल समस्याएँ कर रहे हैं, तो आपके पास सारणीकरण करने के लिए (या कम से कम एक अधिक सक्रिय भूमिका लेने के लिए जहां आप इसे जाना चाहते हैं) के अलावा कोई विकल्प नहीं हो सकता है। इसके अलावा, यदि आप ऐसी स्थिति में हैं जहां अनुकूलन बिल्कुल महत्वपूर्ण है और आपको अनुकूलन करना होगा, तो सारणीकरण आपको अनुकूलन करने की अनुमति देगा, जो कि मेमोनाइजेशन अन्यथा आपको एक समझदार तरीके से नहीं करने देगा। मेरी विनम्र राय में, सामान्य सॉफ्टवेयर इंजीनियरिंग में, इन दोनों मामलों में से कोई भी कभी सामने नहीं आता है, इसलिए मैं सिर्फ संस्मरण ("एक फ़ंक्शन जो इसके जवाबों को कैश करता है") का उपयोग करेगा जब तक कि कुछ (जैसे स्टैक स्पेस) सारणीकरण को आवश्यक नहीं बनाता ... हालांकि तकनीकी रूप से स्टैक ब्लोआउट से बचने के लिए आप 1) भाषाओं में स्टैक के आकार की सीमा को बढ़ा सकते हैं जो इसे अनुमति देते हैं, या 2) अपने स्टैक (ick) को वर्चुअलाइज करने के लिए अतिरिक्त काम का एक स्थिर कारक खाते हैं,


अधिक जटिल उदाहरण

यहां हम विशेष रुचि के उदाहरणों को सूचीबद्ध करते हैं, जो न केवल सामान्य डीपी समस्याएं हैं, बल्कि दिलचस्प रूप से संस्मरण और सारणीकरण को अलग करती हैं। उदाहरण के लिए, एक सूत्रीकरण दूसरे की तुलना में बहुत आसान हो सकता है, या एक अनुकूलन हो सकता है जिसके लिए मूल रूप से सारणीकरण की आवश्यकता होती है:

  • दो-आयामी तालिका-भरने वाले एल्गोरिथ्म के गैर-तुच्छ उदाहरण के रूप में, संपादित-दूरी [ 4 ] की गणना करने के लिए एल्गोरिथ्म

3
@ coder000001: अजगर उदाहरणों के लिए, आप Google खोज सकते हैं python memoization decorator; कुछ भाषाएं आपको एक मैक्रो या कोड लिखने देंगी जो संस्मरण पैटर्न को एन्क्रिप्ट करती है। संस्मरण पैटर्न "फ़ंक्शन को कॉल करने के बजाय, कैश से मूल्य देखें (यदि मूल्य नहीं है, तो इसे गणना करें और इसे पहले कैश में जोड़ें)" से अधिक कुछ नहीं है।
Ninjagecko

16
मैं किसी को भी इसका उल्लेख नहीं करता, लेकिन मुझे लगता है कि टॉप डाउन का एक और फायदा यह है कि आप केवल लुक-अप टेबल / कैश का निर्माण करेंगे। (यानी आप उन मूल्यों को भरते हैं जहां आपको वास्तव में उनकी आवश्यकता है)। तो यह आसान कोडिंग के अलावा भी हो सकता है। दूसरे शब्दों में, ऊपर नीचे आप वास्तविक दौड़ने के समय को बचा सकते हैं क्योंकि आप सब कुछ गणना नहीं करते हैं (आपके पास बहुत बेहतर चलने का समय हो सकता है, लेकिन समान रूप से चलने का समय भी हो सकता है)। फिर भी अतिरिक्त स्टैक फ्रेम (फिर, मेमोरी की खपत) (केवल हो सकता है) को डबल रखने के लिए अतिरिक्त मेमोरी की आवश्यकता होती है लेकिन asymptotically यह एक ही है।
InformedA

2
मैं छाप ऊपर से नीचे दृष्टिकोण है कि ओवरलैपिंग subproblems करने के लिए कैश समाधान नामक तकनीक है के तहत कर रहा हूँ Memoization । एक टेबुल अप तकनीक जो एक टेबल को भरती है और यह भी बचा लेती है कि पुनर्संयोजन अतिव्यापी उपप्रभु को सारणीयन के रूप में संदर्भित किया जाता है । डायनामिक प्रोग्रामिंग का उपयोग करते समय इन तकनीकों को नियोजित किया जा सकता है , जो बहुत बड़ी समस्या को हल करने के लिए उपप्रोमीटर को हल करने के लिए संदर्भित करता है। इस उत्तर के साथ विरोधाभासी लगता है, जहां यह उत्तर कई स्थानों में सारणीकरण के बजाय गतिशील प्रोग्रामिंग का उपयोग करता है । कौन सही है?
20

1
@Sammaron: हम्म, आप एक अच्छी बात करते हैं। मुझे शायद विकिपीडिया पर अपने स्रोत की जाँच करनी चाहिए थी, जो मुझे नहीं मिला। Cstheory.stackexchange को थोड़ा जाँचने पर, मैं अब मानता हूँ "बॉटम-अप" का अर्थ यह होगा कि नीचे पहले से जाना जाता है (सारणीयन), और "टॉप-डाउन" क्या आप सबप्रोब्लेम्स / सबप्रेज़ का समाधान मानते हैं। जिस समय मुझे अस्पष्ट शब्द मिला, और मैंने दोहरे दृश्य में वाक्यांशों की व्याख्या की ("बॉटम-अप" आप उपप्रोब्लेम्स का समाधान मान लेते हैं और याद करते हैं, "टॉप-डाउन" आप जानते हैं कि आप कौन से उपप्रोब्स हैं और सारणीबद्ध कर सकते हैं)। मैं इसे एक संपादन में संबोधित करने का प्रयास करूंगा।
निंजाजेको

1
@ मैगीफ्रीडा: स्टैक स्पेस को कभी-कभी प्रोग्रामिंग भाषा के आधार पर अलग तरह से व्यवहार किया जाता है। अजगर में उदाहरण के लिए, एक यादगार पुनरावर्ती तंतु प्रदर्शन करने की कोशिश करना असफल होगा fib(513)। अतिभारित शब्दावली मुझे लगता है कि यहाँ जिस तरह से हो रही है। 1) आप हमेशा उन सबप्रोब्लेम्स को फेंक सकते हैं जिनकी आपको अब आवश्यकता नहीं है। 2) आप हमेशा उन उपप्रकारों की गणना करने से बच सकते हैं जिनकी आपको आवश्यकता नहीं है। 3) 1 और 2 के लिए कोड में एक स्पष्ट डेटा संरचना के बिना कोड करने के लिए बहुत कठिन हो सकता है, या, कठिन अगर नियंत्रण प्रवाह फ़ंक्शन कॉल (आप राज्य या निरंतरता की आवश्यकता हो सकती है) के बीच बुनाई करनी चाहिए।
निन्जाचेको 20

76

ऊपर और नीचे डीपी एक ही समस्याओं को हल करने के दो अलग-अलग तरीके हैं। एक ज्ञापन पर विचार करें (ऊपर नीचे) बनाम डायनामिक (नीचे ऊपर) प्रोग्रामिंग कंप्यूटिंग नंबरों के लिए समाधान।

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

मुझे व्यक्तिगत रूप से संस्मरण बहुत अधिक स्वाभाविक लगता है। आप एक पुनरावर्ती कार्य ले सकते हैं और इसे एक यांत्रिक प्रक्रिया द्वारा याद कर सकते हैं (पहले कैश में उत्तर देखें और यदि संभव हो तो इसे वापस करें, अन्यथा इसे पुनरावर्ती रूप से गणना करें और फिर लौटने से पहले, आप भविष्य में उपयोग के लिए कैश में गणना को बचाते हैं), जबकि नीचे कर रहे हैं डायनेमिक प्रोग्रामिंग के लिए आपको एक ऐसे ऑर्डर को एनकोड करना पड़ता है जिसमें समाधानों की गणना की जाती है, जैसे कि कोई "बड़ी समस्या" की गणना उस छोटी समस्या से पहले नहीं की जाती है जो उस पर निर्भर करती है।


1
आह, अब मैं देखता हूं कि "टॉप-डाउन" और "बॉटम-अप" का क्या मतलब है; यह वास्तव में डीपी के लिए केवल संस्मरण का जिक्र है। और यह सोचने के लिए कि मैंने डीपी का उल्लेख करने के लिए प्रश्न को संपादित किया था ...
नवजागेको

मेमोइज्ड फ़ाइबर v / s सामान्य पुनरावर्ती फ़ाइबर का रनटाइम क्या है?
सिद्धार्थ

घातीय (2 ^ n) सामान्य coz के लिए एक पुनरावर्तन वृक्ष मुझे लगता है।
सिद्धार्थ

1
हाँ यह रैखिक है! मैंने रिकर्सियन ट्री को बाहर निकाला और देखा कि कौन सी कॉल से बचा जा सकता है और मेमो_फिब (एन - 2) कॉल का एहसास हो सकता है, पहली कॉल के बाद सभी को टाला जाएगा, और इसलिए रिकर्सियन ट्री की सभी सही शाखाओं को काट दिया जाएगा और इसे 'रैखिक को कम कर देंगे।
सिद्धार्थ

1
चूंकि डीपी में अनिवार्य रूप से एक परिणाम तालिका का निर्माण करना शामिल है जहां प्रत्येक परिणाम को एक बार में गणना की जाती है, डीपी एल्गोरिदम के रनटाइम की कल्पना करने का एक सरल तरीका यह देखना है कि तालिका कितनी बड़ी है। इस स्थिति में, यह आकार n (इनपुट मान प्रति एक परिणाम) है तो O (n)। अन्य मामलों में, यह एक n ^ 2 मैट्रिक्स हो सकता है, जिसके परिणामस्वरूप O (n ^ 2), इत्यादि
जॉनसन वोंग

22

डायनेमिक प्रोग्रामिंग की एक प्रमुख विशेषता उप-उप-क्षेत्रों को ओवरलैप करने की उपस्थिति है । यही है, जिस समस्या को आप हल करने का प्रयास कर रहे हैं, उसे उप-प्रक्रमों में तोड़ा जा सकता है, और उन उप-उपप्रकारों में से कई उप -उपकरणों को साझा करते हैं। यह "फूट डालो और जीतो" की तरह है, लेकिन आप एक ही चीज़ को कई बार करते हैं। एक उदाहरण जो मैंने 2003 से इन मामलों को पढ़ाते या समझाते समय इस्तेमाल किया है: आप फाइबोनैचि संख्याओं की पुनरावृत्ति की गणना कर सकते हैं ।

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

अपनी पसंदीदा भाषा का प्रयोग करें और इसके लिए प्रयास करें fib(50)। इसमें बहुत, बहुत लंबा समय लगेगा। मोटे तौर पर fib(50)खुद के रूप में ज्यादा समय ! हालांकि, बहुत से अनावश्यक काम किए जा रहे हैं। fib(50)कॉल करेंगे fib(49)और fib(48), लेकिन फिर वे दोनों कॉलिंग समाप्त कर देंगे fib(47), भले ही मूल्य समान हो। वास्तव में, fib(47)तीन बार गणना की जाएगी: एक प्रत्यक्ष कॉल से fib(49), एक प्रत्यक्ष कॉल से fib(48), और यह भी कि दूसरे से सीधे कॉल से fib(48), वह जो गणना के द्वारा पैदा की गई थी fib(49)... तो आप देखते हैं, हम उपप्रकारों को ओवरलैप कर रहे हैं ।

बड़ी खबर: कई बार एक ही मूल्य की गणना करने की आवश्यकता नहीं है। एक बार जब आप इसे एक बार गणना करते हैं, तो परिणाम को कैश करें, और अगली बार कैश्ड मान का उपयोग करें! यह गतिशील प्रोग्रामिंग का सार है। आप इसे "टॉप-डाउन", "मेमोइज़ेशन" कह सकते हैं, या जो कुछ भी आप चाहते हैं। यह दृष्टिकोण बहुत सहज है और लागू करने में बहुत आसान है। बस पहले एक पुनरावर्ती समाधान लिखें, इसे छोटे परीक्षणों पर परीक्षण करें, मेमोइज़ेशन जोड़ें (पहले से गणना किए गए मूल्यों का कैशिंग), और --- बिंगो! --- आप कर चुके हैं।

आमतौर पर आप एक पुनरावृत्ति कार्यक्रम भी लिख सकते हैं जो पुनरावृत्ति के बिना नीचे से ऊपर तक काम करता है। इस मामले में यह अधिक प्राकृतिक दृष्टिकोण होगा: 1 से 50 तक लूप सभी फाइबोनैचि संख्याओं की गणना करता है जैसा कि आप जाते हैं।

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

किसी भी दिलचस्प परिदृश्य में बॉटम-अप समाधान आमतौर पर समझने में अधिक कठिन होता है। हालाँकि, एक बार जब आप इसे समझ लेते हैं, तो आमतौर पर आपको एल्गोरिथ्म कैसे काम करता है इसकी एक बड़ी स्पष्ट तस्वीर मिल जाएगी। व्यवहार में, जब समस्या का हल करते हैं, तो मैं पहले टॉप-डाउन दृष्टिकोण लिखने और छोटे उदाहरणों पर परीक्षण करने की सलाह देता हूं। फिर नीचे-ऊपर समाधान लिखें और दोनों की तुलना करके सुनिश्चित करें कि आप एक ही चीज़ प्राप्त कर रहे हैं। आदर्श रूप से, दो समाधानों की स्वचालित रूप से तुलना करें। एक छोटी सी दिनचर्या लिखें जो बहुत सारे परीक्षण उत्पन्न करेगा, आदर्श रूप से - सभीकुछ निश्चित आकार तक छोटे परीक्षण --- और मान्य है कि दोनों समाधान समान परिणाम देते हैं। उसके बाद उत्पादन में नीचे-ऊपर के समाधान का उपयोग करें, लेकिन ऊपर-नीचे कोड रखें, टिप्पणी की गई। इससे अन्य डेवलपर्स के लिए यह समझना आसान हो जाएगा कि यह क्या है जो आप कर रहे हैं: बॉटम-अप कोड काफी समझ से बाहर हो सकता है, यहां तक ​​कि आपने इसे लिखा और यहां तक ​​कि अगर आपको पता है कि आप क्या कर रहे हैं।

कई अनुप्रयोगों में पुनरावर्ती कॉल के ओवरहेड के कारण निचला-अप दृष्टिकोण थोड़ा तेज है। स्टैक ओवरफ्लो भी कुछ समस्याओं में एक मुद्दा हो सकता है, और ध्यान दें कि यह बहुत इनपुट डेटा पर निर्भर कर सकता है। कुछ मामलों में यदि आप डायनेमिक प्रोग्रामिंग को अच्छी तरह से नहीं समझते हैं, तो स्टैक ओवरफ्लो का कारण बनने वाला टेस्ट लिखने में सक्षम नहीं हो सकते हैं, लेकिन किसी दिन ऐसा हो सकता है।

अब, ऐसी समस्याएं हैं जहां टॉप-डाउन दृष्टिकोण एकमात्र संभव समाधान है क्योंकि समस्या स्थान इतना बड़ा है कि सभी उपप्रकारों को हल करना संभव नहीं है। हालांकि, "कैशिंग" अभी भी उचित समय में काम करता है क्योंकि आपके इनपुट को हल करने के लिए केवल उपप्रोलेम्स के एक अंश की आवश्यकता होती है --- लेकिन यह स्पष्ट रूप से परिभाषित करने के लिए बहुत मुश्किल है, जो उपप्रोब्लेम्स आपको हल करने की आवश्यकता है, और इसलिए एक नीचे लिखने के लिए- समाधान। दूसरी ओर, ऐसी स्थितियां हैं जब आप जानते हैं कि आपको सभी उपप्रकारों को हल करने की आवश्यकता होगी । इस मामले में नीचे जाएं और नीचे-ऊपर का उपयोग करें।

मैं व्यक्तिगत रूप से पैराग्राफ ऑप्टिमाइजेशन उर्फ वर्ड रैप ऑप्टिमाइज़ेशन समस्या के लिए टॉप-बॉटम का उपयोग करूंगा (नथ-प्लास लाइन-ब्रेकिंग एल्गोरिदम देखें); मैं फास्ट फूरियर ट्रांसफॉर्म के लिए बॉटम-अप का उपयोग करूंगा ।


हैलो!!! मैं यह निर्धारित करना चाहता हूं कि निम्नलिखित प्रस्ताव सही हैं या नहीं। - एक डायनामिक प्रोग्रामिंग अल्गोरिथम के लिए, बॉटम-अप के साथ सभी मानों की गणना असमान रूप से तेज होती है, फिर पुनरावृत्ति और संस्मरण का उपयोग होता है। - एक डायनैमिक अल्गोरिदम का समय हमेशा Ο (dynamic) होता है जहां ble सबप्रोब्लेम्स की संख्या होती है। - एनपी में प्रत्येक समस्या को घातीय समय में हल किया जा सकता है।
मैरी स्टार २

उपरोक्त प्रस्तावों के बारे में मैं क्या कह सकता हूं? क्या आपके पास कोई विचार है? @ डोसा
मैरी स्टार

@ देवता, (1) हमेशा गलत होता है। यह या तो समान या असममित रूप से धीमा है (जब आपको सभी उपप्रोमा की आवश्यकता नहीं है, तो पुनरावृत्ति तेज हो सकती है)। (2) केवल सही है यदि आप O (1) में हर उपप्रकार को हल कर सकते हैं। (३) एक प्रकार का अधिकार है। एनपी में प्रत्येक समस्या को नॉनडेर्मिनिस्टिक मशीन (जैसे क्वांटम कंप्यूटर पर बहुपदीय समय में हल किया जा सकता है, जो एक साथ कई काम कर सकता है: इसका केक है, और एक साथ इसे खाते हैं, और दोनों परिणामों का पता लगाते हैं)। तो एक अर्थ में, एनपी में प्रत्येक समस्या को नियमित कंप्यूटर पर घातीय समय में हल किया जा सकता है। SIde नोट: P में सब कुछ NP में भी है। जैसे दो पूर्णांक
osa

19

एक उदाहरण के रूप में रिटायर श्रृंखला लेते हैं

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

इसे लगाने का दूसरा तरीका,

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

पहले पांच रिटेनर नंबर के मामले में

Bottom(first) number :1
Top (fifth) number: 5 

अब एक उदाहरण के रूप में पुनरावर्ती फाइबोनैचि श्रृंखला एल्गोरिथ्म पर एक नज़र डालते हैं

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

अब अगर हम निम्नलिखित कमांड के साथ इस प्रोग्राम को निष्पादित करते हैं

rcursive(5);

यदि हम एल्गोरिथ्म में बारीकी से देखते हैं, तो पांचवें क्रमांक को उत्पन्न करने के लिए इसे तीसरे और चौथे नंबर की आवश्यकता होती है। इसलिए मेरी पुनरावृत्ति वास्तव में शीर्ष (5) से शुरू होती है और फिर नीचे / निचले नंबर पर सभी तरह से जाती है। यह दृष्टिकोण वास्तव में टॉप-डाउन दृष्टिकोण है।

एक ही गणना को कई बार करने से बचने के लिए हम डायनामिक प्रोग्रामिंग तकनीकों का उपयोग करते हैं। हम पहले गणना किए गए मान को संग्रहीत करते हैं और उसका पुन: उपयोग करते हैं। इस तकनीक को संस्मरण कहा जाता है। डायनेमिक प्रोग्रामिंग के अलावा और भी संस्मरण हैं, जिन्हें वर्तमान समस्या पर चर्चा करने की आवश्यकता नहीं है।

ऊपर से नीचें

हमारे मूल एल्गोरिथ्म को फिर से लिखने और ज्ञापन तकनीकों को जोड़ने की सुविधा देता है।

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

और हम इस विधि को निम्नलिखित की तरह निष्पादित करते हैं

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

यह समाधान अभी भी शीर्ष-नीचे है क्योंकि एल्गोरिथ्म शीर्ष मूल्य से शुरू होता है और हमारे शीर्ष मूल्य को प्राप्त करने के लिए प्रत्येक चरण के नीचे जाता है।

नीचे से ऊपर

लेकिन, सवाल यह है कि क्या हम नीचे से शुरू कर सकते हैं, जैसे पहले रिटायरमेंट नंबर से फिर ऊपर जाने के लिए हमारा रास्ता। इस तकनीक का उपयोग कर इसे फिर से लिखने दें,

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

अब अगर हम इस एल्गोरिथ्म को देखें तो यह वास्तव में निचले मूल्यों से शुरू होता है तो शीर्ष पर जाएं। अगर मुझे 5 वीं रिटेल नंबर की जरूरत है, तो मैं वास्तव में 1 की गणना कर रहा हूं, फिर दूसरा नंबर 5 वें नंबर पर आने वाला है। इस तकनीक को वास्तव में बॉटम-अप तकनीक कहा जाता है।

अंतिम दो, एल्गोरिदम गतिशील प्रोग्रामिंग आवश्यकताओं को पूरा करते हैं। लेकिन एक ऊपर-नीचे है और दूसरा नीचे-ऊपर है। दोनों एल्गोरिथ्म में समान स्थान और समय जटिलता है।


क्या हम कह सकते हैं कि अप-अप दृष्टिकोण को अक्सर गैर-पुनरावर्ती तरीके से लागू किया जाता है?
लुईस चान

नहीं, आप किसी भी लूप लॉजिक को पुनरावृत्ति में बदल सकते हैं
अश्विन शर्मा

3

डायनामिक प्रोग्रामिंग को अक्सर मेमोइज़ेशन कहा जाता है!

1.Memoization ऊपर से नीचे की तकनीक है (इसे तोड़कर समस्या को हल करना शुरू करें) और गतिशील प्रोग्रामिंग एक बॉटम-अप तकनीक है (तुच्छ उप-समस्या से हल करना, दी गई समस्या की ओर बढ़ाना)

2.DP बेस केस (एस) से शुरू करके समाधान ढूंढता है और ऊपर की तरफ काम करता है। डीपी सभी उप-समस्याओं को हल करता है, क्योंकि यह नीचे-ऊपर करता है

संस्मरण के विपरीत, जो केवल आवश्यक उप-समस्याओं को हल करता है

  1. डीपी में घातीय-समय ब्रूट-बल समाधानों को बहुपद-समय एल्गोरिदम में बदलने की क्षमता है।

  2. डीपी अधिक कुशल हो सकता है क्योंकि इसकी पुनरावृत्ति

इसके विपरीत, पुनरावृत्ति के कारण मेमोइज़ेशन को (अक्सर महत्वपूर्ण) ओवरहेड के लिए भुगतान करना होगा।

अधिक सरल होने के लिए, मेमोइज़ेशन समस्या को हल करने के लिए टॉप-डाउन दृष्टिकोण का उपयोग करता है अर्थात यह कोर (मुख्य) समस्या से शुरू होता है फिर इसे उप-समस्याओं में तोड़ता है और इन उप-समस्याओं को समान रूप से हल करता है। इस दृष्टिकोण में एक ही उप-समस्या कई बार हो सकती है और अधिक सीपीयू चक्र का उपभोग कर सकती है, इसलिए समय की जटिलता को बढ़ाएं। जबकि डायनेमिक प्रोग्रामिंग में एक ही उप-समस्या को कई बार हल नहीं किया जाएगा लेकिन समाधान का अनुकूलन करने के लिए पूर्व परिणाम का उपयोग किया जाएगा।


4
यह सच नहीं है, संस्मरण एक कैश का उपयोग करता है जो आपको डीपी के समान समय जटिलता को बचाने में मदद करेगा
सूचित

3

सीधे शब्दों में कहें कि उप डाउन अप्रोच का उपयोग उप समस्याओं को बार-बार कॉल करने के लिए किया
जाता है, जहां नीचे का दृष्टिकोण किसी एक को बुलाए बिना एकल का उपयोग करता है और इसलिए यह अधिक कुशल है।


1

निम्नलिखित डीपी आधारित समाधान एडिट डिस्टेंस समस्या के लिए है जो ऊपर नीचे है। मुझे उम्मीद है कि यह गतिशील प्रोग्रामिंग की दुनिया को समझने में भी मदद करेगा:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

आप अपने घर पर इसके पुनरावर्ती कार्यान्वयन के बारे में सोच सकते हैं। यह काफी अच्छा और चुनौतीपूर्ण है यदि आपने पहले ऐसा कुछ हल नहीं किया है।


1

टॉप-डाउन : अब तक गणना किए गए मूल्य का ट्रैक रखना और आधार स्थिति पूरी होने पर परिणाम वापस करना।

int n = 5;
fibTopDown(1, 1, 2, n);

private int fibTopDown(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibTopDown(j, i + j, count + 1, n);
}

निचला-अप : वर्तमान परिणाम इसकी उप-समस्या के परिणाम पर निर्भर करता है।

int n = 5;
fibBottomUp(n);

private int fibBottomUp(int n) {
    if (n <= 1) return 1;
    return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.