Appearance
Offline Divide and Conquer for DP
Quick Revisit
- USE WHEN: layer-by-layer DP where the optimal split point is monotone: opt(i) ≤ opt(i+1)
- INVARIANT: optimal decision point never decreases as row index increases (monotone minima)
- TIME: O(kn log n) for k layers, n states per layer | SPACE: O(n)
- CLASSIC BUG: off-by-one in search range — opt_hi for left half is best_j, opt_lo for right half is best_j
- PRACTICE: 08-ladder-mixed
When a 1D DP transition satisfies the monotone minima property -- the optimal decision point for row
Difficulty: [Advanced]Prerequisites: DP -- 1D Introduction, Divide and ConquerCF Rating Range: 2000-2400+
Not to Be Confused With
This technique goes by many names: "Divide and Conquer DP optimization," "SMAWK-lite," "monotone minima D&C." It is not the same as:
- CDQ Divide and Conquer (CDQ D&C) -- that's for multi-dimensional partial order problems (counting inversions, etc.).
- Knuth's Optimization (Knuth) -- that exploits the quadrangle inequality on interval DP. Offline D&C optimization works on layer-by-layer DP with a simpler monotonicity condition.
A detailed treatment with full proofs lives in DP -- D&C Optimization. The boundary is intentional: that file is the full mathematical treatment (the quadrangle / SMAWK theory, monotonicity proofs, classic DP examples). This file is the short recognition / template reference -- how to slot the technique into your offline toolkit alongside Mo's, CDQ, and parallel binary search. If you are looking for "why does this work?", go to the DP chapter. If you are looking for "is this the right tool, and what does the recursion look like?", you are in the right place.
The Recurrence
You have a DP with
where
Monotone minima condition:
This holds when
The Algorithm
To compute one DP layer (fixing
solve(lo, hi, opt_lo, opt_hi):
// Compute dp[t][i] for i in [lo, hi]
// knowing opt(i) is in [opt_lo, opt_hi]
mid = (lo + hi) / 2
best_j = argmin over j in [opt_lo, min(mid, opt_hi)]
of dp[t-1][j] + C(j+1, mid)
dp[t][mid] = dp[t-1][best_j] + C(best_j+1, mid)
solve(lo, mid-1, opt_lo, best_j) // left half: opt <= best_j
solve(mid+1, hi, best_j, opt_hi) // right half: opt >= best_jEach level of recursion scans a total of
Trace
Array
solve(0, 4, 0, 4):
mid=2, scan j=0..2:
j=0: dp[1][0] + C(1,2) = 0 + 25 = 25
j=1: dp[1][1] + C(2,2) = 9 + 16 = 25
j=2: dp[1][2] + C(3,2) = invalid
best_j=0, dp[2][2]=25
solve(0, 1, 0, 0):
mid=0: j=0 only -> dp[2][0]=dp[1][0]+C(1,0)=0
solve(1, 1, 0, 0):
mid=1: j=0 -> dp[2][1]=dp[1][0]+C(1,1)=16
solve(3, 4, 0, 4):
mid=3: scan j=0..3 -> find best_j
...Implementation
cpp
#include <bits/stdc++.h>
using namespace std;
int n, k;
long long dp[2][5001]; // rolling two layers
long long cost(int l, int r); // problem-specific cost function
void solve(int lo, int hi, int opt_lo, int opt_hi, int cur) {
if (lo > hi) return;
int mid = (lo + hi) / 2;
long long best = 1e18;
int best_j = opt_lo;
for (int j = opt_lo; j <= min(mid - 1, opt_hi); j++) {
long long val = dp[cur ^ 1][j] + cost(j + 1, mid);
if (val < best) {
best = val;
best_j = j;
}
}
dp[cur][mid] = best;
solve(lo, mid - 1, opt_lo, best_j, cur);
solve(mid + 1, hi, best_j, opt_hi, cur);
}
int main() {
// ... read input, compute prefix sums for cost() ...
// Base layer
for (int i = 0; i < n; i++) dp[0][i] = cost(0, i);
for (int t = 1; t < k; t++) {
fill(dp[t & 1], dp[t & 1] + n, 1e18);
solve(0, n - 1, 0, n - 1, t & 1);
}
cout << dp[(k - 1) & 1][n - 1] << "\n";
}Recognizing the Pattern
Suspect this optimization when you see:
- "Partition into exactly
groups, minimize total cost" -- the layers are your groups. - Cost function
is "convex-ish" -- wider ranges cost more than the sum of parts (e.g., , variance, squared deviation). up to with up to -- is too slow, fits.
Monotonicity is a required property, not a hope. The technique is correct only if
Pitfalls
- Off-by-one in
opt_hibound. The scan range formust not exceed (you can't transition from yourself). - Forgetting to handle empty ranges.
lo > hishould return immediately. - Cost function must be efficient. If
cost(l, r)is, the total is -- worse than brute force. Precompute prefix sums. - Not verifying monotonicity. If
is not monotone, this technique gives wrong answers silently.
Practice Problems
| # | Problem | Difficulty | Notes |
|---|---|---|---|
| 1 | CF 868F -- Yet Another Minimization Problem | 2500 | Partition into |
| 2 | CF 321E -- Ciel and Gondola | 2000 | Classic D&C DP optimization |
| 3 | APIO 2014 -- Sequence | -- | Partition with product-based cost |
| 4 | CF 833B -- The Bakery | 2200 | Partition array into |
Further Reading
- DP -- D&C Optimization -- full treatment with proofs and more examples
- CDQ Divide and Conquer -- the other "D&C" offline technique (different purpose)
- Knuth's Optimization -- a related but distinct optimization for interval DP