并查集+最小生成树 学习笔记+杂题 2
图论系列:
前言:
相关题单:戳我
算法讲解:戳我
CF1829E The Lakes
给定一张
代码:
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 次,肯定也没有解。然后我们把每个点对的两个点
然后判断每个块内有多少个元素,如果有偶数个元素,那么两边集合一边一半,但如果一个集合有奇数个数,就一定无法分配到两个集合去,必然会出现矛盾。(判断一下即可)
代码:
#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
并查集启发式合并。
如果为 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
并查集找环的转化。首先考虑什么城市会成为孤立城市?也就是没有入边的城市。那么在一个环上,每个点肯定要都有入边,所以环上的所有点都不是孤立城市,除此之外的图中,由于边会定向,所以每个集合内必然会有一个城市成为孤立城市。
所以就转化为查询图中有多少个不含环的集合,并查集好判断,如果对于一条边
代码:
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
对于
一个连通块内最大点的度数当然就是让其它点都与这个点相连,最大度数为
注意:对于每一个
代码:
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
给定
考虑将
于是在合并完之后,对于每一个集合统计一下其包含的元素个数以及出现次数最多的颜色出现的次数,答案就是每个集合的大小减去出现次数最多的颜色出现的次数之和。
代码:
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
由于位置
初始化的时候位置
代码:
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 差不多啊,也是只要当前不相等了就去合并,如果确实不在同一个集合就
代码:
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
需要一定的转换。第一位客人肯定把他喜爱的花全会取走,为了让结果更优,我们希望接下来的客人喜爱的花与之有重合,这样他就只会取走一朵花,以此类推……。
于是我们将每个人喜欢的两种花合并在一起,形成若干个连通块,那么对于一个大小为
代码:
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
也就是说只要存在
于是思路清晰,使用并查集维护各个点之间的连通性,同时统计每个集合内最小能到达的点&最大能到达的点,然后将最小位置的那个点与中间的所有点相连,看有多少个与其不在一个集合的点,同时将这些点也合并起来。
代码:
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
神秘转化,保证一开始没有冲突。由于棋子都是车,可以随便开,那么最优解就是每个车开到自己相对应的主对角线上即可,此时会花费
判环就用并查集,答案就是
代码:
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)
对于一张地图,有#
,.
,两种符号,问你改变其中一行/一列全部为 #
,能实现的最大的 #
连通块大小。发现 #
能得到的最大连通块大小,对于初始图将所有连通的 #
合并在一起,然后对于每一行/列减去这上面本来的 #
数量,然后加上附近的 #
集合大小再加
代码:
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
太经典了,详记一下。对于一张带有边权的树,多次询问,每次询问最大权值不超过
实际上是最小生成树的一类经典转化,由于我们做最小生成树的时候是从小到大的加边,那么我们考虑对于每一次加边有什么影响,设边
图是树所以两点之间只会有一条路径,所以加了这条边之后图上就多了
代码:
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
首先题目要求生成树的边权最大值正好等于
代码:
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 上路径的最大值,并给定一些特殊点,对于每一个特殊点,找出离它最远的另一个特殊点之间的距离。
首先根据定义我们就需要跑最小生成树,那么跑最小生成树的时候,只有在其中一条边加入图中之后(由于从小到大嘛),图上的给定的特殊点才全部连通,所以相当于大家的答案都是相同的,就是最后加入的那条使得特殊点连通的边的权值。
那么考虑使用并查集,对于当前一条边,如果连的两边都是特殊点,那么答案更新为这条边的权值(不然的话这两点不连通啊,而且此时这条边的权值是最大的),然后对于一个特殊点与一个普通点的连边,我们将这个普通点也化为特殊点(因为此时所有的特殊点都还没有连通,答案一定大于等于当前边的权值,所以将这个普通点化为特殊点)。
那么操作显然了,对于两边都是特殊点的边,将答案更新为当前边的权值,对于特殊点与普通点的连边,将普通点变为特殊点,最后就是输出
代码:
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
妙妙题。首先由于我们需要传输
-
直接传输,代价为
-
比较以上一个传输文件的差别,代价为
, 是一个给定的常数。
不要求
现在把所有的边都统计下来了,跑一遍最小生成树,然后由于需要输出方案,这个也比较简单,我们只需要将最小生成树上的边记录下来,然后从
代码:
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
给定一个有
由于最后生成的就是一颗树嘛,那么那么一开始使用并查集判断那些边是无效的(即加入之后并不会使连通块的数量减少),这些边就是需要断掉的边。将这些边做一个标记,就是需要断掉的边。然后再选取一个连通块,将剩下的没有与其相连的连通块都与这个连通块的一个点连一条边即可。
代码:
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
首先定义了一个序列的强度是这个序列的最小值,给定了一个长为
设当前答案已经更新到长度为
代码:
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 这题挺类似的。还是建一个超级源点
答案就是最小生成树的边权和,输出方案也简单,只需要将最小生成树连的边记录下来即可,与
代码:
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
给你
明显是最小生成树,但是
代码:
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
给定一些边,求或运算下的最小生成树。有点牛啊,观察到边权只有
代码:
#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
题意很简单啊,给定一棵节点为
于是建出最小生成树了,树的形态已知,
代码:
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
练习题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效