对于acm常有一些题目让人十分棘手,并且没有专门的
算法来解决这些问题。这时候一般都最好从暴力着手来
思考解决方案,而根号分治可以说是一种优雅的暴力。
本文将通过例题的方式从各个领域来剖析根号分治的核心思想。
例题一
题目来源:2020上海高校程序设计竞赛暨第18届上海大学程序设计联赛夏季赛(同步赛)D题:旅行
简化题意:给定一张n<=100000个点,m<=n+5000条边的有向图(随机生成)和q<=300000个询问,每次询问u点是否能够到达v点,能输出Good,否则Bad
题解:最直接的想法就是先对整个有向图进行缩点处理,如果查询的两个点都位于同一个强联通分量内就输出Good,否则再缩点后的图的基础上从u点dfs深搜,看是否能够到达v点。分析一下纯暴力的时间复杂度:连通块的数量最多可达n个,每次查询最坏情况要遍历所有的连通块,因此复杂度是 O ( q n ) \mathbf{O(qn)} O(qn),数量级达到了 3 e 10 \mathbf{3e10} 3e10,显然不可取。
这时候我们考虑根号分治的思想。从所有点中选定 t o t \mathbf{\sqrt{tot}} tot(tot代表连通块个数)个度数最大的点,然后对这些点暴力预处理出它们可以到达的图中的所有点。可以为这些预处理点都开一个集合,然后将它们能够到达的点存入相应地集合中。然后对于每次询问(u,v),直接从u点暴力dfs深搜,遇到v点就返回true,遇到已经预处理过的点就查询该点对应集合中是否存在点v,有的话返回true,否则返回false。
分析一下这种做法的复杂度:由于数据随机的特性,最坏情况假设连通块个数等于点个数,平均深搜 n \mathbf{\sqrt n} n个点就能遇到一个预处理过的点,每个点的度数的期望应该是 2 m n ≈ 2 \mathbf{\frac{2m}{n}\approx 2} n2m≈2,相当于是一根直线,也就是说期望每次询问最跑 2 n \mathbf{2\sqrt n} 2n个点能够确定答案,所以期望复杂度应该是 O ( 2 q n ) \mathbf{O(2q\sqrt n)} O(2qn),数量级约为 1.8 ∗ 1 0 8 \mathbf{1.8*10^8} 1.8∗108,能过本题,由于这是按照最坏的情况进行分析得出的期望,实际运行效率会远远高于此,具体看代码中的注释。
代码实现:
\\加入vis优化后(避免点的重复访问)运行速度实测为734ms
#include
#define FOR(i,a,b) for(register int i=(a);i<(b);++i)
#define ROF(i,a,b) for(register int i=(a);i>=(b);--i)
#define pi pair
#define mk(a,b) make_pair(a,b)
#define mygc(c) (c)=getchar()
#define mypc(c) putchar(c)
#define fi first
#define se second
using namespace std;
typedef long long ll;
typedef double db;
const int maxn = 100005;
const int maxm = 100;
const int inf = 2147483647;
typedef long long ll;
const double eps = 1e-9;
const long long INF = 9223372036854775807ll;
ll qpow(ll a,ll b,ll c){ll ans=1;while(b){if(b&1)ans=ans*a%c;a=a*a%c;b>>=1;}return ans;}
inline void rd(int *x){int k,m=0;*x=0;for(;;){mygc(k);if(k=='-'){m=1;break;}if('0'<=k&&k<='9'){*x=k-'0';break;}}for(;;){mygc(k);if(k<'0'||k>'9')break;*x=(*x)*10+k-'0';}if(m)(*x)=-(*x);}
inline void rd(ll *x){int k,m=0;*x=0;for(;;){mygc(k);if(k=='-'){m=1;break;}if('0'<=k&&k<='9'){*x=k-'0';break;}}for(;;){mygc(k);if(k<'0'||k>'9')break;*x=(*x)*10+k-'0';}if(m)(*x)=-(*x);}
inline void rd(db *x){scanf("%lf",x);}
inline int rd(char c[]){int i,s=0;for(;;){mygc(i);if(i!=' '&&i!='\n'&&i!='\r'&&i!='\t'&&i!=EOF) break;}c[s++]=i;for(;;){mygc(i);if(i==' '||i=='\n'||i=='\r'||i=='\t'||i==EOF) break;c[s++]=i;}c[s]='\0';return s;}
inline void rd(int a[],int n){FOR(i,0,n)rd(&a[i]);}
inline void rd(ll a[],int n){FOR(i,0,n)rd(&a[i]);}
template <class T, class S> inline void rd(T *x, S *y){rd(x);rd(y);}
template <class T, class S, class U> inline void rd(T *x, S *y, U *z){rd(x);rd(y);rd(z);}
template <class T, class S, class U, class V> inline void rd(T *x, S *y, U *z, V *w){rd(x);rd(y);rd(z);rd(w);}
inline void wr(int x){if(x < 10) putchar('0' + x); else wr(x / 10), wr(x % 10);}
inline void wr(int x, char c){int s=0,m=0;char f[10];if(x<0)m=1,x=-x;while(x)f[s++]=x%10,x/=10;if(!s)f[s++]=0;if(m)mypc('-');while(s--)mypc(f[s]+'0');mypc(c);}
inline void wr(ll x, char c){int s=0,m=0;char f[20];if(x<0)m=1,x=-x;while(x)f[s++]=x%10,x/=10;if(!s)f[s++]=0;if(m)mypc('-');while(s--)mypc(f[s]+'0');mypc(c);}
inline void wr(db x, char c){printf("%.15f",x);mypc(c);}
inline void wr(const char c[]){int i;for(i=0;c[i]!='\0';i++)mypc(c[i]);}
inline void wr(const char x[], char c){int i;for(i=0;x[i]!='\0';i++)mypc(x[i]);mypc(c);}
template<class T> inline void wrn(T x){wr(x,'\n');}
template<class T, class S> inline void wrn(T x, S y){wr(x,' ');wr(y,'\n');}
template<class T, class S, class U> inline void wrn(T x, S y, U z){wr(x,' ');wr(y,' ');wr(z,'\n');}
template<class T> inline void wra(T x[], int n){int i;if(!n){mypc('\n');return;}FOR(i,0,n-1)wr(x[i],' ');wr(x[n-1],'\n');}
int n,m,dfn[maxn],st[maxn],top,tot,low[maxn],id[maxn],idtot,deg[maxn],inst[maxn],mark[maxn],vis[maxn];
vector<int>g[maxn],h[maxn];
set<int>s[maxn];
pi a[maxn];
void Tarjan(int u){//缩点
inst[u]=1;
low[u]=dfn[u]=++tot;
st[top++]=u;
FOR(i,0,g[u].size()){
int v=g[u][i];
if(!dfn[v]){
Tarjan(v);
low[u]=min(low[u],low[v]);
}else if(inst[v])low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
++idtot;
int v;
do{
v=st[--top];
id[v]=idtot;
inst[v]=0;
}while(v!=u);
}
}
int tim=0;//时间戳,每次dfs使用不同的时间戳可以避免对vis数组清空
void dfs(int u,int ori,int tm){//对ori点进行预处理,使得s[ori]集合中存放ori能够到达的点(在缩点之后的图上跑)
s[ori].insert(u);
vis[u]=tim;
FOR(i,0,h[u].size()){
int v=h[u][i];
if(vis[v]==tm)continue;
dfs(v,ori,tm);
}
}
void pre(){//预处理的操作,选取度数前sqrt(n)大的点预处理出它们能够在图中遍历到的所有点
FOR(i,1,idtot+1)a[i]=mk(deg[i],i);
sort(a+1,a+1+idtot,greater<pi >());
int sq=sqrt(idtot);
FOR(i,1,sq+1){
mark[a[i].se]=1;
dfs(a[i].se,a[i].se,++tim);
}
}
int qry(int u,int tar,int tm){//查询操作
if(u==tar)return 1;//如果遇到了目标节点直接返回true
if(mark[u])return s[u].count(tar);//如果遇到了预处理过的节点就查询其集合中是否存在目标节点
FOR(i,0,h[u].size()){//如果两种情况都不符合就继续深搜,大约每sqrt(n)次搜索遇到一个预处理过的节点
int v=h[u][i];
if(vis[v]==tm)continue;//vis数组避免重复访问,缩点的时候回出现大量的重边
if(qry(v,tar,tm))return 1;
}
return 0;
}
int main(){
rd(&n,&m);
FOR(i,0,m){
int u,v;
rd(&u,&v);
g[u].push_back(v);
}
FOR(i,0,n)if(!dfn[i])Tarjan(i);
FOR(i,0,n){
FOR(j,0,g[i].size()){
int u=i,v=g[i][j];
if(id[u]==id[v])continue;
h[id[u]].push_back(id[v]);//建立缩点后的新图,注意到缩点的新图上会有大量重边,因此开一个vis数组来避免重复访问是非常有效的优化方式
deg[id[u]]++;
deg[id[v]]++;
}
}
pre();//对度数前sqrt(n)大的点暴力预处理
int q;
rd(&q);
while(q--){
int u,v;
rd(&u,&v);
if(qry(id[u],id[v],++tim))wrn("Good");else wrn("Bad");
}
}
例题二
待续。。。