Overall dichotomy learning notes

Overall dichotomy, a non data structure solution to data structure problems.

Look at the question

A very classic problem - the interval static number \ (k \) is small, and its solutions are also diverse.

For each query \ ((l,r,k) \), consider a dichotomous answer. Let the value of the current dichotomy be \ (mid \), and we find the number \ (x \) of the number less than or equal to \ (mid \) in the interval \ ([l,r] \). If \ (x\ge k \), it proves that the answer is less than or equal to \ (mid \); Otherwise, the answer is greater than \ (mid \). However, the total time complexity is \ (O(nm\log SIZE) \), which is unbearable. (\ (SIZE \) indicates value range)

Note that for each query, our binary \ (mid \) is the same, so can we put all queries together?

Specifically, we perform the following processes:

  1. Take all inquiries offline;
  2. For the binary answer \ (mid \), set all numbers less than or equal to \ (mid \) in the sequence \ (a \) to \ (1 \);
  3. Use the tree array to count the number less than or equal to \ (mid \) in the \ (i \) query interval, so as to determine the relationship between the answer of this query and the size of \ (mid \);
  4. Recursively solve all queries with answers less than or equal to \ (mid \);
  5. Recursively solve all queries with answers greater than \ (mid \). (in this way, the original problem is divided into two smaller subproblems, which can continue recursion)

For convenience, we can regard the number of \ (I \) in the sequence as a modification operation. If the modified number, i.e. \ (a_i\le mid \), this operation is divided into the set of queries with answers less than or equal to \ (mid \); Otherwise, row into the set of queries with answers greater than \ (mid \).

It should be noted that memset cannot be used to reset the tree array. All modification operations should be cancelled one by one, otherwise the complexity will explode.

#include<bits/stdc++.h>
using namespace std;
int n, m;
int a[200005];
struct node {int p, x, y, k, id;} q[400005], lq[400005], rq[400005];
int ans[200005];
int sum[200005];
inline int read() {
	int x = 0, f = 1; char c = getchar();
	while (!isdigit(c)) {if (c == '-') f = -1; c = getchar();}
	while (isdigit(c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar();
	return f == 1 ? x : -x;
}
inline void update(int x, int val) {
	for (; x <= n; x += x & -x)
	    sum[x] += val;
}
inline int query(int x) {
	int res = 0;
	for (; x; x -= x & -x)
	    res += sum[x];
	return res;
}
inline void solve(int l, int r, int st, int ed) {//l. R represents the range of the answer, st and ED represent the range of the query
	if (st > ed) return;
	if (l == r) {
		for (int i = st; i <= ed; ++i)
		    if (q[i].p == 2) ans[q[i].id] = l;
		return;
	} 
	int mid = l + r >> 1, cnt1 = 0, cnt2 = 0;
    //lq stores all queries with answers < = mid, and rq stores queries with answers > mid
	for (int i = st; i <= ed; ++i) {
		if (q[i].p == 1) {//Modify operation
			if (q[i].y <= mid) update(q[i].x, 1), lq[++cnt1] = q[i];
			else rq[++cnt2] = q[i];
		} else {//Query operation
			int x = query(q[i].y) - query(q[i].x - 1);
			if (x >= q[i].k) lq[++cnt1] = q[i];
			else q[i].k -= x, rq[++cnt2] = q[i];
            //If the answer to a query is greater than mid, you are actually looking for the k-x decimal after division
		}
	}
	for (int i = st; i <= ed; ++i)
	    if (q[i].p == 1 && q[i].y <= mid) update(q[i].x, -1);//Undo all modifications
	for (int i = 1; i <= cnt1; ++i) q[i + st - 1] = lq[i];
	for (int i = 1; i <= cnt2; ++i) q[i + st + cnt1 - 1] = rq[i];//copy the two groups of queries back to the original array to prevent space explosion
	solve(l, mid, st, st + cnt1 - 1); solve(mid + 1, r, st + cnt1, ed);//Recursive solution
}
int main() {
	n = read(); m = read();
	for (int i = 1; i <= n; i++)
	    q[i] = (node){1, i, read(), 0, 0};
	for (int i = 1; i <= m; i++) 
		q[i + n] = (node){2, read(), read(), read(), i};//Take the inquiry offline
	solve(-1e9, 1e9, 1, n + m);
	for (int i = 1; i <= m; i++)
	    printf("%d\n", ans[i]);
	return 0;
}

To analyze the time complexity of a wave. Each query will be recursively \ (\ log SIZE \) layer, and each recursion of an operation will be updated \ (\ log n \) in the tree array. After modification, we have a total of \ (n+m \) operations, so the total time complexity is \ (O((n+m)\log n\log SIZE) \).

Incidentally, the immortal practice of \ (O(n\log n) \): arrange all modification operations in order according to \ (a_i \); Divide all queries \ ([l,r] \) into \ ([1,l-1] \) and \ ([1,r] \), and then arrange the disassembled intervals in order according to the right endpoint. Therefore, when we find out how many numbers in \ ([l,r] \) are less than or equal to \ (mid \), we only need to sweep a pointer. This completes a single \ (\ log \). I won't give you the code. You can see it in more detail This blog.

Next is this

Compared with the previous question, it just changes from static to dynamic. However, if we modify it directly according to the meaning of the question, we will need to discuss whether the number is greater than \ (mid \) in the original and greater than \ (mid \) after modification, which increases the code length and is easy to write and hang. Therefore, we need to treat one modification as two operations, that is, we remove a number with a value of \ (a_x \) from the position \ (x \) and increase a number with a value of \ (y \). So we don't need to discuss it by situation.

P.S.: when reading the modification operation, you must assign \ (a_x \) to \ (y \) and correctly maintain the \ (a \) sequence, otherwise only \ (5 \) points - this is the lesson of blood!

#include<bits/stdc++.h>
using namespace std;
int n, m;
int t, cnt;
int a[200005];
struct node {int p, x, y, k, id;} q[400005], lq[400005], rq[400005];
int ans[200005];
int sum[200005];
inline int read() {
	int x = 0, f = 1; char c = getchar();
	while (!isdigit(c)) {if (c == '-') f = -1; c = getchar();}
	while (isdigit(c)) {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
	return f == 1 ? x : -x;
}
inline void update(int x, int val) {
	for (; x <= n; x += x & -x)
	    sum[x] += val;
}
inline int query(int x) {
	int res = 0;
	for (; x; x -= x & -x)
	    res += sum[x];
	return res;
}
inline void solve(int l, int r, int st, int ed) {
	if (st > ed) return;
	if (l == r) {
		for (int i = st; i <= ed; ++i)
		    if (q[i].p == 1) ans[q[i].id] = l;
		return;
	} 
	int mid = l + r >> 1, cnt1 = 0, cnt2 = 0;
	for (int i = st; i <= ed; ++i) {
		if (q[i].p == 0) {
			if (q[i].y <= mid) update(q[i].x, q[i].k), lq[++cnt1] = q[i];
			else rq[++cnt2] = q[i];
		} else {
			int x = query(q[i].y) - query(q[i].x - 1);
			if (x >= q[i].k) lq[++cnt1] = q[i];
			else q[i].k -= x, rq[++cnt2] = q[i];
		}
	}
	for (int i = st; i <= ed; ++i)
	    if (q[i].p == 0 && q[i].y <= mid) update(q[i].x, -q[i].k);
	for (int i = 1; i <= cnt1; ++i) q[i + st - 1] = lq[i];
	for (int i = 1; i <= cnt2; ++i) q[i + st + cnt1 - 1] = rq[i];
	solve(l, mid, st, st + cnt1 - 1); solve(mid + 1, r, st + cnt1, ed);
}//The same method
int main() {
	n = read(); m = read();
	for (int i = 1; i <= n; ++i) {
		a[i] = read();
	    q[++t] = (node){0, i, a[i], 1, 0};
	}
	for (int i = 1; i <= m; ++i) {
		char c; int x, y, k; cin >> c;
		if (c == 'Q') {
			x = read(); y = read(); k = read();
			q[++t] = (node){1, x, y, k, ++cnt};
		} else {
			x = read(); y = read();
			q[++t] = (node){0, x, a[x], -1, 0};
			q[++t] = (node){0, x, y, 1, 0};//Divide the modification into two operations
            //The k in the structure indicates whether this is deleted or added
			a[x] = y;//A very important line, I always forget
		}
	}//All operations must be stored in the array in chronological order
	solve(-1e9, 1e9, 1, t);
	for (int i = 1; i <= cnt; ++i)
	    printf("%d\n", ans[i]);
	return 0;
}

Compared with nested trees, the theoretical complexity of overall dichotomy has changed from \ (O((n+m)\log^2n) \) to \ (O((n+m)\log n\log SIZE) \), but look at the actual effect:

algorithm Segment tree set balance tree Overall dichotomy
time \(23.23s\) \(2.06s\)
space \(88.13MB\) \(21.09MB\)
Code length \(2.77KB\) \(1.90KB\)

It can be found that the overall dichotomy has hanged the tree from any aspect. I haven't written the chairman tree of the tree array, but judging from the submission records of others, it should not be able to run the whole two points.

Several exercises:

Next is the comprehensive application of overall dichotomy, which can not only do interval Kth problems.

This problem It's beginning to deform

We can still make an overall dichotomy to see whether enough meteorites have been collected in the (i \) country after the \ ([l,mid] \) meteorite rain, so as to determine the relationship between the answer and the size of \ (mid \).

This problem also needs to be judged to have no solution. We don't need to spend more energy on special judgment. We can artificially add the \ (k+1 \) meteorite rain. In this way, the unsolved query will recurs to the right in two minutes, and the answer will become \ (k+1 \), which is easy to judge.

#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
int n, m, k;
vector<int> o[300005];
struct option {int l, r, k;} a[300005];
struct node {int p, k;} q[300005], lq[300005], rq[300005];
int sum[300005], ans[300005];
int lowbit(int x) {return x & -x;}
void update(int x, int val) {
	for (; x <= m; x += lowbit(x))
		sum[x] += val;
}
int query(int x) {
	int res = 0;
	for (; x; x -= lowbit(x)) res += sum[x];
	return res;
}
void solve(int l, int r, int st, int ed) {
	if (st > ed) return;
	if (l == r) {
		for (int i = st; i <= ed; i++)
			ans[q[i].p] = l;
		return;
	}
	int mid = l + r >> 1, cnt1 = 0, cnt2 = 0;
	for (int i = l; i <= mid; i++)
		if (a[i].l <= a[i].r) update(a[i].l, a[i].k), update(a[i].r + 1, -a[i].k);
		else update(a[i].l, a[i].k), update(1, a[i].k), update(a[i].r + 1, -a[i].k);
	for (int i = st; i <= ed; i++) {
		int s = 0;
		for (int j = 0; j < o[q[i].p].size(); j++)
			s += query(o[q[i].p][j]);
		if (q[i].k <= s) lq[++cnt1] = q[i];
		else q[i].k -= s, rq[++cnt2] = q[i];
	}
    for (int i = l; i <= mid; i++)
		if (a[i].l <= a[i].r) update(a[i].l, -a[i].k), update(a[i].r + 1, a[i].k);
		else update(a[i].l, -a[i].k), update(1, -a[i].k), update(a[i].r + 1, a[i].k);
	for (int i = 1; i <= cnt1; i++) q[st + i - 1] = lq[i];
	for (int i = 1; i <= cnt2; i++) q[st + cnt1 + i - 1] = rq[i];
	solve(l, mid, st, st + cnt1 - 1); solve(mid + 1, r, st + cnt1, ed);
}
signed main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int x; cin >> x;
		o[x].push_back(i);
	}
	for (int i = 1; i <= n; i++)
		cin >> q[i].k, q[i].p = i;
	cin >> k;
	for (int i = 1; i <= k; i++)
		cin >> a[i].l >> a[i].r >> a[i].k;
	a[k + 1] = (option){1, m, 0};//In order to judge that there is no solution, add the k+1 meteorite rain
	solve(1, k + 1, 1, n);
	for (int i = 1; i <= n; i++)
		if (ans[i] == k + 1) cout << "NIE" << endl;
		else cout << ans[i] << endl;
	return 0;
}

Several exercises:

Finally, apply Xu Haoran's paper.

Problems that can be solved by overall dichotomy need to meet the following properties:

  1. The answer to the question is dichotomous
  2. The contribution of modification to the judgment answer is independent of each other, and the modification does not affect the effect of each other
  3. If the modification contributes to the decision answer, the contribution is a determined value independent of the decision criteria
  4. Contribution satisfies commutative law, associative law, additive
  5. Offline algorithms are allowed

period

Added by pcoder on Sun, 02 Jan 2022 16:09:25 +0200