【做题记录】csp2025-提高组并查集专题

A. Arpa's weak amphitheater and Mehrdad's valuable Hoses
用并查集将每个朋友圈找出,然后 DP。
\(dp_{i,j}\) 表示前 \(i\) 个朋友圈,重量为 \(j\) 的最大美丽度。转移分为从这个朋友圈中选一个转移、用这个朋友圈的和转移和不转移(直接继承)。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pb push_back
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e3+5;
int n,m,tot,dp[maxn][maxn];
int a[maxn],b[maxn];
int fa[maxn],sz[maxn];
int ying[maxn];
vector<int> hao[maxn];
il int find(int x){
	return x!=fa[x]?fa[x]=find(fa[x]):x;
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n)read(m)read(tot);
	for(int i=1;i<=n;i++){
		read(a[i]);
	}
	for(int i=1;i<=n;i++){
		read(b[i]);
	}
	for(int i=1;i<=n;i++){
		fa[i]=i,sz[i]=1;
	}
	while(m--){
		int u,v;
		read(u)read(v);
		u=find(u),v=find(v);
		if(u==v){
			continue;
		}
		if(sz[u]>sz[v]){
			sz[u]+=sz[v],fa[v]=u;
		}
		else{
			sz[v]+=sz[u],fa[u]=v;
		}
	}
	int num=0;
	for(int i=1;i<=n;i++){
		hao[find(i)].pb(i);
		if(find(i)==i){
			ying[++num]=i;
		}
	}
	memset(dp,-0x3f,sizeof dp);
	dp[0][0]=0;
	for(int i=1;i<=num;i++){
		for(int j=0;j<=tot;j++){
			dp[i][j]=dp[i-1][j];
		}
		int sa=0,sb=0;
		for(int x:hao[ying[i]]){
			sa+=a[x],sb+=b[x];
			for(int j=a[x];j<=tot;j++){
				dp[i][j]=max(dp[i][j],dp[i-1][j-a[x]]+b[x]);
			}
		}
		for(int j=sa;j<=tot;j++){
			dp[i][j]=max(dp[i][j],dp[i-1][j-sa]+sb);
		}
	}
	int ans=0;
	for(int i=1;i<=tot;i++){
		ans=max(ans,dp[num][i]);
	}
	printf("%d",ans); 
	return 0;
}
}
int main(){return asbt::main();}

B. Tokitsukaze and Two Colorful Tapes
\(a_i\)\(b_i\) 连边。发现得到若干个相互独立的环。要求是给每个点赋权,边权为两点权的差,使边权最大。
考虑环上点权比两边都大的点,设权值之和为 \(x\),点权比两边都小的点记为 \(y\),则 \(ans=2(x-y)\)
发现只有这两种点会对答案产生贡献,那么我们尽量让这两种点的数量最大。容易发现这两种点的数量相同。对于一个大小为 \(sz\) 的环,数量最大都为 \(\lfloor sz\rfloor\),记为 \(num\)
还是要让答案最大,那么就让大的那些点为 \(n-num+1\dots n\),小的那些为 \(1\dots num\)。则答案为

\[\begin{aligned} &\sum_{i=n-num+1}^n i-\sum_{i=1}^{num}i\\ =&2\times num(n-num) \end{aligned} \]

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int T,n,a[maxn],b[maxn];
int fa[maxn],sz[maxn];
il int find(int x){
	return x!=fa[x]?fa[x]=find(fa[x]):x;
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(T);
	while(T--){
		read(n);
		for(int i=1;i<=n;i++){
			fa[i]=i,sz[i]=1;
		}
		for(int i=1;i<=n;i++){
			read(a[i]);
		}
		for(int i=1;i<=n;i++){
			read(b[i]);
		}
		for(int i=1,u,v;i<=n;i++){
			u=find(a[i]),v=find(b[i]);
			if(u==v){
				continue;
			}
			if(sz[u]>sz[v]){
				sz[u]+=sz[v],fa[v]=u;
			}
			else{
				sz[v]+=sz[u],fa[u]=v;
			}
		}
		int num=0;
		for(int i=1;i<=n;i++){
			if(fa[i]==i){
				num+=sz[i]>>1;
			}
		}
		printf("%lld\n",num*2ll*(n-num));
	}
	return 0;
}
}
int main(){return asbt::main();}

