1. 程式人生 > 實用技巧 >NOI 2020 D1T3 本人題解

NOI 2020 D1T3 本人題解

我看了出題人本題的做法,感覺很難寫,就自己胡了一個\(O((n + m) \sqrt n)\)的做法。

第一步我的想法與出題人一樣,都是考慮容斥降維。對第\(i\)組詢問,我們列舉兩個事件中較大的一個點\((a, b)\),它對答案的貢獻為:所有滿足\(r_{i, 1} \leq x \leq a, c_{i, 1} \leq y \leq b\)\((x, y) \neq (a, b)\)的點數。把這個貢獻按照常見的二維字首和的方式拆成四種貢獻(有些貢獻要乘以\(-1\)的係數):

貢獻一:所有滿足\(x \leq a, y \leq b, (x, y) \neq (a, b)\)的點數。

貢獻二:所有滿足\(x < r_{i, 1}, y < c_{i, 1}\)的點數。

貢獻三:所有滿足\(x < r_{i, 1}, y \leq b\)的點數。

貢獻四:所有滿足\(x \leq a, y < c_{i, 1}\)的點數。

考慮貢獻一只和矩形內的每個點有關,可以用樹狀陣列預處理比每個點“小”的點數再做一次靜態二維數點。貢獻二**只和\(r_{i, 1}, c_{i, 1}\)有關,可以用兩次二維數點解決。由對稱性,我們只需要考慮貢獻三如何計算(貢獻四隻需要把\(x, y\)反過來就變成貢獻三了)。

貢獻三裡面對\((a, b)\)\(r_{i, 1} \leq a \leq r_{i, 2}, c_{i, 1} \leq b \leq c_{i, 2}\)

的限制,我們再把\(c_{i, 1} \leq b \leq c_{i, 2}\)這個條件拆成字首和,於是問題就變成了:

每次詢問是一個三元組\((l, r, w)\),你需要回答滿足\((a, b) \leq (c, d)\)\(a < l \leq c \leq r, b < d \leq w\)的點對數量。

接下來考慮這個三維問題怎麼做。我們把\(0, 1, \cdots, n\)中所有\(\lfloor \sqrt{n} \rfloor\)的倍數稱為關鍵點,建立\(O(\sqrt n)\)個關鍵點。對每個詢問\((l, r, w)\),我們設\(\leq l\)且離\(l\)

最近的關鍵點為\(l'\),那麼考慮\((l, r, w)\)\(l\)每次減1移動到\(l'\)的時候答案的增量都可以用二維數點來計算

這樣,問題變成了\(O(m)\)\(l\)是關鍵點的詢問和\(O(m \sqrt m)\)組二維數點的詢問。後者可以用掃描線 + \(O(\sqrt n)\)修改,\(O(1)\)詢問的分塊資料結構來解決。而前者可以列舉\(l\),算出\(x \ge l\)的每個點\((x, y)\)對答案的貢獻(這隻需要一次字首和),然後對\(r\)做掃描線,問題又變成了:單點加一個數,詢問字首和。這一問題可以用\(O(1)\)修改,\(O(\sqrt n)\)詢問的分塊結構來解決。

總結:

  1. “容斥降維”在某些問題上比較常用,比如靜態詢問座標上一個直角邊平行於座標軸的點數。
  2. 本文後半部分的操作實際上是回滾莫隊的思想,再結合了lxl提出的莫隊二次離線的思想。

程式碼如下(不要在意我的辣雞英文水平了):


#include <bits/stdc++.h>
using namespace std;

const int N = 100600, M = 200005, LOG = 25, LIM = 400;

