1. 程式人生 > 實用技巧 >P3295 [SCOI2016]萌萌噠

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;
}