CF1375H Set Merging

CF1375H Set Merging

题目大意

题目链接

原题题面写的很好,建议自己去看。

本题题解

朴素的做法是,把区间 \([l_i, r_i]\) 里的数值从小到大排序,依次合并。共需要 \(\mathcal{O}(qn)\) 次合并,太多了。

优化上述做法,考虑对值域分块。设块的大小为 \(B\),分出了 \(\lceil\frac{n}{B}\rceil\) 个块。我们在每一块内,把元素按照原序列里的出现位置排序。也就是说,每个块都是原序列的一个子序列

考虑查询 \([l_i, r_i]\) 时,我们从小到大遍历所有块,把每一块里位置在 \([l_i, r_i]\) 内的元素提取出来,显然每一块里提取出的一定是一段连续的元素。假设它们的集合是已知的,那么只需要把所有块对应的集合依次合并即可。单次询问需要 \(\mathcal{O}(\frac{n}{B})\) 次合并操作。在每个块里提取区间时,可能需要二分,所以总时间复杂度是 \(\mathcal{O}(q\frac{n}{B}\log B)\)


那么问题转化为,预处理出每个块内所有区间对应的集合。这样的区间共有 \(\mathcal{O}(\frac{n}{B}\cdot B^2) = \mathcal{O}(nB)\) 个。

每个块单独预处理。在值域上分治。具体来说,我们会发现,原序列的一段区间,在某一段值域里的数,可以表示为该区间的一个子序列。在分治的第一层,这个子序列就是我们的当前块,设它为 \(p_1, p_2, \dots ,p_{|p|}\)。下面把值域一分为二:\([l, m], [m + 1, r]\)。那么子序列 \(p\),又会对应地划分为两个子序列:\(u_1, u_2, \dots ,u_{|u|}\)\(v_1, v_2, \dots, v_{|v|}\)。其中 \(l\leq a_{u_i}\leq m\)\(m < a_{v_i}\leq r\),并且 \(|u| = m - l + 1\)\(|v| = r - m\)\(u \cup v = p\)

我们有 \(\mathcal{O}(B^2)\) 个区间需要求答案(这个“答案”就是指构造出的一个集合)。递归下去。对每个区间,求出它在 \(u\) 序列上的答案,和在 \(v\) 序列上的答案,然后用一次操作合并起来,就能得到它在 \(p\) 序列上的答案。

朴素实现所需的操作次数是 \(\mathcal{O}(B\log B\cdot B^2)\),因为总共递归地调用了 \(\mathcal{O}(B\log B)\) 次函数,每次对 \(\mathcal{O}(B^2)\) 个集合更新答案。这样太多了。考虑某个需要求答案的区间 \([i, j]\)(注意这里 \(i, j\) 指的是位置,即原序列里的下标,而不是数值。\(l, r, m\) 这些都是数值,我们在值域上做的分治),它在 \(p\) 序列上的答案,等同于区间 \([p_{i'}, p_{j'}]\) 的答案。其中 \(p_{i'}\)\(p\) 序列里第一个 \(\geq i\) 的元素,\(p_{j'}\)\(p\) 序列里最后一个 \(\leq j\) 的元素。因此我们只需要对 \(\mathcal{O}(|p|^2)\) 个区间求答案(而不是原来的 \(\mathcal{O}(B^2)\) 个)。分析现在的操作次数,设某次调用分治函数时,\(r - l + 1 = L\),则操作次数为 \(T(L) = 2T(\frac{L}{2}) + \mathcal{O}(L^2)\),解得 \(T(L) = \mathcal{O}(L^2)\)。所以现在操作次数被优化到 \(\mathcal{O}(B^2)\),可以接受。

还有一个小问题就是找 \(i', j'\)。可以用主席树查询,但没必要。我们从前往后、从后往前扫描两遍 \(p\) 序列,就能对所有 \(i,j \in p\),推出它们对应的 \(i', j'\)。所以整个分治的时间复杂度也是 \(\mathcal{O}(B^2)\)

对每个块都预处理一次,总共所需操作次数总时间复杂度都是:\(\mathcal{O}(\frac{n}{B}\cdot B^2) = \mathcal{O}(nB)\)


综上,分析一下所需的操作次数,是 \(\mathcal{O}(nB + q\frac{n}{B})\)。显然取 \(B = \sqrt{q} = 2^{8}\) 时是最优的。操作次数是 \(\mathcal{O}(n\sqrt{q})\)

时间复杂度 \(\mathcal{O}(nB + q\frac{n}{B}\log B) = \mathcal{O}(n\sqrt{q}\log(\sqrt{q}))\)

总结

上述做法的本质是:将一个序列按值域范围划分成若干子序列之后,原序列的每个连续区间都可以表示成划分成的子序列中的连续区间

分块分治都是对这样本质的具体实现(分出的块是这样的子序列,分治时处理的 \(p\) 数组也是这样的子序列)。将它们结合起来,就能得到最优的做法。

参考代码

实际提交时建议使用读入、输出优化,详见本博客公告。

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

#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;

template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }

const int MAXN = 1 << 12;
const int MAXQ = 1 << 16;
const int BLOCK_SIZE = 1 << 8, BLOCK_NUM = MAXN / BLOCK_SIZE;
const int MAXOP = 2200000;

int n, q, a[MAXN + 5];
pii qs[MAXQ + 5];
int ans[MAXQ + 5];

int cnt_block, bel[MAXN + 5];
int cnt_set;

pii op[MAXOP + 5];
int merge(int s1, int s2) {
	op[++cnt_set] = mk(s1, s2);
	return cnt_set;
}

int rev[MAXN + 5];
int nxtL[MAXN + 5], nxtR[MAXN + 5], preL[MAXN + 5], preR[MAXN + 5];
int tmp_res[BLOCK_SIZE + 5][BLOCK_SIZE + 5];
struct Block {
	int L, R; // 值域上的
	int pos[BLOCK_SIZE + 5], cnt_pos;
	
	int res[BLOCK_SIZE + 5][BLOCK_SIZE + 5]; // 位置区间 [i,j] 的集合编号
	
	void solve(int l, int r, const vector<int>& p) { // 值域区间 [l, r]
		assert(SZ(p) == r - l + 1);
		if (l == r) {
			res[p[0]][p[0]] = pos[p[0]]; // 这是初始时就有的 n 个大小为 1 的集合之一
			return;
		}
		
		int mid = (l + r) >> 1;
		
		vector<int> pl, pr;
		for (int _i = 0; _i < SZ(p); ++_i) {
			int i = p[_i];
			assert(a[pos[i]] >= l + L - 1 && a[pos[i]] <= r + L - 1);
			if (a[pos[i]] <= mid + L - 1) {
				pl.push_back(i);
			} else {
				pr.push_back(i);
			}
		}
		solve(l, mid, pl);
		solve(mid + 1, r, pr);
		
		for (int _i = 0; _i < SZ(p); ++_i) {
			int i = p[_i];
			if (a[pos[i]] <= mid + L - 1) {
				preL[_i] = _i;
				preR[_i] = (_i == 0 ? -1 : preR[_i - 1]);
			} else {
				preR[_i] = _i;
				preL[_i] = (_i == 0 ? -1 : preL[_i - 1]);
			}
		}
		for (int _i = SZ(p) - 1; _i >= 0; --_i) {
			int i = p[_i];
			if (a[pos[i]] <= mid + L - 1) {
				nxtL[_i] = _i;
				nxtR[_i] = (_i == SZ(p) - 1 ? SZ(p) : nxtR[_i + 1]);
			} else {
				nxtR[_i] = _i;
				nxtL[_i] = (_i == SZ(p) - 1 ? SZ(p) : nxtL[_i + 1]);
			}
		}
		
		for (int _i = 0; _i < SZ(p); ++_i) {
			int i = p[_i];
			for (int _j = _i; _j < SZ(p); ++_j) {
				int j = p[_j];
				tmp_res[i][j] = res[i][j];
			}
		}
		for (int _i = 0; _i < SZ(p); ++_i) {
			int i = p[_i];
			for (int _j = _i; _j < SZ(p); ++_j) {
				int j = p[_j];
#define x p[_x]
#define y p[_y]
				if (a[pos[i]] <= mid + L - 1 && a[pos[j]] <= mid + L - 1) {
					int _x = nxtR[_i];
					int _y = preR[_j];
					
					if (_x <= _y) {
						res[i][j] = merge(tmp_res[i][j], tmp_res[x][y]);
					}
				} else if (a[pos[i]] > mid + L - 1 && a[pos[j]] > mid + L - 1) {
					int _x = nxtL[_i];
					int _y = preL[_j];
					
					if (_x <= _y) {
						res[i][j] = merge(tmp_res[x][y], tmp_res[i][j]);
					}
				} else if (a[pos[i]] <= mid + L - 1 && a[pos[j]] > mid + L - 1) {
					int _x = nxtR[_i];
					assert(_x <= _j);
					int _y = preL[_j];
					assert(_y >= _i);
					
					res[i][j] = merge(tmp_res[i][y], tmp_res[x][j]);
					
				} else if (a[pos[i]] > mid + L - 1 && a[pos[j]] <= mid + L - 1) {
					int _x = nxtL[_i];
					assert(_x <= _j);
					int _y = preR[_j];
					assert(_y >= _i);
					
					res[i][j] = merge(tmp_res[x][j], tmp_res[i][y]);
				}
#undef x
#undef y
			}
		}
	}
	void build(int _L, int _R) {
		L = _L;
		R = _R;
		
		cnt_pos = 0;
		for (int i = 1; i <= n; ++i) {
			if (a[i] >= L && a[i] <= R) {
				pos[++cnt_pos] = i;
				rev[a[i]] = cnt_pos;
			}
		}
		assert(cnt_pos == R - L + 1);
		
		vector<int> p;
		for (int i = 1; i <= cnt_pos; ++i) {
			p.push_back(i);
		}
		solve(1, cnt_pos, p);
	}
	
	int query(int l, int r) {
		int x = lower_bound(pos + 1, pos + cnt_pos + 1, l) - pos;
		int y = upper_bound(pos + 1, pos + cnt_pos + 1, r) - pos - 1;
		if (x > y) return 0;
		
		return res[x][y];
	}
	
	Block() {}
} blo[BLOCK_NUM + 5];


int main() {
	cin >> n >> q;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	
	cnt_set = n; // 初始时有 n 个集合
	for (int i = 1; i <= n; i += BLOCK_SIZE) {
		int j = min(i + BLOCK_SIZE - 1, n);
		++cnt_block;
		for (int k = i; k <= j; ++k) {
			bel[k] = cnt_block;
		}
		blo[cnt_block].build(i, j);
	}
	
	for (int i = 1; i <= q; ++i) {
		cin >> qs[i].fi >> qs[i].se;
		int l = qs[i].fi, r = qs[i].se;
		
		ans[i] = 0;
		for (int b = 1; b <= cnt_block; ++b) {
			int s = blo[b].query(l, r);
			if (!s)
				continue;
			
			if (!ans[i]) {
				ans[i] = s;
			} else {
				ans[i] = merge(ans[i], s);
			}
		}
	}
	
	cout << cnt_set << endl;
	for (int i = n + 1; i <= cnt_set; ++i) {
		cout << op[i].fi << " " << op[i].se << endl;
	}
	for (int i = 1; i <= q; ++i) {
		cout << ans[i] << " \n"[i == q];
	}
	return 0;
}
posted @ 2021-02-03 01:06  duyiblue  阅读(198)  评论(0编辑  收藏  举报