U120773 森林扩张
原题链接 https://www.luogu.com.cn/problem/U120773
不知道为什么我们正在学习$dp$,教练却让我们做一套图论题 $emmm~$
题解
题目大意
给你一棵森林,连一条边 $( u,v )$ 的代价为 $a [ u ] + a [ v ]$,且每个点最多只能连一条边,问将森林连成一棵树的最小代价 。
我的做法
考虑到连通块内的点不需要再连了,所以我们可以先将原图进行缩点;
这样就变成了 “ 有 $n$ 个点,求将其连成一棵树的最小代价 ”,最小生成树!
考虑到我们需要连 $n-1$ 条边,所以说从一个连通块连向其他连通块的边最多有 $n-1$ 条,所以我们可以贪心地从每个连通块里找前 $n-1$ 小的点,分别于剩下 $n-1$ 个集合的最小的点连边,最后再跑一边最小生成树就搞定了。
当然它 $Re$ 了,不然就不会有这篇博客了$qwq$ 。
#include<iostream> #include<algorithm> #include<cstring> #include<vector> #include<cstdio> using namespace std; const int N=1e5+5; int read() { char ch=getchar(); int a=0,x=1; while(ch<'0'||ch>'9') { if(ch=='-') x=-x; ch=getchar(); } while(ch>='0'&&ch<='9') { a=(a<<1)+(a<<3)+(ch^48); ch=getchar(); } return a*x; } int n,m,tim,top,edge_sum,scc_sum,Edge_sum,ans,tot,bj; int val[N],head[N],dfn[N],low[N],s[N],vis[N],scc[N],minx[N],Head[N],fa[N],Vis[N]; vector<int> ve[N]; struct node { int from,to,next,dis; }a[N],b[N]; void add(int from,int to) { edge_sum++; a[edge_sum].from=from; a[edge_sum].to=to; a[edge_sum].next=head[from]; head[from]=edge_sum; } void tarjan(int u) { dfn[u]=low[u]=++tim; s[++top]=u; vis[u]=1; for(int i=head[u];i;i=a[i].next) { int v=a[i].to; if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); } else if(vis[v]) low[u]=min(low[u],dfn[v]); } if(dfn[u]==low[u]) { scc_sum++; while(s[top]!=u) { vis[s[top]]=0; scc[s[top]]=scc_sum; ve[scc_sum].push_back(s[top]); if(val[s[top]]<minx[scc_sum]) minx[scc_sum]=s[top]; top--; } vis[u]=0; scc[u]=scc_sum; ve[scc_sum].push_back(u); if(val[u]<minx[scc_sum]) minx[scc_sum]=u; top--; } } void Add(int from,int to,int dis) { Edge_sum++; b[Edge_sum].from=from; b[Edge_sum].to=to; b[Edge_sum].dis=dis; b[Edge_sum].next=Head[from]; Head[from]=Edge_sum; } bool cmp(node x,node y) { return x.dis<y.dis; } int getfa(int x) { if(fa[x]==x) return fa[x]; return fa[x]=getfa(fa[x]); } int main() { //freopen("forest.in","r",stdin); //freopen("forest.out","w",stdout); n=read();m=read(); for(int i=1;i<=n;i++) val[i]=read(); for(int i=1;i<=m;i++) { int u=read(); int v=read(); add(u,v); add(v,u); } memset(minx,0x3f,sizeof(minx)); for(int i=1;i<=n;i++) { if(!dfn[i]) tarjan(i); } if(scc_sum==1) { printf("0\n"); return 0; } for(int i=1;i<=scc_sum;i++) { for(int j=0;j<=ve[i].size()-1;j++) { for(int k=1;k<=scc_sum;k++) { if(i==k) continue; Add(ve[i][val[j]],minx[k],val[ve[i][val[j]]]+val[minx[k]]); } } fa[i]=i; } sort(b+1,b+1+Edge_sum,cmp); for(int i=1;i<=Edge_sum;i++) { int u=b[i].from; int v=b[i].to; if(Vis[u]||Vis[v]) continue; int fx=getfa(scc[u]); int fy=getfa(scc[v]); if(fx==fy) continue; Vis[u]=Vis[v]=1; ans+=b[i].dis; fa[fx]=fy; tot++; if(tot==scc_sum-1) { bj=1; break; } } if(bj) printf("%d\n",ans); else printf("-1\n"); return 0; }
神仙做法
因为这道题是无向图,所以求连通块的过程我们可以用并查集 。
还是顺着上面的思路走,既然我们需要连 $n-1$ 条边使得原图变成一棵树,也就是说我们必须要从每个连通块里至少找一个点与其他连通块连边,贪心地来看,这个点一定是连通块里最小的点,即这 $n-1$ 条边的其中一端一定是某个连通块内最小的点,而且其中一条边的两端是某两个连通块内最小的点,否则不会是最优方案;
我们需要连 $n-1$ 条边,则需要先找到 $2 * ( n-1 )$ 个点,现在我们已经找到了 $n$ 个点了,剩下的 $n-2$ 个点怎么找呢?
我们只需要在剩下的所有点出找出 $n-2$ 个最小的点就好了 。
大胆猜想,无需证明。
这个贪心思路想一想就能证明是对的 。
考虑一下极端情况:剩下的 $n-2$ 个都来自于同一个连通块 $k$;
这种情况下就是从连通块 $k$ 总共选了 $n-1$ 个点,再从剩下 $n-1$ 个连通块中各找一个最小的点与之连边,形成一个菊花图的样子,不难证明这确实是最优方案;
Code:
#include<bits/stdc++.h> using namespace std; const int N=1e6+5; int read() { char ch=getchar(); int a=0,x=1; while(ch<'0'||ch>'9') { if(ch=='-') x=-x; ch=getchar(); } while(ch>='0'&&ch<='9') { a=(a<<1)+(a<<3)+(ch^48); ch=getchar(); } return a*x; } int n,m,tot; int val[N],fa[N]; long long ans; vector<long long> f[N]; int getfa(int x) //求点x的父亲 { if(fa[x]==x) return fa[x]; return fa[x]=getfa(fa[x]); //路径压缩 } int main() { n=read();m=read(); if(n<2*(n-m-1)) //给了n个点和m条边,连成一棵树至少再要n-m-1条边,需要2*(n-m-1)个点,如果n不够的话肯定是无解的 { printf("-1\n"); return 0; } for(int i=1;i<=n;i++) { val[i]=read(); fa[i]=i; //并查集初始化别忘了 } for(int i=1;i<=m;i++) { int u=read(); int v=read(); int fx=getfa(u); //求连通块 int fy=getfa(v); fa[fx]=fy; } for(int i=1;i<=n;i++) { f[getfa(i)].push_back(val[i]); //把同一个连通块内的所有点都扔进vector里 if(fa[i]==i) tot++; //求连通块的个数 } if(tot==1) //只有一个连通块,说明原图本来就是一棵树,直接输出0 { printf("0\n"); return 0; } for(int i=1;i<=n;i++) //将每个连通块内的点从小到大排序 sort(f[i].begin(),f[i].end()); for(int i=1;i<=n;i++) //枚举每个连通块 { if(f[i].size()) { ans+=f[i][0]; //每个连通块的最小值肯定是会被连的 for(int j=1;j<f[i].size();j++) f[0].push_back(f[i][j]); //把除了最小值剩下的所有点都扔进大vector里 } } if(f[0].size()<tot-2) //我们还需要找tot-2个点进行连边,如果不够的话,肯定是无解的 { printf("-1\n"); return 0; } sort(f[0].begin(),f[0].end()); //从小到大排序 for(int i=0;i<tot-2;i++) //贪心地找出前tot-2小的点 ans+=f[0][i]; //算答案 printf("%lld\n",ans); //愉快地输出 return 0; }