acm-根号分治在各个领域的应用

引言

对于acm常有一些题目让人十分棘手,并且没有专门的
算法来解决这些问题。这时候一般都最好从暴力着手来
思考解决方案,而根号分治可以说是一种优雅的暴力。

本文将通过例题的方式从各个领域来剖析根号分治的核心思想。


图论

例题一
题目来源:2020上海高校程序设计竞赛暨第18届上海大学程序设计联赛夏季赛(同步赛)D题:旅行
acm-根号分治在各个领域的应用_第1张图片
acm-根号分治在各个领域的应用_第2张图片
简化题意:给定一张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} n2m2,相当于是一根直线,也就是说期望每次询问最跑 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.8108,能过本题,由于这是按照最坏的情况进行分析得出的期望,实际运行效率会远远高于此,具体看代码中的注释。
代码实现:

\\加入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");
	}
}

例题二

待续。。。

你可能感兴趣的:(高级暴力技巧,图论,acm竞赛,算法)