সরাসরি পর্বে যান
পর্ব ৫৫ · ৭২-এর মধ্যে · মডিউল ৭

উন্নত DP — bitmask, digit আর tree DP

Advanced Dynamic Programming — Bitmask DP (Held–Karp TSP, Assignment, Submasks, SOS), Digit DP, Tree DP & Rerooting

Taught in Bangla, this lesson extends DP states to sets, digits and tree nodes. It covers bit tricks, Held–Karp bitmask DP for the travelling salesman problem in O(2^n·n²) with tour reconstruction, the assignment problem, submask enumeration in O(3^n) and SOS DP, digit DP with (pos, tight, sum) states, tree DP for the party problem, subtree sizes and diameter, rerooting, and a checklist for choosing DP states in contests.

এই পর্বে যা শিখবেন

  • সেটকে পূর্ণসংখ্যায় রাখা, আর 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) & 1j = 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
সবচেয়ে নিচের চালু bitmask & -mask0001
সবচেয়ে নিচের bit মুছে ফেলাmask & (mask - 1)1010
পুরো সেট {0, …, n−1}(1 << n) - 1n = 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 সংজ্ঞায়িত করি:

dp[mask][j] = হাব 0 থেকে শুরু করে mask-এর সব এলাকা ঠিক একবার ঘুরে j-তে শেষ হওয়া পথের সবচেয়ে কম দৈর্ঘ্য

এখানে mask-এ bit 0 (হাব) আর bit j সব সময় চালু। j-তে পৌঁছানোর ঠিক আগে রাইডার ছিলেন কোনো এক k-তে, যেখানে k ∈ mask \ {j}. তাই optimal substructure থেকে recurrence:

dp[mask][j] = min over k ∈ mask \ {j} of ( dp[mask \ {j}][k] + d[k][j] ),   dp[{0}][0] = 0

শেষে উত্তর: 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
dp[mask][j] (· = j সেটে নেই)সবুজ = এই ধাপে ভরা

            
এই ধাপের হিসাব
mask
হিসাব
  1. শুরু. শুধু হাব ঘোরা হয়েছে, রাইডার হাবে: dp[0001][0] = 0. বাকি সব ঘর এখনো অজানা (?).
  2. এক এলাকার পথ. mask 0011 = {0, 1}: হাব থেকে সরাসরি 1-এ, 9 km. mask 0101 = {0, 2}: 5 km. এই ঘরগুলোর আগে শুধু হাব, তাই বিকল্প একটিই।
  3. mask 0111 = {0, 1, 2}. 1-এ শেষ করতে হলে আগে ছিলেন 2-এ: dp[0101][2] + d[2][1] = 5 + 2 = 7. 2-এ শেষ করতে আগে 1-এ: 9 + 2 = 11. একই সেট, শেষ বিন্দু আলাদা — তাই আলাদা ঘর।
  4. mask 1001 আর 1011. সংখ্যার ক্রমে এবার 1001 = {0, 3}: 4 km. তারপর 1011 = {0, 1, 3}: 1-এ শেষ = 4 + 7 = 11, 3-এ শেষ = 9 + 7 = 16. লক্ষ করুন, 1011-এর দরকারি ঘর 1001 আর 0011 — দুটিই ছোট সংখ্যা, আগেই ভরা।
  5. mask 1101 = {0, 2, 3}. 2-এ শেষ: 4 + 6 = 10. 3-এ শেষ: 5 + 6 = 11.
  6. পুরো সেট, j = 1. 1-এর আগে ছিলেন 2 বা 3-এ (সেট {0, 2, 3}): 10 + 2 = 12, বা 11 + 7 = 18. সেরা 12, আর par = 2 মনে রাখি।
  7. 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).
  8. হাবে ফেরা. 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 নেবেন:

dp[mask | (1 << j)] = min( …, dp[mask] + cost[popcount(mask)][j] ),   j ∉ mask

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-এ:

