Dynamic Programming III: Advanced
Bitmask DP, Digit DP, Tree DP
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.
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.
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])
#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;
}
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).
#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;
}
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].
#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).
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.
✅ 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
-
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"; } -
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"; } -
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"; } -
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"; } -
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"; } -
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 ismaskending atlast; transition over edgeslast → nxt. Answer = OR overdp[FULL][last]. -
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.