[SCOI2016]萌萌哒

题目

这也太妙啦

首先这个题等价于问最后的联通块数量,一个非常显然的暴力就是暴力把两个区间的按位合并,自然是用并查集,复杂度(O(sum r_i-l_i+1))

我们看到这个题大概在用并查集来维护连边,那么接下俩应该就是优化一下这个连边过程了,考虑一下分块,线段树以及倍增发现就倍增可行一些

线段树和分块不可行的原因就是他们拆分区间的时候对于两段不同的区间可能拆出来的数量和每一段的长度不相同,这样非常不适于这道按位合并的题目

于是考虑倍增,首先倍增拆出来的(log)段区间,每一段对应的长度都是相等的,考虑用并查集合并掉这两个区间,但是是对于每一个二次幂都有一个并查集

比如说在第(k)个并查集里,(i)(j)在一个联通块里,只是说明了([i,i+2^k-1])([j,j+2^k-1])这两段区间是按位相等的

我们从大到小一次扫二次幂的并查集,暴力扫一遍所有点,如果(k)层里(i,j)联通,那么显然等价于在(k-1)层里(i,j)联通,(i+2^{k-1},j+2^{k-1})联通,于是我们每次这样暴力把区间拆成两半,放到下一层去处理,当处理完(2^1)的时候,最底层的并查集就是我们需要的了

这个思路还是相当妙的

代码

#include<bits/stdc++.h>
#define re register
#define pb push_back
const int maxn=1e5+5;
const int mod=1e9+7;
inline int read() {
    char c=getchar();int x=0;while(c<'0'||c>'9') c=getchar();
    while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-48,c=getchar();return x;
}
int n,m,l[2],r[2],vis[maxn];
int fa[18][maxn],lg[maxn];
std::vector<int> v[maxn];
int find(int k,int x) {return fa[k][x]==x?x:fa[k][x]=find(k,fa[k][x]);}
inline void merge(int k,int x,int y) {
    int xx=find(k,x),yy=find(k,y);
    if(xx==yy) return;
    fa[k][xx]=yy;
}
int main() {
    n=read(),m=read();
    for(re int i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
    for(re int i=1;i<=n;i++)
		for(re int j=0;i+(1<<j)-1<=n;j++) fa[j][i]=i;
    while(m--) {
		l[0]=read(),r[0]=read(),l[1]=read(),r[1]=read();
		int len=r[0]-l[0]+1;
		for(re int k=lg[len];k>=0;--k)
	    	if(l[0]+(1<<k)-1<=r[0]) 
				merge(k,l[0],l[1]),l[0]+=(1<<k),l[1]+=(1<<k);
    }
    for(re int k=lg[n];k;--k) {
		for(re int i=1;i<=n;i++)
	    	v[find(k,i)].pb(i);
		for(re int i=1;i<=n;i++) {
	    	for(re int j=1;j<v[i].size();j++)
				merge(k-1,v[i][j],v[i][j-1]);
	    	for(re int j=1;j<v[i].size();j++)
				merge(k-1,v[i][j]+(1<<(k-1)),v[i][j-1]+(1<<(k-1)));
	    	v[i].clear();
		}
    }
    int ans=-1,now=9;
    for(re int i=1;i<=n;i++) if(!vis[find(0,i)]) ++ans,vis[find(0,i)]=1;
    while(ans--) now=1ll*now*10%mod;
    printf("%d
",now);
    return 0;
}
原文地址:https://www.cnblogs.com/asuldb/p/11191431.html