এই নিবন্ধটি উত্পাদন উন্নয়নের সাথে সম্পর্কিত নয়, তবে এটি প্রোগ্রামিং সম্পর্কিত। আমি ডায়নামিক প্রোগ্রামিং (ডিপি) এবং কীভাবে পূর্ববর্তী গণনার অভিজ্ঞতা কার্যকরভাবে ব্যবহার করতে হয় তা নিয়ে আলোচনা করব। আমি আশা করি আপনি এটি আকর্ষণীয় পাবেন.
ডায়নামিক প্রোগ্রামিং শব্দটি প্রথম ব্যবহার করেছিলেন বিখ্যাত আমেরিকান গণিতবিদ, কম্পিউটার প্রযুক্তির একজন নেতৃস্থানীয় বিশেষজ্ঞ রিচার্ড বেলম্যান, 1950 এর দশকে। ডায়নামিক প্রোগ্রামিং (DP) হল জটিল কাজগুলিকে ছোট ছোট উপ-সমস্যায় ভাগ করে সমাধান করার একটি পদ্ধতি।
ডিপি যে ধরনের সমস্যাগুলি সমাধান করতে পারে তা অবশ্যই দুটি মানদণ্ড পূরণ করতে হবে:
1. সর্বোত্তম সাবস্ট্রাকচার । ডায়নামিক প্রোগ্রামিং এর অর্থ হল ছোট উপ-সমস্যাগুলির সমাধান প্রাথমিক একটি সমাধান করতে ব্যবহার করা যেতে পারে। সর্বোত্তম অবস্ট্রাকচার হল "ডিভাইড অ্যান্ড কঙ্কার" দৃষ্টান্তের প্রধান বৈশিষ্ট্য।
বাস্তবায়নের একটি সর্বোত্তম উদাহরণ হল মার্জ সর্ট অ্যালগরিদম, যেখানে আমরা বারবার সমস্যাটিকে সহজতম উপ-সমস্যা (আকার 1 অ্যারে) তে ভাগ করি, যার পরে আমরা প্রতিটির উপর ভিত্তি করে গণনা করি। প্রাথমিক স্তরটি অর্জন না হওয়া পর্যন্ত ফলাফলগুলি পরবর্তী স্তর (বৃহত্তর উপ-সমস্যা) সমাধান করতে ব্যবহৃত হয়।
2. ওভারল্যাপিং সাব-সমস্যা । কম্পিউটার বিজ্ঞানে, একটি সমস্যাকে ওভারল্যাপিং সাব-সমস্যা বলা হয় যদি সমস্যাটিকে সাব-প্রবলেমগুলিতে বিভক্ত করা যায় যা একাধিকবার পুনরায় ব্যবহার করা হয়। অন্য কথায় একই ইনপুট ডেটা এবং একই ফলাফল সহ একই কোড চালানো। এই সমস্যার একটি সর্বোত্তম উদাহরণ হল ফিবোনাচি ক্রমানুসারে একটি N-তম উপাদানের গণনা, যা আমরা নীচে দেখব।
আপনি বলতে পারেন যে ডিপি হ'ল ডিভাইড অ্যান্ড কনক্যুয়ার প্যারাডাইম ব্যবহারের একটি বিশেষ ক্ষেত্রে বা এটির আরও জটিল সংস্করণ। প্যাটার্নটি কম্বিনেটরিক্স জড়িত কাজগুলি সমাধানের জন্য উপযুক্ত, যেখানে প্রচুর পরিমাণে বিভিন্ন সংমিশ্রণ গণনা করা হয়, তবে উপাদানগুলির N- পরিমাণ প্রায়শই একঘেয়ে এবং অভিন্ন।
সর্বোত্তম সাবস্ট্রাকচার এবং ওভারল্যাপিং সাব-সমস্যাগুলির সমস্যাগুলিতে, যদি আমরা একটি ব্রুট-ফোর্স পদ্ধতি প্রয়োগ করি তবে আমাদের অসংখ্য পুনরাবৃত্তি গণনা এবং ক্রিয়াকলাপ রয়েছে। ডিপি আমাদের সমাধানকে অপ্টিমাইজ করতে সাহায্য করে এবং দুটি প্রধান পদ্ধতি ব্যবহার করে গণনার দ্বিগুণ এড়াতে সাহায্য করে: মেমোাইজেশন এবং ট্যাবুলেশন:
1. মেমোাইজেশন (টপ-ডাউন পদ্ধতি) পুনরাবৃত্তির মাধ্যমে বাস্তবায়িত হয়। একটি টাস্ককে ছোট ছোট উপ-সমস্যাগুলিতে বিভক্ত করা হয়, তাদের ফলাফলগুলি একটি মেমরিতে খাওয়ানো হয় এবং বারবার ব্যবহারের মাধ্যমে মূল সমস্যা সমাধানের জন্য একত্রিত হয়।
2. ট্যাবুলেশন (নিচে-আপ পদ্ধতি) একটি পুনরাবৃত্তিমূলক পদ্ধতি ব্যবহার করে প্রয়োগ করা হয়। ক্ষুদ্রতম থেকে প্রাথমিক পর্যন্ত উপ-সমস্যাগুলি পরপর গণনা করা হয়, পুনরাবৃত্তিমূলকভাবে।
তত্ত্বটি ক্লান্তিকর এবং বোধগম্য মনে হতে পারে — আসুন কিছু উদাহরণ দেখি।
DP-এর একটি ক্লাসিক উদাহরণ হল ফিবোনাচি ক্রমানুসারে একটি N-th উপাদান গণনা করা, যেখানে প্রতিটি উপাদান হল দুটি পূর্ববর্তী উপাদানের সমষ্টি এবং প্রথম এবং দ্বিতীয় উপাদানগুলি 0 এবং 1 এর সমান।
অনুমান দ্বারা, আমরা প্রাথমিকভাবে সর্বোত্তম অবস্ট্রাকচারের প্রকৃতির উপর ফোকাস করি, কারণ, একটি মান গণনা করার জন্য, আমাদের পূর্ববর্তীটি খুঁজে বের করতে হবে। পূর্ববর্তী মান গণনা করার জন্য, আমাদের তার আগে যেটি এসেছে তা খুঁজে বের করতে হবে এবং আরও অনেক কিছু। ওভারল্যাপিং সাবটাস্কের প্রকৃতি নিচের চিত্রে দেখানো হয়েছে।
এই কাজের জন্য, আমরা তিনটি ক্ষেত্রে দেখব:
আপনি প্রথম যে জিনিসটির কথা ভাবতে পারেন তা হ'ল পুনরাবৃত্তি প্রবেশ করা, দুটি পূর্ববর্তী উপাদানগুলির সমষ্টি করা এবং তাদের ফলাফল দেওয়া।
/** * Straightforward(Brute force) approach */ fun fibBruteForce(n: Int): Int { return when (n) { 1 -> 0 2 -> 1 else -> fibBruteForce(n - 1) + fibBruteForce(n - 2) } }
নীচের স্ক্রিনে, আপনি ক্রমটির পঞ্চম উপাদান গণনা করতে ফাংশন কলগুলির একটি স্ট্যাক দেখতে পারেন।
উল্লেখ্য যে ফাংশন fib(1)
বলা হয় পাঁচবার, এবং fib(2)
তিনবার। একই পরামিতি সহ একটি স্বাক্ষর সহ ফাংশনগুলি বারবার চালু হয় এবং একই কাজ করে। যখন N সংখ্যা বৃদ্ধি করা হয়, গাছটি রৈখিকভাবে নয়, বরং দ্রুতগতিতে বৃদ্ধি পায়, যা গণনার একটি বিশাল নকলের দিকে পরিচালিত করে।
গণিত বিশ্লেষণ:
সময়ের জটিলতা: O(2^n),
স্থান জটিলতা: O(n) -> পুনরাবৃত্ত ট্রির সর্বোচ্চ গভীরতার সমানুপাতিক, কারণ এটি হল সর্বাধিক সংখ্যক উপাদান যা অন্তর্নিহিত ফাংশন কলের স্ট্যাকে উপস্থিত থাকতে পারে।
ফলাফল: এই পদ্ধতিটি অত্যন্ত অদক্ষ। উদাহরণস্বরূপ, 30 তম উপাদান গণনা করার জন্য 1,073,741,824টি অপারেশন প্রয়োজন।
এই পদ্ধতিতে, বাস্তবায়ন পূর্বোক্ত "ব্রুট-ফোর্স" সমাধান থেকে আলাদা নয়, মেমরির বরাদ্দ ছাড়া। এটি পরিবর্তনশীল মেমো হতে দিন, যেখানে আমরা আমাদের স্ট্যাকের যেকোন সম্পন্ন করা fib() ফাংশনের গণনার ফলাফল সংগ্রহ করি।
এটি উল্লেখ করা উচিত যে পূর্ণসংখ্যার একটি অ্যারে থাকা এবং সূচক দ্বারা এটি উল্লেখ করা যথেষ্ট ছিল, তবে ধারণাগত স্বচ্ছতার জন্য, একটি হ্যাশ মানচিত্র তৈরি করা হয়েছিল।
/** * Memoization(Top-down) approach */ val memo = HashMap<Int, Int>().apply { this[1] = 0 this[2] = 1 } fun fibMemo(n: Int): Int { if (!memo.containsKey(n)) { val result = fibMemo(n - 1) + fibMemo(n - 2) memo[n] = result } return memo[n]!! }
ফাংশন কল স্ট্যাকের উপর আবার ফোকাস করা যাক:
fib(2)
। এটি হাইলাইট করা fib(3)
এ নিয়ন্ত্রণ ফিরিয়ে দেয়।fib(3)
fib(2)
এবং fib(1)
কলের ফলাফলের সারসংক্ষেপের পর এর মান খুঁজে পায়, মেমোতে এর সমাধান প্রবেশ করে এবং fib(4)
এ নিয়ন্ত্রণ ফিরিয়ে দেয়।fib(4)
এ ফিরে আসে, fib(2)
কলটি তার পালাটির জন্য অপেক্ষা করে। fib(2)
ঘুরে, কল করার পরিবর্তে ( fib(1) + fib(0)
), মেমো থেকে সম্পূর্ণ সমাপ্ত সমাধান ব্যবহার করে এবং সরাসরি এটি ফেরত দেয়।fib(4)
গণনা করা হয় এবং fib(5)
এ নিয়ন্ত্রণ ফিরিয়ে দেয়, যা শুধুমাত্র fib(3)
চালু করতে হয়। শেষ উপমায়, fib(3)
অবিলম্বে গণনা ছাড়াই মেমো থেকে একটি মান ফেরত দেয়।
আমরা ফাংশন কল এবং গণনার সংখ্যা হ্রাস লক্ষ্য করতে পারি। এছাড়াও, যখন N বৃদ্ধি করা হয়, তখন তাত্পর্যপূর্ণভাবে কম হ্রাস হবে।
গণিত বিশ্লেষণ:
সময়ের জটিলতা: O(n)
স্থান জটিলতা: O(n)
ফলাফল: অ্যাসিম্পোটিক জটিলতার একটি স্পষ্ট পার্থক্য রয়েছে। এই পদ্ধতিটি আদিম সমাধানের তুলনায় এটিকে সময়ের সাথে রৈখিক হিসাবে হ্রাস করেছে এবং এটি স্মৃতিতে বৃদ্ধি করেনি।
উপরে উল্লিখিত হিসাবে, এই পদ্ধতির মধ্যে একটি ছোট থেকে একটি বড় উপ-সমস্যা পর্যন্ত পুনরাবৃত্তিমূলক গণনা জড়িত। ফিবোনাচির ক্ষেত্রে, ক্ষুদ্রতম "উপ-সমস্যা" হল ক্রমানুসারে প্রথম এবং দ্বিতীয় উপাদান, যথাক্রমে 0 এবং 1।
আমরা ভালভাবে জানি কিভাবে উপাদানগুলি একে অপরের সাথে সম্পর্কিত, যা সূত্রে প্রকাশ করা হয়: fib(n) = fib(n-1) + fib(n-2)
যেহেতু আমরা পূর্বের উপাদানগুলি জানি, আমরা সহজেই পরেরটি, তার পরেরটি এবং আরও অনেক কিছু বের করতে পারি।
আমরা যে মানগুলি সম্পর্কে সচেতন, তার সংক্ষিপ্তকরণ করে আমরা পরবর্তী উপাদানটি খুঁজে পেতে পারি। যতক্ষণ না আমরা পছন্দসই মান খুঁজে পাই ততক্ষণ আমরা এই অপারেশনটি চক্রাকারে বাস্তবায়ন করি।
/** * Tabulation(Bottom-up) approach fun fibTab(n: Int): Int { var element = 0 var nextElement = 1 for (i in 2 until n) { val newNext = element + nextElement element = nextElement nextElement = newNext } return nextElement }
গণিত বিশ্লেষণ:
সময়ের জটিলতা: O(n)
স্থান জটিলতা: O(1)
ফলাফল: এই পদ্ধতিটি গতির পরিপ্রেক্ষিতে মেমোাইজেশনের মতোই কার্যকর, তবে এটি একটি ধ্রুবক পরিমাণ মেমরি ব্যবহার করে।
আসুন একটি আরও জটিল সমস্যা দেখি: N নোড থেকে তৈরি করা যেতে পারে এমন সমস্ত কাঠামোগতভাবে অনন্য বাইনারি গাছের সংখ্যা খুঁজে বের করতে হবে।
একটি বাইনারি ট্রি হল একটি শ্রেণীবদ্ধ ডেটা স্ট্রাকচার, যেখানে প্রতিটি নোডে দুটির বেশি বংশধর থাকে না। একটি সাধারণ নিয়ম হিসাবে, প্রথমটিকে প্যারেন্ট নোড বলা হয় এবং এর দুটি সন্তান রয়েছে - বাম সন্তান এবং ডান সন্তান।
ধরা যাক যে আমাদের N = 3 এর জন্য একটি সমাধান খুঁজে বের করতে হবে। তিনটি নোডের জন্য 5টি কাঠামোগতভাবে অনন্য গাছ রয়েছে। আমরা সেগুলিকে আমাদের মাথায় গণনা করতে পারি, কিন্তু যদি N বৃদ্ধি করা হয়, তবে বৈচিত্রগুলি ব্যাপকভাবে বৃদ্ধি পাবে এবং আমাদের মাথায় এগুলিকে কল্পনা করা অসম্ভব।
এই কাজটি কম্বিনেটরিক্সের জন্য দায়ী করা যেতে পারে। চলুন কাজ করা যাক কোন সমন্বয় গঠিত হতে পারে, এবং কিভাবে আমরা তাদের গঠনের জন্য একটি প্যাটার্ন খুঁজে পেতে পারি।
আসুন একটি কাজের উদাহরণ দেখি, যেখানে ধারণাগত স্তরে N = 6। আমরা আমাদের গণনা ফাংশন numOfUniqueTrees(n: Int) কল করব।
আমাদের উদাহরণে, 6টি নোড দেওয়া হয়েছে, যার মধ্যে একটি, পূর্বে উল্লিখিত নীতি অনুসারে, গাছের শীর্ষবিন্দু গঠন করে এবং অন্য 5টি - অবশিষ্ট।
এখন থেকে, আমরা অবশিষ্ট নোডগুলির সাথে যোগাযোগ করি। এটি বিভিন্ন উপায়ে করা যেতে পারে। উদাহরণস্বরূপ, বাম সাবট্রি গঠনের জন্য পাঁচটি নোড ব্যবহার করে বা ডান সাবট্রিতে পাঁচটি পাঠানো। অথবা নোডগুলিকে দুই ভাগে ভাগ করে — 2 বামে এবং 3টি ডানে (নীচের স্ক্রিনটি দেখুন), আমাদের কাছে এই ধরনের বৈকল্পিক সীমিত সংখ্যক রয়েছে।
ফলাফল numOfUniqueTrees(6)
অর্জন করতে, আমাদের নোডগুলি বিতরণ করার জন্য আমাদের সমস্ত রূপ বিবেচনা করতে হবে। এগুলি টেবিলে দেখানো হয়েছে:
টেবিলটি নোডের 6টি ভিন্ন ডিস্ট্রিবিউশন দেখায়। যদি আমরা খুঁজে পাই যে প্রতিটি বিতরণের জন্য কতগুলি অনন্য গাছ তৈরি করা যেতে পারে এবং এইগুলিকে যোগ করি, আমরা আমাদের চূড়ান্ত ফলাফল অর্জন করব।
বিতরণের জন্য অনন্য গাছের সংখ্যা কীভাবে খুঁজে পাবেন?
আমাদের দুটি প্যারামিটার রয়েছে: বাম নোড এবং ডান নোড (সারণীতে বাম এবং ডান কলাম)।
ফলাফল এর সমান হবে: numOfUniqueTrees(leftNodes) * numOfUniqueTrees(rightNodes)
।
কেন? বামদিকে, আমাদের কাছে X অনন্য গাছ থাকবে এবং প্রতিটির জন্য আমরা ডানদিকে Y অনন্য গাছের বৈচিত্র রাখতে সক্ষম হব। আমরা ফলাফল পেতে এই সংখ্যাবৃদ্ধি.
numOfUniqueTrees(5) * numOfUniqueTrees(0)
, যেহেতু আমাদের ডানদিকে কোন নোড নেই। ফলাফল হল numOfUniqueTrees(5)
— একটি ধ্রুবক ডানদিকে বাম দিকে অনন্য সাবট্রির সংখ্যা।
গণনা করা হচ্ছে numOfUniqueTrees(5)
।
এখন আমাদের সবচেয়ে ছোট উপ-সমস্যা আছে। আমরা এটির সাথে কাজ করব যেমনটি আমরা বড়টির সাথে করেছি। এই পর্যায়ে, ডিপি কাজের বৈশিষ্ট্য স্পষ্ট — সর্বোত্তম অবকাঠামো, পুনরাবৃত্তিমূলক আচরণ।
একটি নোড (সবুজ নোড) শীর্ষবিন্দুতে পাঠানো হয়। আমরা অবশিষ্ট (চার) নোডগুলিকে এমনভাবে বিতরণ করি যা পূর্ববর্তী অভিজ্ঞতা (4:0), (3:1), (2:2), (1:3), (0:4) এর সাথে সাদৃশ্যপূর্ণ।
আমরা প্রথম বিতরণ (4:0) গণনা করি। এটি আগের সাদৃশ্য অনুসারে numOfUniqueTrees(4) * numOfUniqueTrees(0) -> numOfUniqueTrees(4)
এর সমান।
গণনা করা হচ্ছে numOfUniqueTrees(4)
। যদি আমরা শীর্ষবিন্দুতে একটি নোড বরাদ্দ করি, তাহলে আমাদের 3টি অবশিষ্ট থাকবে।
বিতরণ (3:0), (2:1), (1:2), (0:3)।
2টি নোডের জন্য শুধুমাত্র দুটি অনন্য বাইনারি গাছ রয়েছে, 1টির জন্য — একটি৷ পূর্বে আমরা ইতিমধ্যে উল্লেখ করেছি যে তিনটি নোডের জন্য অনন্য বাইনারি গাছের 5টি বৈচিত্র রয়েছে।
ফলস্বরূপ — ডিস্ট্রিবিউশন (3:0), (0:3) 5, (2:1), (1:2) — 2। যদি আমরা 5 + 2 + 2 + 5 যোগ করি, আমরা 14 পাই numOfUniqueTrees(4)
= 14।
আসুন মেমোাইজেশনের অভিজ্ঞতা অনুসারে পরিবর্তনশীল মেমোতে গণনার ফলাফল লিখি। যখনই আমাদের চারটি নোডের জন্য বৈচিত্র্য খুঁজে বের করতে হবে, আমরা সেগুলি আবার গণনা করব না, তবে আগের অভিজ্ঞতা পুনরায় ব্যবহার করব।
আসুন গণনায় ফিরে আসি (5:0), যা বিতরণের যোগফলের সমান (4:0), (3:1), (2:2), (1:3), (0:4)। আমরা জানি যে (4:0) = 14. আসুন মেমোতে ফিরে আসি, (3:1) = 5, (2:2) = 4 (বাম দিকে 2টি পরিবর্তন * ডানদিকে 2), (1:3) = 5, (0:4) = 14. যদি আমরা এইগুলি যোগ করি, আমরা 42, numOfUniqueTrees(5)
= 42 পাব।
আসুন numOfUniqueTrees(6)
এর গণনায় ফিরে আসি, যা বিতরণের যোগফলের সমান।
(5:0) = 42, (4:1) = 14, (3:2) =10 (5 বাম * 2 ডানে), (2:3) = 10, (1:4) = 14, (0: 5) = 42. যদি আমরা এইগুলি যোগ করি, আমরা 132 পাব, numOfUniqueTrees(5) = 132
।
টাস্ক সমাধান!
ফলাফল: আসুন সমাধানের ওভারল্যাপিং সাব-সমস্যাগুলি নোট করি যেখানে N = 6। সরল সমাধানে, numOfUniqueTrees(3)
18 বার বলা হবে (নীচে স্ক্রীন)। N বৃদ্ধির সাথে, একই গণনার পুনরাবৃত্তি অনেক বেশি হবে।
আমি হাইলাইট করি যে (5 বাম, 0 ডান) এবং (0 বাম, 5 ডান) এর জন্য অনন্য গাছের সংখ্যা একই হবে৷ শুধুমাত্র একটি ক্ষেত্রে তারা বাম দিকে এবং অন্যটি ডানদিকে থাকবে। এটি (4 বাম, 1 ডান) এবং (1 বাম, 4 ডান) এর জন্যও কাজ করে।
মেমোাইজেশন, গতিশীল প্রোগ্রামিং এর একটি পদ্ধতি হিসাবে, আমাদের এই ধরনের একটি জটিল কাজের জন্য সমাধান অপ্টিমাইজ করার অনুমতি দিয়েছে।
class Solution { fun calculateTees(n: Int, memo:Array<Int?>): Int { var treesNum = 0 if(n < 1) return 0 if(n == 2) return 2 if(n == 1) return 1 if(memo[n]!=null) return memo[n]!! for (i in 1..n){ val leftSubTrees = calculateTees( i - 1, memo) val rightSubTrees = calculateTees(n - i, memo) treesNum += if(leftSubTrees>0 && rightSubTrees>0){ leftSubTrees*rightSubTrees } else leftSubTrees+leftSubTrees } memo[n] = treesNum return treesNum } }
কিছু ক্ষেত্রে, ডায়নামিক প্রোগ্রামিংয়ের মাধ্যমে কাজগুলি সমাধান করা প্রচুর সময় বাঁচাতে পারে এবং অ্যালগরিদমকে সর্বাধিক দক্ষতার সাথে কাজ করতে পারে।
শেষ পর্যন্ত এই নিবন্ধটি পড়ার জন্য আপনাকে ধন্যবাদ. আমি আপনার প্রশ্নের উত্তর দিতে খুশি হবে.
আয়াত রাখিশেভ পোস্ট করেছেন