zoukankan      html  css  js  c++  java
  • CF1375H Set Merging

    CF1375H Set Merging

    题目大意

    题目链接

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

    本题题解

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

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

    考虑查询 ([l_i, r_i]) 时,我们从小到大遍历所有块,把每一块里位置在 ([l_i, r_i]) 内的元素提取出来,显然每一块里提取出的一定是一段连续的元素。假设它们的集合是已知的,那么只需要把所有块对应的集合依次合并即可。单次询问需要 (mathcal{O}(frac{n}{B})) 次合并操作。在每个块里提取区间时,可能需要二分,所以总时间复杂度是 (mathcal{O}(qfrac{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|})。其中 (lleq 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}(Blog Bcdot B^2)),因为总共递归地调用了 (mathcal{O}(Blog 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 + qfrac{n}{B}))。显然取 (B = sqrt{q} = 2^{8}) 时是最优的。操作次数是 (mathcal{O}(nsqrt{q}))

    时间复杂度 (mathcal{O}(nB + qfrac{n}{B}log B) = mathcal{O}(nsqrt{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] << " 
    "[i == q];
    	}
    	return 0;
    }
    
  • 相关阅读:
    完全背包详解
    0-1背包详解
    优先队列 + 模拟
    循环节 + 矩阵快速幂
    并查集 + 路径压缩(经典) UVALive 3027 Corporative Network
    dp
    动态规划分类(完整版)
    (二)Spring框架之JDBC的基本使用(p6spy插件的使用)
    (一)Spring框架基础
    (十六)客户端验证与struts2中的服务器端验证
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/14364992.html
Copyright © 2011-2022 走看看