Dynamic Programming III: Advanced

Bitmask DP, Digit DP, Tree DP

Read: ~50 min Advanced 7 practice problems Live C++ runner

1. Why Advanced DP Matters

By now you have mastered classical DP — knapsack, LIS, LCS, edit distance. Those problems share one property: state is one or two integers. Advanced DP breaks that comfort zone. The state may be a bitmask over a small set, a digit position with carry information, or a subtree of an arbitrary rooted tree. Once you internalise these three idioms, you can solve problems that earlier looked completely opaque — TSP, counting numbers with digit constraints, independent sets on trees, and many ICPC regional problems.

এতদিনে আপনি ক্লাসিক DP — knapsack, LIS, LCS, edit distance — শিখেছেন। সেগুলোর state সাধারণত এক বা দুটি integer। কিন্তু advanced DP-তে state হয়ে যায় bitmask, digit position, কিংবা একটি subtree। এই তিনটি idiom আয়ত্ত করতে পারলে TSP, digit-constrained counting, tree-এর independent set ইত্যাদি সমস্যাগুলো — যেগুলো আগে দুর্বোধ্য মনে হতো — সরাসরি সমাধান করা যাবে।
Module insight Bitmask DP-এর মাধ্যমে n ≤ 20 হলে subset enumeration করা যায় 2ⁿ × n-এ — TSP-এর সমাধান এখান থেকেই।

2. Bitmask DP — When n ≤ 20, Subsets Are Your State

A bitmask is an integer interpreted as a set: bit i being 1 means element i is in the set. With n ≤ 20 there are at most 2²⁰ ≈ 10⁶ subsets, small enough to enumerate. The classic application is the Travelling Salesperson Problem: given n cities and distances, find the shortest tour visiting every city exactly once and returning home.

bitmask মানে integer-কে set হিসেবে দেখা — i-তম bit 1 মানে i element-টি set-এ আছে। n ≤ 20 হলে subset সংখ্যা 2²⁰ ≈ ১০ লক্ষ, যা enumeration-এর জন্য যথেষ্ট ছোট। ক্লাসিক প্রয়োগ: TSP — n-টি শহর ঘুরে আসার সবচেয়ে ছোট পথ।

State: dp[mask][last] = minimum cost to visit every city in mask ending at city last. Transition: try every city nxt not yet in mask:

dp[mask | (1<<nxt)][nxt] = min(dp[mask | (1<<nxt)][nxt], dp[mask][last] + dist[last][nxt])

TSP graph (5 cities) 7 5 3 6 4 8 2 A B C D E dp[mask][last] mask\last A B C D E 00001 0 ∞ ∞ ∞ ∞ 00011 ∞ 7 ∞ ∞ ∞ 01111 … … … … … 11111 ans ans ans ans ans Final answer: min over last of dp[FULL][last] + dist[last][0] Figure 36.1 — TSP graph ও তার (mask, last)-state DP table।
tsp_bitmask.cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    // 10-city TSP — random distances
    int n = 10;
    vector<vector<int>> dist(n, vector<int>(n));
    srand(42);
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            dist[i][j] = (i == j) ? 0 : 1 + rand() % 100;

    int FULL = (1 << n) - 1;
    vector<vector<int>> dp(1 << n, vector<int>(n, INT_MAX));
    dp[1][0] = 0;                       // start at city 0

    for (int mask = 1; mask <= FULL; ++mask) {
        for (int last = 0; last < n; ++last) {
            if (!(mask & (1 << last)) || dp[mask][last] == INT_MAX) continue;
            for (int nxt = 0; nxt < n; ++nxt) {
                if (mask & (1 << nxt)) continue;
                int nm = mask | (1 << nxt);
                dp[nm][nxt] = min(dp[nm][nxt], dp[mask][last] + dist[last][nxt]);
            }
        }
    }

    int ans = INT_MAX;
    for (int last = 1; last < n; ++last)
        if (dp[FULL][last] != INT_MAX)
            ans = min(ans, dp[FULL][last] + dist[last][0]);

    cout << "Optimal TSP tour length = " << ans << "\n";
    return 0;
}
Complexity: O(2ⁿ · n²)। n = 20 হলে ≈ 4·10⁸ — সাবধানে অপটিমাইজ লাগে। n = 15 খুব আরামের। BUET ICPC-এর preliminary contest-এ এই ধরনের TSP variant প্রায়ই আসে।

3. Digit DP — Counting Numbers with Constraints

"How many integers in [L, R] satisfy property P?" Brute force is O(R−L) which can be 10¹⁸. Digit DP walks through the decimal digits of N from most significant to least significant, carrying a small state: position, a tight flag (are we still bounded above by N's digit?), and any problem-specific accumulator. Compute f(R) − f(L−1).

"[L, R] range-এ এমন কতগুলো integer আছে যেগুলো property P মেনে চলে?" Brute force লাগবে O(R−L) — যা ১০¹⁸ পর্যন্ত হতে পারে। Digit DP N-এর digit গুলোতে সবচেয়ে বড় থেকে ছোট-এর দিকে এগোয়, state হিসেবে রাখে: position, একটি tight flag (আমরা কি এখনো N-এর digit-এর সীমার ভেতরে?), আর problem-specific accumulator। উত্তর = f(R) − f(L−1)।
digit_dp.cpp
#include <bits/stdc++.h>
using namespace std;

// Count integers x in [0, N] with NO two consecutive equal digits.
string S;
long long memo[20][11][2][2];
bool seen[20][11][2][2];

long long solve(int pos, int prev, bool tight, bool started) {
    if (pos == (int)S.size()) return 1;
    if (seen[pos][prev][tight][started]) return memo[pos][prev][tight][started];
    seen[pos][prev][tight][started] = true;
    int hi = tight ? S[pos] - '0' : 9;
    long long res = 0;
    for (int d = 0; d <= hi; ++d) {
        bool ns = started || d != 0;
        if (started && d == prev) continue;       // no two equal in a row
        res += solve(pos + 1, ns ? d : 10, tight && (d == hi), ns);
    }
    return memo[pos][prev][tight][started] = res;
}

long long countUpTo(long long N) {
    if (N < 0) return 0;
    S = to_string(N);
    memset(seen, 0, sizeof seen);
    return solve(0, 10, true, false);
}

int main() {
    long long L = 10, R = 100;
    cout << "Count in [" << L << ", " << R
         << "] with no consecutive equal digits = "
         << countUpTo(R) - countUpTo(L - 1) << "\n";
    return 0;
}
The four standard digit-DP states (1) pos — current digit position. (2) tight — are we still tracking N's prefix? (3) started — have we placed the first non-zero digit yet? (4) Any problem accumulator (sum of digits, mod, last digit, etc.).

4. Tree DP — Subtree as State

On a rooted tree, a natural DP state is "consider the subtree of node u." Children compute their answers first (post-order DFS), and u combines them. The textbook example: Maximum Independent Set on a Tree — pick a subset of vertices, no two adjacent, maximising total weight.

dp[u][0] = best when u is not picked. dp[u][1] = best when u is picked.
Transitions: dp[u][0] = Σ max(dp[c][0], dp[c][1]), dp[u][1] = w[u] + Σ dp[c][0].

Rooted tree-এ একটি স্বাভাবিক state হলো "u-এর subtree বিবেচনা করো"। Children-এর উত্তর আগে compute হয় (post-order DFS), তারপর u combine করে। ক্লাসিক উদাহরণ: tree-এর ওপর Max Independent Set।
tree_mis.cpp
#include <bits/stdc++.h>
using namespace std;

const int N = 100005;
vector<int> g[N];
int w[N];
long long dp[N][2];

void dfs(int u, int par) {
    dp[u][0] = 0;
    dp[u][1] = w[u];
    for (int c : g[u]) if (c != par) {
        dfs(c, u);
        dp[u][0] += max(dp[c][0], dp[c][1]);
        dp[u][1] += dp[c][0];
    }
}

int main() {
    int n = 7;
    int wt[] = {0, 10, 5, 8, 3, 7, 12, 4};
    for (int i = 1; i <= n; ++i) w[i] = wt[i];
    int edges[][2] = {{1,2},{1,3},{2,4},{2,5},{3,6},{3,7}};
    for (auto& e : edges) { g[e[0]].push_back(e[1]); g[e[1]].push_back(e[0]); }

    dfs(1, 0);
    cout << "Max Independent Set weight = " << max(dp[1][0], dp[1][1]) << "\n";
    return 0;
}

5. Rerooting — One DFS per Root, In Two Passes

Sometimes you need every node as a root (e.g., for each node, sum of distances to all others). Naive: run a DFS from each node — O(n²). The rerooting technique computes the answer for one root in pass 1 (post-order), then in pass 2 (pre-order) "moves the root" along each edge, updating in O(1) per move — total O(n).

কখনো প্রতিটি node-কে আলাদা আলাদা root ধরে উত্তর লাগে। সরল উপায়: প্রতিটি node থেকে DFS — O(n²)। Rerooting দুই pass-এ কাজ সারে: প্রথম pass-এ এক root-এর জন্য উত্তর, দ্বিতীয় pass-এ edge ধরে root "সরিয়ে" O(1)-এ update — মোট O(n)।
Pattern
sub[u] = own contribution + Σ sub[c] for c child of u
ans[root] = sub[root]
On moving root from u → v (v is a child of u):
    ans[v] = ans[u] − contrib(v→u) + contrib(u→v)

6. DP on DAGs and SOS DP (Preview)

DP on a DAG is just memoised DFS in topological order: longest path in a DAG, number of paths from s to t, and shortest path with negative edges (no cycles) are all DAG DPs.

SOS (Sum Over Subsets) DP answers, for every mask m, "sum of f(s) over all submasks s of m" in O(2ⁿ · n) instead of the naive O(3ⁿ). It is the bitmask analogue of prefix sums and shows up in inclusion-exclusion problems and counting problems on subsets.

DAG-এর ওপর DP মানে topological order-এ memoised DFS। SOS DP প্রতিটি mask m-এর জন্য তার সব submask-এর f(s)-এর যোগফল O(2ⁿ · n)-এ বের করে — naive O(3ⁿ)-এর জায়গায়। এটি bitmask-এর prefix sum analog।

✅ Good Signals for Bitmask DP

  • n ≤ 20 (often ≤ 16)
  • Subsets / permutations / orderings
  • Assignment problems (workers ↔ tasks)
  • Hamiltonian path / TSP

⚠️ Wrong Tool If…

  • n > 22 — memory blows up
  • State has no natural "set" structure
  • The graph has clear tree/DAG structure (use those instead)
  • You only need any feasible solution (greedy may suffice)

7. Practice Problems

প্রতিটি প্রশ্নে Show Answer আছে। আগে নিজে চেষ্টা করুন, তারপর কোডটি রান করে মিলিয়ে নিন।
  1. Assign N tasks to N workers (cost matrix) minimising total cost — bitmask DP.
    N জন worker ও N tasks-এর cost matrix দেওয়া; minimum total cost বের করুন।
    Show Answer
    assign.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        int n = 4;
        int c[4][4] = {{9,2,7,8},{6,4,3,7},{5,8,1,8},{7,6,9,4}};
        vector<int> dp(1 << n, INT_MAX);
        dp[0] = 0;
        for (int mask = 0; mask < (1 << n); ++mask) {
            int i = __builtin_popcount(mask);
            if (i == n || dp[mask] == INT_MAX) continue;
            for (int j = 0; j < n; ++j) if (!(mask & (1 << j)))
                dp[mask | (1 << j)] = min(dp[mask | (1 << j)], dp[mask] + c[i][j]);
        }
        cout << "Min cost = " << dp[(1 << n) - 1] << "\n";
    }
  2. Count integers in [L, R] whose digit sum equals K (digit DP).
    [L, R] range-এ এমন কতগুলো integer যাদের digit sum = K।
    Show Answer
    digsum.cpp
    #include <bits/stdc++.h>
    using namespace std;
    string S; int K;
    long long memo[20][200][2]; bool seen[20][200][2];
    long long f(int p, int s, bool t) {
        if (p == (int)S.size()) return s == K;
        if (seen[p][s][t]) return memo[p][s][t];
        seen[p][s][t] = 1;
        int hi = t ? S[p] - '0' : 9;
        long long r = 0;
        for (int d = 0; d <= hi; ++d) if (s + d <= K)
            r += f(p + 1, s + d, t && (d == hi));
        return memo[p][s][t] = r;
    }
    long long cnt(long long N) {
        if (N < 0) return 0;
        S = to_string(N); memset(seen, 0, sizeof seen);
        return f(0, 0, true);
    }
    int main() {
        K = 10;
        cout << cnt(10000) - cnt(99) << "\n";
    }
  3. Diameter of a tree using two DFS or via tree DP (longest path in two subtrees).
    Tree-এর diameter (দীর্ঘতম পথ) tree DP দিয়ে।
    Show Answer
    diameter.cpp
    #include <bits/stdc++.h>
    using namespace std;
    vector<int> g[100005];
    int ans = 0;
    int dfs(int u, int par) {
        int m1 = 0, m2 = 0;
        for (int c : g[u]) if (c != par) {
            int d = dfs(c, u) + 1;
            if (d > m1) { m2 = m1; m1 = d; } else if (d > m2) m2 = d;
        }
        ans = max(ans, m1 + m2);
        return m1;
    }
    int main() {
        int e[][2] = {{1,2},{2,3},{2,4},{4,5},{5,6}};
        for (auto& x : e) { g[x[0]].push_back(x[1]); g[x[1]].push_back(x[0]); }
        dfs(1, 0);
        cout << "Diameter = " << ans << "\n";
    }
  4. Partition a set of n ≤ 16 integers into k subsets of equal sum (bitmask DP).
    n ≤ ১৬ সংখ্যা k সমান-যোগ subset-এ ভাগ করা যাবে কি না।
    Show Answer

    State: dp[mask] = remainder of (sum of picked elements) % target if we can fill complete subsets so far, else −1. Try adding each element to the current bucket.

    kparts.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        vector<int> a = {4,3,2,3,5,2,1}; int k = 4;
        int n = a.size(), tot = accumulate(a.begin(), a.end(), 0);
        if (tot % k) { cout << "NO\n"; return 0; }
        int tgt = tot / k;
        vector<int> dp(1 << n, -1);
        dp[0] = 0;
        for (int m = 0; m < (1 << n); ++m) if (dp[m] != -1)
            for (int i = 0; i < n; ++i) if (!(m & (1 << i)) && dp[m] + a[i] <= tgt)
                dp[m | (1 << i)] = (dp[m] + a[i]) % tgt;
        cout << (dp[(1 << n) - 1] == 0 ? "YES" : "NO") << "\n";
    }
  5. Rerooting: for every node compute the sum of distances to all other nodes.
    প্রতিটি node-এর জন্য বাকি সব node-এর সাথে দূরত্বের যোগফল।
    Show Answer
    reroot.cpp
    #include <bits/stdc++.h>
    using namespace std;
    const int N = 100005;
    vector<int> g[N]; long long sub[N], ans[N]; int sz[N], n;
    void dfs1(int u, int p) {
        sz[u] = 1; sub[u] = 0;
        for (int c : g[u]) if (c != p) {
            dfs1(c, u); sz[u] += sz[c]; sub[u] += sub[c] + sz[c];
        }
    }
    void dfs2(int u, int p) {
        for (int c : g[u]) if (c != p) {
            ans[c] = ans[u] - sz[c] + (n - sz[c]);
            dfs2(c, u);
        }
    }
    int main() {
        n = 5;
        int e[][2] = {{1,2},{1,3},{3,4},{3,5}};
        for (auto& x : e) { g[x[0]].push_back(x[1]); g[x[1]].push_back(x[0]); }
        dfs1(1, 0);
        ans[1] = sub[1];
        dfs2(1, 0);
        for (int i = 1; i <= n; ++i) cout << "ans[" << i << "]=" << ans[i] << " ";
        cout << "\n";
    }
  6. Hamiltonian path existence in a directed graph (n ≤ 18) — bitmask DP.
    n ≤ ১৮-এর directed graph-এ Hamiltonian path আছে কি না।
    Show Answer

    Use dp[mask][last] = true iff visited set is mask ending at last; transition over edges last → nxt. Answer = OR over dp[FULL][last].

  7. Count numbers in [L, R] that have at least one digit equal to D — digit DP.
    [L, R] range-এ এমন সংখ্যা কতটি যাদের কমপক্ষে একটি digit D-এর সমান।
    Show Answer

    Easier: count those with no digit equal to D and subtract from R−L+1. State: (pos, tight, started). Use complementary counting.

Summary — Module 36

Three idioms unlock advanced DP: bitmask (n ≤ 20, subset is the state), digit DP (count numbers ≤ N with a property), and tree DP (subtree as state, post-order combine, plus rerooting for "every-root" queries). DP on DAGs and SOS DP are natural extensions. Bangladesh's ICPC contestants — BUET, DU, NSU — repeatedly use these in regional finals.

তিনটি idiom advanced DP-র দরজা খুলে দেয়: bitmask, digit DP, ও tree DP (rerooting সহ)। DP on DAG ও SOS DP এদের স্বাভাবিক সম্প্রসারণ। বাংলাদেশের ICPC contestant-রা — BUET, DU, NSU — regional final-এ এগুলো বারবার ব্যবহার করে।

Next Module → Backtracking & Branch-and-Bound — কখন pruning ছাড়া exponential-ও সম্ভব।