NeFut Logo NeFut
Admin Login

Binary Search on Answer and Divide-and-Conquer Structures

Published at: 2026-05-27 09:17 Last updated: 2026-06-23 12:25
#algorithm #Divide and Conquer #Binary Search

Core Logic and Mathematical Principles

Binary Search on Answer

The original problem is typically a global extremum optimization problem.

Binary search on answer transforms the difficult-to-solve "optimization problem" into an efficiently verifiable "decision problem" (i.e., verifying whether a feasible solution exists when the target value is set to $x$). The key prerequisite for this method is that the answer space possesses monotonicity. Based on this, we can use binary search to quickly locate the global optimal solution within the ordered answer space, thereby avoiding the multi-dimensional search space explosion that may occur when directly enumerating solutions.


Monotonicity is the mathematical foundation of binary search on answer.

Let the solution space $D$ be a feasible bounded totally ordered set (typically a one-dimensional interval), and the check function $f: D \to \{0, 1\}$.

If $f(x)$ satisfies piecewise constant monotonicity, i.e.:

$$\forall x_1, x_2 \in D, x_1 < x_2 \implies f(x_1) \le f(x_2) \quad (\text{or } f(x_1) \ge f(x_2))$$

Then by performing binary partitioning on the solution space $D$, the optimal solution $x^*$ can be precisely located or infinitely approximated within $O(\log |D|)$ checks.

The essence of this algorithm is to reduce continuous or discrete optimization problems to decision problems with logarithmic computational overhead.

Each decision can eliminate half of the invalid value range, thereby achieving rapid pruning of the high-dimensional search space.


Divide and Conquer Tree

The original problem is typically a global sequence combinatorial problem. Its manifestation is: given a sequence of length $N$, requiring the statistics, retrieval, or maintenance of "all element pairs $(i, j)$" or "all contiguous subintervals $[i, j]$" that satisfy a specific topological/algebraic relationship.

Typical examples include inversion counting (finding all non-linear pairs satisfying $i < j$ and $A[i] > A[j]$), or the maximum contiguous subarray sum problem (retrieving the subinterval with the maximum element sum among all possible $[i, j]$).

Since a sequence of length $N$ contains $O(N^2)$ subintervals and pairs, the brute-force computational cost for global relationships naturally has a complexity base of $O(N^2)$.

Divide and conquer trees use the geometric means of "spatial partitioning" to eliminate brute-force enumeration across the entire domain. It classifies the complex relationships of global span into three local forms: relationships purely within the left subinterval, relationships purely within the right subinterval, and cross-interval relationships spanning the midpoint. Through tree-structured preprocessing or same-layer scanning, it successfully compresses the merging cost of the most difficult "cross-interval relationships" to $O(N)$ or even $O(1)$.


Map the spatial subintervals of the sequence $A[1..N]$ to the tree-recursive topology on the time axis of the algorithm's execution flow.

For any subproblem interval $[l, r]$, take the divide-and-conquer midpoint $mid = l + \lfloor \frac{r-l}{2} \rfloor$ to prevent numerical overflow, then recursively construct the left subtree $[l, mid]$ and right subtree $[mid+1, r]$.

The entire divide-and-conquer trajectory is logically abstracted as a regular balanced binary tree of height $O(\log N)$ (since intervals cannot be guaranteed to be perfectly split, it is not a complete binary tree in the strict sense).

Through bottom-up information merging on this logical tree (such as segment tree PushUp), top-down delayed propagation (such as segment tree PushDown), or same-layer cross-interval contribution synthesis (such as offline contributions from the left interval to the right interval in CDQ divide and conquer), the global $O(N^2)$ brute-force complexity is transformed into the superposition of local interval features across $O(\log N)$ layers.

Thanks to the number of layers from the canopy to the root being strictly limited to $O(\log N)$, if the total workload per layer is controlled to $O(N)$, the overall time complexity will be strictly bounded at $O(N \log N)$.

