1. Problem & Motivation
Many counting and aggregation problems do not have just one constraint — they have several, layered on top of each other. The classic version, often called 3D partial order counting, asks: given a list of triples (index, a, b), count how many pairs (i, j) satisfy all three of $i < j$ (index order), $a_i < a_j$ (first value dimension), and $b_i < b_j$ (second value dimension) simultaneously. A single sort only resolves one of these three dimensions; the other two still need to be checked for every candidate pair, and checking pairs one at a time costs $O(n^2)$ or worse.
You have almost certainly already solved a 2-dimensional version of this exact problem. Counting inversions — pairs with $i < j$ but $\text{arr}[i] > \text{arr}[j]$ — is precisely "index order plus one value dimension," and the classic merge-sort-based Inversion Count algorithm (covered in Divide and Conquer Counting: Inversions & Reverse Pairs) solves it in $O(n \log n)$ by recursively splitting the array in half, solving each half, and counting cross-half contributions during a merge step. CDQ divide and conquer — named after the Chinese competitive programmer whose handle is CDQ — is exactly that same idea, generalized to handle one extra constraint dimension per layer of recursion. Where Inversion Count's merge step counts cross-half pairs in $O(1)$ per step because there is nothing left to check once both halves are sorted by value, CDQ's cross-half step needs an $O(\log n)$ Fenwick-tree query per step, because there is a second value dimension still unresolved.
This pattern shows up constantly once you know to look for it: 3D dominance counting in computational geometry, offline range-counting queries with an extra attribute, and certain dynamic-programming transitions that depend on two monotonic conditions on a prior index at once (introduced briefly near the end of this lesson).
2. Prerequisites
This lesson builds directly on two companion lessons and does not re-derive either of them:
- Divide and Conquer Counting: Inversions & Reverse Pairs is the essential prerequisite. Its merge-sort-based Inversion Count is the "one extra dimension" base case that CDQ divide and conquer generalizes by adding a second value dimension and a Fenwick tree to the same recursive skeleton. If the merge-step cross-counting idea is not yet solid, build it there first.
- Offline Query Processing & Mo's Algorithm supplies the general framing this lesson assumes: when you are handed an entire batch of queries (or, here, an entire batch of triples) up front, you are free to reorder or restructure the computation across the whole batch instead of answering pieces one at a time. CDQ divide and conquer is one particular offline strategy built on that freedom.
You should also be comfortable with basic Fenwick tree (Binary Indexed Tree) operations — point update and prefix query in $O(\log n)$. Fenwick Tree Variants covers the mechanics in depth; this lesson uses only the plain point-update / prefix-sum form.
3. Terminology & Definitions
- 3D partial order: a triple of constraints $(i < j,\ a_i < a_j,\ b_i < b_j)$ that must all hold for a pair to count. "3D" refers to the three dimensions being compared — index, and two value attributes — not to physical 3D geometry (though the geometric analogy, points dominating other points in 3D space, is exact and useful).
- Offline processing: the entire input (here, all $n$ triples) is known before computation starts, so it can be sorted, re-ordered, or restructured as a whole rather than processed one arrival at a time.
- Contribution counting: instead of checking every pair directly, the algorithm counts, for each element, how many earlier-processed elements "contribute" a valid pairing with it — the same framing merge sort uses when it counts, for each right-half element, how many left-half elements it forms an inversion with.
- Cross-half (or cross-boundary) pairs: pairs where one element comes from the left half of a recursive split and the other from the right half. A divide-and-conquer counting algorithm's job at each level is to count exactly these, since same-half pairs are handled by recursion.
4. A Concrete Example
Counting 3D-dominant pairs among six triples
Take six triples (index, a, b), already given in index order (this is the order the elements arrived in, or their original array position):
| index | a | b |
|---|---|---|
| 0 | 3 | 1 |
| 1 | 4 | 2 |
| 2 | 1 | 6 |
| 3 | 5 | 3 |
| 4 | 6 | 4 |
| 5 | 2 | 5 |
We want to count pairs $(i, j)$ with $i < j$, $a_i < a_j$, and $b_i < b_j$. CDQ divide and conquer splits by index first — not by $a$ or $b$ — into a left half {0, 1, 2} and a right half {3, 4, 5}. Each half is solved recursively (splitting further, all the way down to single elements), and then one cross-half step counts pairs with the first element in the left half and the second in the right half. Because every left-half index is smaller than every right-half index, the $i < j$ part of the constraint is already guaranteed for every cross-half pair — the cross-half step only needs to check $a_i < a_j$ and $b_i < b_j$.
The cross-half step sorts both halves by $a$-value and sweeps through the right half in increasing $a$ order, inserting left-half elements into a Fenwick tree keyed by $b$-value as soon as their $a$-value becomes smaller than the current right-half element's $a$-value. Each right-half element then queries the Fenwick tree for how many inserted $b$-values are strictly smaller than its own — exactly the merge-step idea from Inversion Count, with an $O(\log n)$ Fenwick query standing in for Inversion Count's $O(1)$ comparison, because there is a second value dimension left to resolve. Section 8 traces every step of this example by hand, and the implementation in Section 11 is verified against it and against brute force.
The diagram below plots the six triples by $(a, b)$, coloring the left half (indices 0-2) and right half (indices 3-5) separately, and drawing the four cross-half pairs that satisfy both value constraints — the same pairs counted in the Section 8 trace.
5. Naive / Brute-Force Approach
The obvious method checks every ordered pair directly:
count = 0
for i in 0 .. n-1:
for j in 0 .. n-1:
if i < j and a[i] < a[j] and b[i] < b[j]:
count += 1
This is correct and easy to verify by hand or by code, which is exactly why it is used as the ground truth for testing the faster algorithm later in this lesson. Its cost is $O(n^2)$: every one of the $\binom{n}{2}$ pairs is checked with $O(1)$ work. For $n$ in the tens of thousands, this is already too slow for most competitive-programming or production time limits; some 3D-partial-order variants (for instance, when $a$ or $b$ must additionally be looked up or recomputed per pair) can even degrade to $O(n^3)$ if the per-pair check itself is not constant time. The repeated work is easy to name: the same "how many earlier elements have a smaller $a$ and a smaller $b$" question is effectively re-derived from scratch for every $j$, instead of being answered incrementally as $j$ grows.
6. Core Insight
The reason this stacking works is that each layer only ever has to deal with the dimensions the previous layer did not already resolve. Divide and conquer on the index resolves $i < j$ for free (Section 9 proves this in detail). Sorting by $a$-value and sweeping resolves $a_i < a_j$ the same way Inversion Count's merge step resolves its single value comparison. What is left over — the $b_i < b_j$ constraint — can no longer be resolved by a second sort, because sorting by $b$ would undo the sort by $a$. Instead, a Fenwick tree keyed by (rank of) $b$-value tracks "how many left-half elements with a smaller $a$-value have I seen so far, broken down by their $b$-value," and answers "how many of those have $b < $ mine" in $O(\log n)$.
flowchart TD
A["solve(lo, hi): points sorted by index"] --> B{"hi - lo <= 1?"}
B -->|yes| C["Base case: 0 or 1 point, contributes 0 pairs"]
B -->|no| D["Split at mid: Left = [lo, mid), Right = [mid, hi)"]
D --> E["Recurse: solve(lo, mid) -- left-half pair count"]
D --> F["Recurse: solve(mid, hi) -- right-half pair count"]
E --> G["Sort Left by a-value"]
F --> H["Sort Right by a-value"]
G --> I["Sweep Right in increasing a order;
insert qualifying Left points into Fenwick tree over b"]
H --> I
I --> J["Query Fenwick tree: count of inserted b-values less than Right point's b"]
J --> K["Roll back every Fenwick insert made at this level"]
K --> L["Combine: total = left count + right count + cross count"]
C --> L
7. Plain-English Algorithm Steps and Pseudocode
- Keep the triples in their original index order; this order is never re-sorted for the recursion itself.
- If the current range holds 0 or 1 elements, it contributes 0 pairs; return.
- Otherwise split the range at its midpoint into a left half and a right half (by index, i.e. by position in the range, not by value).
- Recursively solve the left half and the right half; each recursive call returns the count of qualifying pairs fully inside its own half.
- For the cross-half count: copy the left half and the right half, and sort each copy by $a$-value.
- Sweep through the right-half copy in increasing $a$-value order. Before processing each right-half element, insert into a Fenwick tree (keyed by $b$-value) every left-half element whose $a$-value is still smaller than the current right-half element's $a$-value.
- For each right-half element, query the Fenwick tree for the count of inserted $b$-values strictly less than its own $b$-value; add that count to the running total.
- After the sweep, remove (roll back) everything that was inserted into the Fenwick tree at this level, so sibling recursive calls see a clean structure.
- The answer for this range is left-count + right-count + cross-count.
Pseudocode
function SOLVE(lo, hi): # points[lo:hi] sorted by original index
if hi - lo <= 1:
return 0 # 0 or 1 point: no pairs possible
mid = (lo + hi) / 2
leftCount = SOLVE(lo, mid) # pairs fully inside the left half
rightCount = SOLVE(mid, hi) # pairs fully inside the right half
# cross-half step: i in [lo, mid), j in [mid, hi); i < j already guaranteed
leftByA = points[lo:mid] sorted by a
rightByA = points[mid:hi] sorted by a
pointer = 0
touched = []
crossCount = 0
for r in rightByA (increasing a order):
while pointer < length(leftByA) and leftByA[pointer].a < r.a:
fenwick.update(rank(leftByA[pointer].b), +1)
touched.append(rank(leftByA[pointer].b))
pointer += 1
crossCount += fenwick.query(rank(r.b) - 1) # inserted b-values strictly less than r.b
for rk in touched:
fenwick.update(rk, -1) # roll back: keep the tree clean for sibling calls
return leftCount + rightCount + crossCount
8. Manual Trace
Continuing the worked example from Section 4, here is the full recursion tree for points = [(0,3,1), (1,4,2), (2,1,6), (3,5,3), (4,6,4), (5,2,5)]:
| Segment (by index) | Split | Left subtotal | Right subtotal | Cross-half count | Level total |
|---|---|---|---|---|---|
| {1, 2} | {1} / {2} | 0 | 0 | 0 | 0 |
| {0, 1, 2} | {0} / {1, 2} | 0 | 0 | 1 | 1 |
| {4, 5} | {4} / {5} | 0 | 0 | 0 | 0 |
| {3, 4, 5} | {3} / {4, 5} | 0 | 0 | 1 | 1 |
| {0, 1, 2, 3, 4, 5} | {0,1,2} / {3,4,5} | 1 | 1 | 4 | 6 |
The interesting step is the top-level cross-half count between {0, 1, 2} and {3, 4, 5}. Sorted by $a$-value: left half becomes index 2 ($a{=}1$), index 0 ($a{=}3$), index 1 ($a{=}4$); right half becomes index 5 ($a{=}2$), index 3 ($a{=}5$), index 4 ($a{=}6$). The sweep processes the right half in that order:
| Right element (a, b) | Left elements inserted this step (a < right.a) | Fenwick has b-values | Query: count b < right.b | Contribution |
|---|---|---|---|---|
| idx 5 (a=2, b=5) | idx 2 (a=1, b=6) | {6} | count < 5 | 0 |
| idx 3 (a=5, b=3) | idx 0 (a=3, b=1); idx 1 (a=4, b=2) | {6, 1, 2} | count < 3 | 2 |
| idx 4 (a=6, b=4) | (none new — already inserted) | {6, 1, 2} | count < 4 | 2 |
Cross-half total: $0 + 2 + 2 = 4$, corresponding to the pairs $(0,3), (0,4), (1,3), (1,4)$. Adding the left subtotal (1, from pair $(0,1)$) and the right subtotal (1, from pair $(3,4)$) gives the final answer: $1 + 1 + 4 = 6$ qualifying pairs.
9. Correctness Reasoning
The technique relies on one design decision above all others: the recursion splits by index (insertion order), never by value. Here is why that specific choice is the crux of the whole method.
Because the input is kept sorted by original index throughout the recursion, a range [lo, hi) splits into a left half [lo, mid) and a right half [mid, hi) where every index in the left half is smaller than every index in the right half, by construction — not by anything that needs to be checked. This means:
- Any pair with both elements in the left half, or both in the right half, is a pair fully inside one recursive call, and is correctly handled by that call's own recursion (by induction on range size).
- Any pair with the first element in the left half and the second in the right half automatically satisfies $i < j$, with no explicit check needed — that is exactly the pair category the cross-half step counts.
- There is no fourth category: a pair with the first element in the right half and the second in the left half would require $i < j$ while $i \geq \text{mid} > j$, which is impossible. So the three categories above are exhaustive and non-overlapping.
By induction on range size: the base case (0 or 1 elements) trivially has 0 qualifying pairs. For the inductive step, assume both recursive calls correctly count all pairs fully inside their own half. The cross-half sweep correctly counts the third category because it is structurally identical to Inversion Count's merge step (sort by the next unresolved dimension, sweep, count matches on the remaining dimension) — only the $O(1)$ comparison is replaced by an $O(\log n)$ Fenwick-tree query, since there is one more dimension ($b$) still unresolved at that point. Summing all three categories gives the total for the whole range, which is what the base case, inductive hypothesis, and cross-half correctness together guarantee.
If the recursion instead split by $a$-value (or by $b$-value) first, this guarantee would disappear: a left/right split by value says nothing about which side holds the smaller index, so every cross-"half" pair would need an explicit $i < j$ check, and the elegant "sort by the next dimension, sweep, count" structure would break down into something no better than the brute-force approach. Splitting by index first is what lets every later step assume $i < j$ for free.
10. Complexity Analysis
At each level of the recursion, the ranges at that level partition the full input, so the total work across all ranges at one level is bounded by the cost of one sort-and-sweep over $n$ elements: sorting by $a$-value costs $O(n \log n)$ (or $O(n)$ if pre-sorted subarrays are merged mergesort-style instead of re-sorted), and the sweep performs $O(n)$ Fenwick updates and queries, each $O(\log n)$, for another $O(n \log n)$. So each level costs $O(n \log n)$.
There are $O(\log n)$ levels, since the range size halves each time. The recurrence is:
$$T(n) = 2T(n/2) + O(n \log n)$$
which solves to $T(n) = O(n \log^2 n)$ — one extra $\log n$ factor compared to plain merge sort or Inversion Count, directly attributable to the Fenwick-tree operations (and, if not avoided by merging pre-sorted halves, the repeated sort) needed to resolve the second value dimension.
Space is $O(n)$ for the Fenwick tree and the per-level sorted copies, plus $O(\log n)$ recursion depth. Because every Fenwick update made during a cross-half sweep is rolled back before the function returns, the tree's state never leaks between sibling recursive calls, and its size stays fixed at the number of distinct $b$-values across the whole input (found once, up front, by coordinate-compressing all $b$-values before recursion begins).
11. Implementation
Each panel below implements count_3d_dominance (or its language equivalent) for the exact problem described in Section 4: given triples (index, a, b) already sorted by index, count pairs with $i < j$, $a_i < a_j$, $b_i < b_j$, using CDQ divide and conquer with a Fenwick tree over $b$-ranks. Each panel also includes a brute-force reference implementation and runs both on the Section 4 worked example plus several edge cases, asserting they agree. (For a refresher on the Fenwick tree mechanics used here — update and prefix_count — see Fenwick Tree Variants; this lesson does not re-derive them.)
The Python and Java panels above were executed directly: both print CDQ count: 6 and Brute-force count: 6 for the Section 4 worked example, and both pass the listed edge cases (fully increasing, fully decreasing, tied values, empty input, single element) plus 3000 additional randomized trials against the brute-force reference (random triples, $n \le 9$, small value ranges, to keep brute force cheap to compute for comparison). The C++ panel mirrors the same verified algorithm line-for-line and was reviewed against the executed Python/Java logic; no C++ compiler was available in this environment to execute it directly.
Extending the Pattern to DP Transitions
The same structural trick — offline, sort by one dimension, divide-and-conquer-plus-Fenwick-tree for the rest — extends beyond pure counting to accelerating certain dynamic-programming transitions. Consider a recurrence like $dp[i] = \max_j (dp[j] + w(i, j))$ where the valid $j$ values are restricted by two monotonic conditions relative to $i$ (for example, $j < i$ and some value attribute of $j$ must be smaller than the same attribute of $i$). Instead of scanning all valid $j$ for every $i$ (an $O(n^2)$ transition), the same CDQ skeleton applies: divide by index, recursively finalize $dp$ values within the left half, then use the cross-half sweep — sort both halves by the value dimension, sweep, and query a Fenwick tree that tracks a running maximum (or minimum) instead of a count — to update every right-half $dp[i]$ using only already-finalized left-half $dp[j]$ values, before recursing into the right half's own internal transitions.
One order-of-operations detail matters more here than in pure counting: because $dp[j]$ values feed into $dp[i]$ across levels, the left half's $dp$ values must be fully finalized (including all of its own internal transitions) before the cross-half step uses them to update the right half; only after that does the right half compute its own internal transitions. This lesson does not implement a full worked DP example — the goal here is to recognize the shape ("CDQ divide and conquer optimizing a DP transition") so that when a recurrence resists an $O(n \log n)$ approach because of two entangled monotonic conditions, this pattern is available to try.
12. Testing & Edge Cases
Beyond the tests already exercised in Section 11, worth checking explicitly by construction: an input where every pair satisfies the constraint (strictly increasing $a$ and $b$ alongside index) should return $\binom{n}{2}$; an input where no pair satisfies it (strictly decreasing $a$ and/or $b$) should return 0. Ties in $a$-value or $b$-value need a consistent tie-breaking rule: because both constraints are strict ($a_i < a_j$, not $\le$), the sweep must never insert a left-half element into the Fenwick tree while its $a$-value is only equal (not strictly less) to the current right-half element's $a$-value, and the Fenwick query must count strictly-less $b$-ranks (not less-or-equal). Get either of these backwards and ties silently turn into double-counted or missed pairs, since a tied element could otherwise be treated as satisfying a strict inequality against itself or against another tied element.
13. Applications & Alternatives
Where this pattern shows up
- Competitive programming: 3D dominance counting (sometimes literally posed as "3D partial order") is a recurring problem family, and CDQ divide and conquer is the standard tool once a problem has more constraints than a single sort can resolve.
- Computational geometry: counting or querying points that dominate (or are dominated by) other points in 3 or more dimensions, offline, is directly this same structure — index order can even represent a third coordinate or arrival time rather than a literal array position.
- Alternative: a 2D Fenwick tree. A direct 2D Fenwick tree (indexed by $a$-rank and $b$-rank simultaneously) can answer the same dominance queries without any divide and conquer, but its memory and per-operation cost both grow with the product of the two value ranges (or an extra $\log$ factor with coordinate compression plus a Fenwick-tree-of-Fenwick-trees), which is often worse in practice than CDQ's $O(n \log^2 n)$, especially when the value ranges are large relative to $n$.
- Alternative: a k-d tree or range tree. These general-purpose spatial structures (see Interval & Spatial Trees for the k-d tree and range tree in depth) can also answer multi-dimensional dominance and range-counting queries, but they carry larger constant factors and are usually better suited to online or dynamically-updated point sets than to a purely offline, batch-counted workload like this one.
14. Exercises & Mastery
Easy
- Take a new set of 5 triples of your own choosing and verify, by brute force, the count CDQ divide and conquer would produce — then trace the recursion by hand as in Section 8.
Medium
- Extend the implementation to count pairs with $a_i < a_j$ but $b_i > b_j$ (a mixed-direction constraint). Think about what changes in the sweep and the Fenwick-tree query — does the tree need to be queried from the other end, or does the $b$-ranking need to be reversed?
Hard
- Design (conceptually; no implementation required) how you would generalize this technique to a fourth dimension — four constraints that must all hold simultaneously. What additional structure would the cross-half step need, and does "one divide and conquer, one sort, one data structure" still describe it, or does a fourth dimension require nesting a second data structure inside the first?
Review rhythm: tomorrow, reconstruct the recursion structure and the reason index-first splitting is necessary, without notes; next week, re-implement the Fenwick-tree cross-count sweep from memory in a language of your choice; several weeks later, recognize this pattern when a new problem presents multiple sortable constraints, before being told CDQ divide and conquer applies. Finally, teach the "one divide and conquer, one sort, one data structure" framing to someone who already understands merge-sort-based Inversion Count.
15. Summary Cheat Sheet
- CDQ divide and conquer generalizes merge-sort-based Inversion Count to handle one extra constraint dimension per layer.
- Split by index (insertion order) first — this is what makes the $i < j$ constraint automatic for every cross-half pair, with no explicit check.
- Cross-half step: sort both halves by the second dimension and sweep, using a Fenwick tree over the third dimension to count matches in $O(\log n)$ instead of Inversion Count's $O(1)$.
- Recurrence $T(n) = 2T(n/2) + O(n \log n)$ solves to $O(n \log^2 n)$ — one extra $\log n$ factor over plain divide-and-conquer counting.
- Always roll back Fenwick-tree updates made during a cross-half sweep before returning, so sibling recursive calls start clean.
- Strict inequalities need careful tie-breaking: insert only on strictly-smaller comparisons, and query strictly-less ranks, or ties will be double-counted or missed.
- The same "offline, sort, D&C-plus-Fenwick" shape extends to certain DP transitions with two monotonic conditions, using a max/min Fenwick tree instead of a counting one.