এই পর্বে যা শিখবেন
- সেটকে পূর্ণসংখ্যায় রাখা, আর bit-এর ছোট কৌশলগুলো (যোগ, বাদ, পরীক্ষা, সবচেয়ে নিচের bit) নির্ভুল লেখা
- Held–Karp-এর DP দিয়ে travelling salesman সমাধান, ট্যুর পুনর্গঠন, শুদ্ধতার যুক্তি আর brute force দিয়ে যাচাই
- Assignment সমস্যা, submask ঘোরানো আর তার O(3n) প্রমাণ, আর SOS DP-র ধারণা
- Digit DP-তে (pos, tight, sum) state সাজিয়ে বিশাল রেঞ্জে সংখ্যা গোনা
- Tree DP-তে party-র দাওয়াতের সমস্যা, subtree size, diameter আর rerooting; আর নতুন সমস্যায় DP-র state বেছে নেওয়ার চেকলিস্ট
১ · সমস্যা — ডেলিভারি রাইডারের রুট আর n! টি পথ
ধানমন্ডির একটি রেস্তোরাঁর হাব থেকে একজন ডেলিভারি রাইডার বের হলেন। হাতে ৪টি অর্ডার — চারটি আলাদা এলাকায়। প্রতিটি এলাকায় একবার যেতে হবে, শেষে হাবে ফিরতে হবে। কোন ক্রমে গেলে মোট পথ সবচেয়ে কম? ৪টি এলাকার ক্রম 4! = 24 রকম — হাতেই সব দেখা যায়। কিন্তু অর্ডার ১৫টি হলে ক্রম 15! ≈ 1.3·1012, আর ২০টিতে প্রায় 2.4·1018. প্রতি সেকেন্ডে ১০০ কোটি ক্রম দেখলেও ২০টির জন্য ৭০ বছরের বেশি লাগবে।
এটিই বিখ্যাত travelling salesman problem (TSP). এটি NP-hard (পর্ব ৬৪) — polynomial সময়ের কোনো অ্যালগরিদম জানা নেই। তবু n! আর 2n-এর মধ্যে বিশাল ফাঁক। n = 20-এ 220·20² ≈ 4.2·108 — এক সেকেন্ডের কাছাকাছি। এই পর্বের প্রথম অর্ধেক সেই কৌশল: “কোন কোন এলাকা ঘোরা হয়েছে”, এই সেটটিকেই DP-র state বানানো।
পর্ব ৫২-তে (DP-র ভিত্তি) দেখেছেন, dynamic programming মানে একই উপসমস্যা বারবার না কষে একবার কষে রাখা। পর্ব ৫৩–৫৪-এর knapsack, LIS, LCS-এ state ছিল এক বা দুটি index — “প্রথম i টি জিনিস”, “প্রথম i আর j টি অক্ষর”. এই পর্বে তিন ধরনের নতুন state দেখবেন:
Bitmask DP
state = একটি সেট। যখন জিনিস কম (n ≤ 20) কিন্তু তাদের যেকোনো দল গুরুত্বপূর্ণ — রুট, কাজ ভাগ, দল গঠন।
Digit DP
state = সংখ্যার অঙ্কের অবস্থান। যখন সংখ্যা বিশাল (1018) আর প্রশ্ন “কয়টি সংখ্যা এই শর্ত মানে?”
Tree DP
state = tree-র একটি node (আর তার একটি ছোট তথ্য)। উত্তর আসে সন্তানদের উত্তর জুড়ে, নিচ থেকে ওপরে।
২ · Bitmask — একটি সংখ্যায় একটি পুরো সেট
উপমা — হাজিরা খাতা. ক্লাসে ৮ জন ছাত্র। প্রতিদিনের হাজিরা একটি ঘরের সারি: উপস্থিত হলে ✓, না হলে ফাঁকা। ✓-কে 1 আর ফাঁকাকে 0 লিখলে একদিনের পুরো হাজিরা একটি ৮-অঙ্কের binary সংখ্যা — যেমন 10110010. একটি সংখ্যা দিয়েই বোঝা যায় কে কে এসেছে। ঠিক এভাবে n টি জিনিসের যেকোনো সেট একটি পূর্ণসংখ্যায় রাখা যায়: bit j = 1 মানে জিনিস j সেটে আছে।
এই পূর্ণসংখ্যাকে বলে bitmask. n টি জিনিসের মোট 2n টি সেট, আর তারা ঠিক 0 থেকে 2n − 1 পর্যন্ত সংখ্যাগুলো। যেমন n = 4-এ 1011 = 11 মানে সেট {0, 1, 3} (ডান দিক থেকে bit 0). Left shift 1 << j হলো শুধু bit j চালু থাকা সংখ্যা, 2j. প্রতিদিনের কাজগুলো এক লাইনের:
| কাজ | C++ / Python | উদাহরণ (mask = 1011) |
|---|---|---|
| j কি সেটে আছে? | (mask >> j) & 1 | j = 2: 0 (নেই) |
| j যোগ করা | mask | (1 << j) | j = 2: 1111 |
| j বাদ দেওয়া | mask & ~(1 << j) | j = 1: 1001 |
| j উল্টে দেওয়া | mask ^ (1 << j) | j = 0: 1010 |
| সেটের আকার | __builtin_popcount(mask) / mask.bit_count() | 3 |
| সবচেয়ে নিচের চালু bit | mask & -mask | 0001 |
| সবচেয়ে নিচের bit মুছে ফেলা | mask & (mask - 1) | 1010 |
| পুরো সেট {0, …, n−1} | (1 << n) - 1 | n = 4: 1111 |
mask & -mask কেন কাজ করে? Two's complement-এ −x মানে x-এর সব bit উল্টে 1 যোগ — ফলে সবচেয়ে নিচের চালু bit আর তার ডানের শূন্যগুলো x-এর মতোই থাকে, বাকি সব উল্টো। AND করলে শুধু ওই bit বাঁচে — পর্ব ৩৬-এর Fenwick tree-র lowbit. আর mask & (mask − 1): 1 বিয়োগ করলে সবচেয়ে নিচের 1 হয় 0, তার ডানের 0-গুলো হয় 1; AND করলে শুধু ওই bit মুছে যায়। বারবার করলে চালু bit-গুলো একে একে গোনা যায় — popcount.
অগ্রাধিকারের ফাঁদ. C++-এ ==-এর অগ্রাধিকার &-এর চেয়ে বেশি: mask & 1 << j == 0 আসলে mask & ((1 << j) == 0). সব সময় বন্ধনী দিন। আর n ≥ 31 হলে 1 << n int-এ overflow — 1LL << n লিখুন। তবে n = 31-এ 231 টি state এমনিতেই অসম্ভব বড়।
৩ · Held–Karp — bitmask DP-তে travelling salesman
এলাকাগুলোর নম্বর 0, 1, …, n − 1; এলাকা 0 হলো হাব; d[i][j] হলো i থেকে j-এর দূরত্ব (কিলোমিটারে)। মূল পর্যবেক্ষণ: রাইডার মাঝপথে থাকলে বাকি পথের সেরা পরিকল্পনার জন্য দুটি জিনিসই জানা দরকার — কোন কোন এলাকা ইতিমধ্যে ঘোরা হয়েছে, আর তিনি এখন কোথায়. কোন ক্রমে সেগুলো ঘুরেছেন, তা আর জরুরি নয়। ৪টি এলাকা ঘুরে এলাকা 3-এ আছেন — 0→1→2→3, নাকি 0→2→1→3, তা ভবিষ্যতের পথে কোনো প্রভাব ফেলে না। শুধু অতীতের পথটি যেন সবচেয়ে ছোট হয়।
তাই state সংজ্ঞায়িত করি:
এখানে mask-এ bit 0 (হাব) আর bit j সব সময় চালু। j-তে পৌঁছানোর ঠিক আগে রাইডার ছিলেন কোনো এক k-তে, যেখানে k ∈ mask \ {j}. তাই optimal substructure থেকে recurrence:
শেষে উত্তর: min over j of ( dp[সব][j] + d[j][0] ) — শেষ এলাকা থেকে হাবে ফেরা। mask \ {j} সংখ্যা হিসেবে mask-এর চেয়ে ছোট, তাই mask-গুলো ছোট থেকে বড় ক্রমে ঘুরলেই প্রয়োজনীয় সব ঘর আগে ভরা থাকে। এই DP-র নাম Held–Karp algorithm. প্রথমে ছোট একটি উদাহরণ: হাব আর তিনটি এলাকা।
d 0 1 2 3 0 = hub (Dhanmondi)
0 0 9 5 4 1 = Gulshan
1 9 0 2 7 2 = Banani
2 5 2 0 6 3 = Mirpur
3 4 7 6 0
| mask | |
| হিসাব |
- শুরু. শুধু হাব ঘোরা হয়েছে, রাইডার হাবে: dp[0001][0] = 0. বাকি সব ঘর এখনো অজানা (?).
- এক এলাকার পথ. mask 0011 = {0, 1}: হাব থেকে সরাসরি 1-এ, 9 km. mask 0101 = {0, 2}: 5 km. এই ঘরগুলোর আগে শুধু হাব, তাই বিকল্প একটিই।
- mask 0111 = {0, 1, 2}. 1-এ শেষ করতে হলে আগে ছিলেন 2-এ: dp[0101][2] + d[2][1] = 5 + 2 = 7. 2-এ শেষ করতে আগে 1-এ: 9 + 2 = 11. একই সেট, শেষ বিন্দু আলাদা — তাই আলাদা ঘর।
- mask 1001 আর 1011. সংখ্যার ক্রমে এবার 1001 = {0, 3}: 4 km. তারপর 1011 = {0, 1, 3}: 1-এ শেষ = 4 + 7 = 11, 3-এ শেষ = 9 + 7 = 16. লক্ষ করুন, 1011-এর দরকারি ঘর 1001 আর 0011 — দুটিই ছোট সংখ্যা, আগেই ভরা।
- mask 1101 = {0, 2, 3}. 2-এ শেষ: 4 + 6 = 10. 3-এ শেষ: 5 + 6 = 11.
- পুরো সেট, j = 1. 1-এর আগে ছিলেন 2 বা 3-এ (সেট {0, 2, 3}): 10 + 2 = 12, বা 11 + 7 = 18. সেরা 12, আর par = 2 মনে রাখি।
- j = 2 আর j = 3. 2-এ শেষ: আগের সেট {0, 1, 3}, min(11 + 2, 16 + 6) = 13 (আগে 1). 3-এ শেষ: আগের সেট {0, 1, 2}, min(7 + 7, 11 + 6) = 14 (আগে 1).
- হাবে ফেরা. 12 + 9 = 21, 13 + 5 = 18, 14 + 4 = 18. সেরা 18, শেষ এলাকা 2. par ধরে পেছনে: 2 ← 1 ← 3 ← 0. ট্যুর 0 → 3 → 1 → 2 → 0 = 4 + 7 + 2 + 5 = 18 km. উল্টো ট্যুর 0 → 2 → 1 → 3 → 0-ও 18 — দূরত্ব দুই দিকে সমান বলে।
কেন ঠিক — induction. দাবি: প্রতিটি ঘরে dp[mask][j] ঠিক সংজ্ঞার মান। mask-এ |mask| টি এলাকা থাকলে induction |mask|-এর ওপর। ভিত্তি: dp[{0}][0] = 0, আর {0, j}-এর একমাত্র পথ 0 → j. ধাপ: mask-এর সব এলাকা ঘুরে j-তে শেষ হওয়া যেকোনো পথ নিন, শেষের আগের এলাকা k. পথটির প্রথম অংশ mask \ {j}-এর সব এলাকা ঘুরে k-তে শেষ — তাই তার দৈর্ঘ্য ≥ dp[mask \ {j}][k] (induction hypothesis). ফলে পুরো পথ ≥ dp[mask \ {j}][k] + d[k][j] ≥ recurrence-এর min. উল্টো দিকে, min-টি যে k দেয়, তার সেরা পথের শেষে j জুড়ে দিলেই একটি বৈধ পথ পাওয়া যায় — কারণ j তো mask \ {j}-এ ছিল না, তাই কোনো এলাকা দুবার আসে না। দুই দিক মিলে সমতা। ∎
খরচ. State: 2n টি mask × n টি শেষ বিন্দু। প্রতিটি state-এ n টি k দেখা। তাই সময় O(2n·n²), জায়গা O(2n·n). (bit 0 সব সময় চালু রাখলে আসলে 2n−1 টি mask — ধ্রুবকে অর্ধেক।) n = 20-এ টেবিলে প্রায় 2·107 টি ঘর; int-এ প্রায় ৮৪ MB, আর par টেবিলসহ দ্বিগুণ। এটিই সীমা — n = 25-এ টেবিলই আর মেমরিতে ধরে না।
ইতিহাস. ১৯৬২ সালে প্রায় একই সময়ে দুটি আলাদা গবেষণাপত্রে এই DP প্রকাশিত হয়: Richard Bellman-এর “Dynamic programming treatment of the travelling salesman problem” (Journal of the ACM), আর Michael Held ও Richard Karp-এর “A dynamic programming approach to sequencing problems” (SIAM-এর জার্নাল)। তাই একে কখনো Bellman–Held–Karp algorithm-ও বলা হয়। Bellman নিজেই ১৯৫০-এর দশকে “dynamic programming” নামটি দিয়েছিলেন। ষাট বছরের বেশি পরেও সাধারণ TSP-র জন্য O(1.9999n)-এর মতো কোনো worst-case নির্ভুল অ্যালগরিদম জানা নেই — এটি এখনো একটি খোলা প্রশ্ন।
এবার পূর্ণ সমস্যা: হাব আর ৪টি এলাকা, অর্থাৎ n = 5. প্রোগ্রামটি DP ভরে, par দিয়ে ট্যুর পুনর্গঠন করে, তারপর 4! = 24টি ক্রমের সবগুলো দেখে (brute force) উত্তর মিলিয়ে নেয়।
#include <algorithm>
#include <climits>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
int main() {
const int n = 5, FULL = (1 << n) - 1, INF = INT_MAX / 2;
int d[n][n] = {{0, 9, 5, 4, 8}, {9, 0, 2, 7, 4}, {5, 2, 0, 6, 9},
{4, 7, 6, 0, 3}, {8, 4, 9, 3, 0}}; // km between 5 areas, 0 = hub
vector<vector<int>> dp(FULL + 1, vector<int>(n, INF)), par(FULL + 1, vector<int>(n, -1));
dp[1][0] = 0; // only the hub visited, rider at hub
for (int mask = 3; mask <= FULL; mask += 2) // odd masks: bit 0 always set
for (int j = 1; j < n; ++j) {
if (!((mask >> j) & 1)) continue;
int prev = mask ^ (1 << j); // the set before arriving at j
for (int k = 0; k < n; ++k)
if (((prev >> k) & 1) && dp[prev][k] + d[k][j] < dp[mask][j]) {
dp[mask][j] = dp[prev][k] + d[k][j];
par[mask][j] = k;
}
}
int best = INF, last = -1;
for (int j = 1; j < n; ++j)
if (dp[FULL][j] + d[j][0] < best) { best = dp[FULL][j] + d[j][0]; last = j; }
vector<int> tour{0};
for (int mask = FULL, j = last; j != 0; ) { // walk the parent pointers back
tour.push_back(j);
int k = par[mask][j]; mask ^= 1 << j; j = k;
}
tour.push_back(0); reverse(tour.begin(), tour.end());
cout << "Held-Karp best tour = " << best << " km:";
for (int v : tour) cout << ' ' << v;
vector<int> p(n - 1); iota(p.begin(), p.end(), 1);
int brute = INF, orders = 0;
do { // brute force: all (n-1)! orders
int c = d[0][p[0]] + d[p.back()][0];
for (int i = 0; i + 1 < n - 1; ++i) c += d[p[i]][p[i + 1]];
brute = min(brute, c); ++orders;
} while (next_permutation(p.begin(), p.end()));
cout << "\nbrute force over " << orders << " orders = " << brute << " km\n";
}আউটপুট:
Held-Karp best tour = 18 km: 0 3 4 1 2 0
brute force over 24 orders = 18 km
DP আর ২৪টি ক্রমের brute force একই 18 km দেয়। ট্যুর 0 → 3 → 4 → 1 → 2 → 0 = 4 + 3 + 4 + 2 + 5 = 18. mask += 2 দিয়ে শুধু বিজোড় mask ঘোরা হয়েছে, কারণ হাবের bit 0 সব সময় চালু। INF = INT_MAX / 2 রাখা হয়েছে যাতে INF + d যোগ করলে overflow না হয়। পুনর্গঠনে প্রতিটি ধাপে par[mask][j] বলে j-এর আগে কে ছিল; তারপর j-কে mask থেকে বাদ দিয়ে পেছনে যাই। ট্যুরটি পেছন থেকে জমা হয়, তাই শেষে reverse. সময় O(2n·n²), জায়গা O(2n·n) — dp আর par দুটি টেবিল।
from itertools import permutations
d = [[0, 9, 5, 4, 8], [9, 0, 2, 7, 4], [5, 2, 0, 6, 9],
[4, 7, 6, 0, 3], [8, 4, 9, 3, 0]] # km between 5 areas, 0 = hub
n = len(d); FULL = (1 << n) - 1; INF = float("inf")
dp = [[INF] * n for _ in range(FULL + 1)]
par = [[-1] * n for _ in range(FULL + 1)]
dp[1][0] = 0 # only the hub visited, rider at hub
for mask in range(3, FULL + 1, 2): # odd masks: bit 0 always set
for j in range(1, n):
if not (mask >> j) & 1: continue
prev = mask ^ (1 << j) # the set before arriving at j
for k in range(n):
if (prev >> k) & 1 and dp[prev][k] + d[k][j] < dp[mask][j]:
dp[mask][j] = dp[prev][k] + d[k][j]
par[mask][j] = k
best, last = min((dp[FULL][j] + d[j][0], j) for j in range(1, n))
tour, mask, j = [0], FULL, last
while j != 0: # walk the parent pointers back
tour.append(j)
mask, j = mask ^ (1 << j), par[mask][j]
tour = [0] + tour[::-1]
print(f"Held-Karp best tour = {best} km:", *tour)
costs = [d[0][p[0]] + sum(d[a][b] for a, b in zip(p, p[1:])) + d[p[-1]][0]
for p in permutations(range(1, n))] # brute force: all (n-1)! orders
print(f"brute force over {len(costs)} orders = {min(costs)} km")আউটপুট C++-এর সাথে হুবহু এক:
Held-Karp best tour = 18 km: 0 3 4 1 2 0
brute force over 24 orders = 18 km
not (mask >> j) & 1-এ আগে shift, তারপর &, শেষে not — Python-এ not-এর অগ্রাধিকার সবচেয়ে কম। তবু বন্ধনী দিলে পড়তে সহজ। min((cost, j) …) tuple তুলনা করে, তাই সমান খরচে ছোট j বেছে নেয় — C++-এর কড়া <-এর মতোই। mask, j = mask ^ (1 << j), par[mask][j]-এ ডান পাশের দুটিই পুরোনো mask আর j থেকে হিসাব হয়। n = 20-এ Python-এ এই তিন-স্তরের লুপ কয়েক মিনিট নিতে পারে; সেখানে C++ বা numpy-র মতো vector কৌশল লাগে।
৪ · আরও bitmask DP — assignment, submask আর SOS
Assignment — কে কোন কাজ করবে
৩ জন রাইডার, ৩টি অর্ডার; cost[i][j] = রাইডার i অর্ডার j নিলে খরচ (মিনিটে)। প্রত্যেকে ঠিক একটি অর্ডার নেবেন — মোট খরচ সবচেয়ে কম কীভাবে? এটি assignment problem. সব ভাগ n! রকম। Bitmask DP-তে state: dp[mask] = mask-এর অর্ডারগুলো প্রথম popcount(mask) জন রাইডারকে দেওয়ার সবচেয়ে কম খরচ. কে কোন অর্ডার পেলেন, তার ক্রম আর জরুরি নয় — শুধু কোন অর্ডারগুলো দেওয়া হয়ে গেছে। পরের রাইডার i = popcount(mask) একটি খালি অর্ডার j নেবেন:
2n টি state, প্রতিটিতে n টি পছন্দ — O(2n·n) সময়, O(2n) জায়গা। Held–Karp-এর চেয়ে এক গুণ n কম, কারণ এখানে “এখন কোথায়” জানার দরকার নেই — রাইডারের পরিচয় mask-এর আকার থেকেই আসে। n ≤ 20-এ চমৎকার। বড় n-এ এই সমস্যার polynomial সমাধান আছে — Hungarian algorithm, O(n³) (পর্ব ৪৭-এ assignment problem প্রসঙ্গে এর নাম দেখেছেন)। অর্থাৎ bitmask DP সব সময় সেরা নয়: সমস্যাটি সত্যিই NP-hard কি না, আগে ভাবুন। কিন্তু শর্ত একটু বদলালেই (“অর্ডার 0 অর্ডার 1-এর আগে বিলি হবে — অর্থাৎ অর্ডার 1 পাবেন অর্ডার 0-এর রাইডারের চেয়ে বড় নম্বরের কেউ”) সাধারণ Hungarian আর সরাসরি খাটে না, আর bitmask DP শুধু transition-এ একটি শর্ত যোগ করে টিকে যায় (j = 1 নেওয়া যাবে কেবল mask-এ bit 0 থাকলে)।
সব submask ঘোরা — আর কেন মোট O(3n)
অনেক bitmask DP-তে একটি সেটকে দুই ভাগে ভাগ করতে হয়: “এই দলের কিছু কাজ আজ, বাকিটা কাল”. তখন mask-এর প্রতিটি submask s (s ⊆ mask) ঘুরতে হয়। একটি সুন্দর এক-লাইনের লুপ আছে: for (s = m; s > 0; s = (s - 1) & m). s − 1 সবচেয়ে নিচের চালু bit মুছে তার ডানের সব bit চালু করে; & m শুধু m-এর bit রাখে। ফল: m-এর ভেতরের পরের ছোট submask. দেখুন m = 1011-এ:
| s সেট হিসেবে | |
| কত নম্বর |
- শুরু s = m. পুরো সেটটিও নিজের একটি submask. s − 1 = 1010, তার সাথে AND m = 1010.
- 1010. s − 1 = 1001 — m-এর বাইরের কোনো bit নেই, তাই AND-এর পরেও 1001.
- 1001. s − 1 = 1000. এটিও m-এর ভেতরে।
- 1000 — আসল কৌশল. s − 1 = 0111. কিন্তু bit 2 m-এ নেই! AND m সেটিকে মুছে দেয়: 0011. লাফটি m-এর বাইরের bit এড়িয়ে ঠিক পরের ছোট submask-এ পৌঁছায়।
- 0011. s − 1 = 0010, AND m = 0010.
- 0010. s − 1 = 0001.
- 0001. s − 1 = 0000 — এর পরে s = 0.
- লুপ থামে.
s > 0শর্তে খালি সেট লুপের ভেতরে আসে না — দরকার হলে আলাদা করে দেখুন। m-এ ৩টি চালু bit, তাই ঠিক 2³ = 8টি submask, প্রতিটি একবার, বড় থেকে ছোট সংখ্যার ক্রমে।
মোট খরচ O(3n) — প্রমাণ. সব mask আর তার সব submask-এর জোড়া (mask, s) গুনি। প্রতিটি উপাদান i-এর জন্য ঠিক তিনটি সম্ভাবনা: i mask-এ নেই; i mask-এ আছে কিন্তু s-এ নেই; i দুটিতেই আছে। (s-এ আছে কিন্তু mask-এ নেই — অসম্ভব, কারণ s ⊆ mask.) n টি উপাদানের স্বাধীন তিনটি পছন্দ — মোট 3n জোড়া। একই কথা binomial theorem দিয়ে: k টি bit-এর mask C(n, k) টি, প্রতিটির 2k টি submask, আর Σk C(n, k)·2k = (1 + 2)n = 3n. n = 15-এ 315 ≈ 1.4·107 — দ্রুত; n = 20-এ ≈ 3.5·109 — অনেক ধীর। অনেকে ভুল করে একে O(4n) ধরেন (2n mask × 2n), যা অনেক বেশি।
SOS DP — submask-এর যোগফল দ্রুত. প্রতিটি mask-এর জন্য F[mask] = Σs ⊆ mask a[s] চাই? উপরের লুপে O(3n). SOS DP (sum over subsets) এক এক bit ধরে কাজ করে — প্রতিটি bit i-এর জন্য, যেসব mask-এ i চালু, তাদের সাথে i-বন্ধ সঙ্গীর মান যোগ করে। এটি আসলে n-মাত্রার একটি prefix sum (পর্ব ১৭), প্রতিটি মাত্রার দৈর্ঘ্য 2:
Pseudocode — SOS DP (bits 0..n-1), F starts as a copy of a
for i = 0 to n-1:
for mask = 0 to 2^n - 1:
if mask has bit i: F[mask] += F[mask without bit i]
// now F[mask] = sum of a[s] over all s that are submasks of mask -- O(n 2^n)৫ · Digit DP — 1018 পর্যন্ত সংখ্যা গোনা
প্রশ্ন: 1 থেকে 1018-এর মধ্যে কয়টি সংখ্যার অঙ্কের যোগফল ঠিক 90? বা কয়টি সংখ্যায় 4 অঙ্কটি নেই? (চীনা আর কিছু এশীয় সংস্কৃতিতে 4-কে অশুভ ধরা হয়, তাই অনেক ভবনে ৪ নম্বর তলা থাকে না — “৪ ছাড়া নম্বর” একটি পরিচিত ধাঁধা।) সব সংখ্যা ঘোরা অসম্ভব। কিন্তু খেয়াল করুন: যোগফলের প্রশ্নে একটি সংখ্যার পুরো পরিচয় লাগে না — বাঁ থেকে অঙ্ক বসাতে বসাতে শুধু তিনটি তথ্য মনে রাখলেই চলে। এটিই digit DP.
প্রথম কৌশল: [L, R]-এর বদলে শুধু [0, N] গুনি, কারণ count(L..R) = f(R) − f(L − 1) — পর্ব ১৭-এর prefix sum-এর মতোই। এবার f(N): N-এর অঙ্কগুলো বাঁ থেকে একে একে বসাই। state তিনটি:
- pos — এখন কোন অবস্থানের অঙ্ক বসাচ্ছি (বাঁ থেকে 0, 1, …)।
- tight — এ পর্যন্ত বসানো অঙ্কগুলো কি N-এর শুরুর অঙ্কের সাথে হুবহু মেলে? মিললে এই অবস্থানে N-এর অঙ্কের বেশি বসানো যাবে না; না মিললে (আগেই ছোট অঙ্ক বসেছে) 0–9 সবই চলে। এই tight flag-ই N-এর চেয়ে বড় সংখ্যা ঠেকায়।
- sum — এ পর্যন্ত অঙ্কের যোগফল।
শুরুর শূন্যগুলো (leading zero) যোগফলে কিছু বদলায় না, তাই 7-কে আমরা “007” হিসেবে গুনি — সমস্যা নেই। (অঙ্ক 0-এর সংখ্যা গোনার প্রশ্নে অবশ্য আলাদা started flag লাগে।) N = 253, S = 7-এর হিসাব দেখুন — অর্থাৎ 0 থেকে 253-এর মধ্যে কয়টি সংখ্যার অঙ্ক-যোগফল 7:
| সিদ্ধান্ত | |
| ফল |
- শুরু. state (0, T, 0): প্রথম অবস্থান, এখনো N-এর সাথে মিলছি, যোগফল 0. N-এর প্রথম অঙ্ক 2, তাই এখানে বসতে পারে 0, 1 বা 2.
- অঙ্ক 0. 0 < 2, তাই সংখ্যাটি এখন N-এর চেয়ে ছোট হবেই — tight ভেঙে free. state (1, F, 0): বাকি দুই অঙ্ক যেকোনো, যোগফল 7 লাগবে: 07, 16, 25, 34, 43, 52, 61, 70 — 8টি। রিকার্শন এটি নিজেই গুনে দেয়।
- অঙ্ক 1. এটিও free. state (1, F, 1): বাকি দুই অঙ্কে 6 লাগবে — 06 থেকে 60, 7টি।
- অঙ্ক 2. N-এর অঙ্কের সমান, তাই tight থাকে: (1, T, 2). পরের অবস্থানে সীমা N-এর অঙ্ক 5.
- pos 1-এ 0..4. 5-এর ছোট যেকোনো অঙ্ক tight ভাঙে। (2, F, 2) থেকে (2, F, 6) — প্রতিটিতে বাকি এক অঙ্কে ঠিক 7 − sum বসাতে হবে, যা 1 থেকে 5, সব বৈধ। মোট 5টি: 205, 214, 223, 232, 241.
- pos 1-এ 5. tight থাকে: (2, T, 7). শেষ অবস্থানে সীমা 3, আর দরকার 0 — অঙ্ক 0 চলে (0 < 3, তাই tight ভাঙে; 1–3 বসালে যোগফল 7 ছাড়িয়ে যায়)। (3, F, 7)-এ পৌঁছে যোগফল ঠিক 7: সংখ্যা 250. এই শাখায় মোট 5 + 1 = 6.
- যোগ. 8 + 7 + 6 = 21. brute force দিয়েও 0..253-এ ঠিক 21টি। মূল লাভ: free অবস্থার উত্তর শুধু (pos, sum)-এর ওপর নির্ভর করে — (2, F, 3) একবার কষে রাখলে যেকোনো পথ থেকে এলে আবার ব্যবহার হয় — এটিই memoization-এর লাভ।
খরচ. N-এ D টি অঙ্ক, যোগফল বড়জোর 9D. state D × 2 × (9D + 1), প্রতিটিতে ১০টি অঙ্ক — O(10·D·9D) = O(D²) ধাপ। N = 1018-এ D = 19: প্রায় 19 × 2 × 172 ≈ 6,500 state, ৬৫ হাজারের মতো ধাপ। 1018 টি সংখ্যার জায়গায় কয়েক হাজার state — digit DP-র জাদু এখানেই। tight = 1 state প্রতিটি pos-এ আসলে একটিই (N-এর prefix), তাই বেশিরভাগ কাজ free state-এ।
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
string s; // digits of N
int S; // wanted digit sum
long long memo[20][2][170];
long long go(int pos, int tight, int sum) {
if (sum > S) return 0; // already too big: prune
if (pos == (int)s.size()) return sum == S;
long long &r = memo[pos][tight][sum];
if (r >= 0) return r;
r = 0;
int lim = tight ? s[pos] - '0' : 9; // tight: may not exceed N's digit
for (int dg = 0; dg <= lim; ++dg)
r += go(pos + 1, tight && dg == lim, sum + dg);
return r;
}
long long countUpTo(long long N) { // how many x in [0, N] have digit sum S
if (N < 0) return 0;
s = to_string(N);
memset(memo, -1, sizeof memo); // tight states depend on N: clear
return go(0, 1, 0);
}
int digitSum(long long x) { int t = 0; for (; x > 0; x /= 10) t += x % 10; return t; }
int main() {
S = 7;
cout << "[0, 253] sum 7: " << countUpTo(253) << '\n';
long long L = 123, R = 4567; S = 15;
int brute = 0;
for (long long x = L; x <= R; ++x) brute += digitSum(x) == S;
cout << "[123, 4567] sum 15: digit DP = " << countUpTo(R) - countUpTo(L - 1)
<< ", brute force = " << brute << '\n';
S = 90;
cout << "[1, 10^18] sum 90: " << countUpTo(1000000000000000000LL) - countUpTo(0) << '\n';
}আউটপুট:
[0, 253] sum 7: 21
[123, 4567] sum 15: digit DP = 343, brute force = 343
[1, 10^18] sum 90: 24918647231083239
প্রথম লাইন স্টেপারের 21. দ্বিতীয় লাইনে [123, 4567]-এ digit DP আর একটি একটি করে সংখ্যা দেখা brute force — দুটিই 343. শেষ লাইনে 1018 পর্যন্ত: প্রায় 2.5·1016টি সংখ্যা, যা brute force-এ কখনো গোনা যেত না; long long-এ (প্রায় 9.2·1018 পর্যন্ত) নিরাপদে ধরে। memo[pos][tight][sum]-এ −1 মানে “এখনো কষা হয়নি”; প্রতিটি নতুন N-এর জন্য memo মুছতে হয়, কারণ tight state-গুলো N-এর অঙ্কের ওপর নির্ভর করে। sum > S হলে সাথে সাথে 0 — অপ্রয়োজনীয় শাখা কেটে ফেলা।
from functools import lru_cache
def count_upto(N, S): # how many x in [0, N] have digit sum S
if N < 0: return 0
s = str(N) # digits of N
@lru_cache(maxsize=None) # a fresh memo for every N
def go(pos, tight, total):
if total > S: return 0 # already too big: prune
if pos == len(s): return int(total == S)
lim = int(s[pos]) if tight else 9 # tight: may not exceed N's digit
return sum(go(pos + 1, tight and dg == lim, total + dg)
for dg in range(lim + 1))
return go(0, True, 0)
def digit_sum(x): return sum(map(int, str(x)))
print(f"[0, 253] sum 7: {count_upto(253, 7)}")
L, R, S = 123, 4567, 15
brute = sum(digit_sum(x) == S for x in range(L, R + 1))
print(f"[{L}, {R}] sum {S}: digit DP = {count_upto(R, S) - count_upto(L - 1, S)}"
f", brute force = {brute}")
print(f"[1, 10^18] sum 90: {count_upto(10**18, 90) - count_upto(0, 90)}")আউটপুট C++-এর সাথে হুবহু এক:
[0, 253] sum 7: 21
[123, 4567] sum 15: digit DP = 343, brute force = 343
[1, 10^18] sum 90: 24918647231083239
@lru_cache নিজেই memo রাখে। ফাংশনটি count_upto-র ভেতরে লেখা, তাই প্রতিটি নতুন N-এ নতুন cache তৈরি হয় — C++-এর memset-এর কাজ। Python-এর পূর্ণসংখ্যার কোনো সীমা নেই, তাই overflow-এর ভয় নেই। রিকার্শনের গভীরতা মাত্র অঙ্কের সংখ্যা (১৯), তাই RecursionError-এর প্রশ্নই নেই।
একই ছাঁচে আরও অনেক প্রশ্ন, শুধু state-এর তৃতীয় অংশ বদলায়: “4 নেই” — 4 বসানো নিষেধ, sum-এর দরকারই নেই; “7 দিয়ে বিভাজ্য” — sum-এর বদলে এ পর্যন্ত সংখ্যার mod 7; “পাশাপাশি দুটি অঙ্ক সমান নয়” — আগের অঙ্কটি মনে রাখা।
৬ · Tree DP — অফিসের party আর বসের সমস্যা
একটি কোম্পানির বার্ষিক party. প্রতিটি কর্মীর একটি “আনন্দ-মান” আছে — তিনি এলে party কতটা জমে। কিন্তু একটি শর্ত: কোনো কর্মী আর তাঁর সরাসরি বস একসাথে দাওয়াত পাবেন না (বস থাকলে কর্মী অস্বস্তিতে থাকেন)। মোট আনন্দ সর্বোচ্চ কত? কোম্পানির ক্রমবিন্যাস একটি rooted tree (পর্ব ২৬), আর আমরা খুঁজছি tree-র সর্বোচ্চ ওজনের maximum independent set — এমন node-এর সেট, যার কোনো দুটি edge দিয়ে সরাসরি যুক্ত নয়।
সাধারণ graph-এ এই সমস্যা NP-hard. কিন্তু tree-তে প্রতিটি node তার subtree-কে বাকি tree থেকে আলাদা করে — subtree-র ভেতরের সিদ্ধান্ত বাইরের সাথে শুধু একটি প্রশ্নে যুক্ত: subtree-র root নিজে দাওয়াত পেলেন কি না. তাই tree DP-র state:
in[v] = v-এর subtree-তে সর্বোচ্চ আনন্দ, যদি v দাওয়াত পান — তখন কোনো সন্তান নয়। out[v] = v দাওয়াত না পেলে — তখন প্রতিটি সন্তান স্বাধীনভাবে নিজের ভালোটি বেছে নেন। সন্তানের মান আগে লাগে, তাই হিসাব postorder-এ: আগে সন্তান, পরে বাবা। উত্তর max(in[root], out[root]). বন্ধনীতে প্রতিটি কর্মীর আনন্দ-মান:
| হিসাব | |
| সব node |
- সমস্যা. ৭ জন কর্মী। লোভী চেষ্টা — সবচেয়ে বড় মান 3 (6) আগে — কাজ করবে কি? বা “একটি করে স্তর বাদ দিয়ে” দাওয়াত? দেখা যাক DP কী বলে।
- Leaf. 4 আর 5-এর অধীনে কেউ নেই। দাওয়াত দিলে in = নিজের মান, না দিলে out = 0.
- কর্মী 2. 2 এলে 4, 5 আসতে পারবেন না: in[2] = 3. 2 না এলে 4 আর 5 দুজনেই আসতে পারেন: out[2] = 4 + 2 = 6. এখানে বসকে বাদ দেওয়াই ভালো।
- Leaf 6, 7. in = 1 আর 3, out = 0.
- কর্মী 3. in[3] = 6 (6, 7 বাদ)। out[3] = 1 + 3 = 4. এবার উল্টো — বসকে ডাকাই ভালো।
- MD (1). in[1] = 1 + out[2] + out[3] = 1 + 6 + 4 = 11. out[1] = max(in[2], out[2]) + max(in[3], out[3]) = 6 + 6 = 12.
- উত্তর 12. MD নেই; 2-এর শাখায় out (4, 5 আসেন), 3-এর শাখায় in (3 আসেন)। সেট {3, 4, 5}. তুলনা: জোড় স্তর {1, 4, 5, 6, 7} = 11, বিজোড় স্তর {2, 3} = 9 — স্তরের নিয়ম ভুল। এখানে “সবচেয়ে বড় মান আগে” লোভ কাকতালীয়ভাবে 12 দেয়, কিন্তু সাধারণভাবে ভুল: শিকল a(3) – b(4) – c(3)-এ লোভ নেয় b = 4, DP নেয় a + c = 6. প্রতিটি node একবার, প্রতিটি edge একবার: O(n).
Pseudocode — tree DP (0-based or 1-based, children known after rooting)
dfs(v): // postorder: children first
in[v] = w[v]; out[v] = 0; size[v] = 1; down[v] = 0
best1 = best2 = 0 // two longest downward edge-paths from children
for each child c of v:
dfs(c)
in[v] += out[c]
out[v] += max(in[c], out[c])
size[v] += size[c] // subtree size
h = down[c] + 1; down[v] = max(down[v], h)
if h > best1: best2 = best1; best1 = h else if h > best2: best2 = h
diameter = max(diameter, best1 + best2) // longest path that bends at vএক DFS-এ তিনটি tree DP একসাথে: subtree size — size[v] = 1 + Σ size[c]; diameter — প্রতিটি node-এ নিচে নামা দুটি সবচেয়ে লম্বা পথ জুড়ে বাঁকা পথ, আর সব node-এর মধ্যে সর্বোচ্চটি diameter. পর্ব ৪৮-এ (LCA আর tree-র কৌশল) diameter দেখেছিলেন দুইবার BFS দিয়ে; এই DP সংস্করণের সুবিধা হলো ঋণাত্মক edge-ওজনেও চলে (h-এ ওজন যোগ করুন আর best1, best2-কে 0-তে সীমিত রাখুন)। সবকিছু O(n) সময়ে, O(n) জায়গায়। বড় tree-তে (২ লাখ node-এর শিকল) recursive DFS stack ভরিয়ে ফেলতে পারে — BFS ক্রম বের করে তার উল্টো ক্রমে হিসাব করলেও একই postorder-এর কাজ হয়।
৭ · Rerooting — প্রতিটি node-কে root ধরে উত্তর
নতুন প্রশ্ন: একটি গ্রামের ৬টি বাড়ি রাস্তায় tree আকারে যুক্ত। একটি টিউবওয়েল কোন বাড়ির সামনে বসালে সব বাড়ি থেকে সেখানে যাওয়ার মোট দূরত্ব সবচেয়ে কম হয়? অর্থাৎ প্রতিটি node v-এর জন্য ans[v] = Σu dist(v, u). প্রতিটি node থেকে আলাদা BFS চালালে O(n²). Rerooting-এ দুটি DFS-এ O(n).
প্রথম DFS (root 1, নিচ থেকে ওপরে): size[v] আর ans[1] = সব node-এর depth-এর যোগফল। দ্বিতীয় DFS (ওপর থেকে নিচে): root-কে parent p থেকে সন্তান c-তে এক ধাপ সরালে c-র subtree-র size[c] টি node এক ধাপ কাছে আসে, আর বাকি n − size[c] টি এক ধাপ দূরে যায়:
| হিসাব | |
| ans |
- Subtree size. প্রথম DFS-এ postorder: leaf-দের size 1, তারপর size[2] = 1 + 1 + 1 = 3, size[3] = 1 + 1 = 2, size[1] = 6.
- Root-এর উত্তর. 1 থেকে দূরত্ব: 2 আর 3 এক ধাপে, 4, 5, 6 দুই ধাপে। ans[1] = 8. এটুকু একটি সাধারণ DFS.
- Root সরল 2-তে. 2-এর subtree-র ৩টি বাড়ি (2, 4, 5) এক ধাপ কাছে, বাকি ৩টি (1, 3, 6) এক ধাপ দূরে: 8 − 3 + 3 = 8.
- Root সরল 3-তে. 3-এর subtree-তে ২টি (3, 6) কাছে আসে, বাকি ৪টি দূরে যায়: 8 − 2 + 4 = 10.
- Leaf-দের দিকে. সূত্রটি সব সময় parent-এর উত্তর থেকে: ans[4] = ans[2] − 1 + 5 = 12, একইভাবে ans[5] = 12; ans[6] = ans[3] − 1 + 5 = 14. প্রতিটি edge-এ O(1).
- যাচাই আর উত্তর. 4 থেকে সরাসরি গুনে: 2 (1), 5 (2), 1 (2), 3 (3), 6 (4) = 12 ✓. টিউবওয়েল বসান 1 বা 2-এর সামনে, মোট দূরত্ব 8. দুটি DFS, প্রতিটি O(n) — মোট O(n), যেখানে n বার BFS-এ O(n²).
সাধারণ ছাঁচ. Rerooting কাজ করে যখন “parent-এর দিকের অংশ”-এর উত্তর parent-এর পূর্ণ উত্তর থেকে c-র অবদান বাদ দিয়ে পাওয়া যায়। যোগফলে বাদ দেওয়া সহজ (বিয়োগ)। max-এর মতো কাজে বিয়োগ নেই — তখন প্রতিটি node-এ সন্তানদের prefix আর suffix max রেখে “c ছাড়া বাকিদের max” বের করতে হয়। এটি কনটেস্টের একটি প্রিয় কৌশল: “প্রতিটি node থেকে সবচেয়ে দূরের node”, “প্রতিটি node-কে root ধরলে কয়টি edge উল্টাতে হবে” — সবই এক ছাঁচ।
৮ · DP-র state কীভাবে বাছবেন — DAG, চেকলিস্ট আর কনটেস্টের সংকেত
সব DP আসলে DAG-এ পথ. প্রতিটি state একটি node, আর “এই state-এর উত্তর ওই state থেকে আসে” একটি edge. চক্র থাকলে চলে না — তাই state-গুলোর মধ্যে একটি ক্রম থাকতেই হবে: Held–Karp-এ mask বড় হয়, digit DP-তে pos বাড়ে, tree DP-তে subtree ছোট হয়। এটি একটি DAG, আর DP মানে সেই DAG-এর topological ক্রমে (পর্ব ৪২) সবচেয়ে ছোট বা লম্বা পথ, বা পথের সংখ্যা গোনা। Memoization সেই ক্রম নিজে খুঁজে নেয়; tabulation-এ আপনাকে ক্রমটি ঠিক করে দিতে হয়। মোট সময় = state-এর সংখ্যা × প্রতিটি state-এ transition-এর সংখ্যা।
DP state বাছাইয়ের চেকলিস্ট
- ভবিষ্যতের জন্য অতীতের কী জানা দরকার? শুধু সেটুকু state-এ রাখুন। TSP-তে “ক্রম” নয়, “সেট আর এখনকার জায়গা”.
- State কি যথেষ্ট? একই state-এ দুটি আলাদা অতীত থেকে এলে ভবিষ্যতের সেরা সিদ্ধান্ত কি একই? না হলে state-এ কিছু বাদ পড়েছে।
- Transition লিখুন — শেষ সিদ্ধান্তটি কী? (TSP-তে শেষের আগের এলাকা, party-তে root দাওয়াত পেলেন কি না।)
- ভিত্তি আর উত্তর কোন state-এ, আর হিসাবের ক্রম চক্রহীন কি না।
- গুনে দেখুন: state × transition সময়সীমায় ধরে কি না, আর টেবিল মেমরিতে।
| ইনপুটের সীমা | সম্ভাব্য কৌশল | মোটামুটি কাজ |
|---|---|---|
| n ≤ 10–11 | সব permutation, backtracking (পর্ব ৫৬) | n! ≈ 4·107 |
| n ≤ 20 (কখনো 23–24) | bitmask DP | 2n·n বা 2n·n² |
| n ≤ 15–16, সেট ভাগ | submask DP | 3n |
| n ≤ 40 | meet in the middle | 2n/2 |
| সংখ্যা 1018 পর্যন্ত, “কয়টি সংখ্যা” | digit DP | অঙ্ক × অবস্থা × 10 |
| tree, n ≤ 2·105 | tree DP, rerooting | O(n) |
| grid-এর একটি পাশ ≤ 10 | profile DP | 2w·w·h |
আরও গভীরে: Profile (broken profile) DP-এ grid-এর একটি সরু কলাম বা সারির “সীমানা” একটি bitmask — যেমন 8 × n বোর্ডকে 1 × 2 domino দিয়ে ঢাকার উপায় গোনা, প্রতিটি ঘরে এক এক করে এগিয়ে O(n·w·2w). অন্যদিকে কিছু DP-তে state কম কিন্তু transition বেশি — dp[i] = minj(dp[j] + b[j]·a[i]) ধরনে প্রতিটি j আসলে একটি সরলরেখা, আর convex hull trick (পর্ব ৬৩-এর convex hull-এর আত্মীয়) O(n²)-কে O(n log n) বা O(n)-এ নামায়। আর সর্বোত্তম j যদি i বাড়লে কখনো না কমে, তাহলে divide and conquer optimization একটি সারি O(n log n)-এ ভরে। এগুলো সবই একই প্রশ্নের উত্তর: state না কমিয়ে transition-এর খরচ কীভাবে কমানো যায়।
পরীক্ষা ও ইন্টারভিউতে:
- Held–Karp-এর state, recurrence আর complexity লিখুন। — dp[mask][j] = mask ঘুরে j-তে শেষ সবচেয়ে ছোট পথ; dp[mask][j] = mink dp[mask \ {j}][k] + d[k][j]; সময় O(2n·n²), জায়গা O(2n·n).
- সব mask-এর সব submask ঘোরার মোট সময় কেন O(3n)? — প্রতিটি উপাদান তিন অবস্থার একটিতে: mask-এর বাইরে, mask-এ কিন্তু submask-এ নয়, দুটিতেই।
- Digit DP-তে tight flag কেন লাগে? — এ পর্যন্ত N-এর prefix-এর সাথে মিললে এই অবস্থানের অঙ্ক N-এর অঙ্কে সীমিত; না রাখলে N-এর চেয়ে বড় সংখ্যাও গোনা হয়।
- Tree-তে maximum independent set-এর recurrence? — in[v] = w[v] + Σ out[c], out[v] = Σ max(in[c], out[c]); O(n).
- প্রতিটি node থেকে সবার দূরত্বের যোগফল O(n)-এ? — rerooting: ans[c] = ans[p] − size[c] + (n − size[c]).
৯ · AI-এর যুগে উন্নত DP — state ঠিক, নাকি শুধু দেখতে ঠিক?
AI টুল bitmask বা digit DP-র কোড চেনা চেহারায় দ্রুত লিখে দেয়, কিন্তু উন্নত DP-তে ভুল থাকে state-এর সংজ্ঞায় — যা ছোট উদাহরণে ধরা পড়ে না। সাধারণ ভুলগুলো: Held–Karp-এ শুরুর এলাকাকে mask-এ না রাখা, ফলে হাব দুবার আসে; digit DP-তে tight state-ও memo করে পরের N-এ পুরোনো memo ব্যবহার করা; submask লুপকে O(4n) বা O(2n) বলা; tree DP-তে “একটি করে স্তর বাদ” ধরনের লোভী উত্তরকে সঠিক বলা; আর bitmask DP-কে n = 30-এ চালানোর পরামর্শ, যেখানে 230·30 ঘরের টেবিল মেমরিতেই ধরে না।
AI-কে সমাধান লিখতে বলার আগে নিজের state যাচাই করাতে বলুন:
উন্নত DP যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Held–Karp ১৯৬২ সালের অ্যালগরিদম। ষাট বছর পরেও সাধারণ TSP-র নির্ভুল সমাধানে এর চেয়ে মৌলিকভাবে দ্রুত worst-case অ্যালগরিদম নেই কেন — অথচ বাস্তবে হাজার হাজার শহরের TSP কীভাবে সমাধান হয়?
১৯৬২ সালে Richard Bellman আর আলাদাভাবে Michael Held ও Richard Karp দেখান, “কোন শহরগুলো ঘোরা হয়েছে আর এখন কোথায়” — এই state দিয়ে TSP O(2n·n²) সময়ে সমাধান হয়। n! থেকে 2n-এ নামা বিশাল লাফ: n = 20-এ n! প্রায় 2.4·1018, আর 2n·n² প্রায় 4·108. কিন্তু এরপর গবেষকেরা বহু চেষ্টা করেও যেকোনো দূরত্বের সাধারণ TSP-র জন্য এমন কোনো নির্ভুল অ্যালগরিদম পাননি, যার worst-case সময় কোনো c < 2-এর জন্য O(cn). এটি আজও exact algorithm-এর গবেষণার একটি বিখ্যাত খোলা প্রশ্ন।
আংশিক অগ্রগতি আছে। শুধু “Hamiltonian cycle আছে কি না” — দূরত্ব ছাড়া — এই সিদ্ধান্তের প্রশ্নে Andreas Björklund ২০১০ সালে undirected graph-এর জন্য প্রায় O(1.657n)-এর একটি randomized অ্যালগরিদম দেন, যা বীজগণিতের কৌশল ব্যবহার করে। ছোট পূর্ণসংখ্যার দূরত্বেও কিছু উন্নতি জানা। কিন্তু সাধারণ বাস্তব-সংখ্যার দূরত্বে 2n এখনো দেয়াল। মনে রাখুন, TSP NP-hard (পর্ব ৬৪) — তাই polynomial সমাধান পেলে P = NP হয়ে যেত; আর অনেক গবেষক বিশ্বাস করেন, exponential-এর ভিত্তিও সহজে নামানো যাবে না।
তাহলে বাস্তবে? Concorde নামের TSP solver (David Applegate, Robert Bixby, Vašek Chvátal, William Cook-এর দল) linear programming আর branch and cut (পর্ব ৫৭-এর branch and bound-এর আত্মীয়) দিয়ে হাজার হাজার শহরের instance নির্ভুল সমাধান করেছে — ২০০৬ সালে প্রায় ৮৬ হাজার বিন্দুর একটি চিপ-নকশার instance পর্যন্ত। কারণ বাস্তব instance-এ কাঠামো থাকে, আর ভালো নিম্নসীমা বেশিরভাগ শাখা আগেই কেটে দেয়। তবু worst-case সময় exponential-ই থাকে। আর ডেলিভারি কোম্পানিগুলো দৈনন্দিন রুটে প্রায়ই নির্ভুল উত্তর খোঁজে না — 2-opt, Lin–Kernighan-এর মতো heuristic বা approximation (পর্ব ৬৬) দিয়ে কয়েক সেকেন্ডে “যথেষ্ট ভালো” রুট নেয়।
মূল উপলব্ধি: worst-case-এর সেরা তাত্ত্বিক সীমা আর বাস্তবের সমাধানযোগ্যতা দুটি আলাদা প্রশ্ন। Held–Karp ছোট n-এ নির্ভুল আর নিশ্চিত; বড় n-এ বাস্তবতা আসে কাঠামো, নিম্নসীমা আর আনুমানিক কৌশল থেকে।
প্র ০২Bitmask DP-র সীমা প্রায় n = 20 — সময়, নাকি মেমরি, কোনটি আগে বাধা দেয়? আর 2n-এর দেয়াল পেরোনোর জন্য meet in the middle-এর মতো কৌশল কী করে?
Held–Karp-এর টেবিলে 2n·n টি ঘর। n = 20-এ প্রায় 2.1·107 ঘর — int-এ প্রায় ৮৪ MB, আর ট্যুর পুনর্গঠনের par টেবিল রাখলে দ্বিগুণ। কনটেস্টের সাধারণ মেমরি-সীমা ২৫৬ MB, তাই কোনোরকমে চলে। সময়: 220·400 ≈ 4·108 সরল ধাপ — C++-এ এক সেকেন্ডের কাছাকাছি। n = 24-এ টেবিল প্রায় 4·108 ঘর, ১.৬ GB — মেমরি আগে ভাঙে। অর্থাৎ bitmask DP-তে প্রায়ই মেমরিই প্রথম দেয়াল। কৌশল: শুধু দরকারি state রাখা (bit 0 সব সময় চালু, তাই অর্ধেক), ছোট টাইপ (মান ছোট হলে int-এর বদলে 16-bit-এর uint16_t), আর পুনর্গঠন না লাগলে par বাদ দেওয়া।
অন্যদিকে কিছু সমস্যায় 2n-কে 2n/2-এ নামানো যায়। Meet in the middle: n = 40 টি সংখ্যার কোনো subset-এর যোগফল ঠিক T হয় কি না? সব subset 240 ≈ 1012. বদলে সংখ্যাগুলো দুই ভাগে ভাগ করুন, প্রতি ভাগের 220 টি subset-এর যোগফল বানান, একটি তালিকা sort করুন, আর অন্য তালিকার প্রতিটি x-এর জন্য T − x binary search (পর্ব ১৮) করুন। মোট প্রায় O(2n/2·n). Ellis Horowitz আর Sartaj Sahni ১৯৭৪ সালে subset sum আর knapsack-এ এই ধারণা ব্যবহার করেন। তবে TSP-তে এটি সরাসরি খাটে না, কারণ দুই অর্ধেক জোড়ার সময় কোন শহরগুলো কোন অর্ধেকে গেল, সেই সেটটিই মেলাতে হয় — আবার bitmask-এর দেয়াল।
আরেকটি দিক: inclusion–exclusion নীতি দিয়ে Hamiltonian path গোনা যায় O(2n·poly(n)) সময়ে কিন্তু মাত্র polynomial জায়গায় — Held–Karp-এর মতো বিশাল টেবিল ছাড়াই। অর্থাৎ একই exponential সময়ে মেমরির দেয়াল এড়ানো সম্ভব, যদি শুধু উত্তরের সংখ্যা বা হ্যাঁ/না লাগে।
মূল উপলব্ধি: exponential অ্যালগরিদমে সময় আর জায়গা দুটি আলাদা বাজেট। n দেখে আগে গুনুন — কোন দেয়াল আগে আসবে — তারপর কৌশল বাছুন: টেবিল ছোট করা, মাঝখানে ভাগ করা, বা গোনার সূত্রে জায়গা বাঁচানো।
প্র ০৩Digit DP আসলে একটি finite automaton-এর ওপর DP — এই দৃষ্টিভঙ্গি থেকে কোন ধরনের “কয়টি সংখ্যা” প্রশ্ন digit DP-তে সমাধান করা যায়, আর কোনগুলো যায় না?
Digit DP-তে আমরা সংখ্যাকে অঙ্কের একটি string হিসেবে বাঁ থেকে পড়ি, আর প্রতিটি অঙ্ক পড়ার পর একটি ছোট “অবস্থা” আপডেট করি — অঙ্কের যোগফল, mod 7-এর অবশিষ্ট, আগের অঙ্ক, 4 দেখা গেছে কি না। এটি হুবহু একটি deterministic finite automaton (DFA) — সীমিত সংখ্যক অবস্থার যন্ত্র, যা Michael Rabin আর Dana Scott-এর ১৯৫৯ সালের গবেষণায় আনুষ্ঠানিক রূপ পায় আর compiler-এর lexer থেকে regex পর্যন্ত সর্বত্র ব্যবহৃত। tight flag হলো আরেকটি ছোট automaton: “এ পর্যন্ত N-এর সমান, নাকি ছোট”. digit DP মানে এই দুই automaton-এর গুণফলের ওপর পথ গোনা।
এই দৃষ্টিতে সীমাটা পরিষ্কার: যে শর্ত বাঁ থেকে পড়তে পড়তে সীমিত তথ্য রেখে যাচাই করা যায়, সেটি digit DP-তে গোনা যায়, আর সময় হয় অঙ্কের সংখ্যা × অবস্থার সংখ্যা × ১০. অঙ্ক-যোগফল (অবস্থা ≤ 9D + 1), k দিয়ে বিভাজ্যতা (k টি অবস্থা), “পাশাপাশি দুটি অঙ্ক সমান নয়” (১০টি অবস্থা) — সব চলে। আবার দুটি শর্ত একসাথে — “যোগফল 10 আর 3 দিয়ে বিভাজ্য” — দুই automaton-এর গুণফল, অবস্থা গুণ হয়।
কিন্তু “সংখ্যাটি মৌলিক কি না” — এর জন্য কোনো ছোট automaton নেই। মৌলিক সংখ্যার সেট regular language নয় (এটি pumping lemma দিয়ে প্রমাণ করা যায়), তাই digit DP দিয়ে 1018 পর্যন্ত মৌলিক গোনা যায় না; সেখানে লাগে Meissel–Lehmer বা আরও আধুনিক গণিতভিত্তিক পদ্ধতি। তবে “অঙ্কগুলোর যোগফল নিজেই মৌলিক” ঠিকই চলে — কারণ যোগফল সীমিত, শেষে শুধু যাচাই।
এই ধারণার বাস্তব প্রয়োগও আছে: কোনো regex-এর সাথে মেলে এমন দৈর্ঘ্য-L-এর string কয়টি, বা একটি নিয়ম মেনে কয়টি পাসওয়ার্ড বা ID সম্ভব — সবই automaton-এর ওপর DP.
মূল উপলব্ধি: digit DP-র শক্তি “সীমিত স্মৃতি” থেকে। নতুন প্রশ্নে জিজ্ঞাসা করুন: বাঁ থেকে পড়তে পড়তে কী মনে রাখলেই চলে, আর তা কি সীমিত? উত্তর হ্যাঁ হলে digit DP, না হলে অন্য পথ।
প্র ০৪Maximum independent set সাধারণ graph-এ NP-hard, কিন্তু tree-তে O(n). এই পার্থক্যের কারণ কী, আর treewidth-এর ধারণা কীভাবে tree DP-কে “প্রায়-tree” graph-এ ছড়িয়ে দেয়?
Richard Karp ১৯৭২ সালে তাঁর বিখ্যাত ২১টি NP-complete সমস্যার তালিকায় clique আর vertex cover রাখেন; independent set এদের সাথে প্রায় হুবহু সমতুল্য (একটি graph-এ independent set = তার complement graph-এ clique, আর vertex cover-এর পরিপূরক)। তাই সাধারণ graph-এ সর্বোচ্চ independent set খোঁজা NP-hard. অথচ tree-তে অংশ ৬-এর দুই-লাইনের DP O(n)-এ সমাধান করে। কেন?
কারণ tree-তে প্রতিটি node v তার subtree-কে বাকি graph থেকে আলাদা করে দেয় — subtree আর বাইরের মধ্যে যোগাযোগ শুধু v-এর মাধ্যমে। তাই subtree-র ভেতরের সব সিদ্ধান্তের সারাংশ মাত্র দুটি সংখ্যা: v নির্বাচিত হলে সেরা, না হলে সেরা। সাধারণ graph-এ এমন ছোট “বিচ্ছেদ-বিন্দু” নেই — যেকোনো ভাগের সীমানায় অনেক vertex, আর তাদের সব সম্ভাব্য অবস্থা মনে রাখতে হয়।
এই চিন্তাকেই সাধারণ রূপ দেয় treewidth. একটি graph-কে যদি একটি tree-র মতো সাজানো যায়, যেখানে প্রতিটি “node” আসলে বড়জোর k + 1 টি vertex-এর একটি থলে (bag) — তাহলে graph-এর treewidth k. Neil Robertson আর Paul Seymour-এর graph minor-এর গবেষণা (১৯৮০-এর দশক) এই ধারণাকে কেন্দ্রে আনে। treewidth k হলে প্রতিটি থলের vertex-গুলোর 2k+1 টি সম্ভাব্য অবস্থার (কে নির্বাচিত) ওপর bitmask DP — অর্থাৎ এই পর্বের tree DP আর bitmask DP একসাথে — দিয়ে maximum independent set O(2k·k·n)-এর মতো সময়ে হয়। Bruno Courcelle ১৯৯০ সালে দেখান, monadic second-order logic-এ লেখা যায় এমন যেকোনো graph-ধর্ম bounded treewidth-এর graph-এ রৈখিক সময়ে যাচাই করা যায়।
বাস্তবে এর প্রয়োগ: অনেক রাস্তার নেটওয়ার্ক, সার্কিট আর compiler-এর control-flow graph-এর treewidth ছোট, তাই সেখানে NP-hard সমস্যাও দ্রুত চলে। Mikkel Thorup ১৯৯৮ সালে দেখান, structured প্রোগ্রামের control-flow graph-এর treewidth ছোট থাকে, যা register allocation-এর মতো কাজে লাগে।
মূল উপলব্ধি: কঠিনতা প্রায়ই সমস্যার নয়, ইনপুটের গঠনের। ছোট বিচ্ছেদ-বিন্দু থাকলে DP সীমানার অবস্থা মনে রেখে এগোয় — tree-তে একটি node, প্রায়-tree-তে একটি ছোট থলে।
প্র ০৫Knuth-এর optimization, quadrangle inequality আর convex hull trick — DP-র transition দ্রুত করার এই কৌশলগুলোর পেছনের সাধারণ ধারণা কী, আর কখন এগুলো প্রয়োগ করা নিরাপদ?
পর্ব ৫৪-এর matrix chain-এর আত্মীয় interval DP-র একটি রূপে (যেমন optimal binary search tree) dp[i][j] = mini ≤ k < j(dp[i][k] + dp[k+1][j]) + w(i, j) — n² টি state, প্রতিটিতে n টি k, মোট O(n³). Donald Knuth ১৯৭১ সালে optimal binary search tree-র সমস্যায় লক্ষ করেন, সর্বোত্তম k-এর অবস্থান একমুখী: opt[i][j − 1] ≤ opt[i][j] ≤ opt[i + 1][j]. তাই প্রতিটি state-এ শুধু ওই সরু রেঞ্জে k খুঁজলেই চলে, আর মোট কাজ telescoping যোগফলে O(n²)-এ নামে। F. Frances Yao ১৯৮০ সালে দেখান, খরচ w যদি quadrangle inequality মানে — w(a, c) + w(b, d) ≤ w(a, d) + w(b, c), যখন a ≤ b ≤ c ≤ d — আর একটি একমুখিতার শর্ত, তাহলে এই একমুখিতা সব সময় সত্য।
Convex hull trick আরেক ধরনের DP-তে: dp[i] = minj(dp[j] + b[j]·a[i]). প্রতিটি j-কে একটি সরলরেখা y = b[j]·x + dp[j] ভাবুন; dp[i] মানে x = a[i]-তে সবচেয়ে নিচের রেখা। নিচের রেখাগুলো মিলে একটি উত্তল খাম (lower envelope), যাতে রেখা যোগ আর প্রশ্ন amortized O(1) বা O(log n)-এ হয় — ঢাল একমুখী হলে deque দিয়ে, না হলে Li Chao tree দিয়ে। Divide and conquer optimization-এ opt[i] i-এর সাথে একমুখী হলে মাঝের i-র opt বের করে দুই পাশে সরু রেঞ্জে recursion — একটি সারি O(n log n)-এ।
তিনটির পেছনে একই ধারণা: সর্বোত্তম পছন্দের কাঠামো — একমুখিতা বা উত্তলতা — কাজে লাগিয়ে বেশিরভাগ পছন্দ না দেখেই বাদ দেওয়া। বিপদও একই: শর্তটি সত্য কি না, প্রমাণ না করে প্রয়োগ করলে কোড দ্রুত চলে কিন্তু ভুল উত্তর দেয় — আর ছোট উদাহরণে প্রায়ই ঠিক দেখায়। নিরাপদ পথ: quadrangle inequality বা ঢালের একমুখিতা গাণিতিকভাবে দেখানো, আর না পারলে ছোট এলোমেলো ইনপুটে মূল O(n³) বা O(n²) DP-র সাথে stress test.
মূল উপলব্ধি: DP দ্রুত করার দুটি পথ — কম state, অথবা সস্তা transition. দ্বিতীয় পথের প্রতিটি কৌশল একটি গাণিতিক শর্তের ওপর দাঁড়িয়ে; শর্ত যাচাই না করে কৌশল প্রয়োগ করা মানে লুকানো ভুলের ঝুঁকি।
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।
-
হাতে trace করুন (assignment): ৩ জন রাইডার, ৩টি অর্ডার; cost-এর সারি = রাইডার: রাইডার 0: 9 2 7; রাইডার 1: 6 4 3; রাইডার 2: 5 8 1. dp[mask]-এর ৮টি ঘর ভরুন (mask = দেওয়া অর্ডারের সেট), আর সর্বনিম্ন খরচ ও ভাগটি বলুন।
dp[000] = 0. রাইডার 0 (popcount 0): dp[001] = 9, dp[010] = 2, dp[100] = 7.
রাইডার 1: dp[011] = min(9 + 4, 2 + 6) = 8; dp[101] = min(9 + 3, 7 + 6) = 12; dp[110] = min(2 + 3, 7 + 4) = 5.
রাইডার 2: dp[111] = min(dp[011] + 1, dp[101] + 8, dp[110] + 5) = min(9, 20, 10) = 9. ভাগ: রাইডার 0 → অর্ডার 1 (2), রাইডার 1 → অর্ডার 0 (6), রাইডার 2 → অর্ডার 2 (1). ৬টি permutation-এর brute force-এও সর্বনিম্ন 9.
-
হাতে trace করুন (digit DP): 0 থেকে 120-এর মধ্যে কয়টি সংখ্যার অঙ্ক-যোগফল 5? N = 120-কে “1 2 0” ধরে (pos, tight, sum) tree আঁকুন।
pos 0, সীমা 1. অঙ্ক 0 → free, বাকি দুই অঙ্কে 5: 05, 14, 23, 32, 41, 50 = 6. অঙ্ক 1 → tight, sum 1; pos 1, সীমা 2: অঙ্ক 0 → free, বাকি এক অঙ্কে 4 → 104; অঙ্ক 1 → free, দরকার 3 → 113; অঙ্ক 2 → tight, sum 3, pos 2 সীমা 0, দরকার 2 — অসম্ভব, 0.
মোট 6 + 1 + 1 = 8: 5, 14, 23, 32, 41, 50, 104, 113.
-
Rerooting: ৫টি node-এর একটি শিকল 1 – 2 – 3 – 4 – 5. root 1 থেকে size আর ans[1] বের করে সূত্র ans[c] = ans[p] − size[c] + (n − size[c]) দিয়ে সব ans লিখুন। কোন node-এ মোট দূরত্ব সবচেয়ে কম?
size: 1:5, 2:4, 3:3, 4:2, 5:1. ans[1] = 1 + 2 + 3 + 4 = 10.
ans[2] = 10 − 4 + 1 = 7; ans[3] = 7 − 3 + 2 = 6; ans[4] = 6 − 2 + 3 = 7; ans[5] = 7 − 1 + 4 = 10. সবচেয়ে কম মাঝের node 3-এ, 6 — শিকলের median. যাচাই: 3 থেকে 2 + 1 + 1 + 2 = 6 ✓.
-
কোড লিখুন: [L, R]-এ কয়টি সংখ্যায় অঙ্ক 4 একবারও নেই, digit DP দিয়ে গুনুন (0 ≤ L ≤ R ≤ 1018). ছোট রেঞ্জে brute force দিয়ে যাচাই করুন আর complexity বলুন।
sum-এর দরকার নেই; state (pos, tight). 4 বসানো নিষেধ।
string s; long long memo[20][2]; long long go(int pos, int tight) { // numbers with no digit 4, 0..N if (pos == (int)s.size()) return 1; long long &r = memo[pos][tight]; if (r >= 0) return r; r = 0; int lim = tight ? s[pos] - '0' : 9; for (int dg = 0; dg <= lim; ++dg) if (dg != 4) r += go(pos + 1, tight && dg == lim); return r; } long long f(long long N) { // count in [0, N]; f(-1) = 0 if (N < 0) return 0; s = to_string(N); memset(memo, -1, sizeof memo); return go(0, 1); } // answer = f(R) - f(L - 1); e.g. [1, 100] -> 810 থেকে 99: দুই অঙ্কের প্রতিটি 4 ছাড়া ৯ রকম — 81 (00 সহ)। 100 যোগ করলে f(100) = 82, আর [1, 100] = 82 − f(0) = 82 − 1 = 81. state 19 × 2, প্রতিটিতে ১০টি অঙ্ক — প্রতি f-এ O(10·D), D = অঙ্কের সংখ্যা। যাচাই: L, R ≤ 104-এ এলোমেলো রেঞ্জে
to_string(x).find('4') == nposগুনে মেলান।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমHeld–Karp দিয়ে TSP সমাধান আর ট্যুর বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ভিত্তি বসানো: dp[{0}][0] = 0, বাকি সব ঘর অসীম
- mask-গুলো ছোট থেকে বড় সংখ্যার ক্রমে ঘোরা
- প্রতিটি j ∈ mask-এর জন্য dp[mask \ {j}][k] + d[k][j]-এর min নেওয়া আর par মনে রাখা
- পুরো সেটে প্রতিটি j-এর জন্য d[j][0] যোগ করে সেরা শেষ এলাকা বাছা
- par ধরে পেছনে হেঁটে ট্যুর পুনর্গঠন করা
n টি শহরের travelling salesman সমস্যায় Held–Karp-এর DP-র সময় আর জায়গা কত?
- সময় O(n!), জায়গা O(n)
- সময় O(2n·n²), জায়গা O(2n·n)
- সময় O(n³), জায়গা O(n²)
- সময় O(2n), জায়গা O(2n)
একটি ধনাত্মক পূর্ণসংখ্যা x-এর জন্য x & (x - 1) কী দেয়?
- x-এর শুধু সবচেয়ে নিচের চালু bit
- x-এর সব bit উল্টানো
- x থেকে সবচেয়ে নিচের চালু bit মুছে ফেলা সংখ্যা
- সব সময় x − 1
n টি bit-এর সব mask, আর প্রতিটির সব submask (খালিসহ) ঘুরলে মোট কয়টি (mask, submask) জোড়া দেখা হয়?
- 3n
- 4n
- 2n
- n·2n
একটি প্রশ্নে বলা আছে: “1 ≤ L ≤ R ≤ 1018; [L, R]-এর কয়টি সংখ্যার অঙ্কগুলো বাঁ থেকে ডানে কখনো কমে না?” কোন কৌশল সবচেয়ে মানানসই?
- L থেকে R পর্যন্ত প্রতিটি সংখ্যা পরীক্ষা
- অঙ্কগুলোর ওপর bitmask DP
- Tree DP
- Digit DP, state-এ আগের অঙ্ক রেখে
একটি rooted tree-তে প্রতিটি node v-এর ওজন w[v]. কোনো parent আর তার সন্তান একসাথে না নিয়ে সর্বোচ্চ ওজনের node-সেট খুঁজতে in[v] (v নেওয়া হলো) আর out[v] (v নেওয়া হলো না) রাখা হলো। in[v]-এর সঠিক সূত্র কোনটি?
- in[v] = w[v] + Σ max(in[c], out[c])
- in[v] = w[v] + Σ out[c]
- in[v] = Σ max(in[c], out[c])
- in[v] = w[v] + Σ in[c]
n টি node-এর tree-তে ans[v] = v থেকে সব node-এর দূরত্বের যোগফল। p-এর সন্তান c, আর c-র subtree-তে size[c] টি node. ans[p] জানা থাকলে ans[c] কত?
- ans[p] − size[c]
- ans[p] + size[c] − (n − size[c])
- ans[p] − size[c] + (n − size[c])
- ans[p]
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Bitmask = একটি পূর্ণসংখ্যায় সেট: পরীক্ষা
(m >> j) & 1, যোগm | 1 << j, নিচের bitm & -m; বন্ধনী দিন। n ≤ 20 দেখলে bitmask ভাবুন। - Held–Karp: dp[mask][j] = mink dp[mask \ {j}][k] + d[k][j]; O(2n·n²) সময়, O(2n·n) জায়গা; par দিয়ে ট্যুর। Assignment O(2n·n).
- সব submask:
s = (s − 1) & m; সব mask মিলে O(3n). Submask-এর যোগফল: SOS DP, O(n·2n). - Digit DP: f(R) − f(L − 1), state (pos, tight, …); প্রতিটি N-এ নতুন memo; 1018-এ কয়েক হাজার state.
- Tree DP postorder-এ: in/out-এ party-র সমস্যা, size, diameter — O(n). Rerooting: ans[c] = ans[p] − size[c] + (n − size[c]).
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৬ · Backtracking — N-queens, subset sum আর Hamiltonian cycleপরবর্তী পর্বসব সম্ভাবনা খোঁজা, কিন্তু অকেজো শাখা কেটে।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ৫২ · Dynamic programming-এর ভিত্তি — memoization আর tabulationআগের ভিত্তিstate, recurrence আর হিসাবের ক্রম।
- পর্ব ৫৩ · DP-র ক্লাসিক সমস্যা ১ — knapsack, coin change আর LISআগের ভিত্তিএক-মাত্রা আর দুই-মাত্রার DP.
- পর্ব ৫৪ · DP-র ক্লাসিক সমস্যা ২ — LCS, edit distance আর matrix chainআগের ভিত্তিstring আর interval DP.
- পর্ব ৪৮ · Tree-এর কৌশল — LCA, binary lifting আর diameterআগের ভিত্তিrooting, subtree আর দুইবার BFS-এ diameter.
- C প্রোগ্রামিং · Bitwise operationপাশের ট্র্যাকAND, OR, XOR, shift — bitmask-এর ভিত্তি।
- C++ প্রোগ্রামিং · কম্পিটিটিভ প্রোগ্রামিংয়ে C++পাশের ট্র্যাকদ্রুত ইনপুট, STL আর কনটেস্টের কোডের অভ্যাস।