লংগেস্ট ইনক্রিজিং সাবসিকোয়েন্স বা দীর্ঘতম ক্রমবর্ধমান উপক্রম (Longest Increasing Subsequence)
কয়েকটি এলোমেলো বা র্যান্ডম (random) সংখ্যার একটি সিকোয়েন্স বা অনুক্রমের কথা কল্পনা করুন: 3, 1, 4, 1, 5, 9, 2, 6। এর ভেতরে লুকিয়ে আছে একটি "সিঁড়ি (staircase)" — অর্থাৎ সংখ্যাগুলোর এমন একটি চেইন বা শিকল যেখানে প্রতিটি সংখ্যা তার ঠিক আগের সংখ্যাটির চেয়ে অবশ্যই বড় হতে হবে। আপনি চাইলে মাঝখানের কিছু সংখ্যাকে এড়িয়ে যেতে বা স্কিপ (skip) করতে পারবেন, কিন্তু আপনি কোনোভাবেই তাদের একে অপরের ক্রম বা রিঅর্ডার (reorder) পরিবর্তন করতে পারবেন না। এখানকার এমন দীর্ঘতম সিঁড়িটি হলো [1, 4, 5, 9] অথবা [1, 4, 5, 6] — যাদের উভয়েরই দৈর্ঘ্য (length) হলো 4।
আর এটিই হলো লংগেস্ট ইনক্রিজিং সাবসিকোয়েন্স বা দীর্ঘতম ক্রমবর্ধমান উপক্রম (Longest Increasing Subsequence — LIS)। এটি মূলত বিভিন্ন শিডিউলিং (scheduling) সমস্যায়, অ্যালগরিদমের জটিলতা বিশ্লেষণের সময়, এমনকি বিভিন্ন তাসের বা কার্ড গেমের (card games) সময়ও সামনে এসে থাকে।
\(O(n^2)\) ডিপি-র পদ্ধতি বা অ্যাপ্রোচ (The \(O(n^2)\) DP Approach)
ধরি dp[i] = আপনার দীর্ঘতম ক্রমবর্ধমান উপক্রমের দৈর্ঘ্য যা i সূচকে বা ইনডেক্সে (index) গিয়ে শেষ হয়। প্রতিটি \(i\) এর জন্য, এর পেছনের দিকে থাকা এমন প্রতিটি \(j < i\) এর দিকে লক্ষ্য করুন যেখানে A[j] < A[i] হবে: dp[i] = max(dp[j] + 1)। এর বেস কেস (Base case): dp[i] = 1 (কারণ প্রতিটি সিঙ্গেল বা একক ইলিমেন্ট (element) নিজে নিজেই 1 দৈর্ঘ্যের একটি সাবসিকোয়েন্স)। উত্তরটি হবে মূলত সমস্ত dp[i] মানগুলোর মধ্যে সবচেয়ে বড় বা ম্যাক্সিমাম (maximum) মানটি।
এতে মূলত \(O(n^2)\) টাইম বা সময় লাগে — এটি n ≤ 10,000 এর জন্য বেশ ভালোভাবেই কাজ করবে, তবে n = 100,000 এর জন্য এটি খুব ধীরগতির হয়ে যাবে।
\(O(n \log n)\) পেশেন্স সর্টিং ট্রিক বা কৌশল (The \(O(n \log n)\) Patience Sorting Trick)
তাস খেলার বা কার্ড গেমের (card game) কথা কল্পনা করুন (যাকে আমরা পেশেন্স গেম বা ধৈর্য্যের খেলাও বলি): আপনি কার্ডগুলোকে বিভিন্ন স্তূপে বা পাইলে (piles) সাজিয়ে রাখছেন, এবং প্রতিটি নতুন কার্ডকে সবথেকে বাম দিকের (leftmost) এমন একটি স্তূপের উপর রাখতে হবে যে স্তূপের উপরের কার্ডটি মূলত আপনার বসা নতুন কার্ডটির চেয়ে সমান অথবা বড় (greater than or equal to) হবে। আর যদি এরকম কোনো স্তূপ বা পাইল খুঁজে না পান, তবে একটি সম্পূর্ণ নতুন পাইল তৈরি বা শুরু করুন। একেবারে শেষে গিয়ে আপনার থাকা ওই মোট স্তূপ বা পাইলের সংখ্যাই হলো আপনার LIS-এর দৈর্ঘ্যের সমান।
কোডের (code) ক্ষেত্রে, একটি টেইলস অ্যারে (tails array) মেইনটেইন করুন বা পরিচালনা করুন, যেখানে tails[k] হবে মূলত এখন পর্যন্ত পাওয়া k+1 দৈর্ঘ্যের ক্রমবর্ধমান সমস্ত সাবসিকোয়েন্সগুলোর মধ্যে সম্ভবপর সবচেয়ে ছোট বা স্মলেস্ট (smallest) টেইল ইলিমেন্ট (tail element)। প্রতিটি x ইলিমেন্টের জন্য:
যদি x > tails.last হয় — তাহলে x-কে এর শেষে যুক্ত করুন (অর্থাৎ আপনার LIS-এর দৈর্ঘ্য আরও এক ধাপ বেড়ে গেছে)।
অন্যথায় — tails[pos] ≥ x এই শর্তটি মেনে সবচেয়ে বাম দিকের (leftmost) অবস্থানটি খুঁজতে একটি বাইনারি সার্চ (binary search) প্রয়োগ করুন, এবং তারপর সেই ইলিমেন্টটিকে x দ্বারা পুনস্থাপিত বা রিপ্লেস (replace) করুন।
আপনার এই টেইলস অ্যারেটি (tails array) সবসময়েই সর্টেড বা সাজানো অবস্থায় (sorted) থাকে (এটি এমন একটি অপরিবর্তনীয় সত্য বা ইনভ্যারিয়েন্ট (invariant) যা আপনি চাইলেই প্রমাণ করতে পারবেন), তাই বাইনারি সার্চের (binary search) কারণে এখানকার প্রতিটি পদক্ষেপে মূলত \(O(\log n)\) সময় লাগে — যা আপনাকে সর্বমোট \(O(n \log n)\) রানটাইম প্রদান করে।
রিপ্লেস করা বা প্রতিস্থাপন করা কেন কাজ করে (Why Replacing Works)
tails[pos]-কে এর থেকে ছোট কোনো মান x দিয়ে রিপ্লেস বা প্রতিস্থাপন করা হলে তা বিদ্যমান কোনো সাবসিকোয়েন্সকেই ভেঙে ফেলে না — বরং এটি ভবিষ্যতে আরও বড় সাবসিকোয়েন্স পাওয়ার সম্ভাবনা ও দোরগোড়া খুলে দেয়। একটি অপেক্ষাকৃত ছোট লেজ বা টেইল (tail) মূলত ভবিষ্যতের এক্সটেনশন বা প্রসারণের জন্য সবসময়ই ভালো: কারণ এর ফলে আপনার অ্যারের ভেতরে থাকা আরও অনেক বেশি সংখ্যক ইলিমেন্ট এটিকে সহজে অনুসরণ (follow) করতে সক্ষম হবে।
LIS — \(O(n \log n)\) পেশেন্স সর্টিং (patience sorting)
Complexity
ছোট কুইজ
পড়া চালিয়ে যান