区间划分。
本菜在这一题学到蛮多新东西&&新思想,下面详细记录一下思考过程。
题意: 给出一幅图,每个顶点有(1~n),有nq个询问,问区间l~r的顶点集,在把l~r之外的所有点和边删掉之后,有几个连通块。
解法: 分x个步骤慢慢来~
1.区间划分: 对于l~r的每一个区间询问,最暴力的方法是从l到r一个一个的计算过去,复杂度为O(q*n),对于询问数q=3w,区间长度n=3w的题,必须超时,那么考虑优化。
对每一个询问,我们考虑它们的lr区间的重叠部分(如果不重叠,那就是大水题O(n)秒过),重叠部分的时间是不是可以优化呢。
对于左端点l相同的区间,我们可以把它们按照右端点从小到大排序,那么对一个询问,他只比前一个询问多出了右端的一部分区间,那么就可以利用前一个询问的区间的结果来计算下一段区间的答案,这里就可以省掉区间重叠部分的重复计算。
但是我们知道题目肯定是不会老老实实的给你一段左端对齐的区间询问的,那么,我们就人为的把它们切整齐。如上图的分割线,右部分可以通过逐级计算来优化,左部分我们继续暴力求出来,然后合并一下左右区间,就可以了。
我们还知道一个事实,有些区间是不重叠的,那么他们的结果也就互不影响,那就只好把他们分开处理咯。我们将整个区间1~n划分成x个片段,对于左端点在同一个片段的线段放在一起处理,那么对于每一块片段,处理右区间需要O(n)的时间,处理左区间在最坏情况下需要块长度乘以块内询问数的时间,那么总的复杂度是O(x*n+q*n/x)。
那么,选择多大的x,能使这个多项式时间复杂度最小。在n与q同规模下,很显然是当x = sqrt(n)的时候,时间复杂度为O(n*sqrt(n)+q*sqrt(n))。(PS:对x=sqrt(n)有疑问的自己去列个方程求个导数)。
总结一下区间划分:首先是离线,将给定1~n区间划分成sqrt(n)段,将左端点在每一块内的询问按照右端点从小到大排序,然后对左部分暴力求出,对右部分利用前一个询问的结果求出,合并左右区间即为当前询问的答案。
2.连通块数量的维护:
对于统计连通块数量的问题,用并查集。对于这道题,我们选择用区间划分的方法,将一个个询问处理成了左右部分的形式,现在的问题是如何合并。首先,左右部分都是用并查集维护的,我们要保持右部分的并查集不受左部分的影响,以便于传递给下一个询问。于是,现在的问题是如何在不修改右部分并查集的情况下,获得合并两个并查集后的结果。
朴素的想法是将右部分并查集的fa数组复制一遍,然后合并求解。但是复制的代价是O(n*q),不可行。我们现在考虑一下左右部分合并需要多少次merge:左部分最多有sqrt(n)个点,总共有m条边,由于数据随机,按边是均摊的来计算,左部分的点共有m/sqrt(n)条边,即是说右部分最多只有m/sqrt(n)个点与左部分有边,那么考虑如果能只把这m/sqrt(n)个点复制出来,既能完成合并,时间复杂度也小,是不是很诱惑呢。
考虑设这样一个vis数组:在将左右部分合并时,对于一条边的两个端点,我们询问当前端点是否已经复制进了临时的并查集里,如果没有,就从原并查集复制它的信息。好了,现在到重点了:vis数组用来标记是否复制,那每一次询问完都要花O(n)的时间来重新赋值0?这显然是不必要的,因为每一个询问都有一个独特的id,用这个id来标记vis数组,新的询问的id必然不等于vis数组里的任何值,当然也就不用每次都清0了。
献上本人1500ms代码。。第一次写区间划分,写完一看过了样例,很开心的直接交了一发,结果邻接表开小RE了,ORZ。
原来离线处理区间询问的方法,除了线段树和树状数组,竟然还有一个这么神奇的东西,再次ORZ。
#include <stdio.h> #include <string.h> #include <algorithm> #include <math.h> using namespace std; #define REP(i,a,b) for(int i=(a); i<(b); i++) #define clr(a,b) memset(a,b,sizeof(a)) typedef long long lld; const int INF = ~0u>>1; const int MAXN = 30010; struct Edge { int v, next; }g[MAXN*10]; int head[MAXN], tot; int n,m,nq; struct Q { int l, r, id; int b; bool operator < (const Q &tt) const { if(b == tt.b) return r < tt.r; return b < tt.b; } }q[MAXN]; int ans[MAXN]; int fa[MAXN]; int L,R; int vis[MAXN]; int tfa[MAXN]; int block_size; int rcnt; int now; int Find(int x) { return fa[x] = (fa[x] == x) ? x : Find(fa[x]); } int merge(int a, int b) { a = Find(a); b = Find(b); if(a == b) return 0; fa[a] = b; return 1; } int Tfind(int x) { if(vis[x] != now) { tfa[x] = fa[x]; vis[x] = now; } return tfa[x] = (tfa[x] == x) ? x : Tfind(tfa[x]); } int Tmerge(int a, int b) { a = Tfind(a); b = Tfind(b); if(a == b) return 0; tfa[a] = b; return 1; } int work(int l, int r, int ss) { if(ss != L/block_size - 1) { L = (ss+1) * block_size; rcnt = 0; R = L+1; for(int i=L-block_size+1; i<=L; i++) fa[i] = i; } for(; R<=r; R++) { fa[R] = R; rcnt ++; for(int p=head[R]; ~p; p=g[p].next) { int v = g[p].v; if(v >= R || v <= L) continue; if(merge(R,v)) rcnt --; } } int tr = min(r,L); int ret = 0; if(r>L) ret += rcnt; for(int i=l; i<=tr; i++) { ret ++; for(int p=head[i]; ~p; p=g[p].next) { int v = g[p].v; if(v<=i || v>r) continue; if(Tmerge(i,v)) ret --; } } return ret; } void add_edge(int a, int b) { g[tot].v = b; g[tot].next = head[a]; head[a] = tot ++; } int main() { int cas, ca = 0; scanf("%d", &cas); while(cas--) { clr(head, -1); tot = 0; scanf("%d%d", &n, &m); int a,b; REP(i,0,m) { scanf("%d%d", &a, &b); add_edge(a,b); add_edge(b,a); } scanf("%d", &nq); block_size = (int)sqrt(n*1.0); REP(i,0,nq) { scanf("%d%d", &q[i].l, &q[i].r); q[i].b = q[i].l/block_size; q[i].id = i; } sort(q,q+nq); L = R = -1; clr(vis,-1); REP(i,0,nq) { now = i; ans[q[i].id] = work(q[i].l,q[i].r,q[i].b); } printf("Case #%d:\n", ++ca); REP(i,0,nq) printf("%d\n", ans[i]); } return 0; }