m = 1011 = {0, 1, 3}সবুজ = বর্তমান submask

            
এই ধাপে
s সেট হিসেবে
কত নম্বর
  1. শুরু s = m. পুরো সেটটিও নিজের একটি submask. s − 1 = 1010, তার সাথে AND m = 1010.
  2. 1010. s − 1 = 1001 — m-এর বাইরের কোনো bit নেই, তাই AND-এর পরেও 1001.
  3. 1001. s − 1 = 1000. এটিও m-এর ভেতরে।
  4. 1000 — আসল কৌশল. s − 1 = 0111. কিন্তু bit 2 m-এ নেই! AND m সেটিকে মুছে দেয়: 0011. লাফটি m-এর বাইরের bit এড়িয়ে ঠিক পরের ছোট submask-এ পৌঁছায়।
  5. 0011. s − 1 = 0010, AND m = 0010.
  6. 0010. s − 1 = 0001.
  7. 0001. s − 1 = 0000 — এর পরে s = 0.
  8. লুপ থামে. 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:

পছন্দের tree (T = tight, F = free)সবুজ = এই ধাপ

            
এই ধাপে
সিদ্ধান্ত
ফল
  1. শুরু. state (0, T, 0): প্রথম অবস্থান, এখনো N-এর সাথে মিলছি, যোগফল 0. N-এর প্রথম অঙ্ক 2, তাই এখানে বসতে পারে 0, 1 বা 2.
  2. অঙ্ক 0. 0 < 2, তাই সংখ্যাটি এখন N-এর চেয়ে ছোট হবেই — tight ভেঙে free. state (1, F, 0): বাকি দুই অঙ্ক যেকোনো, যোগফল 7 লাগবে: 07, 16, 25, 34, 43, 52, 61, 70 — 8টি। রিকার্শন এটি নিজেই গুনে দেয়।
  3. অঙ্ক 1. এটিও free. state (1, F, 1): বাকি দুই অঙ্কে 6 লাগবে — 06 থেকে 60, 7টি।
  4. অঙ্ক 2. N-এর অঙ্কের সমান, তাই tight থাকে: (1, T, 2). পরের অবস্থানে সীমা N-এর অঙ্ক 5.
  5. pos 1-এ 0..4. 5-এর ছোট যেকোনো অঙ্ক tight ভাঙে। (2, F, 2) থেকে (2, F, 6) — প্রতিটিতে বাকি এক অঙ্কে ঠিক 7 − sum বসাতে হবে, যা 1 থেকে 5, সব বৈধ। মোট 5টি: 205, 214, 223, 232, 241.
  6. pos 1-এ 5. tight থাকে: (2, T, 7). শেষ অবস্থানে সীমা 3, আর দরকার 0 — অঙ্ক 0 চলে (0 < 3, তাই tight ভাঙে; 1–3 বসালে যোগফল 7 ছাড়িয়ে যায়)। (3, F, 7)-এ পৌঁছে যোগফল ঠিক 7: সংখ্যা 250. এই শাখায় মোট 5 + 1 = 6.
  7. যোগ. 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] = w[v] + Σ out[c]     out[v] = Σ max(in[c], out[c])     (c = v-এর সন্তান)

in[v] = v-এর subtree-তে সর্বোচ্চ আনন্দ, যদি v দাওয়াত পান — তখন কোনো সন্তান নয়। out[v] = v দাওয়াত না পেলে — তখন প্রতিটি সন্তান স্বাধীনভাবে নিজের ভালোটি বেছে নেন। সন্তানের মান আগে লাগে, তাই হিসাব postorder-এ: আগে সন্তান, পরে বাবা। উত্তর max(in[root], out[root]). বন্ধনীতে প্রতিটি কর্মীর আনন্দ-মান:

কোম্পানি: 1 = MD, বন্ধনীতে আনন্দ-মানসবুজ = এই ধাপ

            
এ পর্যন্ত (in, out)
হিসাব
সব node
  1. সমস্যা. ৭ জন কর্মী। লোভী চেষ্টা — সবচেয়ে বড় মান 3 (6) আগে — কাজ করবে কি? বা “একটি করে স্তর বাদ দিয়ে” দাওয়াত? দেখা যাক DP কী বলে।
  2. Leaf. 4 আর 5-এর অধীনে কেউ নেই। দাওয়াত দিলে in = নিজের মান, না দিলে out = 0.
  3. কর্মী 2. 2 এলে 4, 5 আসতে পারবেন না: in[2] = 3. 2 না এলে 4 আর 5 দুজনেই আসতে পারেন: out[2] = 4 + 2 = 6. এখানে বসকে বাদ দেওয়াই ভালো।
  4. Leaf 6, 7. in = 1 আর 3, out = 0.
  5. কর্মী 3. in[3] = 6 (6, 7 বাদ)। out[3] = 1 + 3 = 4. এবার উল্টো — বসকে ডাকাই ভালো।
  6. 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.
  7. উত্তর 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[c] = ans[p] − size[c] + (n − size[c])
৬টি বাড়ি, শুরুর root 1সবুজ = এই ধাপ

            
এই ধাপে
হিসাব
ans
  1. Subtree size. প্রথম DFS-এ postorder: leaf-দের size 1, তারপর size[2] = 1 + 1 + 1 = 3, size[3] = 1 + 1 = 2, size[1] = 6.
  2. Root-এর উত্তর. 1 থেকে দূরত্ব: 2 আর 3 এক ধাপে, 4, 5, 6 দুই ধাপে। ans[1] = 8. এটুকু একটি সাধারণ DFS.
  3. Root সরল 2-তে. 2-এর subtree-র ৩টি বাড়ি (2, 4, 5) এক ধাপ কাছে, বাকি ৩টি (1, 3, 6) এক ধাপ দূরে: 8 − 3 + 3 = 8.
  4. Root সরল 3-তে. 3-এর subtree-তে ২টি (3, 6) কাছে আসে, বাকি ৪টি দূরে যায়: 8 − 2 + 4 = 10.
  5. Leaf-দের দিকে. সূত্রটি সব সময় parent-এর উত্তর থেকে: ans[4] = ans[2] − 1 + 5 = 12, একইভাবে ans[5] = 12; ans[6] = ans[3] − 1 + 5 = 14. প্রতিটি edge-এ O(1).
  6. যাচাই আর উত্তর. 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 বাছাইয়ের চেকলিস্ট

  1. ভবিষ্যতের জন্য অতীতের কী জানা দরকার? শুধু সেটুকু state-এ রাখুন। TSP-তে “ক্রম” নয়, “সেট আর এখনকার জায়গা”.
  2. State কি যথেষ্ট? একই state-এ দুটি আলাদা অতীত থেকে এলে ভবিষ্যতের সেরা সিদ্ধান্ত কি একই? না হলে state-এ কিছু বাদ পড়েছে।
  3. Transition লিখুন — শেষ সিদ্ধান্তটি কী? (TSP-তে শেষের আগের এলাকা, party-তে root দাওয়াত পেলেন কি না।)
  4. ভিত্তি আর উত্তর কোন state-এ, আর হিসাবের ক্রম চক্রহীন কি না।
  5. গুনে দেখুন: state × transition সময়সীমায় ধরে কি না, আর টেবিল মেমরিতে।
ইনপুটের সীমাসম্ভাব্য কৌশলমোটামুটি কাজ
n ≤ 10–11সব permutation, backtracking (পর্ব ৫৬)n! ≈ 4·107
n ≤ 20 (কখনো 23–24)bitmask DP2n·n বা 2n·n²
n ≤ 15–16, সেট ভাগsubmask DP3n
n ≤ 40meet in the middle2n/2
সংখ্যা 1018 পর্যন্ত, “কয়টি সংখ্যা”digit DPঅঙ্ক × অবস্থা × 10
tree, n ≤ 2·105tree DP, rerootingO(n)
grid-এর একটি পাশ ≤ 10profile DP2w·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 যাচাই করাতে বলুন:

আমি DSA শিখছি, এখন উন্নত DP (bitmask / digit / tree DP) নিয়ে কাজ করছি। নিচে একটি সমস্যা, আমার DP state, recurrence আর কোড দিলাম: [এখানে সমস্যা, state-এর সংজ্ঞা আর কোড পেস্ট করুন] পুরো সমাধান নতুন করে লিখে দিও না। শুধু এগুলো করো: ১. আমার state কি যথেষ্ট? একই state-এ দুটি আলাদা অতীত থেকে এলে ভবিষ্যৎ আলাদা হতে পারে, এমন একটি উদাহরণ খোঁজো — না পেলে কেন নেই, বলো। ২. State-এর সংখ্যা আর প্রতিটির transition গুনে time ও space complexity দাও, প্রতিটি দাবির কারণ এক লাইনে। ৩. ৩টি ছোট edge case দাও (n = 1, খালি সেট, L = R বা N = 0 ধরনের) — উত্তর বলো না, আমি হাতে trace করব। ৪. আমার কোড যাচাই করতে একটি brute force লেখার পরিকল্পনা বলো (কোড নয়), যা দিয়ে আমি এলোমেলো ছোট ইনপুটে তুলনা করব।

উন্নত 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. দ্বিতীয় পথের প্রতিটি কৌশল একটি গাণিতিক শর্তের ওপর দাঁড়িয়ে; শর্ত যাচাই না করে কৌশল প্রয়োগ করা মানে লুকানো ভুলের ঝুঁকি।

অনুশীলন

খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।

  1. হাতে 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.

  2. হাতে 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.

  3. 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 ✓.

  4. কোড লিখুন: [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] -> 81

    0 থেকে 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 সমাধান আর ট্যুর বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ভিত্তি বসানো: dp[{0}][0] = 0, বাকি সব ঘর অসীম
  2. mask-গুলো ছোট থেকে বড় সংখ্যার ক্রমে ঘোরা
  3. প্রতিটি j ∈ mask-এর জন্য dp[mask \ {j}][k] + d[k][j]-এর min নেওয়া আর par মনে রাখা
  4. পুরো সেটে প্রতিটি j-এর জন্য d[j][0] যোগ করে সেরা শেষ এলাকা বাছা
  5. 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]

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Bitmask = একটি পূর্ণসংখ্যায় সেট: পরীক্ষা (m >> j) & 1, যোগ m | 1 << j, নিচের bit m & -m; বন্ধনী দিন। n ≤ 20 দেখলে bitmask ভাবুন।
  2. Held–Karp: dp[mask][j] = mink dp[mask \ {j}][k] + d[k][j]; O(2n·n²) সময়, O(2n·n) জায়গা; par দিয়ে ট্যুর। Assignment O(2n·n).
  3. সব submask: s = (s − 1) & m; সব mask মিলে O(3n). Submask-এর যোগফল: SOS DP, O(n·2n).
  4. Digit DP: f(R) − f(L − 1), state (pos, tight, …); প্রতিটি N-এ নতুন memo; 1018-এ কয়েক হাজার state.
  5. Tree DP postorder-এ: in/out-এ party-র সমস্যা, size, diameter — O(n). Rerooting: ans[c] = ans[p] − size[c] + (n − size[c]).

পরের পর্বে: DP যেখানে state গুছিয়ে রাখতে পারে না, সেখানে সব সম্ভাবনা চতুরভাবে খোঁজা — backtracking. N-queens, subset sum আর Hamiltonian cycle-এ ভুল পথ আগেভাগে ছেঁটে ফেলার কৌশল।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান