DSA DSA In Depth
0%
design cdqdivide-and-conqueroffline-queries

Offline Divide-and-Conquer over Time (CDQ Divide-and-Conquer for 3D Partial Orders)

7 min read Module 2.3: Binary Search on Monotonic Spaces & Divide-and-Conquer Updated Oct 23, 2026
📄 Cheat Sheet
MIT 6.851 Lecture 9: Orthogonal Range Searching & Multi-Dimensional Divide-and-Conquer
MIT OpenCourseWare MIT 6.851 Lecture 9: Orthogonal Range Searching & Multi-Dimensional Divide-and-Conquer Watch on YouTube ↗

Explains how divide-and-conquer eliminates one coordinate dimension per recursion level, the foundational principle behind CDQ divide-and-conquer.

0:00 / --:--

What Is CDQ Divide-and-Conquer?

Popularized by IOI gold medalist Chen Danqi (CDQ) in 2008 (and rooted in Bentley's 1980 multidimensional divide-and-conquer), CDQ Divide-and-Conquer is an offline technique that eliminates one dimension of a multidimensional query problem (or eliminates the "time" dimension of a dynamic update-and-query problem) by recursing on array halves $[lo, mid)$ and $[mid, hi)$ and computing only the cross-half contribution where left-half elements update right-half elements.

The Canonical Problem: 3D Partial Order (3D Dominance Counting)

Given $N$ points $P_i = (x_i, y_i, z_i)$ in $\mathbb{R}^3$ (with coordinates compressed to $[1, N]$), for each point $j$ we want to count how many points $i \neq j$ satisfy:

$$x_i \le x_j \quad \land \quad y_i \le y_j \quad \land \quad z_i \le z_j$$

Let's see how each dimension is systematically neutralized:

  1. Dimension 1 ($X$-Coordinate) — Initial Presort: Sort all points lexicographically by $(x, y, z)$ and collapse identical points $(x, y, z)$ into weighted unique points. Now, throughout the entire recursion on $[lo, hi)$, every point in the left half $i \in [lo, mid)$ is guaranteed to satisfy $x_i \le x_j$ for every point in the right half $j \in [mid, hi)$!
  2. Dimension 2 ($Y$-Coordinate) — Two-Pointer Merge: Assume recursively that $[lo, mid)$ and $[mid, hi)$ are sorted by $Y$. Sweep pointer $p_L$ across $[lo, mid)$ and pointer $p_R$ across $[mid, hi)$ in ascending $Y$-order. Whenever $P[p_L].y \le P[p_R].y$, advance $p_L$; otherwise advance $p_R$. At the moment we process $p_R$, every left-half point already passed by $p_L$ satisfies both $x_i \le x_j$ and $y_i \le y_j$!
  3. Dimension 3 ($Z$-Coordinate) — 1D Fenwick Tree (BIT): When advancing $p_L$, insert its $Z$-coordinate into a 1D Fenwick Tree: bit.add(P[p_L].z, P[p_L].weight). When advancing $p_R$, query prefix sum bit.query(P[p_R].z) to count how many active left-half points also satisfy $z_i \le z_j$! Finally, rollback the Fenwick tree by subtracting only the touched $P[p_L].z$ entries in $O(hi - lo)$ time.
Cooley-Tukey Radix-2 FFT Butterfly, Karatsuba & CDQ Divide-and-Conquer
O(N log N) FFT / O(N log² N) CDQ

Inspect the Radix-2 Cooley-Tukey FFT butterfly y_k = E_k + ω_n^k O_k, Karatsuba 3-multiplication algebraic identity, and CDQ Divide-and-Conquer left-to-right contribution transfer.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

Halving Lemma: Because ω_n^(k + n/2) = −ω_n^k and (ω_n^k)² = ω_{n/2}^k, a single product t = ω_n^k · O_k yields both y_k = E_k + t and y_{k+n/2} = E_k − t.
E_k O_k × ω_n^k y_k = E_k + ω_n^k·O_k y_{k+n/2} = E_k − ω_n^k·O_k
Butterfly Operations
1 Complex Multiply + 2 Adds
Stages / Width
log₂(N) stages × (N/2) butterflies
Polynomial Multiply
O(N log N) (vs O(N²) naive)
[Step 1] Combined even DFT E_k and twiddle-multiplied odd DFT ω_n^k·O_k across Radix-2 butterfly.
Approach for 3D Dominance / Dynamic 2D Range Count Time Complexity Space Complexity Cache & Constant Factor
2D Segment Tree / Fenwick Tree of Treaps (Online) $O(N \log^2 N)$ $O(N \log^2 N)$ Heavy pointer chasing (~400 MB for $N=2\cdot 10^5$)
K-D Tree Orthogonal Range Counting $O(N^{5/3})$ worst-case $O(N)$ Suboptimal asymptotic bound
CDQ Divide-and-Conquer + 1D Fenwick Tree (Offline) $\Theta(N \log^2 N)$ $\Theta(N)$ Tiny L1-resident BIT + sequential array scans!
The CDQ Cross-Boundary Invariant & Rollback Requirement

At any node $[lo, hi)$ of the CDQ recursion tree, left-half elements $i \in [lo, mid)$ only update the Fenwick Tree, and right-half elements $j \in [mid, hi)$ only query the Fenwick Tree. Why is it illegal to call std::fill(bit.begin(), bit.end(), 0) to reset the Fenwick Tree at the end of $[lo, hi)$? Because std::fill takes $O(N)$ time at every recursive call, blowing total time up to $O(N^2)$! Instead, you must undo only the insertions performed during that merge by calling bit.add(P[i].z, -P[i].weight) for each $i \in [lo, p_L)$, keeping work at node $[lo, hi)$ strictly $O((hi - lo) \log N)$.

CDQ Divide-and-Conquer: How 3 Dimensions Collapse into a 1D Fenwick Tree Left Half [lo, mid) All have x_i ≤ x_j (Dim 1 Fixed) Sorted by Y (Dim 2) Action: bit.add(z_i, +cnt_i) 1D Fenwick Tree on Z Sweeps p_L and p_R in non-decreasing Y order O(log N) per Update/Query Rollback: bit.add(z_i, -cnt_i) Right Half [mid, hi) All have x_j ≥ x_i (Dim 1 Fixed) Sorted by Y (Dim 2) Action: ans_j += bit.query(z_j) Recurrence: T(N) = 2T(N/2) + O(N log N) = Θ(N log^2 N) Time, Θ(N) Auxiliary Space
flowchart TD A["Sort Points by (X, Y, Z) & Deduplicate Duplicates"] --> B["cdq(lo, hi): Split at mid = (lo + hi) / 2"] B --> C["Recurse cdq(lo, mid) and cdq(mid, hi) Both Halves Return Sorted by Y"] C --> D["Two-Pointer Merge by Y: If P[p_L].y <= P[p_R].y -> bit.add(P[p_L].z, +w) Else -> ans[P[p_R].id] += bit.query(P[p_R].z)"] D --> E["Rollback BIT for all inserted p_L in [lo, mid) Copy Y-Merged Scratch Back to P[lo..hi)"]
Hardware Reality: Why Deduplicating Equal $(x, y, z)$ Triples Is Mandatory

Suppose two distinct input items $A$ and $B$ have the exact same coordinates $(x, y, z)$. After sorting, one of them ($A$) appears before the other ($B$). In CDQ divide-and-conquer, an element on the left can contribute to an element on the right, buuuuut the element on the right ($B$) can never contribute back to the element on the left ($A$)! Consequently, $A$ would undercount its equal partner $B$. By collapsing identical $(x, y, z)$ points into a single representative with multiplicity cnt before starting CDQ (and adding cnt - 1 to its own dominance count), strict ordering between distinct triples is restored.

Production C++20 Implementation: 3D Dominance Counting via CDQ Divide-and-Conquer

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <vector>

struct Point3D {
    int x, y, z;
    int weight;   // Multiplicity of identical (x, y, z) points
    int dom_cnt;  // Number of points dominated by this unique point
    int orig_id;
};

class FenwickTree {
public:
    explicit FenwickTree(std::size_t max_z) : tree_(max_z + 1, 0) {}

    void add(int idx, int delta) noexcept {
        for (std::size_t i = static_cast<std::size_t>(idx); i < tree_.size(); i += i & (~i + 1)) {
            tree_[i] += delta;
        }
    }

    int query(int idx) const noexcept {
        int sum = 0;
        for (std::size_t i = static_cast<std::size_t>(idx); i > 0; i &= i - 1) {
            sum += tree_[i];
        }
        return sum;
    }

private:
    std::vector<int> tree_;
};

// Solves 3D Partial Order (1 <= z_i <= max_z) in O(N log^2 N) time and O(N) space.
// Returns for each unique point the total weight of other points it dominates.
void solve_3d_dominance_cdq(std::vector<Point3D>& pts, int max_z) {
    if (pts.empty()) return;

    // 1. Sort lexicographically by (x, y, z)
    std::sort(pts.begin(), pts.end(), [](const Point3D& a, const Point3D& b) {
        if (a.x != b.x) return a.x < b.x;
        if (a.y != b.y) return a.y < b.y;
        return a.z < b.z;
    });

    // 2. Deduplicate identical (x, y, z) triples and accumulate multiplicity weight
    std::vector<Point3D> uniq;
    uniq.reserve(pts.size());
    for (const auto& p : pts) {
        if (!uniq.empty() && uniq.back().x == p.x &&
            uniq.back().y == p.y && uniq.back().z == p.z) {
            uniq.back().weight += p.weight;
        } else {
            uniq.push_back(p);
        }
    }

    const std::size_t m = uniq.size();
    std::vector<Point3D> scratch(m);
    FenwickTree bit(static_cast<std::size_t>(max_z));

    auto cdq = [&](auto& self, std::size_t lo, std::size_t hi) -> void {
        if (hi - lo <= 1) return;
        const std::size_t mid = lo + (hi - lo) / 2;

        self(self, lo, mid);
        self(self, mid, hi);

        // Both [lo, mid) and [mid, hi) are now sorted by Y.
        // Merge them by Y while applying left-half BIT updates to right-half queries.
        std::size_t pL = lo, pR = mid, out = lo;
        while (pL < mid && pR < hi) {
            if (uniq[pL].y <= uniq[pR].y) {
                bit.add(uniq[pL].z, uniq[pL].weight);
                scratch[out++] = uniq[pL++];
            } else {
                uniq[pR].dom_cnt += bit.query(uniq[pR].z);
                scratch[out++] = uniq[pR++];
            }
        }
        while (pR < hi) {
            uniq[pR].dom_cnt += bit.query(uniq[pR].z);
            scratch[out++] = uniq[pR++];
        }
        // Rollback Fenwick tree for all left-half elements that were inserted!
        for (std::size_t i = lo; i < pL; ++i) {
            bit.add(uniq[i].z, -uniq[i].weight);
        }
        while (pL < mid) {
            scratch[out++] = uniq[pL++];
        }

        std::copy(scratch.begin() + lo, scratch.begin() + hi, uniq.begin() + lo);
    };

    cdq(cdq, 0, m);

    // Account for self-dominance among identical (x, y, z) duplicates
    for (auto& u : uniq) {
        u.dom_cnt += (u.weight - 1);
    }
    pts = std::move(uniq);
}
Pre-Order vs. Post-Order CDQ for Dynamic Programming Transitions

In pure counting problems (like 3D dominance or offline 2D range queries), left and right subproblems are independent, so standard post-order Merge-Sort CDQ (cdq(lo, mid); cdq(mid, hi); merge();) works fastest because the merge step sorts by $Y$ bottom-up. However, if you use CDQ to optimize 1D/2D Dynamic Programming where $dp[j] = \max_{i < j}(dots)$ requires $dp[i]$ for the entire left half $[lo, mid)$ to be fully finalized before propagating to $[mid, hi)$, you MUST execute CDQ in in-order: cdq(lo, mid); cross_update(lo, mid, mid, hi); cdq(mid, hi);!

Check yourself Question 1 of 4
Concept Check How CDQ Divide-and-Conquer Eliminates Three Dimensions recall

In the offline 3D Partial Order problem, we want to count for each point j how many points i != j satisfy x_i <= x_j, y_i <= y_j, and z_i <= z_j in O(N log^2 N) time and O(N) space.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why does post-order traversal produce incorrect DP values here, and what traversal order is required?

Key Takeaways

CDQ Divide-and-Conquer eliminates one coordinate (or time) dimension by sorting on X initially and computing only left-half-to-right-half interactions across the recursion tree.
For 3D Dominance, X is handled by the divide split, Y is handled by a two-pointer merge, and Z is handled by a 1D Fenwick Tree.
Total complexity is O(N log^2 N) time and O(N) space—avoiding the O(N log^2 N) memory explosion and pointer-chasing overhead of 2D Segment Trees.
Always deduplicate identical (x, y, z) points upfront and rollback the Fenwick Tree by undoing only the [lo, p_L) insertions rather than zeroing the entire BIT array.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue