并查集+最小生成树 学习笔记+杂题 2

图论系列:

前言:

相关题单:戳我

算法讲解:戳我

CF1829E The Lakes

给定一张 \(n*m\) 的矩阵,询问正整数四联通块权值和的最大值。并查集维护即可,记录一下集合内的点的权值和。

代码:

const int M=1005;
int T,n,m,ans;
int a[M][M],fa[M*M],siz[M*M];
int fx[5]={0,1,-1,0,0};
int fy[5]={0,0,0,1,-1};
inline int pos(int i,int j) {return (i-1)*m+j;}
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>T;
	while(T--)
	{
		cin>>n>>m,ans=0;
		for(int i=1;i<=n;++i)
		{
			for(int j=1;j<=m;++j) cin>>a[i][j],fa[pos(i,j)]=pos(i,j),siz[pos(i,j)]=a[i][j];
		}
		for(int i=1,sx,sy,ex,ey;i<=n;++i)
		{
			for(int j=1;j<=m;++j)
			{
				if(!a[i][j]) continue;
				for(int k=1;k<=4;k++)
				{
					sx=i+fx[k],sy=j+fy[k];
					if(sx<1||sx>n||sy<1||sy>m||!a[sx][sy]) continue;
					ex=find(pos(i,j)),ey=find(pos(sx,sy));
					if(ex==ey) continue;
					fa[ex]=ey,siz[ey]+=siz[ex];
				}
			}
		}
		/*for(int i=1;i<=n;++i)
		{
			for(int j=1;j<=m;++j) cout<<fa[pos(i,j)]<<" ";
			cout<<"\n"; 
		}*/
		for(int i=1;i<=n;++i)
		{
			for(int j=1;j<=m;++j)
			{
				if(pos(i,j)==fa[pos(i,j)]) ans=max(ans,siz[pos(i,j)]);
			}
		}
		cout<<ans<<"\n";
	}
	return 0;
}

CF1702E Split Into Two Sets

首先对于每一个点对,如果一个点对出现的两个数相同,自然没解,如果一个数出现的次数超过 2 次,肯定也没有解。然后我们把每个点对的两个点 \(x\)\(y\) 合并在一起。

然后判断每个块内有多少个元素,如果有偶数个元素,那么两边集合一边一半,但如果一个集合有奇数个数,就一定无法分配到两个集合去,必然会出现矛盾。(判断一下即可)

代码:

#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e5+5;
int T,n,flag,cnt;
int fa[M],in[M],to[M]; 
struct node{
	int x,y;
};node a[M];
map<int,int> mapp;
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y) return ;
	fa[x]=y;
}
 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>T;
	while(T--)
	{
		cin>>n,flag=0,mapp.clear();
		for(int i=1;i<=n;++i) in[i]=to[i]=0,fa[i]=i;
		for(int i=1;i<=n;++i)
		{
			cin>>a[i].x>>a[i].y;
			++in[a[i].x],++in[a[i].y];
			if(a[i].x==a[i].y) flag=1;
			if(in[a[i].x]>2||in[a[i].y]>2) flag=1;
			merge(a[i].x,a[i].y);
		}
		if(flag) {cout<<"NO\n";continue;}
		for(int i=1,x;i<=n;++i)
		{
			x=find(i);
			if(!mapp[x]) mapp[x]=1;//因为值域有点大嘛,懒得离散化了,就拿map存一下出现过的根
			++to[x]; 
		}
		for(auto it:mapp) if(to[it.first]&1) flag=1;
		if(flag) cout<<"NO\n";
		else cout<<"YES\n";
	}
	return 0;
}

CF722C Destroying Array

典麻了,还是正难则反,由于一开始是删点,那么反过来就是加点,判断一下左右两边是否已经加进去了,加进去了就和它们合并一下,统计一下每个集合内的元素和。

代码:

const int M=1e5+5;
int n,ans;
int a[M],b[M],fa[M],siz[M],vis[M],res[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x]; 
}
inline void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y) return ;
	fa[x]=y,siz[y]+=siz[x],ans=max(ans,siz[y]);
}
 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int i=1;i<=n;++i) cin>>b[i];
	for(int i=n;i>=1;--i)//正难则反
	{
		vis[b[i]]=1,fa[b[i]]=b[i],siz[b[i]]=a[b[i]];//初始化
		ans=max(ans,siz[b[i]]);
		if(vis[b[i]-1]) merge(b[i]-1,b[i]);//左边的已经出来了就和左边的合并
		if(vis[b[i]+1]) merge(b[i],b[i]+1);//同理
		res[i-1]=ans;
	}
	for(int i=1;i<=n;++i) cout<<res[i]<<"\n";
	return 0;
}

CF500B New Year Permutation

并查集启发式合并

如果为 \(mapp_{i,j}=1\) 的话,那么 \(a_i\)\(a_j\) 两个位置就可以交换,于是将能交换的位置全部合并起来。最后要求排列的字典序最小,对于每一个记录都拿一个 map 记录一下里面有哪些元素。经典的,对于两个集合,还是将小集合并入大集合中,最后输出的时候由于 map 是按从小到大的顺序存的,刚好符合我们的要求。每次取出当前集合代表的 map 的头头,输出后删除即可。

代码:

const int M=305;
int n;
int fa[M],a[M];
map<int,int> mapp[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y) return ;
	if(mapp[x].size()>mapp[y].size()) swap(x,y);
	fa[x]=y;
	for(auto it:mapp[x]) mapp[y][it.first]=1;
}//启发式合并
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],fa[i]=i,mapp[i][a[i]]=1;
	char opt;
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=n;++j)
		{
			cin>>opt;
			if(opt=='1') merge(i,j);
		}
	}
	for(int i=1,x,pos;i<=n;++i)
	{
		pos=find(i);
		x=mapp[pos].begin()->first;
		cout<<x<<" ";
		mapp[pos].erase(x);//输出之后就删了,每次都是输出的当前集合剩下的值最小的
	}
	return 0;
}

CF659E New Reform

并查集找环的转化。首先考虑什么城市会成为孤立城市?也就是没有入边的城市。那么在一个环上,每个点肯定要都有入边,所以环上的所有点都不是孤立城市,除此之外的图中,由于边会定向,所以每个集合内必然会有一个城市成为孤立城市

所以就转化为查询图中有多少个不含环的集合,并查集好判断,如果对于一条边 \(u \to v\) 出现了 \(find_u=find_v\),那么这个集合就一定有环了(可以这么理解,因为并查集实际上就是一棵树,在这棵树上再加一条边,一定会形成一个环)。

代码:

const int M=1e5+5;
int n,m;
int fa[M],vis[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) fa[i]=i;
	for(int i=1,a,b;i<=m;++i)
	{
		cin>>a>>b;
		a=find(a),b=find(b);
		if(a==b) vis[a]=1;//当前集合有环了
		else fa[a]=b,vis[b]+=vis[a];//有环的情况在转移的时候也需要一起转移
	}
	int ans=0;
	for(int i=1;i<=n;++i)
	{
		if(i==find(i)&&!vis[i]) ++ans;
	}
	cout<<ans<<"\n";
	return 0;
}

CF939D Love Rescue

注意到这题有点扯,它是可以花费1的代价,指定两个字母,把其中一个全部变为另一个。相当于我们可以花 1 的代价把两种字符合并在一起,那么需要合并的时候合并就是了(也没法咋贪心)。

代码:

const int M=1e5+5;
int n;
int fa[M];
string s,t;
vector<pcc> ans;
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	cin>>s>>t;
	for(int i=1;i<=26;i++) fa[i]=i;
	for(int i=0,x,y;i<s.size();i++)
	{
		if(s[i]!=t[i])
		{
			x=s[i]-'a'+1,y=t[i]-'a'+1;
			x=find(x),y=find(y);
			if(x==y) continue;
			fa[x]=y;//如果需要合并
			ans.push_back(mk(s[i],t[i]));//记录一下是把哪种字符转化成哪种字符
		}
	}
	cout<<ans.size()<<"\n";
	for(auto it:ans) cout<<it.first<<" "<<it.second<<"\n";
	return 0;
}

CF1609D Social Network

对于 \(\forall i\in[1,d]\),求在满足 \([1,i]\) 的规定的前提下恰好连 \(i\) 条边的无向图中度数最大的点的度数最大可能是多少。那么对于每一个 \(i\) 判断一下当前给定的 \(x,y\) 是否相连,如果不相连那么就只能将它们两个连起来,如果两个已经相连了,那么我们就多了一条可以自由连接的边。

一个连通块内最大点的度数当然就是让其它点都与这个点相连,最大度数为 \(siz_i-1\),于是对于每一个 \(i\) 记录下它有 \(x\) 条可以自由使用的边,然后将前 \(x+1\) 大的连通块串一堆就是了,答案就是前 \(x+1\) 大的连通块大小总和-1.

注意:对于每一个 \(i\),生成的图都是不一样的。(不是真连)

代码:

const int M=1005;
int n,m;
int fa[M],siz[M];
vector<int> res;

inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}

inline int merge(int x,int y)
{
	int fx=find(x),fy=find(y);
	if(fx==fy) return 0;
	fa[fx]=fy;
	siz[fy]+=siz[fx];
	return 1;
}

inline bool cmp(int a,int b){return a>b;}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) fa[i]=i,siz[i]=1;
	int cnt=0,x,y,ans;
	while(m--)
	{
		cin>>x>>y,ans=0,res.clear();
		if(!merge(x,y)) cnt++;//当前可以自由使用的边数
		for(int i=1;i<=n;++i)
		{
			if(i==fa[i]) res.push_back(siz[i]);//将所有连通块扔进去,找前cnt+1个
		}
		sort(res.begin(),res.end(),cmp);
		for(int i=0;i<=cnt&&i<res.size();++i) ans+=res[i];
		cout<<ans-1<<"\n";//度数最大的点=连通块大小-1
	}
	return 0;
}

CF731C Socks

给定 \(n\) 只袜子的颜色,有 \(m\) 天,每天需要穿第 \(x_i\) 只和第 \(y_i\) 只袜子,限定每天穿的袜子颜色必须一样,询问至少需要修改几只袜子的颜色。

考虑将\(m\) 天给定的 \(x_i\)\(y_i\) 只袜子合并在一起,这样在一个集合内的袜子应该是同一个颜色的(但是给定的肯定存在不同颜色的袜子)。那么对于一个集合,由于里面的元素需要改成同一个颜色,贪心的让需要改颜色的袜子数量最少,那么肯定都是改成这个集合内原本数量最多的颜色

于是在合并完之后,对于每一个集合统计一下其包含的元素个数以及出现次数最多的颜色出现的次数,答案就是每个集合的大小减去出现次数最多的颜色出现的次数之和。

代码:

const int M=2e5+5;
int n,m,k,ans;
int c[M],fa[M],maxx[M];
vector<int> t[M];
map<int,int> mapp[M]; 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++) cin>>c[i],fa[i]=i;
	for(int i=1,a,b,x,y;i<=m;i++)
	{
		cin>>a>>b;
		x=find(a),y=find(b);
		if(x==y) continue;
		fa[x]=y;
	}
	for(int i=1,pos;i<=n;i++)
	{
		pos=find(i);
		t[pos].push_back(i),++mapp[pos][c[i]];
		maxx[pos]=max(maxx[pos],mapp[pos][c[i]]);//当前这个集合出现次数最多的颜色出现的次数
	}
	for(int i=1;i<=n;i++) ans+=t[i].size()-maxx[i];
	cout<<ans<<"\n"; 
	return 0;
}

CF723D Lakes in Berland

简单题(其实用bfs就可以了),可以使用并查集维护每一个湖的大小,同时对于每一个湖记录一下它包含的水格下标(用启发式合并),然后从小到大一个一个连通块的覆盖。

作为练习题了。(其实是因为我写的 bfs 啦)

CF741B Arpa's weak amphitheater and Mehrdad's valuable

并查集配合dp,因为有时候题目可能需要用到并查集预处理,如这一道题,要判断选择与一个人认识的所有的就需要用到并查集把所有认识的人合并起来。

然后就是典型的分组背包了,对于一个集合有 3 种情况:一个也不请,请一个,请所有人。

学习背包推荐背包九讲(虽然背包一般都还是比较简单)

代码:

const int M=2005;
int n,m,W,tot;
int w[M],v[M],fa[M],dp[M],num[M];
int pos[M][M];
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>W;
	for(int i=1;i<=n;++i) cin>>w[i];
	for(int i=1;i<=n;++i) cin>>v[i],fa[i]=i;
	for(int i=1,x,y;i<=m;++i)
	{
		cin>>x>>y,x=find(x),y=find(y);
		if(x==y) continue;
		fa[x]=y;
	}//使用并查集维护集合
	tot=n;
	for(int i=1,x;i<=n;++i)
	{
		x=find(i);
		++num[x],pos[x][num[x]]=i;
	}
	for(int i=1;i<=n;++i)
	{
		if(num[i]>1)
		{
			++num[i],++tot;
			pos[i][num[i]]=tot;
			for(int j=1;j<num[i];j++) w[tot]+=w[pos[i][j]],v[tot]+=v[pos[i][j]];
		}//统计出请全部人的代价&收益
	}
	for(int i=1;i<=n;++i)
	{
		if(!num[i]) continue;
		for(int j=W;j>=0;--j)
		{
			for(int k=1,x;k<=num[i];k++)
			{
				x=pos[i][k];
				if(j-w[x]<0) continue;
				dp[j]=max(dp[j],dp[j-w[x]]+v[x]);
			}
		}
	}//分组背包
	cout<<dp[W]<<"\n";
	return 0;
}

CF28B pSort

由于位置 \(i\) 上的数可以和位置 \(i \pm d_i\) 互相交换,那么我们就把 \(i\)\(i \pm d_i\) 合并起来,对于一个集合内的位置,随便怎么交换都是可以的。

初始化的时候位置 \(i\) 上的数就是 \(i\),这也方便了我们操作,我们最后只需要看给定位置 \(i\) 上的数与 \(i\) 是不是在同一个集合即可。如果在那么可行,否则无解。(太早之前写的了,有点蠢,代码的逻辑是统计每一个集合出现的数,然后判断当前位置所在的集合有没有这个数,没有就没解)。

代码:

const int M=105;
int n;
int a[M],b[M],fa[M];
map<int,int> mapp[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y) return ;
	fa[x]=y;
}
 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i],fa[i]=i;
	for(int i=1;i<=n;++i)
	{
		cin>>b[i];
		if(i-b[i]>=1) merge(i-b[i],i);
		if(i+b[i]<=n) merge(i+b[i],i);
	}
	for(int i=1;i<=n;++i) ++mapp[find(i)][i];
	for(int i=1,pos;i<=n;++i)
	{
		pos=find(i);
		if(mapp[pos][a[i]]) --mapp[pos][a[i]];
		else {cout<<"NO\n";return 0;}
	}
	cout<<"YES\n";
	return 0;
}

CF1383A String Transformation 1

和前面那道 CF939D Love Rescue 差不多啊,也是只要当前不相等了就去合并,如果确实不在同一个集合就 \(++ans\)

代码:

const int M=1e5+5;
int T,n,flag,ans;
int fa[M];
string s,t;
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y) return ;
	++ans,fa[x]=y; 
}
 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>T;
	while(T--)
	{
		cin>>n,flag=ans=0;
		cin>>s>>t;
		for(int i=1;i<=26;++i) fa[i]=i;
		for(int i=0;i<n;++i)
		{
			if(s[i]>t[i]) {cout<<"-1\n",flag=1;break;}
			merge(s[i]-'a'+1,t[i]-'a'+1);
		}
		if(!flag) cout<<ans<<"\n";
	}
	return 0;
}

CF1209D Cow and Snacks

需要一定的转换。第一位客人肯定把他喜爱的花全会取走,为了让结果更优,我们希望接下来的客人喜爱的花与之有重合,这样他就只会取走一朵花,以此类推……。

于是我们将每个人喜欢的两种花合并在一起,形成若干个连通块,那么对于一个大小为 \(x\) 连通块就有 \(x-1\) 个人获得满足,那么相当于是并查集里成功合并两个集合,肥宅数就会少 1 。

代码:

const int M=1e5+5;
int n,k,ans;
int fa[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;++i) fa[i]=i;
	for(int i=1,x,y;i<=k;++i)
	{
		cin>>x>>y;
		x=find(x),y=find(y);
		if(x==y) continue;
		fa[x]=y,++ans;
	}
	cout<<k-ans<<"\n";
	return 0;
}

CF1253D Harmonious Graph

也就是说只要存在 \(l\)\(r\) 的路径( \(l<r\) ),那么就一定会有 \(l\)\(i, l<i<r\) 的路径,相当于有点像区间覆盖了。这时候并查集有什么用?很显然并查集可以帮助我们快速的得到一个点能到达的下标最大的地方(只需要拿一个 \(maxx\) 数组记录一下),那么同时我们也能得到一个点能到达的下标最小的地方。为什么要记录下标最小的地方,因为此时你和这个最小的已经连通了,和最大的也连通了,那么最小的必然会到达最小和最大的中间的每一个点。

于是思路清晰,使用并查集维护各个点之间的连通性,同时统计每个集合内最小能到达的点&最大能到达的点,然后将最小位置的那个点与中间的所有点相连,看有多少个与其不在一个集合的点,同时将这些点也合并起来。

代码:

const int M=2e5+5;
int n,m,ans;
int fa[M],maxx[M],minn[M],vis[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y) return ;
	fa[x]=y,minn[y]=min(minn[y],minn[x]),maxx[y]=max(maxx[y],maxx[x]);
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) fa[i]=maxx[i]=minn[i]=i;
	for(int i=1,x,y;i<=m;i++) cin>>x>>y,merge(x,y);
	for(int i=1,pos;i<=n;i++)
	{
		pos=find(i);
		if(vis[pos]) continue;
		vis[pos]=1;
		for(int j=minn[pos]+1,x;j<maxx[pos];j++)//还有一个维护minn原因是在合并的过程中集合的minn可能变化
		{
			x=find(j);
			if(x!=pos) merge(x,pos),++ans;
		}
	}
	cout<<ans<<"\n";
	return 0;
}

CF1411C Peaceful Rooks

神秘转化,保证一开始没有冲突。由于棋子都是车,可以随便开,那么最优解就是每个车开到自己相对应的主对角线上即可,此时会花费 \(m-x\) 次操作(\(x\) 是那些原本就在对角线上的车)。但是题目要求移动的过程中不能有两个车可以互相吃掉对方,那么什么时候会产生这种冲突,自然是存在环的时候(如果一个车一开始不在主对角线上的时候,它实质上会占据两个主对角线上的位置,即 \((x,x),(y,y)\)),这个时候我们需要多花费 1 的代价来将这个环破坏掉。

判环就用并查集,答案就是 \(m+环的数量-x\)

代码:

const int M=1e5+5;
int T,n,m,ans;
int fa[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x]; 
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>T;
	while(T--)
	{
		cin>>n>>m,ans=m;
		for(int i=1;i<=n;i++) fa[i]=i;
		for(int i=1,x,y;i<=m;i++)
		{
			cin>>x>>y;
			if(x==y) {--ans;continue;}
			x=find(x),y=find(y);
			if(x==y) ++ans;
			else fa[x]=y;
		}
		cout<<ans<<"\n";
	}
	return 0;
}

CF1131F Asya And Kittens

并查集启发式合并。给定的两个数所在的连通块相邻,但是对于一个连通块内的数字其位置实际上变化是没有什么影响的,那么我们考虑对于两个连通块合并的时候其实就是将其中一个连通块放在合并过去的连通块的左边/右边

于是每一个连通块用 vector 记录一下连通块内包含的数,然后合并的时候启发式合并,将小连通块加到大连通块右边。这样保证正确。

代码:

const int M=2e5+5;
int n;
int fa[M];
vector<int> t[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) fa[i]=i,t[i].push_back(i);
	for(int i=1,x,y;i<n;++i)
	{
		cin>>x>>y;
		x=find(x),y=find(y);
		if(x==y) continue;
		if(t[x].size()>t[y].size()) swap(x,y);//启发式合并
		fa[x]=y;
		for(auto it:t[x]) t[y].push_back(it);
	}
	for(int i=1;i<=n;++i)
	{
		if(i==find(i))
		{
			for(auto it:t[i]) cout<<it<<" ";
		}
	}
	cout<<"\n";
	return 0;
}

CF1985H1 Maximize the Largest Component (Easy Version)

对于一张地图,有#.,两种符号,问你改变其中一行/一列全部为 #,能实现的最大的 # 连通块大小。发现 \(n*m \leq 1e6\),所以我们可以暴力枚举每一行/列变为 # 能得到的最大连通块大小,对于初始图将所有连通的 # 合并在一起,然后对于每一行/列减去这上面本来的 # 数量,然后加上附近的 # 集合大小再加 \(n\)\(m\),找出最大值即可。

代码:

const int M=1e6+5;
int T,n,m;
int t[M],sz[M];
string str[M];
set<int> s;//周围相同的集合自然只能计算一次,所以用set存(相同的数只会存一次,这是c++中的集合STL)
 
inline int find(int x)
{
	if(t[x]!=x) t[x]=find(t[x]);
	return t[x];
}
inline void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x!=y) t[y]=x,sz[x]+=sz[y];
}
inline void solve()
{
	cin>>n>>m;
	for(int i=0;i<n*m;++i) t[i]=i,sz[i]=1;
	for(int i=0;i<n;++i) cin>>str[i];
	vector<int> cnt1(n+5,0),cnt2(m+5,0);
	for(int i=0;i<n;++i)
	{
		for(int j=0;j<m;j++)
		{
			if(str[i][j]=='#')
			{
				++cnt1[i],++cnt2[j];
				if(i+1<n&&str[i+1][j]=='#') merge(i*m+j,(i+1)*m+j);
				if(j+1<m&&str[i][j+1]=='#') merge(i*m+j,i*m+j+1);
			}
		}
	}
	int ans=0;
	//遍历行 
	for(int i=0;i<n;++i)
	{
		int res=m-cnt1[i];s.clear();
		for(int k=i-1;k<=i+1;++k)
		{
			if(k<0||k>=n) continue;
			for(int j=0;j<m;j++)
			{
				if(str[k][j]=='#')
				{
					s.insert(find(k*m+j));
				}
			}
		}
		int sum=0;
		for(auto it:s) sum+=sz[it];
		ans=max(ans,sum+res);
	}
	//遍历列
	for(int j=0;j<m;j++)
	{
		int res=n-cnt2[j];s.clear();
		for(int k=j-1;k<=j+1;++k)
		{
			if(k<0||k>=m) continue;
			for(int i=0;i<n;++i)
			{
				if(str[i][k]=='#') s.insert(find(i*m+k));
			}
		}
		int sum=0;
		for(auto it:s) sum+=sz[it];
		ans=max(ans,sum+res);
	}
	cout<<ans<<"\n";
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>T;
	while(T--) solve();
	return 0;
}

CF691D Swaps in Permutation

这不就是上面 CF28B pSort 的双倍经验,只不过换了可以交换位置的描述方法,合并完用 map 统计一下每个集合包含的元素,然后输出是输出字典序最大,那么每次取出 map 的结尾元素即可。

代码:

const int M=1e6+5;
int n,m;
int a[M],fa[M];
map<int,int> mapp[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>a[i],fa[i]=i;
	for(int i=1,x,y;i<=m;++i)
	{
		cin>>x>>y,x=find(x),y=find(y);
		if(x==y) continue;
		fa[x]=y;
	}
	for(int i=1;i<=n;++i)
	{
		mapp[find(i)][a[i]]=1;
	}
	for(int i=1,pos,x;i<=n;++i)
	{
		pos=find(i);
		auto it=mapp[pos].end();--it;//把结尾找出来
		cout<<it->first<<" ";
		mapp[pos].erase(it->first);//输出完就删掉
	}
	
	return 0;
}

CF1213G Path Queries

太经典了,详记一下。对于一张带有边权的树,多次询问,每次询问最大权值不超过 \(x\) 的简单路径数量。

实际上是最小生成树的一类经典转化,由于我们做最小生成树的时候是从小到大的加边,那么我们考虑对于每一次加边有什么影响,设边 \(x \ to y\) ,记 \(x,y\) 所在的连通块大小分别为 \(siz_x,siz_y\),当前边的权值为 \(w\),由于边是从小到大的加,当前这条边一定就是当前图内权值最大的边,又因为两边原来不连通,现在连通之后 \(x\) 所在连通块内的点每个点都可以到达 \(y\) 所在连通块内的点了。

图是树所以两点之间只会有一条路径,所以加了这条边之后图上就多了 \(siz_x*siz_y\) 条路径,并且由于当前边的权值最大,所以路径最大权值为 \(w\) 的路径数就增加了 \(siz_x*siz_y\) 。从小到大的加边的同时,将路径最大权值为 \(w\) 的路径数就增加的数量记录下来,然后求的是不大于,那么做一遍前缀和即可。询问就可以随便回答了。

代码:

const int M=2e5+5;
int n,q;
int ans[M],fa[M],siz[M];

struct N{
	int u,v,w;
};N p[M<<1];

inline bool cmp(N a,N b)
{
	return a.w<b.w;
}

inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1;
	for(int i=1;i<n;i++) cin>>p[i].u>>p[i].v>>p[i].w;
	sort(p+1,p+n,cmp);//边权从小到大排序
	for(int i=1;i<n;i++)
	{
		int x=find(p[i].u),y=find(p[i].v);
		ans[p[i].w]+=siz[x]*siz[y];//统计贡献,由于是树,每条边一定加的进去
		siz[y]+=siz[x];
		fa[x]=y;
	}
	for(int i=1;i<M;i++) ans[i]+=ans[i-1];
	int x;
	while(q--)
	{
		cin>>x;
		cout<<ans[x]<<" ";
	}
	return 0;
}

CF371D Vessels

这题实际上是用链表做的,但是并查集可以完成一些链表才能做的操作。对于一个沙漏自然如果它满了的话就之后就不用管了,那么如果一个沙漏满了,我们就将这个沙漏于合并到下一个沙漏去(强制让小的合并到大的里面,之后查找小的沙漏返回的就是最下面的沙漏编号),直到流到地板。

代码:

const int M=2e5+5;
int n,q,front;
int a[M],fa[M],t[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y) return ;
	fa[x]=y;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i],fa[i]=i;
	fa[n+1]=n+1;
	cin>>q;
	int opt,x,y;
	while(q--)
	{
		cin>>opt>>x;
		if(opt==1)
		{
			cin>>y,front=0;
			while(1)
			{
				if(a[x]-t[x]>y) {t[x]+=y;break;}
				else
				{
					y-=(a[x]-t[x]),t[x]=a[x];
					if(front) merge(x-1,x);//满了的话就合并到下一个沙漏去
				}
				front=x,x=find(x+1);
				if(x==n+1) break;
			} 
		}
		else
		{
			if(x==find(x)) cout<<t[x]<<"\n";
			else cout<<a[x]<<"\n";
		}
	}
	return 0;
}

CF1468J Road Reform

首先题目要求生成树的边权最大值正好等于 \(k\),那么我们自然需要跑一遍最小生成树,然后判断用到的边的最大权值是否已经大于等于 \(k\)。如果用到的最大权值已经大于等于 \(k\),那么我们就需要把这些边的边权改为 \(k\)。否则对于使用的最大权值都还没有 \(k\) 大的话,那么就在所有边找一条绝对值差与 \(k\) 最小的边即可。(只用去替换一条边)

代码:

const int M=2e5+5;
int T,n,m,k,ans,maxx;
int fa[M];
struct node{
	int u,v,w;
	inline bool operator <(const node &o) const
	{
		return w<o.w;
	}
};node a[M];
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>T;
	while(T--)
	{
		cin>>n>>m>>k,ans=maxx=0;
		for(int i=1;i<=n;++i) fa[i]=i;
		for(int i=1;i<=m;++i) cin>>a[i].u>>a[i].v>>a[i].w;
		sort(a+1,a+m+1);
		for(int i=1,x,y;i<=m;++i)
		{
			x=find(a[i].u),y=find(a[i].v);
			if(x==y) continue;
			fa[x]=y,maxx=max(maxx,a[i].w),ans+=max(0,a[i].w-k);
		}
		if(maxx<k)
		{
			ans=1e9;
			for(int i=1;i<=m;++i) ans=min(ans,abs(k-a[i].w));//最大边权小于 k ,那么随便找一条绝对值差与k最小的边替换最小生成树中的一条边
		}
		cout<<ans<<"\n";
	}
	return 0;
}

CF1081D Maximum Distance

比较有意思的一道题,对于一张无向图给定定义两点之间的距离是其在 MST 上路径的最大值,并给定一些特殊点,对于每一个特殊点,找出离它最远的另一个特殊点之间的距离。

首先根据定义我们就需要跑最小生成树,那么跑最小生成树的时候,只有在其中一条边加入图中之后(由于从小到大嘛),图上的给定的特殊点才全部连通,所以相当于大家的答案都是相同的,就是最后加入的那条使得特殊点连通的边的权值

那么考虑使用并查集,对于当前一条边,如果连的两边都是特殊点,那么答案更新为这条边的权值(不然的话这两点不连通啊,而且此时这条边的权值是最大的),然后对于一个特殊点与一个普通点的连边,我们将这个普通点也化为特殊点(因为此时所有的特殊点都还没有连通,答案一定大于等于当前边的权值,所以将这个普通点化为特殊点)。

那么操作显然了,对于两边都是特殊点的边,将答案更新为当前边的权值,对于特殊点与普通点的连边,将普通点变为特殊点,最后就是输出 \(k\) 次答案。

代码:

const int M=1e5+5;
int n,m,k,ans;
int fa[M],vis[M];
struct node{
	int u,v,w;
	inline bool operator <(const node &o) const
	{
		return w<o.w;
	}
};node a[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>k;
	for(int i=1;i<=n;++i) fa[i]=i;
	for(int i=1,x;i<=k;++i) cin>>x,vis[x]=1;
	for(int i=1;i<=m;++i) cin>>a[i].u>>a[i].v>>a[i].w;
	sort(a+1,a+m+1);
	for(int i=1,x,y;i<=m;++i)
	{
		x=find(a[i].u),y=find(a[i].v);
		if(x==y) continue;
		if(vis[x]&&vis[y]) ans=a[i].w;
		if(vis[x]||vis[y]) vis[x]=vis[y]=1;
		fa[x]=y; 
	}
	for(int i=1;i<=k;++i) cout<<ans<<" ";
	cout<<"\n";
	return 0;
}

CF436C Dungeons and Candies

妙妙题。首先由于我们需要传输 \(k\) 个文件,每个文件大小为 \(n*m\),我们有两种传输文件的方法

  • 直接传输,代价为 \(n*m\)

  • 比较以上一个传输文件的差别,代价为 \(遇上一个文件不同的字符数*w\)\(w\) 是一个给定的常数。

不要求 \(k\) 个文件的传输顺序,可以按任意顺序传送。那么我们就可以考虑将原题抽象为一张图,每个文件就是一个点,点与点之间有边,边就是代价,我们需要让传输的代价最小,相当于就是我们需要让原图连通的选择的边权和最小(那就是最小生成树了)。那么根据传输文件的方法,我们需要连两种边(一种是直接传输的边,我们建立一个虚点 \(S=k+1\),每个点向 \(S\) 连一条边权为 \(n*m\) 的边,这也同时满足了传输文件至少需要使用一次直接传输,否则模板都没有,怎么使用第二种传输方式)。然后对于 \(k\) 个文件,不同文件比对之间的不同的字符,然后计算代价。

现在把所有的边都统计下来了,跑一遍最小生成树,然后由于需要输出方案,这个也比较简单,我们只需要将最小生成树上的边记录下来,然后从 \(S\) 开始 dfs,每次遍历到 \(S\) 点,那么使用的就是方法 1 ,否则使用的是方法 2。

代码:

const int M=1005;
int n,m,k,w,num,tot;ll ans;
int fa[M];
char a[M][11][11];
queue<int> q;
 
struct node{
	int u,v,w;
	inline bool operator <(const node &o) const
	{
		return w<o.w;
	}
};node e[M*M];
 
int cnt=0;
struct N{
	int to,next;
};N p[M<<1];
int head[M],in[M];
inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
 
inline int solve(int x,int y)
{
	int res=0;
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=m;++j) res+=(a[x][i][j]!=a[y][i][j]);
	}
	return res*w;
}
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline void dfs(int u,int f)
{
	if(u<=k) cout<<u<<" "<<(f==k+1?0:f)<<"\n"; 
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		dfs(v,u);
	}
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>k>>w,fa[k+1]=k+1;
	for(int pos=1;pos<=k;++pos)
	{
		fa[pos]=pos,e[++num]={pos,k+1,n*m};
		for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) cin>>a[pos][i][j];
	}//第一种传输方法
	for(int i=1;i<=k;++i)
	{
		for(int j=i+1;j<=k;++j) e[++num]={i,j,solve(i,j)};
	}//第二种传输方法
	sort(e+1,e+num+1);
	for(int i=1,x,y;i<=num;++i)
	{
		x=find(e[i].u),y=find(e[i].v);
		if(x==y) continue;
		fa[x]=y,ans+=e[i].w;//最小生成树
		add(e[i].u,e[i].v),add(e[i].v,e[i].u);//将加入的树边统计下来
	}
	cout<<ans<<"\n";
	dfs(k+1,0);//最后 dfs 一遍输出方案
	return 0;
}

CF25D Roads not only in Berland

给定一个有 \(n\) 个顶点,\(n−1\) 条边的无向图。每一次操作都能去掉一条边,并且重新在某两个顶点之间连一条新边。询问需要多少次操作,才能使得从任一顶点都能到达其他的所有顶点,并构造方案。

由于最后生成的就是一颗树嘛,那么那么一开始使用并查集判断那些边是无效的(即加入之后并不会使连通块的数量减少),这些边就是需要断掉的边。将这些边做一个标记,就是需要断掉的边。然后再选取一个连通块,将剩下的没有与其相连的连通块都与这个连通块的一个点连一条边即可。

代码:

const int M=1005;
int n,res,root,cnt;
int fa[M],siz[M];
struct node{
	int a,b,opt;
};node p[M];
pii t[M];
inline bool cmp(pii a,pii b){return a.first>b.first;}
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x]; 
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) fa[i]=i,siz[i]=1;
	for(int i=1,fx,fy;i<n;++i)
	{
		cin>>p[i].a>>p[i].b;
		fx=find(p[i].a),fy=find(p[i].b);
		if(fx==fy) {++res,p[i].opt=1;continue;}
		fa[fx]=fy,siz[fy]+=siz[fx],siz[fx]=0;
	}
	for(int i=1;i<=n;++i) if(i==fa[i]) {root=i;break;}
	for(int i=1;i<=n;++i) if(find(i)!=root) t[++cnt]=make_pair(siz[i],i);
	sort(t+1,t+cnt+1,cmp);
	cout<<res<<"\n";
	for(int i=1,pos=0;i<=n;++i)
	{
		if(p[i].opt)
		{
			cout<<p[i].a<<" "<<p[i].b<<" "<<root<<" "<<t[++pos].second<<"\n";
		}
	}
	return 0;
}

CF547B Mike and Feet

首先定义了一个序列的强度是这个序列的最小值,给定了一个长为 \(n\) 的序列然后询问对于长度为 \(1 \sim n\) 的所有子序列序列强度最强的强度。那么由于询问的是最强的强度,而强度之和最小值相关,所以我们可以考虑从大到小的加入各个元素,然后和上面题一样的,如果发现左右的数已经存在了,就合并。合并的同时记录一下集合的大小(其实就是序列的长度)并更新答案。

设当前答案已经更新到长度为 \(res\) 的子序列,如果这个数插入进序列后并合并且集合的大小 \(len\) 大于 \(res\),那么由于之后加入的数权值都是小于等于自己的,所以对于长度为 \(res \sim len\) 的子序列强度大小最强就是当前插入的这个点的权值了。

代码:

const int M=2e5+5;
int n,res=1;
int fa[M],siz[M],vis[M],ans[M];
struct node{
	int x,pos;
	inline bool operator <(const node &o) const
	{
		return x>o.x;
	}
};node p[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
 
inline void merge(int x,int y)
{ 
	x=find(x),y=find(y);
	if(x==y) return ;
	fa[x]=y,siz[y]+=siz[x];
}
 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>p[i].x,p[i].pos=i,siz[i]=1,fa[i]=i;
	sort(p+1,p+n+1);//从大到小的插入每一个数
	for(int i=1,fx,fy;i<=n;++i)
	{
		vis[p[i].pos]=1;
		if(vis[p[i].pos-1]) merge(p[i].pos-1,p[i].pos);//左边已经插入了就和左边的合并
		if(vis[p[i].pos+1]) merge(p[i].pos+1,p[i].pos);//右边已经插入了就和右边的合并
		if(siz[find(p[i].pos)]>=res)//如果插入之后使得子序列长度有突破,那么此时一定是当前长度序列强度最强的时候
		{
			ans[res]=p[i].x;
			res=siz[find(p[i].pos)]+1;
		}
	
	for(int i=1;i<=n;++i)
	{
		if(!ans[i]) ans[i]=ans[i-1];//类似于前缀和的做法
		cout<<ans[i]<<" ";
	}
	return 0;
}

CF1620E Replace the Numbers

水题,对于每一个数开一个点,记录一下当前数真实的数即可。(好像并没有用到并查集ing)

代码:

const int M=5e5+5;
int n;
int t[M];
struct node{
	int opt,x,y;
};node p[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=5e5;++i) t[i]=i;
	for(int i=1;i<=n;++i)
	{
		cin>>p[i].opt>>p[i].x;
		if(p[i].opt==2) cin>>p[i].y;
	}
	for(int i=n;i>=1;--i)
	{
		if(p[i].opt==1) p[i].x=t[p[i].x];
		else t[p[i].x]=t[p[i].y];
	}
	for(int i=1;i<=n;++i) if(p[i].opt==1) cout<<p[i].x<<" ";
	cout<<"\n";
	return 0;
}

CF1245D Shichikuji and Power Grid

最小生成树类问题,和上面 CF436C 这题挺类似的。还是建一个超级源点 \(S\),每个点向 \(S\) 连一条边权为 \(c_i\) 的边,代表在 \(i\) 修发电站的代价,然后因为 \(n \leq 2000\),所以对于任意的两座城市都连一条边,然后跑最小生成树即可。

答案就是最小生成树的边权和,输出方案也简单,只需要将最小生成树连的边记录下来即可,与 \(S\) 连边就说明是建发电站,否则是在连电缆。

代码:

const int M=2005;
int n,ans,tot;
int fa[M],k[M];
vector<int> res;
vector<pair<int,int>> e;
 
int cnt=0;
struct N{
	int u,v,w,opt;
	inline bool operator <(const N &o) const
	{
		return w<o.w;
	}
};N p[M*M];
struct node{
	int x,y;
};node a[M];
 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
 
inline int solve(int i,int j) {return (k[i]+k[j])*(abs(a[i].x-a[j].x)+abs(a[i].y-a[j].y));}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n+1;++i) fa[i]=i;
	for(int i=1;i<=n;++i) cin>>a[i].x>>a[i].y;
	for(int i=1,x;i<=n;++i) cin>>x,p[++cnt]=(N){n+1,i,x,1};//建发电站
	for(int i=1;i<=n;++i) cin>>k[i];
	for(int i=1;i<=n;++i)
	{
		for(int j=i+1;j<=n;++j) p[++cnt]=(N){i,j,solve(i,j),0};//连电缆
	}
	sort(p+1,p+cnt+1);//最小生成树
	for(int i=1,fx,fy;i<=cnt;++i)
	{
		fx=find(p[i].u),fy=find(p[i].v);
		if(fx==fy) continue;
		++tot,ans+=p[i].w,fa[fx]=fy;
		if(p[i].opt) res.push_back(p[i].v);
		else e.push_back(make_pair(p[i].u,p[i].v));
		if(tot==n) break;
	}
	cout<<ans<<"\n";
	cout<<res.size()<<"\n";
	for(int it:res) cout<<it<<" ";cout<<"\n";
	cout<<e.size()<<"\n";
	for(auto it:e) cout<<it.first<<" "<<it.second<<"\n"; 
	return 0;
}

CF1095F Make It Connected

给你 \(n\) 个点,每个点有一个权值 \(a_i\),已知连接两点的代价为 \(a_i+a_j\) ,现在还有其他的 \(m\) 种连接方法,连接 \(u,v\) 的费用为 \(w\) 。求出让这个图连通的最小代价。

明显是最小生成树,但是 \(n\) 的范围很不友好 \(n \leq 2e5\),显然没法 \(O(n^2)\) 暴力建边。但是我们可以贪心啊,对于每一个点,肯定是宁愿同 \(a\) 值最小的那个点连边啊,于是我们现在就只需要连 \(n-1\) 条边了,再加上特殊的 \(m\) 条边,统一跑最小生成树即可。

代码:

const int M=2e5+5;
int n,m,ans;
int fa[M];

int cnt=0;
struct N{
	int u,v,w;
	inline bool operator <(const N &o) const
	{
		return w<o.w;
	}
};N p[M<<1];
struct node{
	int x,id;
	inline bool operator <(const node &o) const
	{
		return x<o.x;
	}
};node a[M]; 
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>a[i].x,a[i].id=fa[i]=i;
	sort(a+1,a+n+1);
	for(int i=2;i<=n;++i) p[++cnt]=(N){a[1].id,a[i].id,a[1].x+a[i].x};
	for(int i=1,x,y,z;i<=m;++i) cin>>x>>y>>z,p[++cnt]=(N){x,y,z};
	sort(p+1,p+cnt+1);
	for(int i=1,fx,fy;i<=cnt;++i)
	{
		fx=find(p[i].u),fy=find(p[i].v);
		if(fx==fy) continue;
		fa[fx]=fy,ans+=p[i].w; 
	}
	cout<<ans<<"\n";
	return 0;
}

CF1624G MinOr Tree

给定一些边,求或运算下的最小生成树。有点牛啊,观察到边权只有 \(1e9\) 的级别,转化为 2 进制就是 30 位左右,那么我们可以强行枚举,强行枚举不使用某一位上为 1 的边(当然要从高位向低位枚举了),如果不行,那么答案就需要加上 10 进制下这个位置的值。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e5+5;
int T,n,m,ans;
int fa[M];
struct node{
	int u,v,w;
};node a[M];
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline int check(int x,int k)
{
	for(int i=1;i<=n;++i) fa[i]=i;
	for(int i=1,fx,fy;i<=m;++i)
	{
		if((a[i].w|x)>=(x+(1ll<<(k+1)))) continue;
		if((a[i].w>>k)&1) continue;
		fx=find(a[i].u),fy=find(a[i].v);
		if(fx==fy) continue;
		fa[fx]=fy; 
	}
	for(int i=2;i<=n;++i) if(find(1)!=find(i)) return 0;
	return 1;
}
 
inline void solve()
{
	cin>>n>>m,ans=0;
	for(int i=1;i<=m;++i)
	{
		cin>>a[i].u>>a[i].v>>a[i].w;
	}
	for(int i=31;i>=0;--i) if(!check(ans,i)) ans|=(1ll<<i);//枚举每一位不为1,如果不连通就只能加上权值
	cout<<ans<<"\n";
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>T;
	while(T--) solve(); 
	return 0;
}

CF472D Design Tutorial: Inverse the Problem

题意很简单啊,给定一棵节点为 \(n\) 的带权树后,现在给出两两点对之间的距离,询问是否对应一棵树。那么对原图跑最小生成树就是原树了。为什么是这样,因为你既然已经是最小生成树了,说明在其中一个点对距离已经给定了,你要是替换为一条权值较大的边,那显然违反了这一个点对距离。

于是建出最小生成树了,树的形态已知,\(n \leq 2000\) ,那么 \(O(n^2)\) 枚举一个一个点对去验证就可以了,有很多种方法,这里采用树剖LCA 。

代码:

const int M=2e3+5;
int n,m;
int a[M][M],fa[M];
 
int tot=0,cnt=0;
struct node{
	int u,v,w;
	inline bool operator <(const node &o) const
	{
		return w<o.w;
	}
};node e[M*M/2];
struct N{
	int to,next,val;
};N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b,p[cnt].val=c;
}
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
 
int deep[M],fax[M],siz[M],son[M];
inline void dfs1(int u,int f)
{
	siz[u]=1,fax[u]=f;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		deep[v]=deep[u]+p[i].val;
		dfs1(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
int id[M],num,top[M];
inline void dfs2(int u,int topp)
{
	top[u]=topp,id[u]=++num;
	if(!son[u]) return ;
	dfs2(son[u],topp);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(top[v]) continue;
		dfs2(v,v);
	}
}
inline int lca(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(deep[top[x]]<deep[top[y]]) swap(x,y);
		x=fax[top[x]];
	}
	if(deep[x]>deep[y]) return y;
	return x;
}
inline int length(int x,int y){return deep[x]+deep[y]-2*deep[lca(x,y)];}
 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i)
	{
		fa[i]=i;
		for(int j=1;j<=n;++j) cin>>a[i][j];
	}
	for(int i=1;i<=n;++i)
	{
		if(a[i][i]) {cout<<"NO\n";return 0;}
		for(int j=1;j<=n;++j)
		{
			if(i==j) continue;
			if(a[i][j]!=a[j][i]||!a[i][j]) {cout<<"NO\n";return 0;}
		}
	}
	for(int i=1;i<=n;++i)
	{
		for(int j=i+1;j<=n;++j) e[++tot]=(node){i,j,a[i][j]};
	}
	sort(e+1,e+tot+1);
	for(int i=1,x,y;i<=tot;++i)
	{
		x=find(e[i].u),y=find(e[i].v);
		if(x==y) continue;
		fa[x]=y;
		add(e[i].u,e[i].v,e[i].w),add(e[i].v,e[i].u,e[i].w);
	}
	dfs1(1,0),dfs2(1,1);
	for(int i=1;i<=n;++i)
	{
		for(int j=i+1;j<=n;++j)
		{
			if(length(i,j)!=a[i][j]) {cout<<"NO\n";return 0;}
		}
	}
	cout<<"YES\n";
	return 0;
}

CF2020D Connect the Dots

练习题。

posted @ 2024-11-11 22:05  call_of_silence  阅读(20)  评论(0编辑  收藏  举报