C. AND-MEX Walk
因为是不断按位与,\(w_1,w_1\&w_2,w_1\&w_2\&w_3,\dots\) 这个序列一定是单调不增的。观察样例可以发现,答案只可能为 \(0\)\(1\)\(2\)

简单证明一下:
如果答案大于 \(2\),那么在 \(w_1,w_1\&w_2,w_1\&w_2\&w_3,\dots\) 中一定存在 \(0\)\(1\)\(2\)。又因为单调不增,则一定是在 \(2\) 后面出现了一个 \(1\)。然而 \(2\) 怎么与都是与不出来 \(1\) 的。因此答案不可能大于 \(2\)

于是直接去判断答案是 \(0\)\(1\) 还是 \(2\) 就行了。
首先是 \(0\) 的情况,即这个序列中没出现过 \(0\),那么在二进制位中一定有至少一位是 \(1\),即走过的每一条边在这一位都是 \(1\)。于是可以给每一个二进制位开一个并查集,连接在这一位上是 \(1\) 的边两边的点。如果 \(u\)\(v\) 在某一位上在一个集合里,那么答案就是 \(0\)
考虑判断答案是 \(1\) 的情况。记 \(w_1,w_1\&w_2,w_1\&w_2\&w_3,\dots\)\(a_1,a_2,a_3,\dots\),记总共有 \(tot\) 条边。则一定存在一个 \(k\in[1,tot)\) 满足 \(a_1,a_2,\dots,a_{k-1}\) 均大于 \(1\),而 \(a_k,a_{k+1},\dots,a_{tot}\) 都是 \(0\)。前面那部分是好处理的,只需在除了末位的某一位上都是 \(1\) 就好了,和第一种情况类似。考虑要与出 \(0\),则末位一定得是 \(0\)。因此新开一个并查集,将每条末位为 \(0\) 的边两边的每个点都向一个虚点 \(0\) 连边,只需检查 \(u\)\(v\) 中任意一个在除末位以外的某一位与 \(0\) 联通就行了。这样正确的原因是,因为答案已经不可能是 \(0\) 了,所以每一位到最后都是一定能被消掉的。
如果以上两种情况均不满足,则答案为 \(2\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m,q;
struct dsu{
	int fa[maxn],sz[maxn];
	il void init(){
		for(int i=0;i<=n;i++){
			fa[i]=i,sz[i]=1;
		}
	}
	il int find(int x){
		return x!=fa[x]?fa[x]=find(fa[x]):x;
	}
	il void merge(int u,int v){
//		cout<<u<<" "<<v<<"\n";
		u=find(u),v=find(v);
		if(u==v){
			return ;
		}
		if(sz[u]>sz[v]){
			sz[u]+=sz[v];
			fa[v]=u;
		}
		else{
			sz[v]+=sz[u];
			fa[u]=v;
		}
	}
	il bool check(int u,int v){
		return find(u)==find(v);
	}
}D[2][35];
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
//	cout<<cplx::usdmem();
	read(n)read(m);
	for(int x=0;x<30;x++){
		D[0][x].init();
		D[1][x].init();
	}
	for(int i=1,u,v,w;i<=m;i++){
		read(u)read(v)read(w);
		for(int x=0;x<30;x++){
			if(w>>x&1){
//				puts("666");
				D[0][x].merge(u,v);
				D[1][x].merge(u,v);
			}
		}
		if(w&1){
			continue;
		}
		for(int x=0;x<30;x++){
//			puts("777");
			D[1][x].merge(u,0);
			D[1][x].merge(v,0);
		}
	}
	read(q);
	while(q--){
		int u,v;
		read(u)read(v);
		for(int x=0;x<30;x++){
			if(D[0][x].check(u,v)){
				puts("0");
				goto togo;
			}
		}
		for(int x=1;x<30;x++){
			if(D[1][x].check(u,0)){
				puts("1");
				goto togo;
			}
		}
		puts("2");
		togo:;
	}
	return 0;
}
}
int main(){return asbt::main();}

D. Anton and Tree
直接说结论:将相同颜色的连通块缩成点建出新树,设新树的直径上点数为 \(d\),则答案为 \(\lfloor \frac d 2\rfloor\)