Dimension Divide and Conquer Tree Segment Tree
Conceptual Attribute Abstract logical trajectory / algorithmic idea Concrete entity / advanced data structure
Lifecycle "Use and discard": Typically exists in the recursion stack of the system (e.g., merge sort, CDQ divide and conquer), disappearing once the algorithm finishes. "Resident in memory": Allocates actual array or pointer space (e.g., tree[MAXN << 2]) to support subsequent frequent online modifications and queries.
Solving Scenarios Primarily solves offline, one-time global statistical problems. Primarily solves online interval maintenance problems accompanied by dynamic modifications.

When you store the recursive trajectory of each divide-and-conquer algorithm in actual memory arrays (or pointers) to handle subsequent continuous online modifications and queries, this divide and conquer tree becomes a segment tree.


Algorithm Derivation and Divide-and-Conquer Structure Design

Boundary Derivation for Binary Search on Answer

For a check function satisfying the "all 1s then all 0s" monotonicity, to find the maximum value $x^*$ where $f(x)=1$ holds: initialize two pointers $l = \min(D), r = \max(D)$.

  1. Set $mid = l + \lfloor \frac{r-l+1}{2} \rfloor$. The ceiling correction here effectively avoids infinite loops when $r = l+1$ and $f(mid)==1$ where the pointer would not move right.
  2. If $f(mid) == 1$, the optimal solution lies in the closed interval $[mid, r]$, update $l = mid$.
  3. If $f(mid) == 0$, the optimal solution lies in the closed interval $[l, mid-1]$, update $r = mid - 1$.
  4. The loop terminates safely when $l == r$, and $l$ is the precise maximum value sought.

Interval Decomposition of Divide and Conquer Trees

In sequence divide and conquer, any query interval $[L, R]$ is decomposed into $O(\log N)$ mutually disjoint complete subinterval nodes on the divide and conquer tree.

Let the current divide and conquer node be $u$, governing the sequence interval $[l, r]$.

  1. If $[L, R] \supseteq [l, r]$, directly extract the preprocessed interval features of this node and terminate recursion.
  2. If $L \le mid$, recursively enter the left subtree $(u \ll 1)$.
  3. If $R > mid$, recursively enter the right subtree $(u \ll 1 | 1)$.

Cross-interval statistics spanning $mid$ are mathematically equivalent to the information fusion of the left subtree's suffix features and the right subtree's prefix features based on an associative operator.


C++ Standard Source Code

Binary Search on Answer Template

On a straight line of length $L$, there are $N$ stones. It is required to remove $M$ stones to maximize the minimum distance between any two adjacent remaining stones.

#include <iostream>
#include <algorithm>
using namespace std;

const int MAXN = 50005;
int stones[MAXN], L, N, M;

// Check function: under the constraint of minimum segment length val, can at least K segments be formed
bool chk(int val, int K) {
    int cnt = 0, last = 0; 
    for (int i = 1; i <= N; ++i) {
        if (stones[i] - last >= val) {
            cnt++;
            last = stones[i]; // Constraint satisfied, update sampling point
        }
    }
    // Endpoint settlement: if the last segment distance is less than val, it is invalid, forcibly undo one counted segment
    if (L - last < val) cnt--; 
    return cnt >= K;
}

int main() {
    cin >> L >> N >> M;
    for (int i = 1; i <= N; ++i) cin >> stones[i];

    // Binary search main body: monotonic partitioning on the bounded totally ordered set [1, L]
    int l = 1, r = L, ans = 0;
    while (l <= r) {
        int mid = l + ((r - l) >> 1); // Avoid integer overflow
        if (chk(mid, N - M)) {
            ans = mid;    // Record feasible solution
            l = mid + 1;  // Seeking maximum: answer interval shifts right
        } else {
            r = mid - 1;  // Infeasible: answer interval shifts left
        }
    }
    cout << ans << "\n";
    return 0;
}

