P3295 [SCOI2016]萌萌哒
https://www.luogu.com.cn/problem/P3295
https://darkbzoj.tk/problem/4569
题目描述
一个长度为 \(n\) 的大数,用 \(S_1S_2S_3 \cdots S_n\) 表示,其中 \(S_i\) 表示数的第 \(i\) 位,\(S_1\) 是数的最高位。
告诉你一些限制条件,每个条件表示为四个数,\(l_1,r_1,l_2,r_2\),即两个长度相同的区间,表示子串 \(S_{l_1}S_{l_1+1}S_{l_1+2} \cdots S_{r_1}\) 与 \(S_{l_2}S_{l_2+1}S_{l_2+2} \cdots S_{r_2}\) 完全相同。
比如 \(n=6\) 时,某限制条件 \(l_1=1,r_1=3,l_2=4,r_2=6\),那么 \(123123123123,351351351351\) 均满足条件,但是 \(1201212012,131141131141\) 不满足条件,前者数的长度不为 \(6\),后者第二位与第五位不同。问满足以上所有条件的数有多少个。
\(n,m\le 10^5\)
首先,可以想到把区间的相等转换为两个区间中的每一位对应相等,用并查集,将这些相等的位置放入同一集合
设最后一共有 \(x\) 个集合,那么答案应该是 \(9\cdot 10^{x-1}\),因为这是一个数,那第一位所在的那个集合就不能为 \(0\)
这样是 \(O(nm)\),得 30 分
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
register int x=0;register int y=1;
register char c=std::getchar();
while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
return y?x:-x;
}
#define mod 1000000007
#define N 100005
int fa[N];
int n,m;
int find(int k){return k==fa[k]?k:fa[k]=find(fa[k]);}
inline void merge(int x,int y){
x=find(x);y=find(y);
if(x==y) return;
fa[x]=y;
}
int main(){
n=read();m=read();
for(reg int i=1;i<=n;i++) fa[i]=i;
for(reg int l1,r1,l2,i=1;i<=m;i++){
l1=read();r1=read();l2=read();read();
for(reg int j=0;j<=r1-l1;j++) merge(l1+j,l2+j);
}
int num=0;
for(reg int i=1;i<=n;i++) num+=(find(i)==i);
LL ans=1;
for(reg int i=1;i<num;i++) ans=ans*10%mod;
ans=ans*9%mod;
printf("%lld",ans);
return 0;
}
再考虑满分的做法,实际上是在前一种暴力的基础上,加了一个倍增的优化(也可以说是把并查集放到了 st 表上)
定义 \(fa_{j,i}\) 是从 \(i\) 开始,长度为 \(2^j\) 的区间,在并查集中的父亲的左端点
比如规则中有 \([x,x+2^k-1],[y,y+2^k-1]\) 这两个区间相等,那么将 \([x,x+2^k-1]\) 合并到另一个由 \(y\) 开始的区间上,\(fa_{k,x}=y\)
那么就可以以 \(O(\log^2 n)\) 的复杂度对给出的每一个规则进行并查集操作
计算答案前,将所有层(就是下表里的每个 \(j\) 在这里说成一“层”),对应的点合并
将 \((i,j-1)\) 和 \((find(j,i),j-1)\) 合并:\(find(j,i)\) 是从 \(i\) 开始的 \(2^j\) 的区间的根个左端点,那么从他开始的 \(2^{j-1}\) 长度的区间,应该和从 \(i\) 开始的 \(2^{j-1}\) 长度的区间合并
将 \((i+2^{j-1},j-1)\) 和 \((find(j,i)+2^{j-1},j-1)\) 合并,也是同理,就是上面描述的那两个长度 \(2^{j-1}\) 区间,在整个长度为 \(2^j\) 的区间中,剩下的一半
这样就只用统计有多少 \(find(0,i)=i\) 就行了
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
register int x=0;register int y=1;
register char c=std::getchar();
while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
return y?x:-x;
}
#define mod 1000000007
#define N 100005
int fa[25][N];
int n,m;
int find(int i,int k){return k==fa[i][k]?k:fa[i][k]=find(i,fa[i][k]);}
inline void merge(int x,int y,int len){
x=find(len,x);y=find(len,y);
if(x==y) return;
fa[len][x]=y;
}
int main(){
n=read();m=read();
for(reg int j=0;j<=20;j++)for(reg int i=1;i<=n;i++) fa[j][i]=i;
for(reg int l1,r1,l2,i=1;i<=m;i++){
l1=read();r1=read();l2=read();read();
for(reg int j=20;~j;j--)if(l1+(1<<j)-1<=r1)
merge(l1,l2,j),l1+=(1<<j),l2+=(1<<j);
}
for(reg int j=20;j;j--)
for(reg int i=1;i+(1<<j)-1<=n;i++)
merge(i,find(j,i),j-1),merge(i+(1<<(j-1)),fa[j][i]+(1<<(j-1)),j-1);
int num=0;
for(reg int i=1;i<=n;i++) num+=(find(0,i)==i);
LL ans=1;
for(reg int i=1;i<num;i++) ans=ans*10%mod;
ans=ans*9%mod;
printf("%lld",ans);
return 0;
}