并查集+最小生成树 学习笔记+杂题 4
图论系列:
前言:
相关题单:戳我
算法讲解:戳我
AT_abc218_e [ABC218E] Destruction
最大生成树。
代码:
略\fad
AT_abc235_e [ABC235E] MST + 1
把原本图的边和询问的边混在一起,跑最小生成树的时候看是否有可能将询问的某条边加上(但不能真的加上),可能就是 Yes
,否则是 No
。
代码:
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=4e5+5;
int n,m,q,len;
int fa[M],ans[M];
struct N{
int u,v,w,id;
bool operator <(const N &o) const
{
return w<o.w;
}
};N p[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>>q,len=m+q;
for(int i=1;i<=n;++i) fa[i]=i;
for(int i=1;i<=m;++i) cin>>p[i].u>>p[i].v>>p[i].w;
for(int i=m+1;i<=m+q;++i) cin>>p[i].u>>p[i].v>>p[i].w,p[i].id=i-m;//混一堆了
sort(p+1,p+len+1);
for(int i=1,fx,fy;i<=len;++i)
{
fx=find(p[i].u),fy=find(p[i].v);
if(fx==fy) continue;
if(!p[i].id) fa[fx]=fy;
else ans[p[i].id]=1;//观察是否能加入,但是不真的加入
}
for(int i=1;i<=q;++i)
{
if(ans[i]) cout<<"Yes\n";
else cout<<"No\n";
}
return 0;
}
AT_abc282_e [ABC282E] Choose Two and Eat One
观察到 \(n \leq 500\) ,可以 \(O(n^2)\) 暴力建边之后跑最大生成树。
代码:
const int M=505;
int n,mod,len,ans;
int a[M],fa[M];
struct N{
int u,v,w;
bool operator <(const N &o) const
{
return o.w<w;
}
};N p[M*M];
inline int quick(int a,int n)
{
int res=1;
while(n)
{
if(n&1) res=res*a%mod;
n>>=1,a=a*a%mod;
}
return res;
}
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>>mod;
for(int i=1;i<=n;++i) cin>>a[i],fa[i]=i;
for(int i=1;i<=n;++i)
{
for(int j=i+1;j<=n;++j)
{
p[++len].u=i,p[len].v=j,p[len].w=(quick(a[i],a[j])+quick(a[j],a[i]))%mod;
}
}//暴力建边
sort(p+1,p+len+1);//从大到小
for(int i=1,fx,fy;i<=len;++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;
}
AT_arc076_b [ABC065D] Built?
我记得洛谷上有道题是询问的三维?这里只有两维更简单,那么按 \(x\) 排一次序,然后按 \(y\) 排一次序,将相邻的两个城市连一条边(此时一定是最优的),这样只会建 \(2(n-1)\) 条边,建完后跑最小生成树即可。
代码:
const int M=1e5+5;
int n;
int fa[M];
int cnt=0;
struct node{
int x,y,id;
};node s[M];
struct N{
int u,v,w;
bool operator < (const N &o) const
{
return w<o.w;
}
};N p[M<<1];
inline bool cmp1(node a,node b){return a.x<b.x;}
inline bool cmp2(node a,node b){return a.y<b.y;}
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) cin>>s[i].x>>s[i].y,s[i].id=i,fa[i]=i;
sort(s+1,s+n+1,cmp1);
for(int i=2;i<=n;++i)
{
++cnt;
p[cnt].u=s[i-1].id,p[cnt].v=s[i].id,p[cnt].w=min(abs(s[i].x-s[i-1].x),abs(s[i].y-s[i-1].y));
}
sort(s+1,s+n+1,cmp2);
for(int i=2;i<=n;++i)
{
++cnt;
p[cnt].u=s[i-1].id,p[cnt].v=s[i].id,p[cnt].w=min(abs(s[i].x-s[i-1].x),abs(s[i].y-s[i-1].y));
}
sort(p+1,p+cnt+1);
int ans=0;
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;
}
AT_abc270_f [ABC270F] Transportation
参照前面三个题单,已经有建超级源点的题目了,这题也一样,可以建两个超级源点,分别表示建机场和港口,每个点向这两个点连边,边权就是给定的值,边数为 \(2*n+m\) ,然后跑最小生成树即可。
但是这道题不一样的地方在于它整个图都可以不建机场或者港口,所以不一定两个超级源点需要连通,所以我们枚举四种情况:两个都会建,只会建机场,只会建港口,都不会建,跑四遍最小生成树,去权值最小的即可。
代码:
const int M=1e6+5;
int n,m,t1,t2;
int fa[M];
int cnt=0;
struct N{
int u,v,w,opt;
};N p[M];
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];
}
inline int solve(int flag1,int flag2)
{
int res=0,ans=0;
for(int i=1;i<=t2;++i) fa[i]=i;
for(int i=1,fx,fy;i<=cnt;++i)
{
if(p[i].opt==1&&!flag1) continue;
if(p[i].opt==2&&!flag2) continue;
fx=find(p[i].u),fy=find(p[i].v);
if(fx==fy) continue;
fa[fx]=fy,++res,ans+=p[i].w;
}
if(res!=n+flag1+flag2-1) return inf;
else return ans;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m,t1=n+1,t2=n+2;
for(int i=1;i<=n;++i) cin>>p[++cnt].w,p[cnt].u=i,p[cnt].v=t1,p[cnt].opt=1;
for(int i=1;i<=n;++i) cin>>p[++cnt].w,p[cnt].u=i,p[cnt].v=t2,p[cnt].opt=2;
for(int i=1;i<=m;++i) ++cnt,cin>>p[cnt].u>>p[cnt].v>>p[cnt].w;
sort(p+1,p+cnt+1,cmp);
cout<<min(min(solve(0,0),solve(0,1)),min(solve(1,0),solve(1,1)))<<"\n";
return 0;
}
AT_arc029_3 [ARC029C] 高橋君と国家
和上一题一样的,建超级源点,同每个点连边,表示在这个城市建交易所。这题就需要使超级源点连通了,因为图上至少都需要一个交易所,直接跑最小生成树即可。(woc,我好厉害,我竟然是先钦定每一个地方都建交易所,然后合并的时候是个集合只用建一个即可,由于要代价最小,那么就建权值最小的那个)
代码:
const int M=2e5+5;
int n,m,ans;
int a[M],fa[M];
struct N{
int u,v,w;
inline bool operator <(const N &o) const
{
return w<o.w;
}
};N p[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,ans+=a[i];
for(int i=1;i<=m;++i) cin>>p[i].u>>p[i].v>>p[i].w;
sort(p+1,p+m+1);
for(int i=1,fx,fy;i<=m;++i)
{
fx=find(p[i].u),fy=find(p[i].v);
if(fx==fy) continue;
if(p[i].w<max(a[fx],a[fy]))
{
if(a[fx]<a[fy]) swap(fx,fy);
fa[fx]=fy;
ans=ans+p[i].w-max(a[fx],a[fy]);
}
}
cout<<ans<<"\n";
return 0;
}
AT_arc026_4 [ARC026D] 道を直すお仕事
学生输出 nan
得满分,属于是究极不可以总司令了。
这题其实也不是并查集ing。
后面又加了几道题。
AT_abc350_d [ABC350D] New Friends
首先先将给定的边两两点进行合并,在一个连通块中,根据题目的要求,那么任意两个点之间都会存在一条边,对于一个大小为 \(siz_x\) 的连通块,其应该需要连 \(siz_x*(siz_x-1)/2\) 条边(无向边所以除二),然后再合并的时候统计一下当前连通块已经含有的边数。
最后遍历每一个连通块,用应该含有的边数减去已经含有的边数就是答案。
代码:
const int M=2e5+5;
int n,m;
int a[M],siz[M],b[M],fa[M],cnt[M];
//siz是集合大小,cnt是集合内含的边数
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,siz[i]=1;
for(int i=1,x,y;i<=m;i++)
{
cin>>a[i]>>b[i];
x=find(a[i]),y=find(b[i]);
if(x==y)
{
++cnt[y];
continue;
}
fa[x]=y,siz[y]+=siz[x],siz[x]=0,cnt[y]+=cnt[x]+1,cnt[x]=0;
}
int ans=0;
for(int i=1;i<=n;i++)
{
if(i==fa[i])
{
ans+=siz[i]*(siz[i]-1)/2-cnt[i];
}
}
cout<<ans<<"\n";
return 0;
}
AT_abc181_f [ABC181F] Silver Woods
二分答案题,因为问的是最大半径,自然圆的半径越大越可能会被钉子拦住,所以是否可从起点到达终点对于圆的半径是有单调性的,所以考虑二分出圆的半径。
对于一个半径,由于 \(n\) 范围较小,将每两个点之间的距离算出来,若直径小于这个距离那么就滚的过去,再设两个虚点,一个表示起点,一个表示终点,合并完所有能合并的点之后,观察起点和终点是否连通。
代码:
const int M=5e5+5;
int n;
int fa[M];
double x[M],y[M];
inline double dist(int a,int b)
{
return sqrt((x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b]));
}
inline int find(int x)
{
if(x!=fa[x]) fa[x]=find(fa[x]);
return fa[x];
}
inline int check(double x)
{
for(int i=1;i<=n+2;i++) fa[i]=i;//预留两个空间
for(int i=1;i<=n;i++)
{
if(y[i]+x>=100-x) fa[find(i)]=find(n+1);
}
for(int i=1;i<=n;i++)
{
if(y[i]-x<=x-100) fa[find(i)]=find(n+2);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(dist(i,j)<=2*x)
{
fa[find(i)]=find(j);
}
}
}
return find(n+1)!=find(n+2);
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>x[i]>>y[i];
double l=0,r=100;
while(r-l>=eps)
{
double mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
printf("%.12Lf",l);
return 0;
}
AT_abc120_d [ABC120D] Decayed Bridges
经典思路,对于删边考虑正难则反。让时间倒流,那么一开始没有边相连,割裂的点对数就是 \(n*(n-1)/2\) 。
对于加入一条边 \(x \to y\) ,\(x,y\) 所在连通块的大小为 \(siz_x,siz_y\) ,那么割裂的点对增加 \(siz_x*(siz_x-1)/2+siz_y*(siz_y-1)/2\) (相当于是将这两个小连通块删去了),设 \(siz=siz_x+siz_y\) 那么点对减少 \(siz*(siz-1)/2\) (计算新生成的大连通块减少的点对数量)。
代码:
const int M=1e5+5;
int n,m;
int fa[M],ans[M],siz[M];
struct N{
int u,v;
};N p[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,siz[i]=1;
for(int i=1;i<=m;i++) cin>>p[i].u>>p[i].v;
int res=0;//由于是按照从前向后删边的,所以从后向前加边
for(int i=m;i>=1;i--)
{
int x=find(p[i].u),y=find(p[i].v);
if(x==y)
{
ans[i]=res;continue;
}
fa[x]=y;
res-=(siz[x]*(siz[x]-1)/2)+(siz[y]*(siz[y]-1)/2);
siz[y]+=siz[x];
res+=siz[y]*(siz[y]-1)/2;
ans[i]=res;
}
res=n*(n-1)/2;
for(int i=2;i<=m+1;i++) cout<<res-ans[i]<<"\n";
return 0;
}
AT_keyence2019_e Connecting Cities
Boruvka
最小生成树板子题。
代码后面补。