证明:
缩点后要把整棵树变成同一个颜色,那直径肯定也得变成同一种颜色。将直径变为同一种颜色的最小操作次数显然为 \(\lfloor \frac d 2\rfloor\)
然后直径上的点如果有连出直径外的边,这条多出来的链长度一定 \(\le\) 这个点与较近的直径端点的距离,否则就会有更长的路径充当直径。因此从这个点开始扩充,扩充到较近的直径端点时一定已经把这个点连出去的链都扩充完了,于是就可以在直径上换个点来扩充了。因此答案就是 \(\lfloor \frac d 2\rfloor\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pb push_back
#define pii pair<int,int>
#define mp make_pair
#define fir first
#define sec second
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e5+5;
int n,a[maxn],fa[maxn],sz[maxn];
pii ed[maxn];
vector<int> e[maxn];
il int find(int x){
	return fa[x]!=x?fa[x]=find(fa[x]):x;
}
il void merge(int u,int v){
	u=find(u),v=find(v);
	if(u==v){
		return;
	}
	if(sz[u]>sz[v]){
		sz[u]+=sz[v],fa[v]=u;
	}
	else{
		sz[v]+=sz[u],fa[u]=v;
	}
}
int dep[maxn],mxd[maxn],des[maxn];
il void dfs(int u,int fa){
	mxd[u]=dep[u]=dep[fa]+1;
	des[u]=u;
	for(int v:e[u]){
		if(v==fa){
			continue;
		}
		dfs(v,u);
		if(mxd[v]>mxd[u]){
			mxd[u]=mxd[v];
			des[u]=des[v];
		}
	}
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n);
	for(int i=1;i<=n;i++){
		read(a[i]);
		fa[i]=i,sz[i]=1;
	}
	for(int i=1,u,v;i<n;i++){
		read(u)read(v);
		ed[i]=mp(u,v);
		if(a[u]==a[v]){
			merge(u,v);
		}
	}
	for(int i=1,u,v;i<n;i++){
		u=find(ed[i].fir);
		v=find(ed[i].sec);
		if(u!=v){
			e[u].pb(v),e[v].pb(u);
		}
	}
	int rt=find(1);
	dfs(rt,0);
	rt=des[find(1)];
	dfs(rt,0);
	printf("%d",mxd[rt]>>1);
	return 0;
}
}
int main(){return asbt::main();}

E. Sanae and Giant Robot
\(c_i=a_i-b_i\)。问题转化为每次选择一个区间 \([l,r]\) 满足 \(\sum_{i=l}^{r}c_i=0\),将 \(c_{l\dots r}\leftarrow 0\)。要求是最后要满足 \(\forall i\in[1,n],c_i=0\)
再记 \(sc_i\)\(c_i\) 的前缀和。问题再次转化为每次选择一个区间 \([l,r]\) 满足 \(sc_{l-1}=sc_r\),将 \(sc_{l\dots r-1}\leftarrow sc_r\)。要求是最后要满足 \(\forall i\in[1,n],sc_i=0\)
因为最后要将 \(sc\) 数组推平成 \(0\),所以只有操作 \(sc_{l-1}=sc_r=0\) 的区间才有意义。于是可以用 set 维护 \(sc\) 值不为 \(0\) 的位置,进行 \(bfs\),每次用 \(sc=0\) 的位置,枚举它能更新的区间并将区间内的元素从 set 中删除即可。
因为每个位置最多进出 set 各一次,所以时间复杂度为 \(O(n\log n)\)
突然发现这道题没有用到并查集。。。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pb push_back
#define lwrb lower_bound
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e5+5;
int T,n,m;
ll a[maxn],b[maxn],sc[maxn]; 
queue<int> q;
set<int> ji;
vector<int> e[maxn];
il void solve(){
	read(n)read(m);
	for(int i=1;i<=n;i++){
		read(a[i]);
	}
	for(int i=1;i<=n;i++){
		read(b[i]);
	}
	for(int i=1,l,r;i<=m;i++){
		read(l)read(r);
		e[l-1].pb(r),e[r].pb(l-1);
	}
	for(int i=1;i<=n;i++){
		sc[i]=sc[i-1]+a[i]-b[i];
	}
	ji.clear();
	for(int i=0;i<=n;i++){
		if(sc[i]){
			ji.insert(i);
		}
		else{
			q.push(i);
		}
	}
	while(q.size()){
		int u=q.front();
		q.pop();
		for(int v:e[u]){
			if(sc[v]){
				continue;
			}
			int l=min(u,v),r=max(u,v);
			auto tmp=ji.lwrb(l);
			while(tmp!=ji.end()&&*tmp<=r){
				sc[*tmp]=0,q.push(*tmp);
				tmp=ji.erase(tmp);
			}
		}
	}
	puts(ji.empty()?"YES":"NO");
	for(int i=0;i<=n;i++){
		e[i].clear();
	}
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(T);
	while(T--){
		solve();
	}
	return 0;
}
}
int main(){return asbt::main();}

F. Nene and the Passing Game
先推式子。
\(i>j\),则:

\[\begin{aligned} &l_i+l_j\le i-j\le r_i+r_j\\ \Leftrightarrow&i-l_i\ge j+l_j\land i-r_i\le j+r_j\\ \Leftrightarrow&[i-r_i,i-l_i]\cap[j+l_j,j+r_j]\ne\varnothing \end{aligned} \]

又因为对于 \(i>j\)\(i+l_i\) 一定大于 \(j-l_j\)
所以对于所有 \(i\)\(j\),能相互传球的条件就是

\[([i-r_i,i-l_i]\cap[j+l_j,j+r_j])\cup([j-r_j,j-l_j]\cap[i+l_i,i+r_i])\ne\varnothing \]

将这样的 \(i\)\(j\) 连边,答案显然就是连通块的数量。
于是建虚点,将 \(i\)\([i-r_i,i-l_i]\)\([i+l_i,i+r_i]\) 中的虚点连边。时间无法接受,因此将 \(i\) 向区间中的一个点连边,区间内部再互相连边。这可以用并查集 \(+\) 类似扫描线的方式完成。
但是问题在于,这样连边可能会使 \(i\)\(j\) 因为 \([i-r_i,i-l_i]\)\([j-r_j,j-l_j]\) 相交而连通,但这显然不是我们希望得到的。
那怎么办呢,答案是对于所有 \(x\),如果 \(\forall i\in[1,n],x\notin[i-r_i,i-l_i]\) 或者 \(\forall i\in[1,n],x\notin[i+l_i,i+r_i]\),那就直接将这个点删掉。这样就能保证两个点相连一定是合法的了。这也可以用类似扫描线的方式完成。
连边时需要二分,复杂度为 \(O(n\log n)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define lwrb lower_bound
#define uprb upper_bound
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e6+5;
int T,n,a[maxn],b[maxn];
int fa[maxn<<2],sz[maxn<<2];
int cl[maxn<<2],cr[maxn<<2];
int hao[maxn<<2],c[maxn<<2];
bitset<maxn<<2> vis;
il int find(int x){
	return x!=fa[x]?fa[x]=find(fa[x]):x;
}
il void merge(int u,int v){
	u=find(u),v=find(v);
	if(u==v){
		return ;
	}
	if(sz[u]>sz[v]){
		sz[u]+=sz[v],fa[v]=u;
	}
	else{
		sz[v]+=sz[u],fa[u]=v;
	}
}
il int id(int x){
	return x+(n<<1|1);
}
il void solve(){
	read(n);
	for(int i=1;i<=n;i++){
		read(a[i])read(b[i]);
	}
//	cout<<(n<<2|1);
	for(int i=1;i<=(n<<2|1);i++){
//		cout<<i<<"\n";
		fa[i]=i,sz[i]=1;
		cl[i]=cr[i]=c[i]=vis[i]=0;
	}
//	puts("666");
	for(int i=1,l,r;i<=n;i++){
		l=id(i-b[i]),r=id(i-a[i]);
		cl[l]++,cl[r+1]--;
		l=id(i+a[i]),r=id(i+b[i]);
		cr[l]++,cr[r+1]--;
	}
	int tot=0;
	for(int i=1;i<=(n<<2|1);i++){
		cl[i]+=cl[i-1],cr[i]+=cr[i-1];
		if(cl[i]&&cr[i]){
			hao[++tot]=i;
		}
	}
	for(int i=1,l,r;i<=n;i++){
		l=lwrb(hao+1,hao+tot+1,id(i-b[i]))-hao;
		r=uprb(hao+1,hao+tot+1,id(i-a[i]))-hao-1;
		if(l<=r){
			merge(i,hao[l]);
			c[l]++,c[r]--;
		}
		l=lwrb(hao+1,hao+tot+1,id(i+a[i]))-hao;
		r=uprb(hao+1,hao+tot+1,id(i+b[i]))-hao-1;
		if(l<=r){
			merge(i,hao[l]);
			c[l]++,c[r]--;
		}
	}
	for(int i=1;i<=tot;i++){
		c[i]+=c[i-1];
		if(c[i]){
			merge(hao[i],hao[i+1]);
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		if(!vis[find(i)]){
			vis[find(i)]=1;
			ans++;
		}
	}
	printf("%d\n",ans);
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(T);
	while(T--){
		solve();
	}
	return 0;
}
}
int main(){return asbt::main();}

G. Clearing Up
思路是首先在不成环的前提下加入所有 \(S\) 边,然后加入若干条 \(M\) 边使加入的边构成一棵树。此时加入的所有 \(M\) 边都钦定必须选。然后再删掉所有 \(S\) 边,继续在不成环的前提下加 \(M\) 边使 \(M\) 边数量达到 \(\lfloor\frac{n}{2}\rfloor\)。然后再加入 \(\lfloor\frac{n}{2}\rfloor\)\(S\) 边使图变成一棵树即可。操作过程中不断判无解。
证明看似麻烦,实际一点都不简单很容易。要证明的其中一点是:如果我们选定的这 \(\lfloor\frac{n}{2}\rfloor\)\(M\) 边中的一部分能与所有不成环的 \(S\) 边形成一棵树,那么就一定能构造出方案。其实这是显然的,考虑生成树的构造方式,当前我们选定的 \(M\) 边因为不成环一定是生成树的一部分,而剩下的点一定是能由 \(S\) 边加进来的。所以正确性是有的。
另一点是:一开始加入 \(S\) 边时加了一些边而舍去了另一些边,这会不会影响答案?答案是不会。原因是,首先不论以怎样的顺序加入,加进来的边数都一定是相同的。其次,在最后加 \(S\) 边时,可供选择的 \(S\) 边实际上是变多了的(就是说第一步中没选的现在也可以选),而根据上面的证明,一定是能构造出方案的,所以多一些可供选的边也不会影响正确性。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pii pair<int,int>
#define mp make_pair
#define fir first
#define sec second
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e3+5,maxm=1e5+5;
int n,m,num1,num2;
int fa[maxn],sz[maxn];
int a[maxm],b[maxm];
pii e[maxm];
bool vis[maxm];
il void wuj(){
	puts("-1");
	exit(0);
}
il void init(){
	for(int i=1;i<=n;i++){
		fa[i]=i,sz[i]=1;
	}
}
il int find(int x){
	return fa[x]!=x?fa[x]=find(fa[x]):x;
}
il void merge(int u,int v){
	u=find(u),v=find(v);
	if(u==v){
		return ;
	}
	if(sz[u]>sz[v]){
		sz[u]+=sz[v];
		fa[v]=u;
	}
	else{
		sz[v]+=sz[u];
		fa[u]=v;
	}
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n)read(m);
	if(n%2==0){
		wuj();
	}
	for(int i=1,u,v;i<=m;i++){
		read(u)read(v);
		e[i]=mp(u,v);
		char w;
		scanf(" %c",&w);
		if(w=='S'){
			a[++num1]=i;
		}
		else{
			b[++num2]=i;
		}
	}
	init();
	int cnt1=0,cnt2=0;
	for(int i=1,u,v;i<=num1;i++){
		u=e[a[i]].fir,v=e[a[i]].sec;
		if(find(u)==find(v)){
			continue;
		}
		cnt1++,merge(u,v);
	}
	if(cnt1<n>>1){
		wuj();
	}
	for(int i=1,u,v;i<=num2;i++){
		u=e[b[i]].fir,v=e[b[i]].sec;
		if(find(u)==find(v)){
			continue;
		}
		cnt2++,merge(u,v),vis[b[i]]=1;
	}
	if(cnt1+cnt2<n-1){
		wuj();
	}
	init();
	for(int i=1;i<=m;i++){
		if(vis[i]){
			merge(e[i].fir,e[i].sec);
		}
	}
	for(int i=1,u,v;i<=num2;i++){
		if(cnt2==n>>1){
			break;
		}
		u=e[b[i]].fir,v=e[b[i]].sec;
		if(find(u)==find(v)){
			continue;
		}
		cnt2++,merge(u,v),vis[b[i]]=1;
	}
	if(cnt2<n>>1){
		wuj();
	}
	for(int i=1,u,v;i<=num1;i++){
		u=e[a[i]].fir,v=e[a[i]].sec;
		if(find(u)==find(v)){
			continue;
		}
		merge(u,v),vis[a[i]]=1;
	}
	printf("%d\n",n-1);
	for(int i=1;i<=m;i++){
		if(vis[i]){
			printf("%d ",i);
		}
	}
	return 0;
}
}
int main(){return asbt::main();}

H. [HEOI2016/TJOI2016] 树
离这个节点最近的一个祖先,那必然是 \(dfn\) 最大的一个。直接用线段树区间取 \(\max\) 和单点查询就行了。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define read(x){\
	char ch;\
	int fu=1;\
	while(!isdigit(ch=getchar()))\
		fu-=(ch=='-')<<1;\
	x=ch&15;\
	while(isdigit(ch=getchar()))\
		x=(x<<1)+(x<<3)+(ch&15);\
	x*=fu;\
}
#define pb push_back
#define lid id<<1
#define rid id<<1|1
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,m,dfn[maxn],idx[maxn],sz[maxn],cnt;
vector<int> e[maxn];
il void dfs(int u,int fa){
	dfn[u]=++cnt;
	idx[cnt]=u;
	sz[u]=1;
	for(int v:e[u]){
		if(v==fa){
			continue;
		}
		dfs(v,u);
		sz[u]+=sz[v];
	}
}
int zhi[maxn<<2];
il void build(int id,int l,int r){
	zhi[id]=1;
	if(l==r){
		return ;
	}
	int mid=(l+r)>>1;
	build(lid,l,mid);
	build(rid,mid+1,r);
}
il void upd(int id,int L,int R,int l,int r,int val){
	if(L>=l&&R<=r){
		zhi[id]=max(zhi[id],val);
		return ;
	}
	int mid=(L+R)>>1;
	if(l<=mid){
		upd(lid,L,mid,l,r,val);
	}
	if(r>mid){
		upd(rid,mid+1,R,l,r,val);
	}
}
il int query(int id,int l,int r,int pos){
	int res=zhi[id];
	if(l==r){
		return res;
	}
	int mid=(l+r)>>1;
	if(pos<=mid){
		return max(res,query(lid,l,mid,pos));
	}
	return max(res,query(rid,mid+1,r,pos));
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	read(n)read(m);
	for(int i=1,u,v;i<n;i++){
		read(u)read(v);
		e[u].pb(v),e[v].pb(u);
	}
	dfs(1,0);
	build(1,1,n);
	while(m--){
		char opt;
		int u;
		scanf(" %c",&opt);
		read(u);
		if(opt=='Q'){
			printf("%d\n",idx[query(1,1,n,dfn[u])]);
		}
		else{
			upd(1,1,n,dfn[u],dfn[u]+sz[u]-1,dfn[u]);
		}
	}
	return 0;
}
}
int main(){return asbt::main();}

本来还以为是又放错题了,结果去洛谷题解区一看还真能拿并查集做,有点类似 \(tarjan\)\(lca\)。就是离线下来,如果这个点被染色了就把 \(fa\) 指向自己,否则指向自己的父亲,倒序处理询问,操作就将这个节点的染色次数 \(-1\) 就行。方法不错,就不打了(逃

I. [HNOI2016] 最小公倍数
显然能将最小公倍数转化为 \(a\)\(b\) 分别取 \(\max\)。显然对于一个询问,只有 \(a\)\(b\) 都不超过它的边能产生贡献。将能产生贡献的边加入,并查集判断连通性再判断 \(a\)\(b\) 的最大值是否都符合要求即可。
将所有边先按 \(a\) 排序,分块。然后对于每个询问,令它归属的块编号为最大的 \(i\),使 \(1\)\(i-1\) 块中的边的 \(a\)\(\le\) 这个询问的 \(a\)。接下来顺序扫描每一个块,维护当前扫过的所有块中的边按 \(b\) 排序的序列。每扫到一个块,先将这个块中的询问插入到维护的序列中,然后遍历插入后的序列,若遍历到边则直接合并,若遍历到询问则将当前块中能对这个询问产生贡献的边加入,统计答案后再撤销。需要可撤销并查集。最后再将当前块中的边插入序列即可。可以用归并排序完成。
然后就是玄学的复杂度分析了。设块长为 \(B\)。加入所有边的复杂度为 \(O(\frac{m^2\log n}{B})\),而处理询问的复杂度为 \(O(qB\log n)\)。令二者相等,显然最佳的块长应为 \(\frac{m}{\sqrt{q}}\),时间复杂度为 \(O(m\log n\sqrt{q})\) 约为 \(3\times 10^8\),然而会 TLE 2 个点。考虑处理询问那一块每个询问都是跑不满 \(B\) 次的,不妨将那个 \(\log n\) 舍掉,于是令 \(\frac{m^2\log n}{B}=qB\),解出块长应为 \(m\sqrt{\frac{\log n}{q}}\),然而会 TLE 4 个点。但若将加边的 \(\log n\) 舍掉,解出块长为 \(\frac{m}{\sqrt{q\log n}}\),此时加边那部分的复杂度为 \(O(m\log n\sqrt{q\log n})\),是 \(1.3\times 10^9\) 左右的,却通过了。所以说做分块时一定要多试试不同的块长跑极限数据,以实际跑下来的时间为准。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define pb push_back
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
namespace IO{
	char buf[1<<20],*p1=buf,*p2=buf;
	#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
	il int read(){
		char ch=getchar();
		while(ch<'0'||ch>'9'){
			ch=getchar();
		}
		int x=ch^48;
		ch=getchar();
		while(ch>='0'&&ch<='9'){
			x=(x<<1)+(x<<3)+(ch^48);
			ch=getchar();
		}
		return x;
	}
}
using namespace IO;
const int maxn=1e5+5;
int n,m,q,blen,bnum,st[maxn],ed[maxn],bel[maxn],wel[maxn];
int fa[maxn],sz[maxn],mxa[maxn],mxb[maxn],top;
bool ans[maxn];
vector<int> xwn[maxn];
struct node{
	int u,v,a,b,id;
	bool typ;
}bian[maxn],wen[maxn],hp1[maxn<<1],hp2[maxn<<1];
il bool cmpa(const node &x,const node &y){
	return x.a<y.a;
}
il bool cmpb(const node &x,const node &y){
	return x.b<y.b;
}
il void gwel(){
	int p1=1,p2=1,p3=1;
	while(p1<=m&&p2<=q){
		if(cmpa(wen[p2],bian[p1])){
			hp1[p3++]=wen[p2++];
		}
		else{
			hp1[p3++]=bian[p1++];
		}
	}
	while(p1<=m){
		hp1[p3++]=bian[p1++];
	}
	while(p2<=q){
		hp1[p3++]=wen[p2++];
	}
	bel[0]=++bnum;
	for(int i=1,li=0,cnt=0;i<=p3;i++){
		if(hp1[i].typ){
			continue;
		}
		for(int j=li+1;j<i;j++){
			wel[hp1[j].id]=bel[hp1[i].id];
			xwn[bel[hp1[i].id]].pb(++cnt);
		}
		li=i;
	}
}
il int find(int x){
	return x!=fa[x]?find(fa[x]):x;
}
struct unod{
	int u,v,fau,fav,mxau,mxbu,mxav,mxbv;
}zhan[maxn];
il void merge(int u,int v,int a,int b){
	u=find(u),v=find(v);
	zhan[++top]=(unod){u,v,fa[u],fa[v],mxa[u],mxb[u],mxa[v],mxb[v]};
	if(u==v){
		mxa[u]=max(mxa[u],a),mxb[u]=max(mxb[u],b);
		return ;
	}
	if(sz[u]<sz[v]){
		swap(u,v);
	}
	sz[u]+=sz[v],fa[v]=u;
	mxa[u]=max({mxa[u],mxa[v],a});
	mxb[u]=max({mxb[u],mxb[v],b});
}
il void che(){
	unod tmp=zhan[top--];
	int u=tmp.u,v=tmp.v;
	fa[u]=tmp.fau,fa[v]=tmp.fav;
	mxa[u]=tmp.mxau,mxb[u]=tmp.mxbu;
	mxa[v]=tmp.mxav,mxb[v]=tmp.mxbv;
}
namespace cplx{
	bool end;
	il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
	n=read(),m=read();
	for(int i=1,u,v,a,b;i<=m;i++){
		u=read(),v=read(),a=read(),b=read();
		bian[i]=(node){u,v,a,b,0,0};
	}
	q=read();
	for(int i=1,u,v,a,b;i<=q;i++){
		u=read(),v=read(),a=read(),b=read();
		wen[i]=(node){u,v,a,b,i,1};
	}
	sort(bian+1,bian+m+1,cmpa);
	sort(wen+1,wen+q+1,cmpa);
	blen=max(sqrt(m*1.0*m/q/(log2(n)>1?log2(n):1)),1.0);
	bnum=(m+blen-1)/blen;
	for(int i=1;i<=bnum;i++){
		st[i]=ed[i-1]+1;
		ed[i]=min(ed[i-1]+blen,m);
		for(int j=st[i];j<=ed[i];j++){
			bel[j]=i;
		}
	}
	for(int i=1;i<=m;i++){
		bian[i].id=i;
	}
	gwel();
	for(int i=1;i<=bnum;i++){
		sort(xwn[i].begin(),xwn[i].end(),[](const int &x,const int &y){return cmpb(wen[x],wen[y]);});
		sort(bian+st[i],bian+ed[i]+1,cmpb);
	}
	st[bnum]=m+1;
	for(int i=1,p1,p2,p3;i<=bnum;i++){
		p1=p3=1,p2=0;
		while(p1<st[i]&&p2<xwn[i].size()){
			if(cmpb(wen[xwn[i][p2]],hp2[p1])){
				hp1[p3++]=wen[xwn[i][p2++]];
			}
			else{
				hp1[p3++]=hp2[p1++];
			}
		}
		while(p1<st[i]){
			hp1[p3++]=hp2[p1++];
		}
		while(p2<xwn[i].size()){
			hp1[p3++]=wen[xwn[i][p2++]];
		}
		for(int j=1;j<=n;j++){
			fa[j]=j,sz[j]=1,mxa[j]=mxb[j]=-1;
		}
		top=0;
		for(int j=1;j<p3;j++){
			if(hp1[j].typ){
				int tmp=top;
				for(int k=st[i];k<=ed[i];k++){
					if(bian[k].a<=hp1[j].a&&bian[k].b<=hp1[j].b){
						merge(bian[k].u,bian[k].v,bian[k].a,bian[k].b);
					}
				}
				int rt=find(hp1[j].u);
				if(rt==find(hp1[j].v)&&mxa[rt]==hp1[j].a&&mxb[rt]==hp1[j].b){
					ans[hp1[j].id]=1;
				}
				while(top>tmp){
					che();
				}
			}
			else{
				merge(hp1[j].u,hp1[j].v,hp1[j].a,hp1[j].b);
			}
		}
		p1=p3=1,p2=st[i];
		while(p1<st[i]&&p2<=ed[i]){
			if(cmpb(hp2[p1],bian[p2])){
				hp1[p3++]=hp2[p1++];
			}
			else{
				hp1[p3++]=bian[p2++];
			}
		}
		while(p1<st[i]){
			hp1[p3++]=hp2[p1++];
		}
		while(p2<=ed[i]){
			hp1[p3++]=bian[p2++];
		}
		for(int j=1;j<p3;j++){
			hp2[j]=hp1[j];
		}
	}
	for(int i=1;i<=q;i++){
		puts(ans[i]?"Yes":"No");
	}
	return 0;
}
}
int main(){return asbt::main();}
posted @ 2025-01-15 18:00  zhangxy__hp  阅读(33)  评论(0)    收藏  举报