區間劃分。
本菜在這一題學到蠻多新東西&&新思想,下面詳細記錄一下思考過程。
題意: 給出一幅圖,每個頂點有(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;}