template <class T>
void read(T &x) {
	int sgn = 1;
	char ch;
	x = 0;
	for (ch = getchar(); (ch < '0' || ch > '9') && ch != '-'; ch = getchar()) ;
	if (ch == '-') ch = getchar(), sgn = -1;
	for (; '0' <= ch && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
	x *= sgn;
}
template <class T>
void write(T x) {
	if (x < 0) putchar('-'), write(-x);
	else if (x < 10) putchar(x + '0');
	else write(x / 10), putchar(x % 10 + '0');
}

//Fenwick tree and persistent segment tree
//num_i denotes the number of integer j such that 1 <= j < i and p_j < p_i

int n, m, lim, p[N], num[N], fenwick[N];
int rt[N], sgt1[N * LOG], lch[N * LOG], rch[N * LOG], cnt = 0;
long long sgt2[N * LOG], ans[M];

//Fenwick tree
int lowbit(int x) {
	return x & -x;
}
void add(int x, int val) {
	for (; x <= n; x += lowbit(x)) fenwick[x] += val;
}
int query(int pos) {
	int res = 0;
	for (; pos; pos ^= lowbit(pos)) res += fenwick[pos];
	return res;
}
void get_num() {
	for (int i = 0; i <= n; i++) fenwick[i] = 0;
	for (int i = 1; i <= n; i++) {
		num[i] = query(p[i]);
		add(p[i], 1);
	}
}
//Persistent segment tree
int newnode() {
	int x = ++cnt;
	sgt1[x] = 0, sgt2[x] = 0ll;
	lch[x] = rch[x] = 0;
	return x;
}
int build(int l, int r) {
	int x = newnode(), mid = l + r >> 1;
	if (l < r) lch[x] = build(l, mid), rch[x] = build(mid + 1, r);
	return x;
}
int insert(int pos, int l, int r, int now, int val1, int val2) {
	int mid = l + r >> 1, x = newnode();
	sgt1[x] = sgt1[now] + val1;
	sgt2[x] = sgt2[now] + val2;
	lch[x] = lch[now], rch[x] = rch[now];
	if (l < r) {
		if (pos <= mid) lch[x] = insert(pos, l, mid, lch[now], val1, val2);
		else rch[x] = insert(pos, mid + 1, r, rch[now], val1, val2);
	}
	return x;
}
int query1(int left, int right, int l, int r, int now) {
	int mid = l + r >> 1;
	if (left > right) return 0;
	if (l == left && r == right) return sgt1[now];
	else if (right <= mid) return query1(left, right, l, mid, lch[now]);
	else if (left > mid) return query1(left, right, mid + 1, r, rch[now]);
	else return query1(left, mid, l, mid, lch[now]) + query1(mid + 1, right, mid + 1, r, rch[now]);
}
long long query2(int left, int right, int l, int r, int now) {
	int mid = l + r >> 1;
	if (left > right) return 0ll;
	if (l == left && r == right) return sgt2[now];
	else if (right <= mid) return query2(left, right, l, mid, lch[now]);
	else if (left > mid) return query2(left, right, mid + 1, r, rch[now]);
	else return query2(left, mid, l, mid, lch[now]) + query2(mid + 1, right, mid + 1, r, rch[now]);
}
void build_tree() {
	rt[0] = build(1, n);
	for (int i = 1; i <= n; i++) {
		rt[i] = insert(p[i], 1, n, rt[i - 1], 1, num[i]);
	}
}

struct qry {
	int id, coef;
	int l, r, w;
} ;
// A data structure called D1
// O(sqrt(n)) modify
// O(1) query
struct D1 {
	int pre1[LIM], pre2[N];
	void init() {
		for (int i = 0; i <= lim; i++) pre1[i] = 0;
		for (int i = 0; i <= n; i++) pre2[i] = 0;
	}
	void add(int pos, int val) {
		for (int i = pos / lim; i <= lim; i++) pre1[i] += val;
		for (int i = pos; i < pos / lim * lim + lim; i++) pre2[i] += val;
	}
	int query(int pos) {
		if (pos / lim) return pre1[pos / lim - 1] + pre2[pos];
		else return pre2[pos];
	}
} DS1;
// A data structure called D2
// O(1) modify
// O(sqrt(n)) query
struct D2 {
	long long vec1[LIM], vec2[N];
	void init() {
		for (int i = 0; i <= lim; i++) vec1[i] = 0ll;
		for (int i = 0; i <= n; i++) vec2[i] = 0ll;
	}
	void add(int pos, int val) {
		vec1[pos / lim] += val;
		vec2[pos] += val;
	}
	long long query(int pos) {
		long long res = 0;
		for (int i = 0; i < pos / lim; i++) res += vec1[i];
		for (int i = pos / lim * lim; i <= pos; i++) res += vec2[i];
		return res;
	}
} DS2;
struct D3 {
	int perm[N], num[N];
	vector<qry> qry1[N], qry2[N];
	void add_qry(qry q) {
		qry1[q.l].push_back(q), qry2[q.r].push_back(q);
	}
	// We proceed the impact of O(sqrt(n)) values in the part.
	// Then we can assume that lim | l or l = n.
	void proceed_small() {
		DS1.init();
		for (int i = 1; i <= n; i++) {
			DS1.add(perm[i], 1);
			for (int j = 0; j < qry2[i].size(); j++) {
				int id = qry2[i][j].id, coef = qry2[i][j].coef;
				int l = qry2[i][j].l, w = qry2[i][j].w;
				for (int i = l / lim * lim; i < l; i++) {
					if (i && perm[i] <= w) {
						ans[id] += coef * (DS1.query(w) - DS1.query(perm[i]));
						ans[id] -= coef * num[i];
					}
				}
			}
			int ri = min(n, (i / lim + 1) * lim - 1);
			for (int j = i + 1; j <= ri; j++) {
				for (int k = 0; k < qry1[j].size(); k++) {
					int id = qry1[j][k].id, coef = qry1[j][k].coef;
					int w = qry1[j][k].w;
					if (perm[i] <= w) ans[id] -= coef * (DS1.query(w) - DS1.query(perm[i]));
				}
			} 
		}
	}
	
	int tmp[N];
	void proceed_big() {
		for (int i = 0; i < n; i += lim) {
			for (int j = 0; j <= n; j++) tmp[j] = 0;
			for (int j = 1; j < i; j++) tmp[perm[j]]++;
			for (int j = 1; j <= n; j++) tmp[j] += tmp[j - 1];
			DS2.init();
			for (int j = i; j <= n; j++) {
				DS2.add(perm[j], tmp[perm[j]]);
				for (int k = 0; k < qry2[j].size(); k++) {
					int id = qry2[j][k].id, coef = qry2[j][k].coef;
					int l = qry2[j][k].l, w = qry2[j][k].w;
					if (i <= l && l < i + lim) ans[id] += 1ll * coef * DS2.query(w);
				}
			}
		}
	}
} P1, P2;
//P1 and P2 denotes the two different but similar parts of the algorithm.

int main() {
	read(n), read(m);
	for (int i = 1; i <= n; i++) read(p[i]);
	get_num();
	build_tree();
	//Initialize P1 and P2.
	while (lim * lim <= n) lim++;
	for (int i = 1; i <= n; i++) {
		P1.perm[i] = p[i], P2.perm[p[i]] = i;
		P1.num[i] = num[i], P2.num[p[i]] = num[i];
	}
	for (int i = 0; i <= n; i++) {
		P1.qry1[i].clear(), P1.qry2[i].clear();
		P2.qry1[i].clear(), P2.qry2[i].clear(); 
	}
	for (int i = 1; i <= m; i++) {
		int lx, rx, ly, ry;
		read(lx), read(rx), read(ly), read(ry);
		// The first part
		// You can avoid persistent segment tree since offline queries are allowed.
		// But as a matter of convenient, I use persistent segment tree.
		ans[i] = query2(ly, ry, 1, n, rt[rx]) - query2(ly, ry, 1, n, rt[lx - 1]);
		int cnt1 = query1(1, ly - 1, 1, n, rt[lx - 1]), cnt2 = query1(ly, ry, 1, n, rt[rx]) - query1(ly, ry, 1, n, rt[lx - 1]);
		ans[i] += 1ll * cnt1 * cnt2;
		// The second part
		// We divide the whole query into four parts.
		// Then we should proceed these 4m queries offline, applying block algorithm.
		qry qry1 = {i, -1, lx, rx, ry}, qry2 = {i, 1, lx, rx, ly - 1};
		qry qry3 = {i, -1, ly, ry, rx}, qry4 = {i, 1, ly, ry, lx - 1};
		P1.add_qry(qry1), P1.add_qry(qry2);
		P2.add_qry(qry3), P2.add_qry(qry4);
	}
	P1.proceed_small(), P1.proceed_big();
	P2.proceed_small(), P2.proceed_big();
	for (int i = 1; i <= m; i++) write(ans[i]), putchar('\n');
	return 0;
}