Divide and Conquer Tree Template

Given an integer sequence of length $N$, find the maximum contiguous subarray sum (allowing all elements to be negative).

#include <iostream>
#include <algorithm>
using namespace std;

const int MAXN = 50005;
int A[MAXN];

// Encapsulate multi-algebraic state satisfying associativity
struct Node {
    long long sum, lmax, rmax, tmax;
} tree[MAXN << 2]; // 4x space to prevent out-of-bounds

// Associative operator fusion: bottom-up PushUp
void pushup(int u) {
    int ls = u << 1, rs = u << 1 | 1;
    tree[u].sum = tree[ls].sum + tree[rs].sum;
    tree[u].lmax = max(tree[ls].lmax, tree[ls].sum + tree[rs].lmax);
    tree[u].rmax = max(tree[rs].rmax, tree[rs].sum + tree[ls].rmax);
    // Core logic for spanning mid fusion: algebraic composition of left subtree's right suffix and right subtree's left prefix
    tree[u].tmax = max({tree[ls].tmax, tree[rs].tmax, tree[ls].rmax + tree[rs].lmax});
}

// Recursively construct the divide and conquer tree
void build(int u, int l, int r) {
    if (l == r) { // Base case: leaf node initialization
        tree[u] = {A[l], A[l], A[l], A[l]};
        return;
    }
    int mid = l + ((r - l) >> 1); // Bitwise operation has low precedence; must add outer parentheses
    build(u << 1, l, mid);
    build(u << 1 | 1, mid + 1, r);
    pushup(u); // Horizontal fusion of same-layer information
}

int main() {
    int n; cin >> n;
    for (int i = 1; i <= n; ++i) cin >> A[i];
    build(1, 1, n);
    cout << tree[1].tmax << "\n"; // Root node is the maximum contiguous subarray sum of the entire sequence
    return 0;
}

Divide and Conquer Tree Template (Pure Recursive Version Without tree Array)

#include <iostream>
#include <algorithm>
using namespace std;

const int MAXN = 50005;
int A[MAXN];

// Encapsulate multi-algebraic state satisfying associativity
struct Node {
    long long sum, lmax, rmax, tmax;
};

// Pure recursive divide and conquer computation: solve the maximum subarray sum state for interval [l, r]
Node solve(int l, int r) {
    if (l == r) { // Base case: single point interval, directly initialize and return
        return {A[l], A[l], A[l], A[l]};
    }

    int mid = l + ((r - l) >> 1); // Division midpoint
    Node left = solve(l, mid);     // Recursively solve left interval
    Node right = solve(mid + 1, r); // Recursively solve right interval

    // Associative operator fusion: merge local features of left and right subproblems into global features of the current interval
    Node res;
    res.sum = left.sum + right.sum;
    res.lmax = max(left.lmax, left.sum + right.lmax);
    res.rmax = max(right.rmax, right.sum + left.rmax);
    // Core logic for spanning mid fusion: algebraic composition of left subtree's right suffix and right subtree's left prefix
    res.tmax = max({left.tmax, right.tmax, left.rmax + right.lmax});

    return res;
}

int main() {
    int n; cin >> n;
    for (int i = 1; i <= n; ++i) cin >> A[i];

    // Directly trigger divide and conquer; the system stack returns the final root node state
    Node ans = solve(1, n);
    cout << ans.tmax << "\n"; 
    return 0;
}

NOIP Practical Pitfall Guide

1. Integer Overflow Caused by Bitwise Operator Precedence

To pursue constant optimization, contestants often abbreviate mid = (l + r) / 2 as mid = l + r >> 1.

Since the addition operator + has higher precedence than the shift operator >>, this expression is internally interpreted as mid = (l + r) >> 1.

When $l$ and $r$ are large (e.g., close to $2 \times 10^9$), the l + r executed first will directly cause integer overflow (becoming negative), making the calculated midpoint completely wrong.

Correct Standard: Outer parentheses must be forced to isolate the bitwise operation, written as mid = l + ((r - l) >> 1).

2. Infinite Loop Vulnerability from Non-Shrinking Binary Search Boundaries

When writing the maximum-value binary search for "maximizing the minimum," if the pointer update logic is l = mid, be wary of boundary infinite loops.

If mid uses the default floor rounding mid = l + ((r - l) >> 1), when the interval shrinks to the critical state l = r - 1, the computed mid will always equal l.

If the check succeeds and l = mid is executed, the search interval will no longer shrink, causing the program to fall into an infinite loop within a bounded space (TLE).

Correct Standard: For branches with l = mid, mid must be forced to use ceiling rounding, written as mid = l + ((r - l + 1) >> 1) to push the pointer right.


Classic NOIP/Luogu Problems

Luogu P2678 [NOIP2015 Advanced Group] Jumping Stones

bool chk(int dist) {
    int cnt = 0, last = 0; 
    for (int i = 1; i <= N; ++i) {
        if (stones[i] - last < dist) cnt++; // Distance insufficient, must remove current stone
        else last = stones[i]; // Distance sufficient, retain current stone and update sampling point
    }
    if (L - last < dist) cnt++; // Endpoint settlement: if endpoint distance is insufficient, must forcibly remove the last stone
    return cnt <= M;
}

// Binary search main body
int l = 1, r = L, ans = 0;
while (l <= r) {
    int mid = l + ((r - l) >> 1);
    if (chk(mid)) {
        ans = mid;   // Record feasible solution
        l = mid + 1; // Seeking maximum: answer interval shifts right
    } else {
        r = mid - 1; // Infeasible: answer interval shifts left
    }
}

Luogu P4145 God's Creation of the Seven Minutes 2 / Huashen's Travels Through Various Countries

Problem Description: Given a sequence, support two operations: take the square root of each element in a range (floor), and query the range sum. Element values $\le 10^{12}$.

Problem Essence and Core Solution: An application of segment trees maintaining interval maximums. The key is to exploit the data characteristics: a number $\le 10^{12}$ will inevitably degenerate to $1$ or $0$ after at most 6 square root operations. Based on this, a segment tree is used to manage intervals, with each node maintaining the interval maximum. When performing a range square root operation, if the interval maximum of the current node is $\le 1$, it indicates that all elements in this subtree have already been reduced to $1$ or $0$, and the square root operation will no longer change their values, allowing direct pruning and backtracking. This utilizes the divide-and-conquer tree structure to prune ineffective operations, fundamentally employing amortized potential analysis: although a single modification may recurse deeply, the number of times each element's value is effectively modified (reduced) has a strict upper bound. Therefore, the total number of modifications is linear. Combined with the segment tree structure, the overall time complexity is $O((N+M) \log N)$, where $M$ is the total number of operations.

struct Node {
    long long sum, max_val;
} tree[MAXN << 2];

void pushup(int u) {
    tree[u].sum = tree[u << 1].sum + tree[u << 1 | 1].sum;
    tree[u].max_val = max(tree[u << 1].max_val, tree[u << 1 | 1].max_val);
}

// Range square root modification: divide-and-conquer downward propagation with potential pruning
void update(int u, int l, int r, int L, int R) {
    if (tree[u].max_val <= 1) return; // Core pruning: when max value <= 1, square root does not change the value, directly block recursion

    if (l == r) {
        tree[u].sum = sqrt(tree[u].sum);
        tree[u].max_val = tree[u].sum;
        return;
    }
    int mid = l + ((r - l) >> 1);
    if (L <= mid) update(u << 1, l, mid, L, R);
    if (R > mid) update(u << 1 | 1, mid + 1, r, L, R);
    pushup(u); // Re-merge subtree features
}

[h] Back to Home